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.
Related
Background
Suppose you have an app you've created that has a similar UI as the one you can create via the wizard of "scrolling activity", yet you wish the scrolling flags to have snapping, as such:
<android.support.design.widget.CollapsingToolbarLayout ... app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" >
The problem
As it turns out, on many cases it has issues of snapping. Sometimes the UI doesn't snap to top/bottom, making the CollapsingToolbarLayout stay in between.
Sometimes it also tries to snap to one direction, and then decides to snap to the other .
You can see both issues on the attached video here.
What I've tried
I thought it's one of the issues that I got for when I use setNestedScrollingEnabled(false) on a RecyclerView within, so I asked about it here, but then I noticed that even with the solution and without using this command at all and even when using a simple NestedScrollView (as is created by the wizard), I can still notice this behavior.
That's why I decided to report about this as an issue, here.
Sadly, I couldn't find any workaround for those weird bugs here on StackOverflow.
The question
Why does it occur, and more importantly: how can I avoid those issues while still using the behavior it's supposed to have?
EDIT: here's a nice improved Kotlin version of the accepted answer:
class RecyclerViewEx #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : RecyclerView(context, attrs, defStyle) {
private var mAppBarTracking: AppBarTracking? = null
private var mView: View? = null
private var mTopPos: Int = 0
private var mLayoutManager: LinearLayoutManager? = null
interface AppBarTracking {
fun isAppBarIdle(): Boolean
fun isAppBarExpanded(): Boolean
}
override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?, type: Int): Boolean {
if (mAppBarTracking == null)
return super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
if (type == ViewCompat.TYPE_NON_TOUCH && mAppBarTracking!!.isAppBarIdle()
&& isNestedScrollingEnabled) {
if (dy > 0) {
if (mAppBarTracking!!.isAppBarExpanded()) {
consumed!![1] = dy
return true
}
} else {
mTopPos = mLayoutManager!!.findFirstVisibleItemPosition()
if (mTopPos == 0) {
mView = mLayoutManager!!.findViewByPosition(mTopPos)
if (-mView!!.top + dy <= 0) {
consumed!![1] = dy - mView!!.top
return true
}
}
}
}
if (dy < 0 && type == ViewCompat.TYPE_TOUCH && mAppBarTracking!!.isAppBarExpanded()) {
consumed!![1] = dy
return true
}
val returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
if (offsetInWindow != null && !isNestedScrollingEnabled && offsetInWindow[1] != 0)
offsetInWindow[1] = 0
return returnValue
}
override fun setLayoutManager(layout: RecyclerView.LayoutManager) {
super.setLayoutManager(layout)
mLayoutManager = layoutManager as LinearLayoutManager
}
fun setAppBarTracking(appBarTracking: AppBarTracking) {
mAppBarTracking = appBarTracking
}
fun setAppBarTracking(appBarLayout: AppBarLayout) {
val appBarIdle = AtomicBoolean(true)
val appBarExpanded = AtomicBoolean()
appBarLayout.addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener {
private var mAppBarOffset = Integer.MIN_VALUE
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
if (mAppBarOffset == verticalOffset)
return
mAppBarOffset = verticalOffset
appBarExpanded.set(verticalOffset == 0)
appBarIdle.set(mAppBarOffset >= 0 || mAppBarOffset <= -appBarLayout.totalScrollRange)
}
})
setAppBarTracking(object : AppBarTracking {
override fun isAppBarIdle(): Boolean = appBarIdle.get()
override fun isAppBarExpanded(): Boolean = appBarExpanded.get()
})
}
override fun fling(velocityX: Int, inputVelocityY: Int): Boolean {
var velocityY = inputVelocityY
if (mAppBarTracking != null && !mAppBarTracking!!.isAppBarIdle()) {
val vc = ViewConfiguration.get(context)
velocityY = if (velocityY < 0) -vc.scaledMinimumFlingVelocity
else vc.scaledMinimumFlingVelocity
}
return super.fling(velocityX, velocityY)
}
}
Update
I have changed the code slightly to address remaining issues - at least the ones that I can reproduce. The key update was to dispose of dy only when the AppBar is expanded or collapsed. In the first iteration, dispatchNestedPreScroll() was disposing of scroll without checking the status of the AppBar for a collapsed state.
Other changes are minor and fall under the category of clean up. The code blocks are updated below.
This answer addresses the question's issue regarding RecyclerView. The other answer I have given still stands and applies here. RecyclerView has the same issues as NestedScrollView that were introduced in 26.0.0-beta2 of the support libraries.
The code below is base upon this answer to a related question but includes the fix for the erratic behavior of the AppBar. I have removed the code that fixed the odd scrolling because it no longer seems to be needed.
AppBarTracking.java
public interface AppBarTracking {
boolean isAppBarIdle();
boolean isAppBarExpanded();
}
MyRecyclerView.java
public class MyRecyclerView extends RecyclerView {
public MyRecyclerView(Context context) {
this(context, null);
}
public MyRecyclerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyRecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
private AppBarTracking mAppBarTracking;
private View mView;
private int mTopPos;
private LinearLayoutManager mLayoutManager;
#Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
// App bar latching trouble is only with this type of movement when app bar is expanded
// or collapsed. In touch mode, everything is OK regardless of the open/closed status
// of the app bar.
if (type == ViewCompat.TYPE_NON_TOUCH && mAppBarTracking.isAppBarIdle()
&& isNestedScrollingEnabled()) {
// Make sure the AppBar stays expanded when it should.
if (dy > 0) { // swiped up
if (mAppBarTracking.isAppBarExpanded()) {
// Appbar can only leave its expanded state under the power of touch...
consumed[1] = dy;
return true;
}
} else { // swiped down (or no change)
// Make sure the AppBar stays collapsed when it should.
// Only dy < 0 will open the AppBar. Stop it from opening by consuming dy if needed.
mTopPos = mLayoutManager.findFirstVisibleItemPosition();
if (mTopPos == 0) {
mView = mLayoutManager.findViewByPosition(mTopPos);
if (-mView.getTop() + dy <= 0) {
// Scroll until scroll position = 0 and AppBar is still collapsed.
consumed[1] = dy - mView.getTop();
return true;
}
}
}
}
boolean returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
// Fix the scrolling problems when scrolling is disabled. This issue existed prior
// to 26.0.0-beta2.
if (offsetInWindow != null && !isNestedScrollingEnabled() && offsetInWindow[1] != 0) {
offsetInWindow[1] = 0;
}
return returnValue;
}
#Override
public void setLayoutManager(RecyclerView.LayoutManager layout) {
super.setLayoutManager(layout);
mLayoutManager = (LinearLayoutManager) getLayoutManager();
}
public void setAppBarTracking(AppBarTracking appBarTracking) {
mAppBarTracking = appBarTracking;
}
#SuppressWarnings("unused")
private static final String TAG = "MyRecyclerView";
}
ScrollingActivity.java
public class ScrollingActivity extends AppCompatActivity
implements AppBarTracking {
private MyRecyclerView mNestedView;
private int mAppBarOffset;
private boolean mAppBarIdle = false;
private int mAppBarMaxOffset;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scrolling);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mNestedView = findViewById(R.id.nestedView);
final AppBarLayout appBar = findViewById(R.id.app_bar);
appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
#Override
public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
mAppBarOffset = verticalOffset;
// mAppBarOffset = 0 if app bar is expanded; If app bar is collapsed then
// mAppBarOffset = mAppBarMaxOffset
// mAppBarMaxOffset is always <=0 (-AppBarLayout.getTotalScrollRange())
// mAppBarOffset should never be > zero or less than mAppBarMaxOffset
mAppBarIdle = (mAppBarOffset >= 0) || (mAppBarOffset <= mAppBarMaxOffset);
}
});
appBar.post(new Runnable() {
#Override
public void run() {
mAppBarMaxOffset = -appBar.getTotalScrollRange();
}
});
findViewById(R.id.disableNestedScrollingButton).setOnClickListener(new OnClickListener() {
#Override
public void onClick(final View v) {
// If the AppBar is fully expanded or fully collapsed (idle), then disable
// expansion and apply the patch; otherwise, set a flag to disable the expansion
// and apply the patch when the AppBar is idle.
setExpandEnabled(false);
}
});
findViewById(R.id.enableNestedScrollingButton).setOnClickListener(new OnClickListener() {
#Override
public void onClick(final View v) {
setExpandEnabled(true);
}
});
mNestedView.setAppBarTracking(this);
mNestedView.setLayoutManager(new LinearLayoutManager(this));
mNestedView.setAdapter(new Adapter() {
#Override
public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(
android.R.layout.simple_list_item_1,
parent,
false)) {
};
}
#SuppressLint("SetTextI18n")
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
((TextView) holder.itemView.findViewById(android.R.id.text1)).setText("item " + position);
}
#Override
public int getItemCount() {
return 100;
}
});
}
private void setExpandEnabled(boolean enabled) {
mNestedView.setNestedScrollingEnabled(enabled);
}
#Override
public boolean isAppBarExpanded() {
return mAppBarOffset == 0;
}
#Override
public boolean isAppBarIdle() {
return mAppBarIdle;
}
#SuppressWarnings("unused")
private static final String TAG = "ScrollingActivity";
}
What is happening here?
From the question, it was apparent that the layout was failing to snap the app bar closed or open as it should when the user's finger was not on the screen. When dragging, the app bar behaves as it should.
In version 26.0.0-beta2, some new methods were introduced - specifically dispatchNestedPreScroll() with a new type argument. The type argument specifies if the movement specified by dx and dy are due to the user touching the screen ViewCompat.TYPE_TOUCH or not ViewCompat.TYPE_NON_TOUCH.
Although the specific code that causes the problem was not identified, the tack of the fix is to kill vertical movement in dispatchNestedPreScroll() (dispose of dy) when needed by not letting vertical movement propagate. In effect, the app bar is to be latched into place when expanded and will not allowed to start to close until it is closing through a touch gesture. The app bar will also be latched when closed until the RecyclerView is positioned at its topmost extent and there is sufficient dy to open the app bar while performing a touch gesture.
So, this is not so much a fix as much as a discouragement of problematic conditions.
The last part of the MyRecyclerView code deals with an issue that was identified in this question dealing with improper scroll movements when nested scrolling is disabled. This is the part that comes after the call to the super of dispatchNestedPreScroll() that changes the value of offsetInWindow[1]. The thinking behind this code is the same as presented in the accepted answer for the question. The only difference is that since the underlying nested scrolling code has changed, the argument offsetInWindow is sometime null. Fortunately, it seems to be non-null when it matters, so the last part continues to work.
The caveat is that this "fix" is very specific to the question asked and is not a general solution. The fix will likely have a very short shelf life since I expect that such an obvious problem will be addressed shortly.
Looks like onStartNestedScroll and onStopNestedScroll calls can be reordered and it lead to "wobbly" snap. I made a small hack inside AppBarLayout.Behavior. Don't really want to mess up with all that stuff in activity as proposed by other answers.
#SuppressWarnings("unused")
public class ExtAppBarLayoutBehavior extends AppBarLayout.Behavior {
private int mStartedScrollType = -1;
private boolean mSkipNextStop;
public ExtAppBarLayoutBehavior() {
super();
}
public ExtAppBarLayoutBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {
if (mStartedScrollType != -1) {
onStopNestedScroll(parent, child, target, mStartedScrollType);
mSkipNextStop = true;
}
mStartedScrollType = type;
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type);
}
#Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target, int type) {
if (mSkipNextStop) {
mSkipNextStop = false;
return;
}
if (mStartedScrollType == -1) {
return;
}
mStartedScrollType = -1;
// Always pass TYPE_TOUCH, because want to snap even after fling
super.onStopNestedScroll(coordinatorLayout, abl, target, ViewCompat.TYPE_TOUCH);
}
}
Usage in XML layout:
<android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.AppBarLayout
app:layout_behavior="com.example.ExtAppBarLayoutBehavior">
<!-- Put here everything you usually add to AppBarLayout: CollapsingToolbarLayout, etc... -->
</android.support.design.widget.AppBarLayout>
<!-- Content: recycler for example -->
<android.support.v7.widget.RecyclerView
app:layout_behavior="#string/appbar_scrolling_view_behavior" />
...
</android.support.design.widget.CoordinatorLayout>
It is very likely that the root cause of the problem in the RecyclerView. Do not have an opportunity to dig deeper now.
Edit The code has been updated to bring it more in line with the code for the accepted answer. This answer concerns NestedScrollView while the accepted answer is about RecyclerView.
This is an issue what was introduced in the API 26.0.0-beta2 release. It does not happen on the beta 1 release or with API 25. As you noted, it also happens with API 26.0.0. Generally, the problem seems to be related to how flings and nested scrolling are handled in beta2. There was a major rewrite of nested scrolling (see "Carry on Scrolling"), so it is not surprising that this type of issue has cropped up.
My thinking is that excess scroll is not being disposed of properly somewhere in NestedScrollView. The work-around is to quietly consume certain scrolls that are "non-touch" scrolls (type == ViewCompat.TYPE_NON_TOUCH) when the AppBar is expanded or collapsed. This stops the bouncing, allows snaps and, generally, makes the AppBar better behaved.
ScrollingActivity has been modified to track the status of the AppBar to report whether it is expanded or not. A new class call "MyNestedScrollView" overrides dispatchNestedPreScroll() (the new one, see here) to manipulate the consumption of the excess scroll.
The following code should suffice to stop AppBarLayout from wobbling and refusing to snap. (XML will also have to change to accommodate MyNestedSrollView. The following only applies to support lib 26.0.0-beta2 and above.)
AppBarTracking.java
public interface AppBarTracking {
boolean isAppBarIdle();
boolean isAppBarExpanded();
}
ScrollingActivity.java
public class ScrollingActivity extends AppCompatActivity implements AppBarTracking {
private int mAppBarOffset;
private int mAppBarMaxOffset;
private MyNestedScrollView mNestedView;
private boolean mAppBarIdle = true;
#Override
protected void onCreate(Bundle savedInstanceState) {
AppBarLayout appBar;
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scrolling);
final Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
appBar = findViewById(R.id.app_bar);
mNestedView = findViewById(R.id.nestedScrollView);
mNestedView.setAppBarTracking(this);
appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
#Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
mAppBarOffset = verticalOffset;
}
});
appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
#Override
public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
mAppBarOffset = verticalOffset;
// mAppBarOffset = 0 if app bar is expanded; If app bar is collapsed then
// mAppBarOffset = mAppBarMaxOffset
// mAppBarMaxOffset is always <=0 (-AppBarLayout.getTotalScrollRange())
// mAppBarOffset should never be > zero or less than mAppBarMaxOffset
mAppBarIdle = (mAppBarOffset >= 0) || (mAppBarOffset <= mAppBarMaxOffset);
}
});
mNestedView.post(new Runnable() {
#Override
public void run() {
mAppBarMaxOffset = mNestedView.getMaxScrollAmount();
}
});
}
#Override
public boolean isAppBarIdle() {
return mAppBarIdle;
}
#Override
public boolean isAppBarExpanded() {
return mAppBarOffset == 0;
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_scrolling, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
#SuppressWarnings("unused")
private static final String TAG = "ScrollingActivity";
}
MyNestedScrollView.java
public class MyNestedScrollView extends NestedScrollView {
public MyNestedScrollView(Context context) {
this(context, null);
}
public MyNestedScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyNestedScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setOnScrollChangeListener(new View.OnScrollChangeListener() {
#Override
public void onScrollChange(View view, int x, int y, int oldx, int oldy) {
mScrollPosition = y;
}
});
}
private AppBarTracking mAppBarTracking;
private int mScrollPosition;
#Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
// App bar latching trouble is only with this type of movement when app bar is expanded
// or collapsed. In touch mode, everything is OK regardless of the open/closed status
// of the app bar.
if (type == ViewCompat.TYPE_NON_TOUCH && mAppBarTracking.isAppBarIdle()
&& isNestedScrollingEnabled()) {
// Make sure the AppBar stays expanded when it should.
if (dy > 0) { // swiped up
if (mAppBarTracking.isAppBarExpanded()) {
// Appbar can only leave its expanded state under the power of touch...
consumed[1] = dy;
return true;
}
} else { // swiped down (or no change)
// Make sure the AppBar stays collapsed when it should.
if (mScrollPosition + dy < 0) {
// Scroll until scroll position = 0 and AppBar is still collapsed.
consumed[1] = dy + mScrollPosition;
return true;
}
}
}
boolean returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
// Fix the scrolling problems when scrolling is disabled. This issue existed prior
// to 26.0.0-beta2. (Not sure that this is a problem for 26.0.0-beta2 and later.)
if (offsetInWindow != null && !isNestedScrollingEnabled() && offsetInWindow[1] != 0) {
Log.d(TAG, "<<<<offsetInWindow[1] forced to zero");
offsetInWindow[1] = 0;
}
return returnValue;
}
public void setAppBarTracking(AppBarTracking appBarTracking) {
mAppBarTracking = appBarTracking;
}
#SuppressWarnings("unused")
private static final String TAG = "MyNestedScrollView";
}
Since the issue is still not fixed as of February 2020 (latest material library version is 1.2.0-alpha5) I want to share my solution to the buggy AppBar animation.
The idea is to implmenet custom snapping logic by extending AppBarLayout.Behavior (Kotlin version):
package com.example
import android.content.Context
import android.os.Handler
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.LayoutParams
#Suppress("unused")
class AppBarBehaviorFixed(context: Context?, attrs: AttributeSet?) :
AppBarLayout.Behavior(context, attrs) {
private var view: AppBarLayout? = null
private var snapEnabled = false
private var isUpdating = false
private var isScrolling = false
private var isTouching = false
private var lastOffset = 0
private val handler = Handler()
private val snapAction = Runnable {
val view = view ?: return#Runnable
val offset = -lastOffset
val height = view.run { height - paddingTop - paddingBottom - getChildAt(0).minimumHeight }
if (offset > 1 && offset < height - 1) view.setExpanded(offset < height / 2)
}
private val updateFinishDetector = Runnable {
isUpdating = false
scheduleSnapping()
}
private fun initView(view: AppBarLayout) {
if (this.view != null) return
this.view = view
// Checking "snap" flag existence (applied through child view) and removing it
val child = view.getChildAt(0)
val params = child.layoutParams as LayoutParams
snapEnabled = params.scrollFlags hasFlag LayoutParams.SCROLL_FLAG_SNAP
params.scrollFlags = params.scrollFlags removeFlag LayoutParams.SCROLL_FLAG_SNAP
child.layoutParams = params
// Listening for offset changes
view.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, offset ->
lastOffset = offset
isUpdating = true
scheduleSnapping()
handler.removeCallbacks(updateFinishDetector)
handler.postDelayed(updateFinishDetector, 50L)
})
}
private fun scheduleSnapping() {
handler.removeCallbacks(snapAction)
if (snapEnabled && !isUpdating && !isScrolling && !isTouching) {
handler.postDelayed(snapAction, 50L)
}
}
override fun onLayoutChild(
parent: CoordinatorLayout,
abl: AppBarLayout,
layoutDirection: Int
): Boolean {
initView(abl)
return super.onLayoutChild(parent, abl, layoutDirection)
}
override fun onTouchEvent(
parent: CoordinatorLayout,
child: AppBarLayout,
ev: MotionEvent
): Boolean {
isTouching =
ev.actionMasked != MotionEvent.ACTION_UP && ev.actionMasked != MotionEvent.ACTION_CANCEL
scheduleSnapping()
return super.onTouchEvent(parent, child, ev)
}
override fun onStartNestedScroll(
parent: CoordinatorLayout,
child: AppBarLayout,
directTargetChild: View,
target: View,
nestedScrollAxes: Int,
type: Int
): Boolean {
val started = super.onStartNestedScroll(
parent, child, directTargetChild, target, nestedScrollAxes, type
)
if (started) {
isScrolling = true
scheduleSnapping()
}
return started
}
override fun onStopNestedScroll(
coordinatorLayout: CoordinatorLayout,
abl: AppBarLayout,
target: View,
type: Int
) {
isScrolling = false
scheduleSnapping()
super.onStopNestedScroll(coordinatorLayout, abl, target, type)
}
private infix fun Int.hasFlag(flag: Int) = flag and this == flag
private infix fun Int.removeFlag(flag: Int) = this and flag.inv()
}
And now apply this behavior to the AppBarLayout in xml:
<android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.AppBarLayout
app:layout_behavior="com.example.AppBarBehaviorFixed">
<com.google.android.material.appbar.CollapsingToolbarLayout
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<!-- Toolbar declaration -->
</com.google.android.material.appbar.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<!-- Scrolling view (RecyclerView, NestedScrollView) -->
</android.support.design.widget.CoordinatorLayout>
That is still a hack but it seems to work quite well, and it does not require to put dirty code into your activity or extend RecyclerView and NestedScrollView widgets (thanks to #vyndor for this idea).
I'm using a LinearSnapHelper to make items in my RecyclerView "snap" into place on the screen (my cards take up most of the screen, so I want them to snap into place and fill the screen on every swipe/fling/scroll).
I'm struggling with how to make the cards snap into place faster. I've tried creating a custom LinearLayoutManager (and editing the calculateSpeedPerPixel method in scrollToPosition or smoothScrollToPosition), as well as a custom RecyclerView (and editing the fling method). But nothing effects the speed that cards "snap" into place.
I suppose the issue is that I don't really understand how LinearSnapHelper "scrolls" the cards into position. It doesn't seem to use LinearLayoutManager's scrollToPosition or smoothScrollToPosition methods.
snapHelper = new LinearSnapHelper() {
#Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
View centerView = findSnapView(layoutManager);
if (centerView == null) {
return RecyclerView.NO_POSITION;
}
int position = layoutManager.getPosition(centerView);
int targetPosition = -1;
if (layoutManager.canScrollHorizontally()) {
if (velocityX < 0) {
targetPosition = position - 1;
} else {
targetPosition = position + 1;
}
}
if (layoutManager.canScrollVertically()) {
if (velocityY > 0) {
targetPosition = position + 1;
} else {
targetPosition = position - 1;
}
}
final int firstItem = 0;
final int lastItem = layoutManager.getItemCount() - 1;
targetPosition = Math.min(lastItem, Math.max(targetPosition, firstItem));
return targetPosition;
}
};
snapHelper.attachToRecyclerView(mRecyclerView);
As 郭玉龙 mentioned, SnapHelper call RecyclerView.smoothScrollBy() method. And it use default sQuinticInterpolator.
To change speed of snap you can do next:
public class SlowdownRecyclerView extends RecyclerView {
// Change pow to control speed.
// Bigger = faster. RecyclerView default is 5.
private static final int POW = 2;
private Interpolator interpolator;
public SlowdownRecyclerView(Context context) {
super(context);
createInterpolator();
}
public SlowdownRecyclerView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
createInterpolator();
}
public SlowdownRecyclerView(Context context, #Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
createInterpolator();
}
private void createInterpolator(){
interpolator = new Interpolator() {
#Override
public float getInterpolation(float t) {
t = Math.abs(t - 1.0f);
return (float) (1.0f - Math.pow(t, POW));
}
};
}
#Override
public void smoothScrollBy(int dx, int dy) {
super.smoothScrollBy(dx, dy, interpolator);
}
Or you can implement your own interpolator.
The speed of snapping scroll is affected by RecyclerView.smoothScrollBy().
Here's the snippet of source code.
Override this function to increase or decrease the speed of snapping scroll.
I wound up doing this by adding a ScrollListener to my RecycleView, and then creating a custom LinearLayoutManager and custom smoothScrollToPosition method.
final CustomLinearLayoutManager mLayoutManager = new CustomLinearLayoutManager(getActivity());
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
private boolean scrollingUp;
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
scrollingUp = dy < 0;
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
int visiblePosition = scrollingUp ? mLayoutManager.findFirstVisibleItemPosition() : mLayoutManager.findLastVisibleItemPosition();
int completelyVisiblePosition = scrollingUp ? mLayoutManager
.findFirstCompletelyVisibleItemPosition() : mLayoutManager
.findLastCompletelyVisibleItemPosition();
if (visiblePosition != completelyVisiblePosition) {
recyclerView.smoothScrollToPosition(visiblePosition);
return;
}
}
});
I achieved this using a library https://github.com/rubensousa/GravitySnapHelper
you can also override findTargetSnapPosition to get pager like scroll
tweek the scrollMsPerInch to increase / decrease speed
val snapHelper : GravitySnapHelper = GravitySnapHelper(Gravity.CENTER)
// the lower the higher the speed, default is 100f
snapHelper.scrollMsPerInch = 40f
snapHelper.attachToRecyclerView(binding?.mRecyclerView)
Actually you can modify the LinearSnapHelper and SnapHelperClass by simply copy/paste the existing code the only thing you will do is to set MILLISECONDS_PER_INCH on SnapHelper as you want and then use simply use the LinearSnapHelper you created
I have a CoordinatorLayout with two children, a View that acts as header and a RecyclerView:
<android.support.design.widget.CoordinatorLayout
android:id="#+id/coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="#+id/view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
app:layout_behavior="some.package.AlphaBehavior">
<ImageView
android:id="#+id/header_iv"
style="#style/some_style"/>
<TextView
android:id="#+id/header_retails_tv"
style="#style/some_style_tv"
android:text="#string/some_text"/>
</LinearLayout>
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
</android.support.design.widget.CoordinatorLayout>
I set a padding dynamically to RecyclerView with the size of header and I set the clipToPadding to false, so the RecyclerView is displayed below the header and when the user makes scroll up, the RecyclerView is shown above the header view.
I made a custom CoordinatorLayout.Behavior in order to accomplish a fade out of the view when the user scrolls up the list and a fade in when the header have to be visible again, the AlphaBehavior:
public class AlphaBehavior extends CoordinatorLayout.Behavior {
private float alpha = 1.0f;
private float scrolly = 0.f;
private int headerSize = 0;
private Animation defaultFadeInAnimation;
public AlphaBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
defaultFadeInAnimation = AnimationUtils.loadAnimation(context, android.R.anim.fade_in);
}
public void setHeaderSize(int headerSize) {
this.headerSize = headerSize;
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof RecyclerView;
}
#Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
scrolly += dyConsumed;
Log.d(Constants.TAG, dyConsumed + "/" + dyUnconsumed + "/" + scrolly);
float totalScrollY = ((RecyclerView)target).computeVerticalScrollOffset();
Log.d(Constants.TAG, "totalScrollY:" + totalScrollY);
alpha = (headerSize - totalScrollY) / headerSize;
if (alpha < 0.f) alpha = 0.f;
if (alpha > 1.0f) alpha = 1.f;
if (dyConsumed < 0 && totalScrollY > headerSize) {
alpha = 0.f;
}
Log.d(Constants.TAG, "alpha:" + alpha);
child.setAlpha(alpha);
}
#Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
int pos = ((LinearLayoutManager)((RecyclerView)target).getLayoutManager()).findFirstCompletelyVisibleItemPosition();
Log.d(Constants.TAG, "pos:" + pos);
if (pos == 0 && child.getAlpha() == 0.f) {
child.startAnimation(defaultFadeInAnimation);
}
}
// overriding this in case we don't the other events are not called
#Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
}
But I'm facing an issue: if the user make scroll very fast the events of the behaviour are not calling correctly. The scrollY member is not well-related to the total scroll and the totalScrollY member (obtained from computing the scroll from RecyclerView) is not correct. Even I tried to find the firstCompletelyVisibleItem in the onStopNestedScroll event, but it is returning the position 2 or 3 when the recyclerView achieves the start of the list.
Finally, I solved it using an OnScrollListener instead of using a CoordinatorLayout.Behavior and it is working like a charm. I put the code, maybe is useful for someone:
The custom onScrollListener to hide a view:
public class HideViewOnScrollListener extends RecyclerView.OnScrollListener {
private float alpha = 1.f;
private float scrolly = 0.f;
private int heightViewToHide;
private final View viewToHide;
public HideViewOnScrollListener(View viewToHide) {
this.viewToHide = viewToHide;
heightViewToHide = viewToHide.getHeight();
if (heightViewToHide == 0) {
ViewTreeObserver viewTreeObserver = viewToHide.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
heightViewToHide = viewToHide.getHeight();
if (heightViewToHide > 0)
viewToHide.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
scrolly += dy;
alpha = (heightViewToHide - scrolly) / heightViewToHide;
if (alpha < 0.f) alpha = 0.f;
if (alpha > 1.0f) alpha = 1.f;
if (dy < 0 && scrolly > heightViewToHide) {
alpha = 0.f;
}
viewToHide.setAlpha(alpha);
}
}
And you can add to a RecyclerView that way:
recyclerView.addOnScrollListener(new HideViewOnScrollListener(viewToHide));
I have implemented ItemTouchHelper like descriped in this articel:
https://medium.com/#ipaulpro/drag-and-swipe-with-recyclerview-b9456d2b1aaf#.k7xm7amxi
All works fine if the RecyclerView is a child of the CoordinatorLayout.
But if the RecyclerView is a child of NestedScrollView in CoordinatorLayout, the drag scrolling not working anymore.
Draging an item and move it to the top or bottom of the screen, the RecyclerView not scrolling like it do if its not a child of NestedScrollView.
Any ideas?
You have to disable the nestedScrolling for the recyclerView:
recyclerView.setIsNestedScrollingEnabled(false);
I have run into this same problem and I spent nearly a whole day to solve it.
Precondition:
First of all, my xml layout looks like this:
<CoordinatorLayout>
<com.google.android.material.appbar.AppBarLayout
...
</com.google.android.material.appbar.AppBarLayout>
<NestedScrollView>
<RecyclerView/>
</NestedScrollView>
</CoordinatorLayout>
And to make the scrolling behavior normal, I also let the nestedScrolling for the RecyclerView disabled by: RecyclerView.setIsNestedScrollingEnabled(false);
Reason:
But with ItemTouchHelper I still cannot make the Recyclerview auto scroll as expected when I drag the item in it. The reason why IT CANNOT SCROLL is in the method scrollIfNecessary() of ItemTouchHelper:
boolean scrollIfNecessary() {
RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
if (mTmpRect == null) {
mTmpRect = new Rect();
}
int scrollY = 0;
lm.calculateItemDecorationsForChild(mSelected.itemView, mTmpRect);
if (lm.canScrollVertically()) {
int curY = (int) (mSelectedStartY + mDy);
final int topDiff = curY - mTmpRect.top - mRecyclerView.getPaddingTop();
if (mDy < 0 && topDiff < 0) {
scrollY = topDiff;
} else if (mDy > 0) {
final int bottomDiff = curY + mSelected.itemView.getHeight() + mTmpRect.bottom
- (mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom());
if (bottomDiff > 0) {
scrollY = bottomDiff;
}
}
}
if (scrollY != 0) {
scrollY = mCallback.interpolateOutOfBoundsScroll(mRecyclerView,
mSelected.itemView.getHeight(), scrollY,
mRecyclerView.getHeight(), scrollDuration);
}
if (scrollY != 0) {
mRecyclerView.scrollBy(scrollX, scrollY);
return true;
}
return false;
}
Reason 1: when nestedScrolling for the RecyclerView is set to false, actually the effective scrolling object is the NestedScrollView, which is the parent of RecyclerView. So RecyclerView.scrollBy(x, y) here does not work at all!
Reason 2: mRecyclerView.getHeight() is much bigger than NestedScrollView.getHeight(). So when I drag the item in RecyclerView to bottom, the result of scrollIfNecessary() is also false.
Reason 3: mSelectedStartY does not seem like the expected value when in our case. Because we need to calculate the scrollY of NestedScrollView in our case.
Therefore, we need to override this method to fullfill our expectation. Here comes the solution:
Solution:
Step 1:
In order to override this scrollIfNecessary()(This method is not public), you need to new a class under a package named the same as ItemTouchHelper's. Like this:
Step 2:
Besides overriding scrollIfNecessary(), we also need to override select() in order to get the value of mSelectedStartY and the scrollY of NestedScrollView when starting draging.
public override fun select(selected: RecyclerView.ViewHolder?, actionState: Int) {
super.select(selected, actionState)
if (selected != null) {
mSelectedStartY = selected.itemView.top
mSelectedStartScrollY = (mRecyclerView.parent as NestedScrollView).scrollY.toFloat()
}
}
Notice: mSelectedStartY and mSelectedStartScrollY are both very important for scrolling the NestedScrollView up or down.
Step 3:
Now we can override scrollIfNecessary(), and you need to pay attention to the comments below:
public override fun scrollIfNecessary(): Boolean {
...
val lm = mRecyclerView.layoutManager
if (mTmpRect == null) {
mTmpRect = Rect()
}
var scrollY = 0
val currentScrollY = (mRecyclerView.parent as NestedScrollView).scrollY
// We need to use the height of NestedScrollView, not RecyclerView's!
val actualShowingHeight = (mRecyclerView.parent as NestedScrollView).height
lm!!.calculateItemDecorationsForChild(mSelected.itemView, mTmpRect!!)
if (lm.canScrollVertically()) {
// The true current Y of the item in NestedScrollView, not in RecyclerView!
val curY = (mSelectedStartY + mDy - currentScrollY).toInt()
// The true mDy should plus the initial scrollY and minus current scrollY of NestedScrollView
val checkDy = (mDy + mSelectedStartScrollY - currentScrollY).toInt()
val topDiff = curY - mTmpRect!!.top - mRecyclerView.paddingTop
if (checkDy < 0 && topDiff < 0) {// User is draging the item out of the top edge.
scrollY = topDiff
} else if (checkDy > 0) { // User is draging the item out of the bottom edge.
val bottomDiff = (curY + mSelected.itemView.height + mTmpRect!!.bottom
- (actualShowingHeight - mRecyclerView.paddingBottom))
if (bottomDiff > 0) {
scrollY = bottomDiff
}
}
}
if (scrollY != 0) {
scrollY = mCallback.interpolateOutOfBoundsScroll(
mRecyclerView,
mSelected.itemView.height, scrollY, actualShowingHeight, scrollDuration
)
}
if (scrollY != 0) {
...
// The scrolling behavior should be assigned to NestedScrollView!
(mRecyclerView.parent as NestedScrollView).scrollBy(0, scrollY)
return true
}
...
return false
}
Result:
I can just show you my work through the Gif below:
This is the solution that works for me.
Create 2 custom classes
1> LockableScrollView
public class LockableScrollView extends NestedScrollView {
// true if we can scroll (not locked)
// false if we cannot scroll (locked)
private boolean mScrollable = true;
public LockableScrollView(#NonNull Context context) {
super(context);
}
public LockableScrollView(#NonNull Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public LockableScrollView(#NonNull Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setScrollingEnabled(boolean enabled) {
mScrollable = enabled;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// Don't do anything with intercepted touch events if
// we are not scrollable
if (ev.getAction() == MotionEvent.ACTION_MOVE) {// if we can scroll pass the event to the superclass
return mScrollable && super.onInterceptTouchEvent(ev);
}
return super.onInterceptTouchEvent(ev);
}
}
2>LockableRecyclerView extends RecyclerView
public class LockableRecyclerView extends RecyclerView {
private LockableScrollView scrollview;
public LockableRecyclerView(#NonNull Context context) {
super(context);
}
public LockableRecyclerView(#NonNull Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public LockableRecyclerView(#NonNull Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setScrollview(LockableScrollView lockedscrollview) {
this.scrollview = lockedscrollview;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_MOVE) {
scrollview.setScrollingEnabled(false);
return super.onInterceptTouchEvent(ev);
}
scrollview.setScrollingEnabled(true);
return super.onInterceptTouchEvent(ev);
}
#Override
public boolean onTouchEvent(MotionEvent e) {
if (e.getAction() == MotionEvent.ACTION_MOVE) {
scrollview.setScrollingEnabled(false);
return super.onTouchEvent(e);
}
scrollview.setScrollingEnabled(true);
return super.onTouchEvent(e);
}
}
Use this views instead of NestedScrollView and RecyclerView in xml
in kotlin file set
recyclerView.setScrollview(binding.scrollView)
recyclerView.isNestedScrollingEnabled = false
ItemTouchHelper(object :
ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.UP) {
override fun onMove(
#NonNull recyclerView: RecyclerView,
#NonNull viewHolder: RecyclerView.ViewHolder,
#NonNull target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(#NonNull viewHolder: RecyclerView.ViewHolder, direction: Int) {
// when user swipe thr recyclerview item to right remove item from favorite list
if (direction == ItemTouchHelper.UP) {
val itemToRemove = favList[viewHolder.absoluteAdapterPosition]
}
}
}).attachToRecyclerView(binding.recyclerView)
android:descendantFocusability="blocksDescendants"
add in NestedScrollView and add
android:focusableInTouchMode="true"
in child layout it look like below
<androidx.core.widget.NestedScrollView
android:descendantFocusability="blocksDescendants">
<androidx.constraintlayout.widget.ConstraintLayout
android:focusableInTouchMode="true">
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
check this github repo
https://github.com/khambhaytajaydip/Drag-Drop-recyclerview
I implemented a collapsingtoolbar layout with a recyclerview as shown in the sample code attached. My issue is that, when I fling the list downward, it does not go all the way to the top.
What happens is that, the scrolling stops right at the point where the AppBarLayout is supposed to end.
The effect that I want is upon flinging the list downward, the list will go all the way to the top AND reveal/expand the AppBarLayout
My minSdk is 14. Any help or suggestion is greatly appreciated.
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.AppBarLayout>
<android.support.design.widget.CollapsingToolbarLayout
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<LinearLayout
app:layout_collapseMode="parallax">
//some elements
</LinearLayout>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
app:layout_behavior="#string/appbar_scrolling_view_behavior"/> //value android.support.design.widget.AppBarLayout$ScrollingViewBehavior
<android.support.v7.widget.Toolbar
app:popupTheme="#style/AppTheme.PopupOverlay"
app:layout_collapseMode="parallax" />
I had similar problem and I used a simple trick to expand AppBarLayout when RecyclerView fling to top (you need to have support library >= 23.x.x)
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
int firstVisiblePosition = linearLayoutManager.findFirstCompletelyVisibleItemPosition();
if (firstVisiblePosition == 0) {
mAppBarLayout.setExpanded(true, true);
}
}
}
});
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);
}