How to define a CoordinatorLayout.Behavior class for BottomNavigationView which scrolls synchronized with the RecyclerView veritical scroll.
I have seen this and this, but all it does is just show/hide the NavigationView on veritical scroll events immediately. I don't want to show/hide the NavigationView immediately, instead i want a behavior something like the AppbarLayout having a Toolbar with scroll flag as app:layout_scrollFlags="scroll|enterAlways".
public class BottomNavigationBehavior extends CoordinatorLayout.Behavior<BottomNavigationView> {
public BottomNavigationBehavior() {
super();
}
public BottomNavigationBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, BottomNavigationView child, View dependency) {
boolean dependsOn = dependency instanceof FrameLayout;
return dependsOn;
}
#Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, BottomNavigationView child, View directTargetChild, View target, int nestedScrollAxes) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
#Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, BottomNavigationView child, View target, int dx, int dy, int[] consumed) {
if(dy < 0) {
showBottomNavigationView(child);
}
else if(dy > 0) {
hideBottomNavigationView(child);
}
}
private void hideBottomNavigationView(BottomNavigationView view) {
view.animate().translationY(view.getHeight());
}
private void showBottomNavigationView(BottomNavigationView view) {
view.animate().translationY(0);
}
}
After some trying I came up with this solution:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if(dy > 0 && visible){
mBinding.bnv.test.setY(mBinding.bnv.getY() + dy);
DisplayMetrics metrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics);
visible = mBiding.bnv.getY() > metrics.heightPixels;
if(!visible) {
mBinding.bnv.setY(metrics.heightPixels);
}
} else {
mBinding.bnv.setY(mBinding.bnv.getY() + dy);
DisplayMetrics metrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics);
visible = mBinding.bnv.getY() > metrics.heightPixels;
}
}
So you are scrollling the BottomNavigationView with the recycler view
Or with CoordinatorLayout.Behavior class:
public class ViewScrollWithRecyclerViewBehavior extends CoordinatorLayout.Behavior<View> {
private boolean visible = true;
private boolean inStartPosition = true;
private float oldY;
private DisplayMetrics metrics;
public ViewScrollWithRecyclerViewBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
metrics = Resources.getSystem().getDisplayMetrics();
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, View fab, View dependency) {
return dependency instanceof AppBarLayout;
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
if (dependency instanceof AppBarLayout) {
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
float dy = oldY - dependency.getY();
if(dy > 0 && visible){
moveDown(child, oldY);
} else if(!inStartPosition) {
moveUp(child, oldY);
}
oldY = dependency.getY();
}
return true;
}
private void moveUp(View child, float dy){
if(child.getY() + dy >= metrics.heightPixels - child.getHeight()){
child.setY(metrics.heightPixels - child.getHeight());
} else {
child.setY(child.getY() + dy);
}
inStartPosition = child.getY() == metrics.heightPixels - child.getHeight();
visible = child.getY() > metrics.heightPixels;
}
private void moveDown(View child, float dy){
child.setY(child.getY() + dy);
visible = child.getY() > metrics.heightPixels;
if(!visible) {
child.setY(metrics.heightPixels);
}
}
#Override
public boolean onStartNestedScroll(final CoordinatorLayout coordinatorLayout, final View child,
final View directTargetChild, final View target, final int nestedScrollAxes) {
return true;
}
#Override
public void onNestedScroll(final CoordinatorLayout coordinatorLayout,
final View child,
final View target, final int dxConsumed, final int dy,
final int dxUnconsumed, final int dyUnconsumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dy, dxUnconsumed, dyUnconsumed);
if(dy > 0 && visible){
moveDown(child, dy);
} else if(!inStartPosition) {
moveUp(child, dy);
}
}
}
Im using the FloatingActionButton from the android.support.design.widget package:
<android.support.design.widget.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginBottom="20dp"
android:layout_marginEnd="16dp"
android:clickable="true"
android:backgroundTint="#color/primaryColor"
android:src="#drawable/ic_search_white_24dp"
app:borderWidth="0dp"
app:elevation="6dp"
app:backgroundTint="#color/primaryColorDark"
app:rippleColor="#color/accentColor" />
Is it possible to configure that button to hide with an animation when the listview is scrolling down and to show it again when listview is scrolling up to the top?
Those who are looking to make it with recyclerview can do this:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dy > 0 || dy < 0 && fab.isShown())
fab.hide();
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE)
fab.show();
super.onScrollStateChanged(recyclerView, newState);
}
});
Sorry! I am late by years to answer this. I hope this still helps someone. This is also my first answer.
Mates! No need to implement scroll listeners.
Add the following to the floating action button xml:
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
giving:
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="#+id/fabAddOItransferIn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="#dimen/fab_margin"
android:text="#string/btn_text_transfer_in"
app:icon="#android:drawable/ic_input_add"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
In response to the following comment of mine,
"Sorry! I just noticed this has a weird side effect. Any snackbars will overlap this floating action button if app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior is added. ☹️ Taking this line off will prevent the overlap and the floating action button will behave as it is intended to inside the coordinator layout. "
To counter this, do use the following:
Snackbar.make(floating_action_button, "Some snackbar text!", BaseTransientBottomBar.LENGTH_SHORT).setAnchorView(floating_action_button).show();
A small improvement to the code from Irfan Raza:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener(){
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy){
if (dy<0 && !fab.isShown())
fab.show();
else if(dy>0 && fab.isShown())
fab.hide();
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
});
The Floating Action Button hides when scrolling down and shows when scrolling up.
See this. Here it tells how to do what you are trying to achieve. You have to use it like this in a CoordinatorLayout and ListView :
<android.support.design.widget.CoordinatorLayout
android:id="#+id/main_content"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="#+id/lvToDoList"
android:layout_width="match_parent"
android:layout_height="match_parent"></ListView>
<android.support.design.widget.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_margin="16dp"
android:src="#drawable/ic_done"
app:layout_anchor="#id/lvToDoList"
app:layout_anchorGravity="bottom|right|end" />
</android.support.design.widget.CoordinatorLayout>
using this class you can easily animate you FAB, here I have implemented onStopNestedScroll() method to show your Fab whenever scroll stop.
I set 1000 miliSeconds as delay using Handler();
public class FabBehaviour extends CoordinatorLayout.Behavior<FloatingActionButton> {
private static final String TAG = "ScrollingFABBehavior";
Handler mHandler;
public FabBehaviour(Context context, AttributeSet attrs) {
super();
}
public FabBehaviour() {
super();
}
#Override
public void onStopNestedScroll(#NonNull CoordinatorLayout coordinatorLayout, #NonNull final FloatingActionButton child, #NonNull View target, int type) {
super.onStopNestedScroll(coordinatorLayout, child, target, type);
if (mHandler == null)
mHandler = new Handler();
mHandler.postDelayed(new Runnable() {
#Override
public void run() {
child.animate().translationY(0).setInterpolator(new LinearInterpolator()).start();
Log.d("FabAnim", "startHandler()");
}
}, 1000);
}
#Override
public void onNestedScroll(#NonNull CoordinatorLayout coordinatorLayout, #NonNull FloatingActionButton child, #NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
if (dyConsumed > 0) {
Log.d("Scrolling", "Up");
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
int fab_bottomMargin = layoutParams.bottomMargin;
child.animate().translationY(child.getHeight() + fab_bottomMargin).setInterpolator(new LinearInterpolator()).start();
} else if (dyConsumed < 0) {
Log.d("Scrolling", "down");
child.animate().translationY(0).setInterpolator(new LinearInterpolator()).start();
}
}
#Override
public boolean onStartNestedScroll(#NonNull CoordinatorLayout coordinatorLayout, #NonNull FloatingActionButton child, #NonNull View directTargetChild, #NonNull View target, int axes, int type) {
if (mHandler != null) {
mHandler.removeMessages(0);
Log.d("Scrolling", "stopHandler()");
}
return axes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
}
your_layout.xml
<android.support.design.widget.FloatingActionButton
android:id="#+id/imageViewYes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end|right"
android:layout_margin="#dimen/fab_margin"
android:src="#drawable/ic_yes"
app:backgroundTint="#color/white"
android:scaleType="center"
app:elevation="6dp"
app:fabSize="normal"
app:layout_behavior="com.your.package.FabBehaviour"
app:pressedTranslationZ="12dp"
app:rippleColor="#color/gray" />
hey there is o require to take the recyclerview for auto hiding the floating action button on scrolling down for this purpose we can use default listview with floating action button in normal way only make modifications on listview.onscroll listener then we can get feel like recycle
listview.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
int lastItem = firstVisibleItem + visibleItemCount;
if (lastItem == totalItemCount) {
fab.setVisibility(View.INVISIBLE);
}else {
fab.setVisibility(View.VISIBLE);
}
}
});
There is my code in kotlin.
class ScrollAwareFABBehavior (val recyclerView: RecyclerView, val floatingActionButton: FloatingActionButton) {
fun start() {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (dy > 0) {
if (floatingActionButton!!.isShown) {
floatingActionButton?.hide()
}
} else if (dy < 0) {
if (!floatingActionButton!!.isShown) {
floatingActionButton?.show()
}
}
}
})
}
}
Now, you just need to call the ScrollAwareFABBehavior with the recyclerView and the fab on constructor, then call method start().
ScrollAwareFABBehavior(recyclerView = recyclerViewPlaceFormContainer, floatingActionButton = floatingActionButton).start()
Another method for recycleView using kotlin extensions.
fun RecyclerView.attachFab(fab : FloatingActionButton) {
this.addOnScrollListener(object : RecyclerView.OnScrollListener(){
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (dy > 0)
fab.hide()
else if (dy < 0)
fab.show()
}
})
}
Now you can attach fab to any recycleView with:
rv.attachFab(requireActivity().fab)
// in my case i made fab public on activity
Here I am adding extra padding for last view item to avoid overlapping list item with floating action button
I used this in a RecyclerView.Adapter's onBindViewHolder method to set the bottom margin of the last item in the list to 72dp so that it will scroll up above the floating action button.
This does not require a dummy entry in the list.
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
// other binding code goes here.
if (position + 1 == getItemCount()) {
// set bottom margin to 72dp.
setBottomMargin(holder.itemView, (int) (72 * Resources.getSystem().getDisplayMetrics().density));
} else {
// reset bottom margin back to zero. (your value may be different)
setBottomMargin(holder.itemView, 0);
}
}
public static void setBottomMargin(View view, int bottomMargin) {
if (view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
params.setMargins(params.leftMargin, params.topMargin, params.rightMargin, bottomMargin);
view.requestLayout();
}
}
Kotlin + DataBinding Adapter
#BindingAdapter("bindAdapter:attachFloatingButton")
fun bindRecyclerViewWithFB(recyclerView: RecyclerView, fb: FloatingActionButton) {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (dy > 0 && fb.isShown) {
fb.hide()
} else if (dy < 0 && !fb.isShown) {
fb.show()
}
}
})
}
and the xml
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/main_recyclerview"
android:layout_width="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingBottom="8dp" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp"
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="8dp"
android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="#+id/main_chips"
android:layout_marginBottom="8dp"
**bindAdapter:attachFloatingButton="#{mainFb}"**
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.0"/>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="#+id/main_fb"
android:layout_width="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
style="#style/Widget.Design.FloatingActionButton"
app:layout_constraintEnd_toEndOf="parent"
android:layout_height="wrap_content"
android:layout_margin="18dp"
android:background="#color/colorPrimaryDark"
app:icon="#drawable/ic_add_black_24dp"/>
According to me the best way to implement this would be as below.
public class ScrollingFABBehavior extends FloatingActionButton.Behavior {
private static final String TAG = "ScrollingFABBehavior";
public ScrollingFABBehavior(Context context, AttributeSet attrs) {
super();
// Log.e(TAG, "ScrollAwareFABBehavior");
}
public boolean onStartNestedScroll(CoordinatorLayout parent, FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {
return true;
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, FloatingActionButton child, View dependency) {
if (dependency instanceof RecyclerView)
return true;
return false;
}
#Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout,
FloatingActionButton child, View target, int dxConsumed,
int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
// TODO Auto-generated method stub
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed);
//Log.e(TAG, "onNestedScroll called");
if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {
// Log.e(TAG, "child.hide()");
child.hide();
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
// Log.e(TAG, "child.show()");
child.show();
}
}}
For detailed answer check this out. Hide FloatingActionButton on scroll of RecyclerView
for Kotlin it is very simple (API 23+)
myRecyclerView.setOnScrollChangeListener { _, _, _, _, oldScrollY ->
if (oldScrollY < 0) myFAB.hide() else myFAB.show()
}
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0 && mFloatingActionButton.getVisibility() == View.VISIBLE) {
mFloatingActionButton.hide();
} else if (dy < 0 && mFloatingActionButton.getVisibility() != View.VISIBLE) {
mFloatingActionButton.show();
}
}});
Just to add, for NestedScrollView the approach will be something like the following:
// register the extended floating action Button
final ExtendedFloatingActionButton extendedFloatingActionButton = findViewById(R.id.extFloatingActionButton);
// register the nestedScrollView from the main layout
NestedScrollView nestedScrollView = findViewById(R.id.nestedScrollView);
// handle the nestedScrollView behaviour with OnScrollChangeListener
// to extend or shrink the Extended Floating Action Button
nestedScrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
#Override
public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
// the delay of the extension of the FAB is set for 12 items
if (scrollY > oldScrollY + 12 && extendedFloatingActionButton.isExtended()) {
extendedFloatingActionButton.shrink();
}
// the delay of the extension of the FAB is set for 12 items
if (scrollY < oldScrollY - 12 && !extendedFloatingActionButton.isExtended()) {
extendedFloatingActionButton.extend();
}
// if the nestedScrollView is at the first item of the list then the
// extended floating action should be in extended state
if (scrollY == 0) {
extendedFloatingActionButton.extend();
}
}
});
I've taken this code from GeeksForGeeks
I am using the new CoordinatorLayout with AppBarLayout and CollapsingToolbarLayout. Below AppBarLayout, I have a RecyclerView with a list of content.
I have verified that fling scrolling works on the RecyclerView when I am scrolling up and down the list. However, I would also like the AppBarLayout to smoothly scroll during expansion.
When scrolling up to expand the CollaspingToolbarLayout, scrolling immediately stops once lifting your finger off the screen. If you scroll up in a quick motion, sometimes the CollapsingToolbarLayout re-collapses as well. This behavior with the RecyclerView seems to function much differently than when using a NestedScrollView.
I've tried to set different scroll properties on the recyclerview but I haven't been able to figure this out.
Here is a video showing some of the scrolling issues.
https://youtu.be/xMLKoJOsTAM
Here is an example showing the issue with the RecyclerView (CheeseDetailActivity).
https://github.com/tylerjroach/cheesesquare
Here is the original example that uses a NestedScrollView from Chris Banes.
https://github.com/chrisbanes/cheesesquare
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.
Seems that v23 update did not fix it yet.
I have found sort of of hack to fix it with flinging down. The trick is to reconsume fling event if ScrollingView's top child is close to the beginning of data in Adapter.
public final class FlingBehavior extends AppBarLayout.Behavior {
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 (target instanceof ScrollingView) {
final ScrollingView scrollingView = (ScrollingView) target;
consumed = velocityY > 0 || scrollingView.computeVerticalScrollOffset() > 0;
}
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
}
Use it in your layout like that:
<android.support.design.widget.AppBarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="your.package.FlingBehavior">
<!--your views here-->
</android.support.design.widget.AppBarLayout>
EDIT: Fling event reconsuming is now based on verticalScrollOffset instead of amount of items on from top of RecyclerView.
EDIT2: Check target as ScrollingView interface instance instead of RecyclerView. Both RecyclerView and NestedScrollingView implement it.
I have found the fix by applying OnScrollingListener to the recyclerView. now it works very well. The issue is that recyclerview provided the wrong consumed value and the behavior doesn't know when the recyclerview is scrolled to the top.
package com.singmak.uitechniques.util.coordinatorlayout;
import android.content.Context;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
/**
* Created by maksing on 26/3/2016.
*/
public final class RecyclerViewAppBarBehavior extends AppBarLayout.Behavior {
private Map<RecyclerView, RecyclerViewScrollListener> scrollListenerMap = new HashMap<>(); //keep scroll listener map, the custom scroll listener also keep the current scroll Y position.
public RecyclerViewAppBarBehavior() {
}
public RecyclerViewAppBarBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
*
* #param coordinatorLayout
* #param child The child that attached the behavior (AppBarLayout)
* #param target The scrolling target e.g. a recyclerView or NestedScrollView
* #param velocityX
* #param velocityY
* #param consumed The fling should be consumed by the scrolling target or not
* #return
*/
#Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
if (target instanceof RecyclerView) {
final RecyclerView recyclerView = (RecyclerView) target;
if (scrollListenerMap.get(recyclerView) == null) {
RecyclerViewScrollListener recyclerViewScrollListener = new RecyclerViewScrollListener(coordinatorLayout, child, this);
scrollListenerMap.put(recyclerView, recyclerViewScrollListener);
recyclerView.addOnScrollListener(recyclerViewScrollListener);
}
scrollListenerMap.get(recyclerView).setVelocity(velocityY);
consumed = scrollListenerMap.get(recyclerView).getScrolledY() > 0; //recyclerView only consume the fling when it's not scrolled to the top
}
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
private static class RecyclerViewScrollListener extends RecyclerView.OnScrollListener {
private int scrolledY;
private boolean dragging;
private float velocity;
private WeakReference<CoordinatorLayout> coordinatorLayoutRef;
private WeakReference<AppBarLayout> childRef;
private WeakReference<RecyclerViewAppBarBehavior> behaviorWeakReference;
public RecyclerViewScrollListener(CoordinatorLayout coordinatorLayout, AppBarLayout child, RecyclerViewAppBarBehavior barBehavior) {
coordinatorLayoutRef = new WeakReference<CoordinatorLayout>(coordinatorLayout);
childRef = new WeakReference<AppBarLayout>(child);
behaviorWeakReference = new WeakReference<RecyclerViewAppBarBehavior>(barBehavior);
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
dragging = newState == RecyclerView.SCROLL_STATE_DRAGGING;
}
public void setVelocity(float velocity) {
this.velocity = velocity;
}
public int getScrolledY() {
return scrolledY;
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
scrolledY += dy;
if (scrolledY <= 0 && !dragging && childRef.get() != null && coordinatorLayoutRef.get() != null && behaviorWeakReference.get() != null) {
//manually trigger the fling when it's scrolled at the top
behaviorWeakReference.get().onNestedFling(coordinatorLayoutRef.get(), childRef.get(), recyclerView, 0, velocity, false);
}
}
}
}
It has been fixed since support design 26.0.0.
compile 'com.android.support:design:26.0.0'
This is a smooth version of Google Support Design AppBarLayout. If you are using AppBarLayout, you will know it has an issue with fling.
compile "me.henrytao:smooth-app-bar-layout:<latest-version>"
See Library here.. https://github.com/henrytao-me/smooth-app-bar-layout
It's a recyclerview bug . It's supposed to be fixed in v23.1.0.
look https://code.google.com/p/android/issues/detail?id=177729
This is my Layout and the scroll It's working as it should.
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:id="#+id/container">
<android.support.design.widget.AppBarLayout
android:id="#+id/appbarLayout"
android:layout_height="192dp"
android:layout_width="match_parent">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/ctlLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:contentScrim="?attr/colorPrimary"
app:layout_collapseMode="parallax">
<android.support.v7.widget.Toolbar
android:id="#+id/appbar"
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent"
app:layout_scrollFlags="scroll|enterAlways"
app:layout_collapseMode="pin"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="#+id/catalogueRV"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>
My solution so far, based on Mak Sing and Manolo Garcia answers.
It's not totally perfect. For now I don't know how to recalculate a valide velocity to avoid a weird effect: the appbar can expand faster than the scroll speed.
But the state with an expanded appbar and a scrolled recycler view cannot be reached.
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;
import java.lang.ref.WeakReference;
public class FlingAppBarLayoutBehavior
extends AppBarLayout.Behavior {
// The minimum I have seen for a dy, after the recycler view stopped.
private static final int MINIMUM_DELTA_Y = -4;
#Nullable
RecyclerViewScrollListener mScrollListener;
private boolean isPositive;
public FlingAppBarLayoutBehavior() {
}
public FlingAppBarLayoutBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
public boolean callSuperOnNestedFling(
CoordinatorLayout coordinatorLayout,
AppBarLayout child,
View target,
float velocityX,
float velocityY,
boolean consumed) {
return super.onNestedFling(
coordinatorLayout,
child,
target,
velocityX,
velocityY,
consumed
);
}
#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) {
RecyclerView recyclerView = (RecyclerView) target;
if (mScrollListener == null) {
mScrollListener = new RecyclerViewScrollListener(
coordinatorLayout,
child,
this
);
recyclerView.addOnScrollListener(mScrollListener);
}
mScrollListener.setVelocity(velocityY);
}
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;
}
private static class RecyclerViewScrollListener
extends RecyclerView.OnScrollListener {
#NonNull
private final WeakReference<AppBarLayout> mAppBarLayoutWeakReference;
#NonNull
private final WeakReference<FlingAppBarLayoutBehavior> mBehaviorWeakReference;
#NonNull
private final WeakReference<CoordinatorLayout> mCoordinatorLayoutWeakReference;
private int mDy;
private float mVelocity;
public RecyclerViewScrollListener(
#NonNull CoordinatorLayout coordinatorLayout,
#NonNull AppBarLayout child,
#NonNull FlingAppBarLayoutBehavior barBehavior) {
mCoordinatorLayoutWeakReference = new WeakReference<>(coordinatorLayout);
mAppBarLayoutWeakReference = new WeakReference<>(child);
mBehaviorWeakReference = new WeakReference<>(barBehavior);
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
if (mDy < MINIMUM_DELTA_Y
&& mAppBarLayoutWeakReference.get() != null
&& mCoordinatorLayoutWeakReference.get() != null
&& mBehaviorWeakReference.get() != null) {
// manually trigger the fling when it's scrolled at the top
mBehaviorWeakReference.get()
.callSuperOnNestedFling(
mCoordinatorLayoutWeakReference.get(),
mAppBarLayoutWeakReference.get(),
recyclerView,
0,
mVelocity, // TODO find a way to recalculate a correct velocity.
false
);
}
}
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
mDy = dy;
}
public void setVelocity(float velocity) {
mVelocity = velocity;
}
}
}
In my case, I was getting the issue where the flinging the RecyclerView would not scroll it smoothly, making it get stuck.
This was because, for some reason, I had forgotten that I had put my RecyclerView in a NestedScrollView.
It's a silly mistake, but it took me a while to figure it out...
Already some pretty popular solutions here but after playing with them I came up with a rather simpler solution that worked well for me. My solution also ensures that the AppBarLayout is only expanded when the scrollable content reaches the top, an advantage over other solutions here.
private int mScrolled;
private int mPreviousDy;
private AppBarLayout mAppBar;
myRecyclerView.addOnScrollListener(new OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
mScrolled += dy;
// scrolled to the top with a little more velocity than a slow scroll e.g. flick/fling.
// Adjust 10 (vertical change of event) as you feel fit for you requirement
if(mScrolled == 0 && dy < -10 && mPrevDy < 0) {
mAppBar.setExpanded(true, true);
}
mPreviousDy = dy;
});
I add a view of 1dp height inside the AppBarLayout an then it works much better. This is my layout.
<android.support.design.widget.CoordinatorLayout 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:layout_height="match_parent"
android:background="#android:color/white"
tools:context="com.spof.spof.app.UserBeachesActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="#+id/user_beaches_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_alignParentTop="true"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
android:theme="#style/WhiteTextToolBar"
app:layout_scrollFlags="scroll|enterAlways" />
<View
android:layout_width="match_parent"
android:layout_height="1dp" />
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="#+id/user_beaches_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="#string/appbar_scrolling_view_behavior" />
The accepted answer didn't work for me because I had RecyclerView inside a SwipeRefreshLayout and a ViewPager. This is the improved version that seeks a RecyclerView in the hierarchy and should work for any layout:
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) {
RecyclerView recycler = findRecycler((ViewGroup) target);
if (recycler != null){
target = recycler;
}
}
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;
}
#Nullable
private RecyclerView findRecycler(ViewGroup container){
for (int i = 0; i < container.getChildCount(); i++) {
View childAt = container.getChildAt(i);
if (childAt instanceof RecyclerView){
return (RecyclerView) childAt;
}
if (childAt instanceof ViewGroup){
return findRecycler((ViewGroup) childAt);
}
}
return null;
}
}
Answer: It's fixed in support library v26
but v26 has some issue in flinging. Sometimes, AppBar bounces back again even if fling is not too hard.
How do I remove the bouncing effect on appbar?
If you encounter the same issue when updating to support v26, here's the summary of this answer.
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.
Julian Os is right.
Manolo Garcia's answer does not work if the recyclerview is below the threshold and scrolls. You must compare the offset of the recyclerview and the velocity to the distance, not the item position.
I made java version by referring to julian's kotlin code and subtract reflection.
public final class FlingBehavior extends AppBarLayout.Behavior {
private boolean isPositive;
private float mFlingFriction = ViewConfiguration.getScrollFriction();
private float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
private final float INFLEXION = 0.35f;
private float mPhysicalCoeff;
public FlingBehavior(){
init();
}
public FlingBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
final float ppi = BaseApplication.getInstance().getResources().getDisplayMetrics().density * 160.0f;
mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
* 39.37f // inch/meter
* ppi
* 0.84f; // look and feel tuning
}
#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) {
RecyclerView recyclerView = (RecyclerView) target;
double distance = getFlingDistance((int) velocityY);
if (distance < recyclerView.computeVerticalScrollOffset()) {
consumed = true;
} else {
consumed = false;
}
}
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;
}
public double getFlingDistance(int velocity){
final double l = getSplineDeceleration(velocity);
final double decelMinusOne = DECELERATION_RATE - 1.0;
return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
}
private double getSplineDeceleration(int velocity) {
return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
}
}
I have found the fix by Eniz Bilgin https://stackoverflow.com/a/45090239/7639018
The problem has been solved with the libraries in this repository.
(https://developer.android.com/topic/libraries/support-library/setup.html)
allprojects {
repositories {
jcenter()
maven {
url "https://maven.google.com"
}
}
}
With reference to Google issue tracker, it has been fixed with Android 26.0.0-beta2 version of support library
Please update your Android support library version 26.0.0-beta2.
If any issue persists, please report at Google issue tracker they will re-open to examine.
Adding another answer here as the above ones did either not fulfill my needs completely or didn't work very well. This one is partially based on ideas spread here.
So what does this one do?
Scenario downwards fling:
If the AppBarLayout is collapsed, it lets the RecyclerView fling on its own without doing anything. Otherwise, it collapses the AppBarLayout and prevents the RecyclerView from doing its fling. As soon as it is collapsed (up to the point that the given velocity demands) and if there is velocity left, the RecyclerView gets flung with the original velocity minus what the AppBarLayout just consumed collapsing.
Scenario upwards fling:
If the RecyclerView's scroll offset is not zero, it gets flung with the original velocity. As soon as that is finished and if there is still velocity left (i.e. the RecyclerView scrolled to position 0), the AppBarLayout gets expanded up to the point that the original velocity minus the just consumed demands.
Otherwise, the AppBarLayout gets expanded up to the point that the original velocity demands.
AFAIK, this is the indended behavior.
There is a lot of reflection involved, and it's pretty custom. No issues found yet though.
It is also written in Kotlin, but understanding it should be no problem.
You can use the IntelliJ Kotlin plugin to compile it to bytecode -> and decompile it back to Java.
To use it, place it in the android.support.v7.widget package and set it as the AppBarLayout's CoordinatorLayout.LayoutParams' behavior in code (or add the xml applicable constructor or something)
/*
* Copyright 2017 Julian Ostarek
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.v7.widget
import android.support.design.widget.AppBarLayout
import android.support.design.widget.CoordinatorLayout
import android.support.v4.widget.ScrollerCompat
import android.view.View
import android.widget.OverScroller
class SmoothScrollBehavior(recyclerView: RecyclerView) : AppBarLayout.Behavior() {
// We're using this SplineOverScroller from deep inside the RecyclerView to calculate the fling distances
private val splineOverScroller: Any
private var isPositive = false
init {
val scrollerCompat = RecyclerView.ViewFlinger::class.java.getDeclaredField("mScroller").apply {
isAccessible = true
}.get(recyclerView.mViewFlinger)
val overScroller = ScrollerCompat::class.java.getDeclaredField("mScroller").apply {
isAccessible = true
}.get(scrollerCompat)
splineOverScroller = OverScroller::class.java.getDeclaredField("mScrollerY").apply {
isAccessible = true
}.get(overScroller)
}
override fun onNestedFling(coordinatorLayout: CoordinatorLayout?, child: AppBarLayout, target: View?, velocityX: Float, givenVelocity: Float, consumed: Boolean): Boolean {
// Making sure the velocity has the correct sign (seems to be an issue)
var velocityY: Float
if (isPositive != givenVelocity > 0) {
velocityY = givenVelocity * - 1
} else velocityY = givenVelocity
if (velocityY < 0) {
// Decrement the velocity to the maximum velocity if necessary (in a negative sense)
velocityY = Math.max(velocityY, - (target as RecyclerView).maxFlingVelocity.toFloat())
val currentOffset = (target as RecyclerView).computeVerticalScrollOffset()
if (currentOffset == 0) {
super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, false)
return true
} else {
val distance = getFlingDistance(velocityY.toInt()).toFloat()
val remainingVelocity = - (distance - currentOffset) * (- velocityY / distance)
if (remainingVelocity < 0) {
(target as RecyclerView).addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
recyclerView.post { recyclerView.removeOnScrollListener(this) }
if (recyclerView.computeVerticalScrollOffset() == 0) {
super#SmoothScrollBehavior.onNestedFling(coordinatorLayout, child, target, velocityX, remainingVelocity, false)
}
}
}
})
}
return false
}
}
// We're not getting here anyway, flings with positive velocity are handled in onNestedPreFling
return false
}
override fun onNestedPreFling(coordinatorLayout: CoordinatorLayout?, child: AppBarLayout, target: View?, velocityX: Float, givenVelocity: Float): Boolean {
// Making sure the velocity has the correct sign (seems to be an issue)
var velocityY: Float
if (isPositive != givenVelocity > 0) {
velocityY = givenVelocity * - 1
} else velocityY = givenVelocity
if (velocityY > 0) {
// Decrement to the maximum velocity if necessary
velocityY = Math.min(velocityY, (target as RecyclerView).maxFlingVelocity.toFloat())
val topBottomOffsetForScrollingSibling = AppBarLayout.Behavior::class.java.getDeclaredMethod("getTopBottomOffsetForScrollingSibling").apply {
isAccessible = true
}.invoke(this) as Int
val isCollapsed = topBottomOffsetForScrollingSibling == - child.totalScrollRange
// The AppBarlayout is collapsed, we'll let the RecyclerView handle the fling on its own
if (isCollapsed)
return false
// The AppbarLayout is not collapsed, we'll calculate the remaining velocity, trigger the appbar to collapse and fling the RecyclerView manually (if necessary) as soon as that is done
val distance = getFlingDistance(velocityY.toInt())
val remainingVelocity = (distance - (child.totalScrollRange + topBottomOffsetForScrollingSibling)) * (velocityY / distance)
if (remainingVelocity > 0) {
(child as AppBarLayout).addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener {
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
// The AppBarLayout is now collapsed
if (verticalOffset == - appBarLayout.totalScrollRange) {
(target as RecyclerView).mViewFlinger.fling(velocityX.toInt(), remainingVelocity.toInt())
appBarLayout.post { appBarLayout.removeOnOffsetChangedListener(this) }
}
}
})
}
// Trigger the expansion of the AppBarLayout
super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, false)
// We don't let the RecyclerView fling already
return true
} else return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY)
}
override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout?, child: AppBarLayout?, target: View?, dx: Int, dy: Int, consumed: IntArray?) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed)
isPositive = dy > 0
}
private fun getFlingDistance(velocity: Int): Double {
return splineOverScroller::class.java.getDeclaredMethod("getSplineFlingDistance", Int::class.javaPrimitiveType).apply {
isAccessible = true
}.invoke(splineOverScroller, velocity) as Double
}
}
this is my solution in my project.
just stop the mScroller when get Action_Down
xml:
<android.support.design.widget.AppBarLayout
android:id="#+id/smooth_app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/white"
app:elevation="0dp"
app:layout_behavior="com.sogou.groupwenwen.view.topic.FixAppBarLayoutBehavior">
FixAppBarLayoutBehavior.java :
public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
if (ev.getAction() == ACTION_DOWN) {
Object scroller = getSuperSuperField(this, "mScroller");
if (scroller != null && scroller instanceof OverScroller) {
OverScroller overScroller = (OverScroller) scroller;
overScroller.abortAnimation();
}
}
return super.onInterceptTouchEvent(parent, child, ev);
}
private Object getSuperSuperField(Object paramClass, String paramString) {
Field field = null;
Object object = null;
try {
field = paramClass.getClass().getSuperclass().getSuperclass().getDeclaredField(paramString);
field.setAccessible(true);
object = field.get(paramClass);
} catch (Exception e) {
e.printStackTrace();
}
return object;
}
//or check the raw file:
//https://github.com/shaopx/CoordinatorLayoutExample/blob/master/app/src/main/java/com/spx/coordinatorlayoutexample/util/FixAppBarLayoutBehavior.java
for androidx,
If your manifest file has a android:hardwareAccelerated="false" line, delete it.