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>
Related
I have a list item with three swipe actions which looks like this:
The regular list item and the buttons are two different layouts defined in xml.
To reveal the button actions I use ItemTouchHelper.SimpleCallback. In onChildDraw I tell the item list item's x-axis to be only drawn until it reaches the width of the button controls.
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
val foreground = (viewHolder as? NachrichtViewHolder)?.binding?.nachrichtListItem
val background = (viewHolder as? NachrichtViewHolder)?.binding?.background
val x: Float = when {
dX.absoluteValue > background?.measuredWidth?.toFloat() ?: dX -> background?.measuredWidth?.toFloat()
?.unaryMinus() ?: dX
else -> dX
}
getDefaultUIUtil().onDraw(
c,
recyclerView,
foreground,
x,
dY,
actionState,
isCurrentlyActive
)
}
Here is an abbreviated layout file demonstrating the way I built the ui:
<FrameLayout
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/background"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:clickable="#{backgroundVisible}"
android:focusable="#{backgroundVisible}"
android:focusableInTouchMode="#{backgroundVisible}"
android:elevation="#{backgroundVisible ? 4 : 0}">
<ImageButton
android:id="#+id/actionReply"/>
<ImageButton
android:id="#+id/actionShare"/>
<ImageButton
android:id="#+id/actionDelete"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/nachrichtListItem"
android:elevation="#{backgroundVisible ? 0 : 4}"
android:clickable="#{!backgroundVisible}"
android:focusable="#{!backgroundVisible}"
android:focusableInTouchMode="#{!backgroundVisible}">
<!-- regular list item -->
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
My problem is that the buttons are not clickable.
What I tried so far:
set elevation to bring element on top
set items clickable depending on the visibility state of the buttons
This can be seen in the layout file. I want to define the elements inside xml and not draw them manually if possible.
The problem is solved. ItemTouchHelper.SimpleCallback swallows all your touch events. So you need to register a TouchListener for the buttons. The buttons come in my case from xml. Inspired by this I came up with the following solution:
#SuppressLint("ClickableViewAccessibility")
class NachrichtItemSwipeCallback(private val recyclerView: RecyclerView) :
ItemTouchHelper.SimpleCallback(0, LEFT) {
private val itemTouchHelper: ItemTouchHelper
private var binding: ListItemNachrichtBinding? = null
private var lastSwipedPosition: Int = -1
init {
// Disable animations as they don't work with custom list actions
(this.recyclerView.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
this.recyclerView.setOnTouchListener { _, touchEvent ->
if (lastSwipedPosition < 0) return#setOnTouchListener false
if (touchEvent.action == MotionEvent.ACTION_DOWN) {
val viewHolder =
this.recyclerView.findViewHolderForAdapterPosition(lastSwipedPosition)
val swipedItem: View = viewHolder?.itemView ?: return#setOnTouchListener false
val rect = Rect()
swipedItem.getGlobalVisibleRect(rect)
val point = Point(touchEvent.rawX.toInt(), touchEvent.rawY.toInt())
if (rect.top < point.y && rect.bottom > point.y) {
// Consume touch event directly
val buttons =
binding?.buttonActionBar?.children
.orEmpty()
.filter { it.isClickable }
.toList()
val consumed = consumeTouchEvents(buttons, point.x, point.y)
if (consumed) {
animateClosing(binding?.nachrichtListItem)
}
return#setOnTouchListener false
}
}
return#setOnTouchListener false
}
this.itemTouchHelper = ItemTouchHelper(this)
this.itemTouchHelper.attachToRecyclerView(this.recyclerView)
}
// Only for drag & drop functionality
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
override fun onChildDraw(
canvas: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
binding = (viewHolder as? NachrichtViewHolder)?.binding
val foreground = binding?.nachrichtListItem
val background = binding?.buttonActionBar
val backgroundWidth = background?.measuredWidth?.toFloat()
// only draw until start of action buttons
val x: Float = when {
dX.absoluteValue > backgroundWidth ?: dX -> backgroundWidth?.unaryMinus() ?: dX
else -> dX
}
foreground?.translationX = x
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
this.lastSwipedPosition = viewHolder.adapterPosition
recyclerView.adapter?.notifyItemChanged(this.lastSwipedPosition)
}
private fun animateClosing(
foreground: ConstraintLayout?
) {
foreground ?: return
ObjectAnimator.ofFloat(foreground, "translationX", 0f).apply {
duration = DURATION_ANIMATION
start()
}.doOnEnd { applyUiWorkaround() }
}
// See more at https://stackoverflow.com/a/37342327/3734116
private fun applyUiWorkaround() {
itemTouchHelper.attachToRecyclerView(null)
itemTouchHelper.attachToRecyclerView(recyclerView)
}
private fun consumeTouchEvents(
views: List<View?>,
x: Int,
y: Int
): Boolean {
views.forEach { view: View? ->
val viewRect = Rect()
view?.getGlobalVisibleRect(viewRect)
if (viewRect.contains(x, y)) {
view?.performClick()
return true
}
}
return false
}
companion object {
private const val DURATION_ANIMATION: Long = 250
}
}
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)
}
I am trying to make a recycler view like tumblr app. You can see here: https://streamable.com/s/gpyec/kxvjnz
My question is how to add a video (or any clickable) below recyclerview? I added an item decoration implementation as follows:
class RecyclerViewAdItemDecoration(private val func:() -> Unit) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildLayoutPosition(view)
val mLayoutManager = parent.layoutManager
if (mLayoutManager is GridLayoutManager) {
setGridParams(view, position, parent)
} else if (mLayoutManager is LinearLayoutManager) {
setLinearParams(view, position, parent)
}
}
private fun setGridParams(view: View, position: Int, parent: RecyclerView) {
val p = view.layoutParams as ViewGroup.MarginLayoutParams
if (position == 0) {
p.setMargins(0,0,0, 0)
} else if (position >= 10 && (position % 10 == 0 || position % 11 == 0)) {
p.setMargins(0,0,0, parent.height)
func()
} else {
p.setMargins(0,0,0, 0)
}
}
private fun setLinearParams(view: View, position: Int, parent: RecyclerView) {
val p = view.layoutParams as ViewGroup.MarginLayoutParams
if (position == 0) {
p.setMargins(0,0,0, 0)
} else if (position >= 10 && (position % 10 == 0)) {
p.setMargins(0,0,0, parent.height)
func()
} else {
p.setMargins(0,0,0, 0)
}
}
}
This way I could add enough space for background view but it's not clickable now. I also couldn't find any library for such implementation. Appreciated for any help.
Edit:
To clarify, I want to show background video (or any view) right after every 10th item in recycler view. Like it's seen in video in the link, there is a space between every 10 item in recycler view, which also triggers to play the video in the background (below recycler view)
show background video (or any view) right after every 10th item in
recycler view.
If the background video is after every 10th item, it means there is an item (11th), which is transparent.
What you actually want here is recyclerview with multiple view types.
Use a RelativeLayout for activity_main.xml which allows to place views
on top of others (in Z axis).
ex: RecyclerView is the top most view here.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:focusable="true">
<View
android:id="#+id/ad"
android:background="#color/colorAccent"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_posts"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</RelativeLayout>
Create two item layouts for two types of recycler view types
ex:
item_normal.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="100dp">
<TextView
android:id="#+id/tv_post"
tools:text="Post"
android:background="#android:color/white"
android:textSize="32sp"
android:gravity="center_horizontal|center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
item_transparent.xml (where layout background is transparent that allow to see the view below the surface area)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/transparent"/>
Setting android:clickable=false in item_transparent does not stop triggering the click event on transparent item, so use communication flow using interfaces, to bring the other view(ad) to the front when clicked on transparent item.
MainActivity.kt
class MainActivity : AppCompatActivity(), RvAdpater.OnItemClick {
private lateinit var adView: View
private lateinit var rvPosts: RecyclerView
override fun onClick() {
bringAdFront()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
rvPosts = findViewById(R.id.rv_posts)
rvPosts.layoutManager = LinearLayoutManager(this)
val rvAdpater = RvAdpater()
rvAdpater.setListener(this)
rvPosts.adapter = rvAdpater
}
private fun bringAdFront() {
adView = findViewById<View>(R.id.ad)
adView.bringToFront()
}
override fun onBackPressed() {
// to go back to the normal recycler view when back button is pressed
val parent = rvPosts.parent as ViewGroup
parent.removeAllViews()
parent.addView(adView, 0)
parent.addView(rvPosts, 1)
}
}
RvAdapter.kt
const val TAG = "RvAdpater"
class RvAdpater : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private lateinit var listener:OnItemClick
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val viewNormal = LayoutInflater.from(parent.context).inflate(R.layout.item_normal, parent, false)
val viewTransparent = LayoutInflater.from(parent.context).inflate(R.layout.item_transparent, parent, false)
return when(viewType){
0 -> NormalViewHolder(viewNormal)
2 -> TransparentViewHolder(viewTransparent)
else -> NormalViewHolder(viewNormal)
}
}
override fun getItemCount(): Int = 10
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when(getItemViewType(position)){
0 -> {
val normalHolder = holder as NormalViewHolder
normalHolder.tv.text = "Post"
normalHolder.itemView.setOnClickListener {
Log.d(TAG, "Clicked on Normal item")
}
}
2 -> {
val transparentHolder = holder as TransparentViewHolder
transparentHolder.itemView.setOnClickListener {
listener.onClick()
}
}
}
}
fun setListener(onItem:OnItemClick){
listener = onItem
}
interface OnItemClick{
fun onClick()
}
override fun getItemViewType(position: Int): Int = position % 2 * 2
class NormalViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
val tv:TextView = itemView.findViewById(R.id.tv_post)
}
class TransparentViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)
}
Checkout the repo for an working example
For handling multiple view types, you may use Epoxy library.
I am making a list, that will get more items when I go to the bottom of the list. I want this list to be refreshable too, so I need a SwipeRefreshLayout.
Let's say I have an adapter like this :
class OrdersListAdapter(val selected : (Order) -> Unit) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
fun addOrders(orders: ArrayList<Order>) {
this.orders.addAll(orders)
notifyItemRangeInserted(this.orders.size - orders.size + FIRST_ITEM_INDEX, orders.size)
}
fun setFirstOrders(orders: ArrayList<Order>) {
this.orders = orders
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return (orders.size + 1 + FIRST_ITEM_INDEX)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
TYPE_ORDERS -> OrdersViewHolder(DataBindingUtil.inflate<OrdersItemBinding>(LayoutInflater.from(parent.context),
R.layout.orders_item, parent, false).apply {
viewModel = OrdersViewModel()
})
else -> LoadingMoreOrdersViewHolder(DataBindingUtil.inflate<LoadingMoreOrdersItemBinding>(LayoutInflater.from(parent.context),
R.layout.loading_more_orders_item, parent, false).apply {
viewModel = LoadingMoreOrdersViewModel()
})
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (getItemViewType(position)) {
TYPE_ORDERS -> (holder as? OrdersViewHolder)?.setData(orders[position - FIRST_ITEM_INDEX])
TYPE_LOADING -> (holder as? LoadingMoreOrdersViewHolder)?.setState(loading, error, noMoreItems)
}
}
override fun getItemViewType(position: Int): Int =
if (position == FIRST_ITEM_INDEX - 1) TYPE_HEADER else if (position == itemCount - 1) TYPE_LOADING else TYPE_ORDERS
Well if I put my RecyclerView like this, inside a SwipeRefreshLayout :
<android.support.v4.widget.SwipeRefreshLayout
android:id="#+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/toolbar">
<android.support.v7.widget.RecyclerView
android:id="#+id/orders_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="android.support.v7.widget.LinearLayoutManager" />
</android.support.v4.widget.SwipeRefreshLayout>
This will display all items, but the refreshLayout will not work : If I pull my view, nothing happens.
But if I do not set any loading layout inside my adapter (the one at bottom of the list, to load more items, inflated from R.layout.loading_more_orders_item), then the SwipeRefreshLayout will work properly.
How can I have 2 view types in my adapter, AND set my RecyclerView in a SwipeRefreshLayout ?
EDIT :
Here is my onRefreshListener :
refreshLayout = binding.swipeRefreshLayout
refreshLayout?.setOnRefreshListener { reloadOrders() }
And in reloadOrders() :
private fun reloadOrders() {
viewModel.setLoading()
refreshLayout?.isRefreshing = false
Observable.timer(1, TimeUnit.SECONDS)
.subscribe { getOrders() }
}
I believe that what happens is the recyclerview takes over the ontouch events from swiperefreshlayout. You could try overwriting linearlayout and its canScrollVertically() method to return false. Cheers!
Answering your question, compatibility is not the issue you're having. Not sure of the size of your layout, as its height is 0dp. I'd play around with the view sizes and view hierarchy.
Okay so thanks to #r2rek, I found a solution that worked for me :
In the code, we create a class that extends LinearLayoutManager and overrides method canScrollVertically() to always return false :
inner class MyLinearLayout(context: Context) : LinearLayoutManager(context) {
override fun canScrollVertically(): Boolean {
return (false)
}
}
Then, when we init our RecyclerView, we apply this class to our layoutManager :
ordersRv?.layoutManager = MyLinearLayout(this)
This will allow you to refresh, however you will not be able to scroll in your RecyclerView (duh).
To fix this, add a NestedScrollView in your xml, as following :
<android.support.v4.widget.SwipeRefreshLayout
android:id="#+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:visibility="#{viewModel.ordersVisibility}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/toolbar">
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="#+id/orders_rv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v4.widget.NestedScrollView>
</android.support.v4.widget.SwipeRefreshLayout>
You can now scroll in your RecyclerView, and pull to refresh with your SwipeRefreshLayout.
I think this method is kinda hacky, so if someone have a better solution, feel free to tell us !
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">