How to calculate speed of recyclerView inside NestedScrollView in Android - android

I have recyclerView inside NestedScrollView. I want to calculate speed to recyclerview but it is not working inside nestedScrollView.
This is code to calculate scroll speed which is working without NestedScrollView. I want to make it work with NestedScrollView also.
I have set nested scrolling false but its not working
recyclerView.setNestedScrollingEnabled(false)
Class:
abstract class ScrollSpeedRecycleViewScrollListener(private val maxScrollSpeedForAdInjection: Int) :
RecyclerView.OnScrollListener() {
var currentScrollSpeed: Int = 0
private var previousFirstVisibleItem = 0
private var previousEventTime: Long = 0
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager
if (layoutManager is LinearLayoutManager) {
val firstItemPosition = layoutManager.findFirstVisibleItemPosition()
if (previousFirstVisibleItem != firstItemPosition) {
}
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> {
if (currentScrollSpeed > maxScrollSpeedForAdInjection) {
listNeedsRefresh()
}
currentScrollSpeed = 0
}
}
}
}

The concept of speed involves distance covered over a certain period of time. You can get the values scrolled (distance) using the dx or dy in the onScrolled method. To get time you would need to get some timestamps at the point where you're getting the distance values and then use a calculation of speed = distance / time. But your also going to need calculate the difference between calls of onScrolled. so it'll actually end up as
speed = (distance2 - distance1) / (time2 - time1)
The problem you're going to have here is that the method will be getting called loads when the user is scrolling fast, and the calculation will need to be done each time and that will have a detrimental effect on the smoothness of the scroll.
I'm willing to bet there is a better way to overcome your problem.

Related

Set item opacity in RecyclerView depending if item is in center

I want to display items in a horizontal list using RecyclerView. At a time, only 3 items will be displayed. 1 in the middle and the other 2 on the side, below is an image of what I'm trying to achieve:
I'm using LinearSnapHelper which centers an item all of the time. When an item is moved away from the center I would like the opacity to progessively change from 1f to 0.5f.
Here is the below code which I've written to help:
class CustomRecyclerView(context: Context, attrs: AttributeSet) : RecyclerView(context, attrs) {
private var itemBoundsRect: Rect? = null
init {
itemBoundsRect = Rect()
addOnScrollListener(object : OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
calculateVisibility()
}
})
}
private fun calculateVisibility() {
val linearLayoutManger: LinearLayoutManager = layoutManager as LinearLayoutManager
val firstVisibleItem = linearLayoutManger.findFirstVisibleItemPosition()
val lastVisibleItem = linearLayoutManger.findLastVisibleItemPosition()
var indexes: MutableList<Int> = mutableListOf()
for (i in firstVisibleItem..lastVisibleItem) {
indexes.add(i)
val item: View = layoutManager?.findViewByPosition(i) ?: continue
item.getGlobalVisibleRect(itemBoundsRect)
var itemSize = layoutManager!!.findViewByPosition(i)!!.width
var visibleSize = 0
if (indexes.size == 1) {
visibleSize = itemBoundsRect!!.right
} else {
visibleSize = itemBoundsRect!!.right - itemBoundsRect!!.left
}
var visibilty = visibleSize * 100 / itemSize
if (visibilty > 0) {
visibilty = 100 - visibilty
}
val viewHolder = findViewHolderForLayoutPosition(i)
viewHolder!!.itemView.alpha = (100 - visibilty).toFloat() / 100f
}
}
}
It doesn't work as expected as the opacity changes at the wrong time. The image below demonstrates this better. I expect the opacity to progressively begin to change when the item edges come out of the red box. However, it only starts when the item reaches the yellow edges.
Is there a way to achieve this effect?
Thank you :)
Your code for calculateVisibility() is looking at global position when looking at the relative position within the RecyclerView is sufficient. Maybe there is more to the code than you posted, but try the following. This code looks at the x position of each visible view and calculates the alpha value as a function of displacement from the center of the RecyclerView. Comments are in the code.
private fun calculateVisibility(recycler: RecyclerView) {
val midRecycler = recycler.width / 2
val linearLayoutManger: LinearLayoutManager = recycler.layoutManager as LinearLayoutManager
val firstVisibleItem = linearLayoutManger.findFirstVisibleItemPosition()
val lastVisibleItem = linearLayoutManger.findLastVisibleItemPosition()
for (i in firstVisibleItem..lastVisibleItem) {
val viewHolder = recycler.findViewHolderForLayoutPosition(i)
viewHolder?.itemView?.apply {
// This is the end of the view in the parent's coordinates
val viewEnd = x + width
// This is the maximum pixels the view can slide left or right until it disappears.
val maxSlide = (midRecycler + width / 2).toFloat()
// Alpha is determined by the percentage of the maximum slide the view has moved.
// This assumes a linear fade but can be adjusted to fade in alternate ways.
alpha = 1f - abs(maxSlide - viewEnd) / maxSlide
Log.d("Applog", String.format("pos=%d alpha=%f", i, alpha))
}
}
}
The foregoing assumes that sizes remain constant.
if you need the center View, you can call
View view = snapHelper.findSnapView(layoutManagaer);
once you have the View, you should be able to get the position on the dataset for that View. For instance using
int pos = adapter.getChildAdapterPosition(view)
And then you can update the center View opacity and invoke
adapter.notifyItemChanged(pos);

How to get the current height of a BottomSheetDialogFragment programmatically? Or are there any default values for the different states?

I want to change a view size based on the current size of the BottomSheetDialogFragment programmatically, but I don't know how to get or calculate it. I need this information in the onStateChanged callback.
On sliding of BottomSheetDialogFragment you can get the height by a little calculation using locationOnScreen of the BottomSheetDialogFragment.
view.getLocationOnScreen(int[] outLocation): Computes the coordinates of this view on
the screen. The argument must be an array of two integers. After the
method returns, the array contains the x and y location in that order.
bottomSheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
//your code here
} else if (newState == BottomSheetBehavior.STATE_EXPANDED) {
//your code here
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
var locationOnScreen: IntArray = intArrayOf(0, 0)
bottomSheet.getLocationOnScreen(locationOnScreen)
// coordinates of bottomsheet when sliding up/down
val (x, y) = locationOnScreen
}
})

Android ScrollView inside RecyclerView.ViewHolder: prevent ItemTouchHelper from getting touch event?

Setup: I have a RecyclerView with a vertical LinearLayoutManager. Each ViewHolder has 2 TextViews (green and red in the picture), which can be very long. RED can be scrolled horizontally thanks to a HorizontalScrollView. GREEN can be scrolled vertically thanks to a ScrollView.
Now I have implemented ItemTouchHelper, to swipe LEFT, UP or DOWN. The problem is that my ScrollViews don't work anymore. Instead, even when I swipe GREEN UP or DOWN, it is my ViewHolder that moves. How do I prevent my ItemTouchHelper from getting the touch event?
I've tried:
-android:descendantFocusability="blocksDescendants"
-NestedScrollView instead of ScrollView
-implementing setOnTouchListener inside my adapter to return true (the listener is triggered, but the event is not consumed)
-thought about using RecyclerView.findChildViewUnder inside onChildDraw to scroll manually and block the super method but I can't find a way to get the coordinates
Nothing works so far. How do I do this? Here is my ItemTouchHelper:
private fun enableSwipe() {
val simpleItemTouchCallback =
object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
if (direction == ItemTouchHelper.LEFT) {
val content = mAdapter.getContentForPosition(position)
//do stuff...
}
}
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
val mdX = if (abs(dY) > abs(dX)) {
0f
}else{
dX
}
val mdY = if (abs(dY) > abs(dX)) {
when{
dY > 0 -> if (dY > 500f) 500f else dY
else -> if (dY < -500f) -500f else dY
}
}else{
0f
}
//override viewHolder in case we swipe up or down on the other content
val mViewHolder: RecyclerView.ViewHolder = if (abs(dY) > abs(dX)) {
if (dY < 0) {
//swiping up
getViewHolder(1) ?: viewHolder
} else {
getViewHolder(0) ?: viewHolder
}
}else {
viewHolder
}
super.onChildDraw(c, recyclerView, mViewHolder, mdX, mdY, actionState, isCurrentlyActive)
}
}
val itemTouchHelper = ItemTouchHelper(simpleItemTouchCallback)
itemTouchHelper.attachToRecyclerView(recyclerViewClash)
}
Here is the only thing that works, finally found it. It is important to understand that the ItemTouchHelper will implement onItemTouchListener:
An OnItemTouchListener allows the application to intercept touch events in progress at the view hierarchy level of the RecyclerView before those touch events are considered for RecyclerView's own scrolling behavior.
textView.setOnTouchListener { v, event ->
//let the scrollView handle the scroll event
scrollView.onTouchEvent(event)
//this is the key feature
v.parent.requestDisallowInterceptTouchEvent(true)
//return true to consume the event (mandatory)
true
}
Nothing else is needed. Also, a ScrollView is enough, no need for a NestedScrollView.

Recyclerview withy LinearSnapHelper - snap to next item when release finger from the screen

I have followed this Stack Overflow post to create a recyclerview with viewpager behavior.
Works fine but I need to trigger the scroll to next item when user lift the finger of the screen. Right now only snap to next item when it's 50% visible.
UPDATE WITH SOLUTION
The Crysxd response was the key. To detect the snap direction I added this code at the top of the findTargetSnapPosition function:
if (velocityY < 0)
snapToPrevious = true
else
snapToNext = true
There's no way around a custom SnapHelper. Here is a class you can copy to your project.
The solution
class ControllableSnapHelper(val onSnapped: ((Int) -> Unit)? = null) : LinearSnapHelper() {
var snappedPosition = 0
private var snapToNext = false
private var snapToPrevious = false
var recyclerView: RecyclerView? = null
override fun attachToRecyclerView(recyclerView: RecyclerView?) {
super.attachToRecyclerView(recyclerView)
this.recyclerView = recyclerView
}
override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager, velocityX: Int, velocityY: Int): Int {
if (snapToNext) {
snapToNext = false
snappedPosition = Math.min(recyclerView?.adapter?.itemCount ?: 0, snappedPosition + 1)
} else if (snapToPrevious) {
snapToPrevious = false
snappedPosition = Math.max(0, snappedPosition - 1)
} else {
snappedPosition = super.findTargetSnapPosition(layoutManager, velocityX, velocityY)
}
onSnapped?.invoke(snappedPosition)
return snappedPosition
}
fun snapToNext() {
snapToNext = true
onFling(Int.MAX_VALUE, Int.MAX_VALUE)
}
fun snapToPrevious() {
snapToPrevious = true
onFling(Int.MAX_VALUE, Int.MAX_VALUE)
}
}
You can replace LinearSnapHelper with PagerSnapHelper as well. Simply call snapToNext() or snapToPrevious(). I also implemented a callback which is passed in the constructor.
How it works
We simply overwrite the findTargetSnapPosition(...) method. This method can return any position and the SnapHelper implementation will then compute the scroll animation to get there. The functions snapToNext() and snapToPrevious() simply set a boolean which tells us to snap to the next or previous view the next time findTargetSnapPosition(...) is called. It would also be very simple to add a snapTo(index: Int) function. After setting the boolean, we need to start the snap progress. To do so, we call onFling(...) which would be called when the user moved the RecyclerView. We need to pass in a value which exceeds the minimum velocity set by SnapHelper, so we just use Int.MAX_VALUE. As we never call super.findTargetSnapPosition(...), the actual velocity used by PagerSnapHelper doesn't matter.

RecyclerView.Adapter.notifyItemMoved(0,1) scrolls screen

I have a RecyclerView managed by a LinearlayoutManager, if I swap item 1 with 0 and then call mAdapter.notifyItemMoved(0,1), the moving animation causes the screen to scroll. How can I prevent it?
Sadly the workaround presented by yigit scrolls the RecyclerView to the top. This is the best workaround I found till now:
// figure out the position of the first visible item
int firstPos = manager.findFirstCompletelyVisibleItemPosition();
int offsetTop = 0;
if(firstPos >= 0) {
View firstView = manager.findViewByPosition(firstPos);
offsetTop = manager.getDecoratedTop(firstView) - manager.getTopDecorationHeight(firstView);
}
// apply changes
adapter.notify...
// reapply the saved position
if(firstPos >= 0) {
manager.scrollToPositionWithOffset(firstPos, offsetTop);
}
Call scrollToPosition(0) after moving items. Unfortunately, i assume, LinearLayoutManager tries to keep first item stable, which moves so it moves the list with it.
Translate #Andreas Wenger's answer to kotlin:
val firstPos = manager.findFirstCompletelyVisibleItemPosition()
var offsetTop = 0
if (firstPos >= 0) {
val firstView = manager.findViewByPosition(firstPos)!!
offsetTop = manager.getDecoratedTop(firstView) - manager.getTopDecorationHeight(firstView)
}
// apply changes
adapter.notify...
if (firstPos >= 0) {
manager.scrollToPositionWithOffset(firstPos, offsetTop)
}
In my case, the view can have a top margin, which also needs to be counted in the offset, otherwise the recyclerview will not scroll to the intended position. To do so, just write:
val topMargin = (firstView.layoutParams as? MarginLayoutParams)?.topMargin ?: 0
offsetTop = manager.getDecoratedTop(firstView) - manager.getTopDecorationHeight(firstView) - topMargin
Even easier if you have ktx dependency in your project:
offsetTop = manager.getDecoratedTop(firstView) - manager.getTopDecorationHeight(firstView) - firstView.marginTop
I've faced the same problem. Nothing of the suggested helped. Each solution fix and breakes different cases.
But this workaround worked for me:
adapter.registerAdapterDataObserver(object: RecyclerView.AdapterDataObserver() {
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
if (fromPosition == 0 || toPosition == 0)
binding.recycler.scrollToPosition(0)
}
})
It helps to prevent scrolling while moving the first item for cases: direct notifyItemMoved and via ItemTouchHelper (drag and drop)
I have faced the same problem. In my case, the scroll happens on the first visible item (not only on the first item in the dataset). And I would like to thanks everybody because their answers help me to solve this problem.
I inspire my solution based on Andreas Wenger' answer and from resoluti0n' answer
And, here is my solution (in Kotlin):
RecyclerViewOnDragFistItemScrollSuppressor.kt
class RecyclerViewOnDragFistItemScrollSuppressor private constructor(
lifecycleOwner: LifecycleOwner,
private val recyclerView: RecyclerView
) : LifecycleObserver {
private val adapterDataObserver = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
suppressScrollIfNeeded(fromPosition, toPosition)
}
}
init {
lifecycleOwner.lifecycle.addObserver(this)
}
#OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun registerAdapterDataObserver() {
recyclerView.adapter?.registerAdapterDataObserver(adapterDataObserver) ?: return
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun unregisterAdapterDataObserver() {
recyclerView.adapter?.unregisterAdapterDataObserver(adapterDataObserver) ?: return
}
private fun suppressScrollIfNeeded(fromPosition: Int, toPosition: Int) {
(recyclerView.layoutManager as LinearLayoutManager).apply {
var scrollPosition = -1
if (isFirstVisibleItem(fromPosition)) {
scrollPosition = fromPosition
} else if (isFirstVisibleItem(toPosition)) {
scrollPosition = toPosition
}
if (scrollPosition == -1) return
scrollToPositionWithCalculatedOffset(scrollPosition)
}
}
companion object {
fun observe(
lifecycleOwner: LifecycleOwner,
recyclerView: RecyclerView
): RecyclerViewOnDragFistItemScrollSuppressor {
return RecyclerViewOnDragFistItemScrollSuppressor(lifecycleOwner, recyclerView)
}
}
}
private fun LinearLayoutManager.isFirstVisibleItem(position: Int): Boolean {
apply {
return position == findFirstVisibleItemPosition()
}
}
private fun LinearLayoutManager.scrollToPositionWithCalculatedOffset(position: Int) {
apply {
val offset = findViewByPosition(position)?.let {
getDecoratedTop(it) - getTopDecorationHeight(it)
} ?: 0
scrollToPositionWithOffset(position, offset)
}
}
and then, you may use it as (e.g. fragment):
RecyclerViewOnDragFistItemScrollSuppressor.observe(
viewLifecycleOwner,
binding.recyclerView
)
LinearLayoutManager has done this for you in LinearLayoutManager.prepareForDrop.
All you need to provide is the moving (old) View and the target (new) View.
layoutManager.prepareForDrop(oldView, targetView, -1, -1)
// the numbers, x and y don't matter to LinearLayoutManager's implementation of prepareForDrop
It's an "unofficial" API because it states in the source
// This method is only intended to be called (and should only ever be called) by
// ItemTouchHelper.
public void prepareForDrop(#NonNull View view, #NonNull View target, int x, int y) {
...
}
But it still works and does exactly what the other answers say, doing all the offset calculations accounting for layout direction for you.
This is actually the same method that is called by LinearLayoutManager when used by an ItemTouchHelper to account for this dreadful bug.

Categories

Resources