Flinging with Recyclerview + Appbarlayout

RecyclerView fling causes laggy while AppBarLayout is scrolling

I have found a solution (credits to yangchong: https://developpaper.com/coordinator-layout-sliding-jitter-problem/):

AppBarBehavior:

/**
* <pre>
* @author yangchong
* blog : https://github.com/yangchong211
* time : 2019/03/13
* desc: Custom Behavior
* Revision: Solving Some Problems of AppbarLayout
* 1) Fast sliding appbarLayout will rebound
* 2) Fast sliding appbarLayout to fold state, immediately sliding down, there will be the problem of jitter.
* 3) Slide appbarLayout, unable to stop sliding by pressing it with your finger
*/
public class AppBarLayoutBehavior extends AppBarLayout.Behavior {

private static final String TAG = "AppbarLayoutBehavior";
private static final int TYPE_FLING = 1;
private boolean isFlinging;
private boolean shouldBlockNestedScroll;

public AppBarLayoutBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
LogUtil.d(TAG, "onInterceptTouchEvent:" + child.getTotalScrollRange());
shouldBlockNestedScroll = isFlinging;
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// Stop fling when your finger touches the screen
stopAppbarLayoutFling(child);
break;
default:
break;
}
return super.onInterceptTouchEvent(parent, child, ev);
}

/**
* Reflect to get private flingRunnable attributes, considering the problem of variable name modification after support 28
* @return Field
* @throws NoSuchFieldException
*/
private Field getFlingRunnableField() throws NoSuchFieldException {
Class<?> superclass = this.getClass().getSuperclass();
try {
// Support design 27 and the following version
Class<?> headerBehaviorType = null;
if (superclass != null) {
headerBehaviorType = superclass.getSuperclass();
}
if (headerBehaviorType != null) {
return headerBehaviorType.getDeclaredField("mFlingRunnable");
}else {
return null;
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
// Possibly 28 or more versions
Class<?> headerBehaviorType = superclass.getSuperclass().getSuperclass();
if (headerBehaviorType != null) {
return headerBehaviorType.getDeclaredField("flingRunnable");
} else {
return null;
}
}
}

/**
* Reflect to get private scroller attributes, considering the problem of variable name modification after support 28
* @return Field
* @throws NoSuchFieldException
*/
private Field getScrollerField() throws NoSuchFieldException {
Class<?> superclass = this.getClass().getSuperclass();
try {
// Support design 27 and the following version
Class<?> headerBehaviorType = null;
if (superclass != null) {
headerBehaviorType = superclass.getSuperclass();
}
if (headerBehaviorType != null) {
return headerBehaviorType.getDeclaredField("mScroller");
}else {
return null;
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
// Possibly 28 or more versions
Class<?> headerBehaviorType = superclass.getSuperclass().getSuperclass();
if (headerBehaviorType != null) {
return headerBehaviorType.getDeclaredField("scroller");
}else {
return null;
}
}
}

/**
* Stop appbarLayout's fling event
* @param appBarLayout
*/
private void stopAppbarLayoutFling(AppBarLayout appBarLayout) {
// Get the flingRunnable variable in HeaderBehavior by reflection
try {
Field flingRunnableField = getFlingRunnableField();
Field scrollerField = getScrollerField();
if (flingRunnableField != null) {
flingRunnableField.setAccessible(true);
}
if (scrollerField != null) {
scrollerField.setAccessible(true);
}
Runnable flingRunnable = null;
if (flingRunnableField != null) {
flingRunnable = (Runnable) flingRunnableField.get(this);
}
OverScroller overScroller = (OverScroller) scrollerField.get(this);
if (flingRunnable != null) {
LogUtil.d (TAG,'Flying Runnable');
appBarLayout.removeCallbacks(flingRunnable);
flingRunnableField.set(this, null);
}
if (overScroller != null && !overScroller.isFinished()) {
overScroller.abortAnimation();
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}

@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
View directTargetChild, View target,
int nestedScrollAxes, int type) {
LogUtil.d(TAG, "onStartNestedScroll");
stopAppbarLayoutFling(child);
return super.onStartNestedScroll(parent, child, directTargetChild, target,
nestedScrollAxes, type);
}

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout,
AppBarLayout child, View target,
int dx, int dy, int[] consumed, int type) {
LogUtil.d(TAG, "onNestedPreScroll:" + child.getTotalScrollRange()
+ " ,dx:" + dx + " ,dy:" + dy + " ,type:" + type);
// When type returns to 1, it indicates that the current target is in a non-touch sliding.
// The bug is caused by the sliding of the NestedScrolling Child2 interface in Coordinator Layout when the AppBar is sliding
// The subclass has not ended its own fling
// So here we listen for non-touch sliding of subclasses, and then block the sliding event to AppBarLayout
if (type == TYPE_FLING) {
isFlinging = true;
}
if (!shouldBlockNestedScroll) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
}
}

@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dxConsumed, int dyConsumed, int
dxUnconsumed, int dyUnconsumed, int type) {
LogUtil.d(TAG, "onNestedScroll: target:" + target.getClass() + " ,"
+ child.getTotalScrollRange() + " ,dxConsumed:"
+ dxConsumed + " ,dyConsumed:" + dyConsumed + " " + ",type:" + type);
if (!shouldBlockNestedScroll) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed, type);
}
}

@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl,
View target, int type) {
LogUtil.d(TAG, "onStopNestedScroll");
super.onStopNestedScroll(coordinatorLayout, abl, target, type);
isFlinging = false;
shouldBlockNestedScroll = false;
}

private static class LogUtil{
static void d(String tag, String string){
Log.d(tag,string);
}
}

}

Flinging with RecyclerView + AppBarLayout

The answer of Kirill Boyarshinov was almost correct.

The main problem is that the RecyclerView sometimes is giving incorrect fling direction, so if you add the following code to his answer it works correctly:

public final class FlingBehavior extends AppBarLayout.Behavior {
private static final int TOP_CHILD_FLING_THRESHOLD = 3;
private boolean isPositive;

public FlingBehavior() {
}

public FlingBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
if (velocityY > 0 && !isPositive || velocityY < 0 && isPositive) {
velocityY = velocityY * -1;
}
if (target instanceof RecyclerView && velocityY < 0) {
final RecyclerView recyclerView = (RecyclerView) target;
final View firstChild = recyclerView.getChildAt(0);
final int childAdapterPosition = recyclerView.getChildAdapterPosition(firstChild);
consumed = childAdapterPosition > TOP_CHILD_FLING_THRESHOLD;
}
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
isPositive = dy > 0;
}
}

I hope that this helps.

fling with AppBarLayout and ViewPager(recycler view)

This is only happening when AppBar is scrolled/flung while the NestedScrollView(or RecyclerView) has not yet finish flinging.

Solution: Extend AppBar's default Behavior and block the call for AppBar.Behavior's onNestedPreScroll() and onNestedScroll() when AppBar is touched while NestedScroll hasn't stopped yet.

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) {
if (type == TYPE_FLING) {
isFlinging = true;
}
if (!shouldBlockNestedScroll) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
}
}

@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
if (!shouldBlockNestedScroll) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
}
}

then use it on the layout:

<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
...
app:layout_behavior="com.mypackage.NoBounceBehavior"/>

Reference for full code:
https://gist.github.com/ampatron/9d56ea401094f67196f407f82f14551a

Expand appbarlayout when recyclerview is scrolled/fling to top

You can fully expand or collapse the App Bar with the setExpanded() method. One implementation could involve overriding dispatchTouchEvent() in your Activity class, and auto-collapsing/expanding your App Bar based on whether it is collapsed past the halfway point:

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
float per = Math.abs(mAppBarLayout.getY()) / mAppBarLayout.getTotalScrollRange();
boolean setExpanded = (per <= 0.5F);
mAppBarLayout.setExpanded(setExpanded, true);
}
return super.dispatchTouchEvent(event);
}

In respect to automatically scrolling to the last position on a fling, I have put some code on GitHub that shows how to programmatically smooth scroll to a specific location that may help. Calling a scroll to list.size() - 1 on a fling for instance could replicate the behaviour. Parts of this code by the way are adapted from the StylingAndroid and Novoda blogs:

public class RecyclerLayoutManager extends LinearLayoutManager {

private AppBarManager mAppBarManager;
private int visibleHeightForRecyclerView;

public RecyclerLayoutManager(Context context) {
super(context);
}

@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
View firstVisibleChild = recyclerView.getChildAt(0);
final int childHeight = firstVisibleChild.getHeight();
int distanceInPixels = ((findFirstVisibleItemPosition() - position) * childHeight);
if (distanceInPixels == 0) {
distanceInPixels = (int) Math.abs(firstVisibleChild.getY());
}
//Called Once
if (visibleHeightForRecyclerView == 0) {
visibleHeightForRecyclerView = mAppBarManager.getVisibleHeightForRecyclerViewInPx();
}
//Subtract one as adapter position 0 based
final int visibleChildCount = visibleHeightForRecyclerView/childHeight - 1;

if (position <= visibleChildCount) {
//Scroll to the very top and expand the app bar
position = 0;
mAppBarManager.expandAppBar();
} else {
mAppBarManager.collapseAppBar();
}

SmoothScroller smoothScroller = new SmoothScroller(recyclerView.getContext(), Math.abs(distanceInPixels), 1000);
smoothScroller.setTargetPosition(position);
startSmoothScroll(smoothScroller);
}

public void setAppBarManager(AppBarManager appBarManager) {
mAppBarManager = appBarManager;
}

private class SmoothScroller extends LinearSmoothScroller {
private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
private final float distanceInPixels;
private final float duration;

public SmoothScroller(Context context, int distanceInPixels, int duration) {
super(context);
this.distanceInPixels = distanceInPixels;
float millisecondsPerPx = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
this.duration = distanceInPixels < TARGET_SEEK_SCROLL_DISTANCE_PX ?
(int) (Math.abs(distanceInPixels) * millisecondsPerPx) : duration;
}

@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return RecyclerLayoutManager.this
.computeScrollVectorForPosition(targetPosition);
}

@Override
protected int calculateTimeForScrolling(int dx) {
float proportion = (float) dx / distanceInPixels;
return (int) (duration * proportion);
}
}
}

Edit:

AppBarManager in the above code snippet refers to an interface used to communicate with the AppBarLayout in an Activity. Collapse/expand app bar methods do just that, with animations. The final method is used to calculate the number of RecyclerView rows visible on screen:

AppBarManager.java

public interface AppBarManager {

void collapseAppBar();
void expandAppBar();
int getVisibleHeightForRecyclerViewInPx();

}

MainActivity.java

public class MainActivity extends AppCompatActivity implements AppBarManager{

@Override
public void collapseAppBar() {
mAppBarLayout.setExpanded(false, true);
}

@Override
public void expandAppBar() {
mAppBarLayout.setExpanded(true, true);
}

@Override
public int getVisibleHeightForRecyclerViewInPx() {

if (mRecyclerFragment == null) mRecyclerFragment =
(RecyclerFragment) getSupportFragmentManager().findFragmentByTag(RecyclerFragment.TAG);

int windowHeight, appBarHeight, headerViewHeight;
windowHeight = getWindow().getDecorView().getHeight();
appBarHeight = mAppBarLayout.getHeight();
headerViewHeight = mRecyclerFragment.getHeaderView().getHeight();
return windowHeight - (appBarHeight + headerViewHeight);
}

Android CoordinatorLayout with AppbarLayout Double Fling Bug

I have the same problem with RecyclerView

Double Fling Bug

See HeaderViewBehavior.setHeaderTopBottomOffset, it's invoked with different directions for newOffset parameter.
You should cancel AppBarLayout.Behavior fling when onStartNestedScroll invoked.

How to fix:

Override AppBarLayout.Behavior

package com.google.android.material.appbar // original package

import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.OverScroller
import androidx.coordinatorlayout.widget.CoordinatorLayout

class FixFlingBehavior @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null
) : AppBarLayout.Behavior(context, attributeSet) {

private var isAppbarFlinging: Boolean = false
private var originalScroller: OverScroller? = null
private val fakeScroller: OverScroller = object : OverScroller(context) {
override fun computeScrollOffset(): Boolean {
scroller = originalScroller
return false // it cancels HeaderBehavior FlingRunnable
}
}

override fun setHeaderTopBottomOffset(coordinatorLayout: CoordinatorLayout, appBarLayout: AppBarLayout, newOffset: Int, minOffset: Int, maxOffset: Int): Int {
isAppbarFlinging = minOffset == Int.MIN_VALUE && maxOffset == Int.MAX_VALUE
if (scroller === fakeScroller) {
scroller = originalScroller
}
return super.setHeaderTopBottomOffset(coordinatorLayout, appBarLayout, newOffset, minOffset, maxOffset)
}

override fun onFlingFinished(parent: CoordinatorLayout, layout: AppBarLayout) {
super.onFlingFinished(parent, layout)
isAppbarFlinging = false
}

override fun onStartNestedScroll(parent: CoordinatorLayout, child: AppBarLayout, directTargetChild: View, target: View, nestedScrollAxes: Int, type: Int): Boolean {
if (isAppbarFlinging && scroller !== fakeScroller) {
originalScroller = scroller
scroller = fakeScroller
}
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type)
}
}

And use it in your layout

...
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.appbar.FixFlingBehavior">
...

Intercepting RecyclerView downwards fling in NestedScrollView

Thanks @Henry, I have successfully recreate the behaviour from the article you mentioned by @AlexLockwood in kotlin. I've dealing with this problem for two days, the other answers usually suggest using WRAP_CONTENT attribute and basically defeat the purpose of RecyclerView as the View is not being recycled anymore. I'm providing the code below just in case this may help someone in the future.

class NestedScrollLayout(
context: Context,
attrs: AttributeSet?
) : NestedScrollView(context, attrs),
NestedScrollingParent3 {

private var parentHelper = NestedScrollingParentHelper(this)

override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
return (axes and ViewCompat.SCROLL_AXIS_VERTICAL) != 0
}

override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
parentHelper.onNestedScrollAccepted(child, target, axes)
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type)
}

override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
if(target is RecyclerView) {
if ((dy < 0 && isRvScrolledToTop(target)) || (dy > 0 && !isNsvScrolledToBottom(this))) {
scrollBy(0, dy)
consumed[1] = dy
return
}
}
dispatchNestedPreScroll(dx, dy, consumed, null, type)
}

override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int
) {
val oldScrollY = scrollY
scrollBy(0, dyUnconsumed)
val mConsumed = scrollY - oldScrollY
val mUnconsumed = dyUnconsumed - mConsumed
dispatchNestedScroll(0, mConsumed, 0, mUnconsumed, null, type)
}

override fun onStopNestedScroll(target: View, type: Int) {
parentHelper.onStopNestedScroll(target, type)
stopNestedScroll(type)
}

override fun onStartNestedScroll(child: View, target: View, axes: Int): Boolean {
Log.println(Log.ASSERT, "NestedScrollLayout:onStartNestedScroll", "Requested")
return onStartNestedScroll(child, target, axes, ViewCompat.TYPE_TOUCH)
}

override fun onNestedScrollAccepted(child: View, target: View, axes: Int) {
onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH)
}

override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH)
}

override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int
) {
onNestedScroll(
target,
dxConsumed,
dyConsumed,
dxUnconsumed,
dyUnconsumed,
ViewCompat.TYPE_TOUCH
)
}

override fun onStopNestedScroll(target: View) {
onStopNestedScroll(target, ViewCompat.TYPE_TOUCH)
}

override fun getNestedScrollAxes(): Int {
return parentHelper.nestedScrollAxes
}

companion object {
private fun isNsvScrolledToBottom(nsv: NestedScrollView): Boolean {
return !nsv.canScrollVertically(1)
}

private fun isRvScrolledToTop(rv: RecyclerView): Boolean {
rv.layoutManager?.let { lm ->
return when (lm) {
is LinearLayoutManager -> {
lm.findViewByPosition(0)?.top == 0 && lm.findFirstVisibleItemPosition() == 0
}
is GridLayoutManager -> {
lm.findViewByPosition(0)?.top == 0 && lm.findFirstVisibleItemPosition() == 0
}
is StaggeredGridLayoutManager -> {
lm.findViewByPosition(0)?.top == 0 && lm.findFirstVisibleItemPositions(
intArrayOf(0)
)[0] == 0
}
else -> lm.findViewByPosition(0)?.top == 0
}
}
return false
}
}
}

Then we can use NestedScrollLayout as follows:

<com.organization.appname.NestedScrollLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

...

<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="400dp" />

...

</com.organization.appname.NestedScrollLayout>

RecyclerView scrolling glitches when disabling AppBarLayout in a CoordinatorLayout?

What is happening here?!?

Setting isNestedScrollingEnabled to false, in fact, breaks the communication between your itemRecyclerView as scrolling child and AppBarLayout as it's parent. The normal behaviour is itemRecyclerView notifies it's parent AppBarLayout it's scrolling progress for which the parent is supposed to react to it by calculating it's collapsed height given any scrolling progress and all other stuffs.

I found somewhere that setting isNestedScrollingEnabled to false would cause the RecyclerView to not recycle its views. I can't say exactly if it's true but if it is then, I think it's cause for that glitch.

The solution that I would like to propose is to change the scroll_flags programmatically to NO_SCROLL so that AppBarLayout wouldn't react/scroll in scrolling of its child scrolling view.

Although, it's in java but following code snippet should help you.

            // get a reference for your constraint layout
ConstraintLayout constraintLayout = findViewById(R.id.search_constraint);
// get the layout params object to change the scroll flags programmatically
final AppBarLayout.LayoutParams layoutParams = (AppBarLayout.LayoutParams) constraintLayout.getLayoutParams();

// flags variable to switch between
final int noScrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL;
final int defaultScrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
| AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED
| AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP;

// now we will set appropriate scroll flags according to the focus
searchField.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
layoutParams.setScrollFlags(hasFocus ? noScrollFlags : defaultScrollFlags);
}
});


Related Topics



Leave a reply



Submit