Horizontal Scrolling is not working in RowsSupportFragment Android Leanback? - android-tv

I am trying to implement a rowsSupportFragment using Android Leanback for android tv.
But the vertical Scrolling is not working is Keeps glitching.
class DetailsRowListFragment: RowsSupportFragment() {
private var rootAdapter : ArrayObjectAdapter = ArrayObjectAdapter(
ListRowPresenter(
FocusHighlight.ZOOM_FACTOR_MEDIUM,true)
)
private var episodeItemSelectedListener: ((Episode)->Unit)?= null
private var relatedItemSelectedListener: ((Relation)->Unit)?= null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//setting up adapter from within the class
adapter = rootAdapter
//Setting up listener
onItemViewSelectedListener = ItemViewSelectedListener()
}
fun bindData(animeData : AnimeDetails ){
val episodeObjectAdapter = ArrayObjectAdapter(EpisodePresenter())
animeData.episodes.forEach { it->
episodeObjectAdapter.add(it)
}
val episodeHeaderItem= HeaderItem("Episodes")
val listRow = ListRow(episodeHeaderItem,episodeObjectAdapter)
rootAdapter.add(listRow)
}
fun setOnContentSelectedListener(listener: (Episode)->Unit){
this.episodeItemSelectedListener=listener
}
private fun setRelatedRow(itemList: List<Relation>){
val relatedObjectAdapter = ArrayObjectAdapter(RelatedPresenter())
itemList.forEach { it->
relatedObjectAdapter.add(it)
}
val headerItem = HeaderItem("Related Media")
val listRow = ListRow(headerItem,relatedObjectAdapter)
rootAdapter.add(listRow)
}
inner class ItemViewSelectedListener : OnItemViewSelectedListener {
override fun onItemSelected(
itemViewHolder: Presenter.ViewHolder?,
item: Any?,
rowViewHolder: RowPresenter.ViewHolder?,
row: Row?
) {
if( row !=null && row!!.headerItem!!.name=="Episodes" && item is Episode){
episodeItemSelectedListener?.invoke(item as Episode)
}else{
Log.d("Selection Listener","Listener Activated")
}
}
}
The Implemetation of My RowsSupportFragment Class is given above
enter image description here
Whenever I try to scroll side to side using the emulator's remote the row jsut glitches and comes back to its initial position.
But I am able to move the list by dragging with my mouse cursor.

Related

Problems updating recycler views with sqlite database in kotlin

I am writing a code that deletes the item from the database when the button of the popup menu is pressed , and deletes it according to the recycler viewer.
selected item is deleted from the database, but not from the recycler view.
error code: No error code
PrintActivity.kt
class PrintActivity : AppCompatActivity() {
val helper = SqliteHelper(this, "myDB.sql", 1)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val vBinding = ActivityPrintBinding.inflate(layoutInflater)
setContentView(vBinding.root)
var recyclerViewAdapter = CustomAdapter()
recyclerViewAdapter.listData = helper.select()
vBinding.myRecyclerView.adapter = recyclerViewAdapter
vBinding.myRecyclerView.layoutManager = LinearLayoutManager(this)
vBinding.myRecyclerView.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
}
inner class CustomAdapter : RecyclerView.Adapter<CustomAdapter.Holder>(){
var listData = ArrayList<questionType>()
inner class Holder(val vBinding: QuestionLayoutRecyclerBinding) :
RecyclerView.ViewHolder(vBinding.root){
fun setData(id:Int?, question: String, answer: String, exp: String) {
vBinding.printId.text=id.toString()
vBinding.myLinear.setOnClickListener {
var myPopupMenu = PopupMenu(this#PrintActivity, it)
menuInflater?.inflate(R.menu.menu, myPopupMenu.menu)
var listener = PopupMenuListener()
myPopupMenu.setOnMenuItemClickListener(listener)
myPopupMenu.show()
}
}
inner class PopupMenuListener:PopupMenu.OnMenuItemClickListener{
override fun onMenuItemClick(p0: MenuItem?): Boolean {
listData[adapterPosition].id?.let { helper.delete(it) }
//here
this#CustomAdapter.notifyDataSetChanged()
return false
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomAdapter.Holder {
val vBinding = QuestionLayoutRecyclerBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return Holder(vBinding)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
val question = listData[position]
holder.setData(question.id, question.question, question.answer, question.exp)
}
override fun getItemCount(): Int {
return listData.size
}
}
}
You can try this approach using higher order function as callback mechanism.
One recommendation I would give is you can try to avoid these nested inner classes and make into separate files. Hope it works
inner class CustomAdapter : RecyclerView.Adapter<CustomAdapter.Holder>(){
var listData = ArrayList<questionType>()
fun deleteItem(position: Int) { //Function to remove recyclerview item
listData.remove(position)
notifyItemRemoved(position)
notifyItemRangeChanged(position, getItemCount())
}
inner class Holder(val vBinding: QuestionLayoutRecyclerBinding) :
RecyclerView.ViewHolder(vBinding.root){
fun setData(id:Int?, question: String, answer: String, exp: String,itemRemovedCallback:(a:Int)->Unit) { //higher order callback function
vBinding.printId.text=id.toString()
vBinding.myLinear.setOnClickListener {
var myPopupMenu = PopupMenu(this#PrintActivity, it)
menuInflater?.inflate(R.menu.menu, myPopupMenu.menu)
var listener = PopupMenuListener(itemRemovedCallback) //Passed to popupmenu listener
myPopupMenu.setOnMenuItemClickListener(listener)
myPopupMenu.show()
}
}
inner class PopupMenuListener(val itemRemovedCallback:(a:Int)->Unit):PopupMenu.OnMenuItemClickListener{
override fun onMenuItemClick(p0: MenuItem?): Boolean {
listData[adapterPosition].id?.let { helper.delete(it) }
itemRemovedCallback.invoke(adapterPosition) //Calling function. It will pass position to function in adapter
return false
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomAdapter.Holder {
val vBinding = QuestionLayoutRecyclerBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return Holder(vBinding)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
val question = listData[position]
holder.setData(question.id, question.question, question.answer, question.exp,::deleteItem) //Passing function as higher order function to viewholder
}
override fun getItemCount(): Int {
return listData.size
}
}

Paging3 - How to do Reverse Pagination in Chat App

I am recently migrate to Paging3. However, I noticed that most of the tutorials and guides are mainly on normal pagination (from top to bottom).
I need to implement the REVERSE pagination as user scroll to top boundary, will load for page 2,3,4..
Is there any tutorial/guide for this?
PS: Now the initial loading is working fine, but when I scroll to top-most, I have no idea how to load Page 2 data.
My current approach
PagingSource
class ChatPagingSource(
private val apiService: ApiService,
private val roomId: String
): PagingSource<Int, Message>() {
override fun getRefreshKey(state: PagingState<Int, Message>): Int? = null
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Message> {
return try{
val page = params.key?: 1
val pageSize = params.loadSize
val call = apiService.getMessageFeedPaging(
room_id = roomId,
page = page,
max = pageSize,
exclude = EXCLUDE_TYPE
)
val repoItems = call.messages
val prevKey = if(page > 1) page - 1 else null
val nextKey = if(repoItems.isNotEmpty()) page + 1 else null
if(repoItems.isNotEmpty()) {
val messageList = mutableListOf<Message>()
for (i in repoItems) {
val replyData = Converters.convertReplyDataAPItoReplyData(i.reply_data)
val msg = Converters.convertMessageAPItoMessage(replyData, i, hasError = false)
messageList.add(msg)
}
LoadResult.Page(messageList, prevKey, nextKey)
} else {
LoadResult.Page(listOf(), prevKey, nextKey)
}
}catch (e: Exception) {
LoadResult.Error(e)
}
}
}
Repository
fun loadRemoteMessageStream(roomId: String): LiveData<PagingData<Message>> {
return Pager(
config = PagingConfig(20),
pagingSourceFactory = { ChatPagingSource(apiService, roomId) }
).liveData
}
ViewModel
private val _remoteMessage = chatRepository.loadRemoteMessageStream(currentRoomId)
.cachedIn(viewModelScope)
.let { it as MutableLiveData<PagingData<Message>> }
val remoteMessage: LiveData<PagingData<Message>> = _remoteMessage
Fragment
chatViewModel.remoteMessage.observe(viewLifecycleOwner, {
chatAdapter.submitData(viewLifecycleOwner.lifecycle, it)
})
In case this helps anyone, I will post out my own answer.
The key is to reverse prevKey and nextKey and fixed the pageSize that your API required (in my case is 20).
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Message> {
val pageSize = 20
val prevKey = if(repoItems.isNotEmpty()) page + 1 else null
val nextKey = if(page > 1) page - 1 else null
}
Then, in the recyclerview, you should use stackFromEnd = true so that initially the recyclerview will auto-scroll to bottom.

Fragment already added issue, when using navigation with bottom navigation bar android

I have a bottom navigation view which I have implemented in androidx navigation, but the problem is it is showing me the error of
java.lang.IllegalStateException: Fragment already added: BaseFragment{27d5b00 (2156a830-7756-4fc9-bc63-7c6f3d6705f0) id=0x7f08008c android:switcher:2131230860:0}
I have a base fragment which provides views to different fragments
class MainActivity : AppCompatActivity(),
ViewPager.OnPageChangeListener,
BottomNavigationView.OnNavigationItemReselectedListener,
BottomNavigationView.OnNavigationItemSelectedListener {
// overall back stack of containers
private val backStack = Stack<Int>()
// list of base destination containers
private val fragments = listOf(
BaseFragment.newInstance(R.layout.content_home_base, R.id.toolbar_home, R.id.nav_host_home),
BaseFragment.newInstance(R.layout.content_library_base, R.id.toolbar_library, R.id.nav_host_library),
BaseFragment.newInstance(R.layout.content_settings_base, R.id.toolbar_settings, R.id.nav_host_settings))
// map of navigation_id to container index
private val indexToPage = mapOf(0 to R.id.home, 1 to R.id.library, 2 to R.id.settings)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// setup main view pager
main_pager.addOnPageChangeListener(this)
main_pager.adapter = ViewPagerAdapter()
main_pager.post(this::checkDeepLink)
main_pager.offscreenPageLimit = fragments.size
// set bottom nav
bottom_nav.setOnNavigationItemSelectedListener(this)
bottom_nav.setOnNavigationItemReselectedListener(this)
// initialize backStack with elements
if (backStack.empty()) backStack.push(0)
}
/// BottomNavigationView ItemSelected Implementation
override fun onNavigationItemSelected(item: MenuItem): Boolean {
val position = indexToPage.values.indexOf(item.itemId)
if (main_pager.currentItem != position) setItem(position)
return true
}
override fun onNavigationItemReselected(item: MenuItem) {
val position = indexToPage.values.indexOf(item.itemId)
val fragment = fragments[position]
fragment.popToRoot()
}
override fun onBackPressed() {
val fragment = fragments[main_pager.currentItem]
val hadNestedFragments = fragment.onBackPressed()
// if no fragments were popped
if (!hadNestedFragments) {
if (backStack.size > 1) {
// remove current position from stack
backStack.pop()
// set the next item in stack as current
main_pager.currentItem = backStack.peek()
} else super.onBackPressed()
}
}
/// OnPageSelected Listener Implementation
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(p0: Int, p1: Float, p2: Int) {}
override fun onPageSelected(page: Int) {
val itemId = indexToPage[page] ?: R.id.home
if (bottom_nav.selectedItemId != itemId) bottom_nav.selectedItemId = itemId
}
private fun setItem(position: Int) {
main_pager.currentItem = position
backStack.push(position)
}
private fun checkDeepLink() {
fragments.forEachIndexed { index, fragment ->
val hasDeepLink = fragment.handleDeepLink(intent)
if (hasDeepLink) setItem(index)
}
}
inner class ViewPagerAdapter : FragmentPagerAdapter(supportFragmentManager) {
override fun getItem(position: Int): Fragment = fragments[position]
override fun getCount(): Int = fragments.size
}
}
Base Fragment Class :
class BaseFragment: Fragment() {
private val defaultInt = -1
private var layoutRes: Int = -1
private var toolbarId: Int = -1
private var navHostId: Int = -1
private val appBarConfig = AppBarConfiguration(rootDestinations)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
layoutRes = it.getInt(KEY_LAYOUT)
toolbarId = it.getInt(KEY_TOOLBAR)
navHostId = it.getInt(KEY_NAV_HOST)
} ?: return
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return if (layoutRes == defaultInt) null
else inflater.inflate(layoutRes, container, false)
}
override fun onStart() {
super.onStart()
// return early if no arguments were parsed
if (toolbarId == defaultInt || navHostId == defaultInt) return
// setup navigation with toolbar
val toolbar = requireActivity().findViewById<Toolbar>(toolbarId)
val navController = requireActivity().findNavController(navHostId)
NavigationUI.setupWithNavController(toolbar, navController, appBarConfig)
// NavigationUI.setupWithNavController(toolbar,navController)
}
fun onBackPressed(): Boolean {
return requireActivity()
.findNavController(navHostId)
.navigateUp(appBarConfig)
}
fun popToRoot() {
val navController = requireActivity().findNavController(navHostId)
navController.popBackStack(navController.graph.startDestination, false)
}
fun handleDeepLink(intent: Intent) = requireActivity().findNavController(navHostId).handleDeepLink(intent)
companion object {
private const val KEY_LAYOUT = "layout_key"
private const val KEY_TOOLBAR = "toolbar_key"
private const val KEY_NAV_HOST = "nav_host_key"
fun newInstance(layoutRes: Int, toolbarId: Int, navHostId: Int) = BaseFragment().apply {
arguments = Bundle().apply {
putInt(KEY_LAYOUT, layoutRes)
putInt(KEY_TOOLBAR, toolbarId)
putInt(KEY_NAV_HOST, navHostId)
}
}
}
}
I tried empty the stack first but that did not work, i have three navgraphs for my three viewpager elements/ fragments.
We reverted back back to 2.1.0 (nav library) and it worked again
Error is not quite connected with Navigation library. It's most probably fragment's error itself. Try testing that fragment in empty activity without using Navigation.
Navigation library is somehow treating error wrongly.
p.s. in my case it was just and view's id missing in layout file.
Thanks for the support, I found the solution I created different navigation graphs for each page.

TableView cell request Focus

I'm new in JavaFX and have the following issue:
I have a tableview inside a BorderPane. I want it to focus on the last row/1st column when it's loaded. I have tried the following:
requestfocus()
scrollTo()
focusModel.focus()
selectionModel.select()
What happens is that the cell I want is indeed blue (as if it was selected) but the first cell has a blue border. So, when I try to use the arrow keys, the selected cell moves to the first row.
BTW, I'm using TornadoFX.
Any ideas?
Thanks in advance!
class CashflowTab : View() {
override val root: HBox by fxml()
private val mController : CashflowController by inject()
private val mainView : MainView by inject()
// Get the buttons
private val buttonCashflow : Button by fxid("btnCashflow")
init {
// Setup the buttons
buttonCashflow.action {
setupCashflowTable()
}
}
/** Displays the TableView for the Cashflow */
private fun setupCashflowTable() {
var initialFocus = true
// List of entries for the category ComboBox
val categoryList = mController.getCashFlowCategoryList()
// Create the table
val cashTable = tableview<CashEntry>(mController.getCashEntryList()) {
isEditable = true
column(Constants.COL_COUNT, CashEntry::countProperty)
column(Constants.COL_DATE, CashEntry::dateProperty).makeEditable(LocaleDateConverter())
column(Constants.COL_INCOME, CashEntry::incomeProperty).makeEditable(CurrencyConverter())
column(Constants.COL_EXPENSES, CashEntry::expensesProperty).makeEditable(CurrencyConverter())
column(Constants.COL_PROFIT, CashEntry::profitProperty).converter(CurrencyConverter())
column(Constants.COL_TOTAL_PROFIT, CashEntry::totalProfitProperty).converter(CurrencyConverter())
column(Constants.COL_COMMENTS, CashEntry::commentsProperty).makeEditable()
column(Constants.COL_CATEGORY, CashEntry::categoryProperty).useComboBox(categoryList)
// Scroll to and focus on the last cell on startup
if (initialFocus) {
val lastRow = mController.getCashEntryList().size - 1
requestFocus()
scrollTo(lastRow)
focusModel.focus(lastRow)
selectionModel.select(lastRow)
initialFocus = false
}
onEditCommit {entry ->
// Update the list
mController.updateCashEntryList(entry)
// Move to the next cell
requestFocus()
focusModel.focusRightCell()
#Suppress("UNCHECKED_CAST")
selectionModel.select(focusModel.focusedCell.row, focusModel.focusedCell.tableColumn as TableColumn<CashEntry, *>)
}
enableCellEditing()
// Enable edit on key typed
addEventHandler(KeyEvent.KEY_PRESSED) {keyEvent ->
if (keyEvent.code.isDigitKey || keyEvent.code.isLetterKey) {
if (editingCell == null) {
val currentSelectedCell = selectedCell
if (currentSelectedCell != null && currentSelectedCell.tableColumn.isEditable) {
edit(currentSelectedCell.row, currentSelectedCell.tableColumn)
}
}
}
}
}
// Add the table to the view
mainView.root.center = cashTable
cashTable.tableMenuButtonVisibleProperty()
// Ensure no other node can get focus
cashTable.focusedProperty().onChange {
val focusOwner = currentStage?.scene?.focusOwnerProperty()?.value
// Check if the focus owner is the table or a cell
if (focusOwner !is TableView<*> && focusOwner !is TextField) {
cashTable.requestFocus()
}
}
}
}
You should use
Platform.runLater(() -> {
requestFocus();
scrollTo(lastRow);
...
});
to update the GUI.

TornadoFX JavaFX Sync Scroll across tableviews

I am trying to synchronise scrolls across the tableviews. (Both Horizontal & Vertical)
The SyncScrollEx View has two tableView which is basically one Fragment placed side by side, with same dataset, and hence same table size layout.
Expected Behaviour: When I scroll on one tableview, the other tableview's scrollbar should also scroll for the same amount.
Below is my current progress:
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleStringProperty
import javafx.collections.FXCollections
import javafx.scene.control.ScrollBar
import tornadofx.*
class SyncScrollEx : View() {
override val root = hbox {
setPrefSize(300.0, 150.0)
this += find<MyTableFrag>()
this += find<MyTableFrag>()
}
}
class MyTableFrag : Fragment() {
var addEventOnlyOnceFlag = false
val persons = FXCollections.observableArrayList<GameWarrior>(
GameWarrior(1,"Tyrion Lannister", "M"),
GameWarrior(2,"Ned Stark", "M"),
GameWarrior(3,"Sansa Stark", "F"),
GameWarrior(4,"Daenerys Targaryen", "F"),
GameWarrior(5,"Bran Stark", "M"),
GameWarrior(6,"Jon Snow", "M"),
GameWarrior(7,"Arya Stark", "F")
)
override val root = vbox {
tableview(persons) {
column("ID", GameWarrior::idProperty)
column("Name", GameWarrior::nameProperty)
column("Gender", GameWarrior::genderProperty)
subscribe<SyncScrollEvent> { event ->
//Sync the ScrollX & ScrollY of both the tables
event.node.value = event.newVal.toDouble()
}
//Hack, need to initialize this when the table/scroll is rendered
setOnMouseEntered {
//Hack for not triggering the lookupAll event on every mouse enter
if (!addEventOnlyOnceFlag) {
addEventOnlyOnceFlag = true
//INFO: Look up for the scroll bars in tableView and add a listener
this.lookupAll(".scroll-bar").map { node ->
if (node is ScrollBar) {
node.valueProperty().addListener {
value, oldValue, newValue ->
println(node.orientation.toString() + " " + newValue)
fire(SyncScrollEvent(node, newValue))
}
}
}
}
}
}
}
}
class GameWarrior(id: Int, name: String, gender: String) {
val idProperty = SimpleIntegerProperty(id)
var id by idProperty
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
val genderProperty = SimpleStringProperty(gender)
var gender by genderProperty
}
class SyncScrollEvent(val node: ScrollBar, val newVal: Number) : FXEvent()
The comments highlight the problems I am facing.
Also, I fail to understand how the "subscribe" will get invoked for both the tableviews in such scenario where Fire() happens inside a EventListener
First we need clean access to the scrollbars. When the TableView is assigned it's skin, the scrollbars will be available. We'll create a map keyed on orientation to keep track of them:
val scrollbars = HashMap<Orientation, ScrollBar>()
Once the skin is available we look up the scrollbars and assign them to our map and listen for changes so we can fire the event
skinProperty().onChange {
this.lookupAll(".scroll-bar").map { it as ScrollBar }.forEach { bar ->
scrollbars[bar.orientation] = bar
bar.valueProperty().onChange {
fire(SyncScrollEvent(bar, this))
}
}
}
We don't need the position in the event, since we can query the scrollbar for it's value, but it's easier to filter out the events if we add the source TableView. The SyncScrollEvent now looks like this:
class SyncScrollEvent(val scrollbar: ScrollBar, val table: TableView<*>) : FXEvent()
Let's listen for the scroll events and make sure we only change our scrollbar value if the event originates from the other tableview, for the corresponding orientation:
subscribe<SyncScrollEvent> { event ->
if (event.table != this)
scrollbars[event.scrollbar.orientation]?.value = event.scrollbar.value
}
For completeness, here is the whole modified app:
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleStringProperty
import javafx.collections.FXCollections
import javafx.geometry.Orientation
import javafx.scene.control.ScrollBar
import javafx.scene.control.TableView
import tornadofx.*
import java.util.*
class SyncScrollEx : View() {
override val root = hbox {
setPrefSize(300.0, 150.0)
add(MyTableFrag::class)
add(MyTableFrag::class)
}
}
class MyTableFrag : Fragment() {
val persons = FXCollections.observableArrayList<GameWarrior>(
GameWarrior(1, "Tyrion Lannister", "M"),
GameWarrior(2, "Ned Stark", "M"),
GameWarrior(3, "Sansa Stark", "F"),
GameWarrior(4, "Daenerys Targaryen", "F"),
GameWarrior(5, "Bran Stark", "M"),
GameWarrior(6, "Jon Snow", "M"),
GameWarrior(7, "Arya Stark", "F")
)
val scrollbars = HashMap<Orientation, ScrollBar>()
override val root = vbox {
tableview(persons) {
column("ID", GameWarrior::idProperty)
column("Name", GameWarrior::nameProperty)
column("Gender", GameWarrior::genderProperty)
subscribe<SyncScrollEvent> { event ->
if (event.table != this)
scrollbars[event.scrollbar.orientation]?.value = event.scrollbar.value
}
skinProperty().onChange {
this.lookupAll(".scroll-bar").map { it as ScrollBar }.forEach { bar ->
scrollbars[bar.orientation] = bar
bar.valueProperty().onChange {
fire(SyncScrollEvent(bar, this))
}
}
}
}
}
}
class GameWarrior(id: Int, name: String, gender: String) {
val idProperty = SimpleIntegerProperty(id)
var id by idProperty
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
val genderProperty = SimpleStringProperty(gender)
var gender by genderProperty
}
class SyncScrollEvent(val scrollbar: ScrollBar, val table: TableView<*>) : FXEvent()

Resources