I have a Fragment with a ViewModel (so Mvvm) and in the xml I have 2 RecyclerView with both having their own Adapter using the same itemList because I want to see the same data in 2 different layout.
Before switching to Mvvm, I could reference the adapter and the recyclerView and could reach "getChildAdapterPosition"
But right now this is quite impossible it seem. I made some custom BindingAdapter to bind my Adapter, a CustomPageSnaperHelper and a scrollListener. but now I can't seem to be able to sync the 2 to show the same item even if i scroll the bottom one or the top one.
Gonna post some code snippet, but not sure which one would help more:
#JvmStatic #BindingAdapter("adapter")
fun <T : RecyclerView.ViewHolder> setRvAdapter(rv: RecyclerView, adapter: RecyclerView.Adapter<T>?) {
if(adapter != null) rv.adapter = adapter
}
#JvmStatic #BindingAdapter("pageSnapper")
fun setPageSnapper(rv: RecyclerView, cs: CustomSnapHelper?){
cs?.attachToRecyclerView(rv)
}
#JvmStatic #BindingAdapter("rvScrollListener")
fun setRvScrollListener(rv: RecyclerView, sl: RecyclerView.OnScrollListener?){
if (sl == null) return
rv.addOnScrollListener(sl)
}
Xml:
<androidx.recyclerview.widget.RecyclerView
adapter="#{vm.OCardAdapter}"
pageSnapper="#{vm.OCardPageSnaper}"
rvScrollListener="#{vm.OCardScrollListener}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/top_tabs"
tools:listItem="#layout/card_layout" />
<androidx.recyclerview.widget.RecyclerView
adapter="#{vm.OCarouselAdapter}"
pageSnapper="#{vm.OCarouselPageSnaper}"
rvScrollListener="#{vm.OCarouselScrollListener}"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:background="#color/colorWhite"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:listItem="#layout/carousel_layout"/>
To be fair I could paste in more of my code, but I don't think it could help more ish.
val scrollListenerCard = object: RecyclerView.OnScrollListener(){
var mScrolled = false
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
}
}
You could create
#InverseBindingAdapter(attribute = "currentPosition")
public static int getCurrentPosition(final RecyclerView rv) {
return ((LinearLayoutManager) rv.getLayoutManager()).findFirstVisibleItemPosition();
}
#BindingAdapter(value = "currentPositionAttributeChanged")
public static void setListener(final RecyclerView rv, final InverseBindingListener l) {
final int prevPos = ((LinearLayoutManager)rv.getLayoutManager()).findFirstVisibleItemPosition();
rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy == 0) {
return;
}
int newPosition = linearLayoutManager.findFirstVisibleItemPosition();
if (previousPosition != newPosition) {
l.onChange();
}
}
});
}
#BindingAdapter("currentPosition")
public void setCurrentPosition(final RecyclerView rv, final int pos) {
// no need to do anything here - setter required by databinding
}
These would give you access to the positions, or you can set these listeners by accessing bindings inside your fragment/activity and update your viewModel from there.
I'm not sure that I understand your problem well but if I had to create two rw-s that are always scrolled to the same position with databindig, than I would give adapter A to scroll listener B as a constructor parameter (and inversely too) and this way I would call the adapter's scrollTo() method when the listener fires.
class MyScrollListener(private val recyclerView: RecyclerView): RecyclerView.OnScrollListener(){
var mScrolled = false
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
this.recyclerView.smoothScrollToPosition(newState)
}
}
val scrollListenerCard = MyScrollListener(recyclerView)
Based on Mirza's answer above, I came up with this in kotlin, which works nicely:
#JvmStatic
#InverseBindingAdapter(attribute = "currentPosition", event = "currentPositionAttributeChanged")
fun getCurrentPosition(rv: RecyclerView): Int {
return (rv.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
}
#JvmStatic
#BindingAdapter(value = ["currentPositionAttributeChanged"])
fun setListener(rv: RecyclerView, l: InverseBindingListener) {
val layoutManager = (rv.layoutManager as LinearLayoutManager?)!!
var prevPos = layoutManager.findFirstVisibleItemPosition()
rv.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (dy == 0) {
return
}
val newPos = layoutManager.findFirstVisibleItemPosition()
if (prevPos != newPos) {
prevPos = newPos
l.onChange()
}
}
})
}
#JvmStatic
#BindingAdapter("currentPosition")
fun setCurrentPosition(rv: RecyclerView, pos: Int) {
(rv.layoutManager as LinearLayoutManager).scrollToPosition(pos)
}
I use it like this:
ViewModel has:
val currentPos = MutableLiveData<Int>()
And the layout has:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
currentPosition="#={viewModel.currentPos}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager">
Related
I have this weird bug where my RecyclerView scrolls back to top position whenever I start dragging an item in it. It's inside ViewPager if that has any difference. You can see the behavior in .gif attached.
EDIT:
It seems that RecyclerView view scrolls to top when notifyItemMoved is called and it scrolls just as much for the first view to be at least partially displayed on screen.
View
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/accounts_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="none"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="#layout/view_account_list_item" />
Adapter
class AccountListAdapter(
private val onAccountClickListener: OnAccountClickListener) :
ListAdapter<Account, AccountListAdapter.ViewHolder>(
AccountDiffCallback()
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return ViewHolder(
inflater.inflate(
R.layout.view_account_list_item,
parent,
false
)
)
}
override fun getItemId(position: Int): Long {
return getItem(position).accountId.toLong()
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position), onAccountClickListener)
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), OnItemDragged {
fun bind(account: Account, onAccountClickListener: OnAccountClickListener) {
itemView.account_name.text = account.name
itemView.setOnClickListener {
onAccountClickListener.onAccountClick(account)
}
}
override fun onItemSelected() {
itemView.setBackgroundColor(
ContextCompat.getColor(
itemView.context,
R.color.background_contrast
)
)
}
override fun onItemClear() {
itemView.setBackgroundColor(
ContextCompat.getColor(
itemView.context,
R.color.background
)
)
}
}
class AccountDiffCallback : DiffUtil.ItemCallback<Account>() {
override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean {
return oldItem.accountId == newItem.accountId
}
override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean {
return (oldItem.balance == newItem.balance
&& oldItem.annualReturn == newItem.annualReturn
&& oldItem.name == newItem.name)
}
}
interface OnAccountClickListener {
fun onAccountClick(account: Account)
}
interface OnItemDragged {
fun onItemSelected()
fun onItemClear()
}}
ItemTouchHelper
private fun setupListAdapter() {
accountListAdapter = AccountListAdapter(this)
accountListAdapter.setHasStableIds(true)
accounts_recycler_view.adapter = accountListAdapter
accounts_recycler_view.addItemDecoration(
DividerItemDecoration(
requireContext(),
DividerItemDecoration.VERTICAL
)
)
val accountTouchHelper = ItemTouchHelper(
object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
0
) {
override fun onSelectedChanged(
viewHolder: RecyclerView.ViewHolder?,
actionState: Int
) {
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
val accountViewHolder = viewHolder as AccountListAdapter.ViewHolder
accountViewHolder.onItemSelected()
}
super.onSelectedChanged(viewHolder, actionState)
}
override fun clearView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) {
val accountViewHolder = viewHolder as AccountListAdapter.ViewHolder
accountViewHolder.onItemClear()
super.clearView(recyclerView, viewHolder)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val fromPos: Int = viewHolder.adapterPosition
val toPos: Int = target.adapterPosition
Collections.swap(_accountList, fromPos, toPos)
accountListAdapter.notifyItemMoved(fromPos, toPos)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
})
accountTouchHelper.attachToRecyclerView(accounts_recycler_view)
}
So the issues was that my Recyclerview was inside ViewPager which was inside a ConstraintLayout . View pager was constrained vertically with height set to 0dp but width was set to match_parent. All I needed was to constrain it horizontally with width set to 0dp and setHasFixedSize = true to RecyclerView
When calling notifyItemMoved for adapter, if RecyclerView is flexible, all items are redrawn and by default it focuses on the first item.
If you are using NestedScrollView just remove and instead of using other layouts use ConstraintLayout
Steps:
Use ConstrainLayout and RecylerView like this:
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvDraggable"
android:layout_width="match_parent"
android:layout_height="#dimen/dp_0"
android:layout_marginStart="#dimen/dp_16"
android:layout_marginEnd="#dimen/dp_16"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="none"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Note- you can also try to use:
viewBinding.recycler.setHasFixedSize(true)
If you are using notifyDataSetChanged() so just replace it by notifyItemMoved() in onMove() method
Using below code to check that whether RecyclerView reached to bottom or not..
Means Checking that last item of the Recyclerview is visible or not..
For that I have googled and added Scroll listener in Recyclerview.
Using below code:
MyRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (!MyRecyclerView.canScrollVertically(1)) {
Toast.makeText(mContext, "Last", Toast.LENGTH_LONG).show();
}
}
})
Here, I am trying to check that Recyclerview reached to bottom or not.
Toast is not displaying when I scroll recyclerview to the bottom.
But, Toast displayed suddenly when items in the recyclerview binds first time. It should toast only when user scroll up to the bottom of the Recyclerview.
What might be the issue ?
Please guide. Thanks.
I think, using RecyclerView.LayoutManager api will better suit your needs.
You can check LinearLayoutManager::findLastCompletelyVisibleItemPosition or findLastVisibleItemPosition method in your RecyclerView.OnScrollListener
rvPlayers?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (dy > 0) {
if (isLastVisable()) {
//code here
}
}
}
})
private fun isLastVisable(): Boolean {
val layoutManager = rvPlayers.layoutManager as LinearLayoutManager
val pos = layoutManager.findLastCompletelyVisibleItemPosition()
val numItems = adapter.itemCount
return (pos >= numItems - 1)
}
image of two listviews
I am trying to build this image that I gave the link. Second listview to the right of the first listview. Both listview items are clickable separately. Along the y-axis, second listview's items must be in between first listview's items. Both of them should scroll if user scrolls one of them. I have some solutions.
First one is to control two listview in one touch.
Another solution approach is using one list view. But it has problems of touch events for the second listview. It seems first approach is right but I don't know how to connect two listviews. Thanks in advance.
I thing it can be achieved using OnScrollListener, essentially you can scroll other list/recyclerview on scroll of it's sibling view.
Here is snippet, assuming you use Recycleview
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val left = findViewById<RecyclerView>(R.id.left)
val right = findViewById<RecyclerView>(R.id.right)
left.adapter = Adapter()
right.adapter = Adapter()
val switcher = FocusSwitchListener(left, right)
left.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (switcher.isLeftLastFocused) {
right.stopScroll()
right.scrollBy(dx, dy)
}
}
})
right.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (switcher.isRightLastFocused) {
left.stopScroll()
left.scrollBy(dx, dy)
}
}
})
}
}
FocusSwitchListener
class FocusSwitchListener(left: View, right: View) {
var isLeftLastFocused: Boolean = false
private set
var isRightLastFocused: Boolean = false
private set
init {
left.setOnTouchListener { _, event ->
if (event?.action == MotionEvent.ACTION_DOWN) {
isLeftLastFocused = true
isRightLastFocused = false
}
false
}
right.setOnTouchListener { _, event ->
if (event?.action == MotionEvent.ACTION_DOWN) {
isRightLastFocused = true
isLeftLastFocused = false
}
false
}
}
}
and activity xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="0dp"
android:id="#+id/left"
android:layout_weight="0.5"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_height="match_parent"/>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="0dp"
android:id="#+id/right"
android:layout_weight="0.5"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_height="match_parent"/>
</LinearLayout>
I am trying to add an infinite scroll to my Android Application but the onScrolled method doesnt work correctly I think so.
It will be called only once if I call the addOnScrollListener. But I think it should be called every time the RecyclerView has been scrolled.
linearLayoutManager = LinearLayoutManager(this)
recyclerViewNeuheiten.layoutManager = linearLayoutManager//LinearLayoutManager(this)
recyclerViewHistory.layoutManager = LinearLayoutManager(this)
recyclerViewBestSeller.layoutManager = LinearLayoutManager(this)
recyclerViewFavorites.layoutManager = LinearLayoutManager(this)
recyclerViewNeuheiten.adapter = neuheitenAdapter
recyclerViewHistory.adapter = historyAdapter
recyclerViewBestSeller.adapter = bestsellerAdapter
recyclerViewFavorites.adapter = favoriteAdapter
//setRecyclerViewScrollListener()
setRecyclerViewScrollListener()
private fun setRecyclerViewScrollListener() {
Log.v("scroll", "set listener")
recyclerViewNeuheiten.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
Log.v("scroll", "onScrollStateChanged newState $newState")
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
Log.v("scroll", "onScrolled !!!!!")
super.onScrolled(recyclerView, dx, dy)
val currentItem = recyclerView.layoutManager!!.childCount
val totalItemCount = recyclerView.layoutManager!!.itemCount
Log.v("scroll", "currentItem $currentItem")
Log.v("scroll", "totalItemCount $totalItemCount")
Log.v("scroll", "lastVisibleItemPosition $lastVisibleItemPosition")
Log.v("scroll", "scroll out items ${linearLayoutManager.findFirstVisibleItemPosition()}")
}
})
}
I found the problem. I am using my RecyclerView inside a NestedScrollView. The onScrolled method triggers each scroll without a ScrollView.
I am using the new ExtendedFloatingActionButton from the Material Components for Android library 1.1.0-alpha06. It is rendered just fine but the 'extend' and 'shrink' methods are not doing anything.
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="#+id/extended_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_anchor="#id/bottom_sheet"
android:text="Drag map to change location"
app:icon="#drawable/my_location"
app:backgroundTint="#color/white"
app:iconTint="#color/quantum_googblueA200"
android:textColor="#color/quantum_googblueA200"
app:iconSize="18dp"
style="#style/Widget.MaterialComponents.ExtendedFloatingActionButton"
android:padding="4dp"
android:textSize="12sp"
android:textAllCaps="false"
android:layout_margin="8dp"
app:layout_anchorGravity="right|top"/>
Here's the rendered layout:
Here is a Kotlin version which matches the behavior of the built-in Contacts app. The FAB is extended whenever the RecyclerView is at the top, and the FAB shrinks whenever the user scrolls away from the top.
class FabExtendingOnScrollListener(
private val floatingActionButton: ExtendedFloatingActionButton
) : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState == RecyclerView.SCROLL_STATE_IDLE
&& !floatingActionButton.isExtended
&& recyclerView.computeVerticalScrollOffset() == 0
) {
floatingActionButton.extend()
}
super.onScrollStateChanged(recyclerView, newState)
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy != 0 && floatingActionButton.isExtended) {
floatingActionButton.shrink()
}
super.onScrolled(recyclerView, dx, dy)
}
}
Usage:
recyclerView.addOnScrollListener(FabExtendingOnScrollListener(fab))
I have done this in my code.
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
extendedFloatingActionButton.extend();
}
super.onScrollStateChanged(recyclerView, newState);
}
#Override
public void onScrolled(#NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy > 0 || dy < 0 && extendedFloatingActionButton.isExtended()) {
extendedFloatingActionButton.shrink();
}
}
});
For Kotlin users:
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (dy > 0 && extendedFab.isExtended) {
extendedFab.shrink()
} else if (dy < 0 && !extendedFab.isExtended) {
extendedFab.extend()
}
}
})