使用 RecyclerView + AppBarLayout
Flinging with RecyclerView + AppBarLayout
我将新的 CoordinatorLayout 与 AppBarLayout 和 CollapsingToolbarLayout 一起使用。在 AppBarLayout 下面,我有一个带有内容列表的 RecyclerView。
我已验证当我上下滚动列表时快速滚动在 RecyclerView 上有效。但是,我也希望 AppBarLayout 在扩展期间能够平滑滚动。
向上滚动展开CollaspingToolbarLayout时,一旦手指离开屏幕,滚动立即停止。如果快速向上滚动,有时 CollapsingToolbarLayout 也会重新折叠。 RecyclerView 的这种行为似乎与使用 NestedScrollView 时的行为有很大不同。
我曾尝试在 recyclerview 上设置不同的滚动属性,但我一直无法解决这个问题。
这是一个显示一些滚动问题的视频。
https://youtu.be/xMLKoJOsTAM
这是一个显示 RecyclerView (CheeseDetailActivity) 问题的示例。
https://github.com/tylerjroach/cheesesquare
这是使用 Chris Banes 的 NestedScrollView 的原始示例。
https://github.com/chrisbanes/cheesesquare
我在 AppBarLayout 中添加了一个高度为 1dp 的视图,然后效果就好多了。这是我的布局。
<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" />
这是我的布局和滚动条它正在正常工作。
<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>
似乎 v23
更新还没有修复它。
我发现了一种破解方法,可以通过向下滑动来修复它。诀窍是如果 ScrollingView 的顶部子项接近 Adapter 中数据的开头,则重新使用 fling 事件。
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);
}
}
像这样在您的布局中使用它:
<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>
编辑: Fling 事件重新消费现在基于 verticalScrollOffset
而不是 RecyclerView
.
之上的项目数量
EDIT2: 检查目标为 ScrollingView
接口实例而不是 RecyclerView
。 RecyclerView
和 NestedScrollingView
都实现了它。
的回答几乎是正确的。
主要问题是 RecyclerView 有时会给出不正确的投掷方向,因此如果您将以下代码添加到他的答案中,它就会正常工作:
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;
}
}
希望对您有所帮助。
这是一个 recyclerview 错误。它应该在 v23.1.0 中修复。
看https://code.google.com/p/android/issues/detail?id=177729
这是Google流畅版支持设计AppBarLayout。如果您使用的是 AppBarLayout,您就会知道它存在 fling 问题。
compile "me.henrytao:smooth-app-bar-layout:<latest-version>"
在此处查看图书馆.. https://github.com/henrytao-me/smooth-app-bar-layout
我通过将 OnScrollingListener 应用于 recyclerView 找到了解决方法。现在它工作得很好。问题是 recyclerview 提供了错误的消耗值,并且当 recyclerview 滚动到顶部时行为不知道。
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);
}
}
}
}
我目前的解决方案基于 and 个答案。
它并不完全完美。现在我不知道如何重新计算有效速度以避免奇怪的效果:appbar 可以比滚动速度更快地扩展。
但是无法达到扩展应用栏和滚动回收视图的状态。
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;
}
}
}
在我的例子中,我遇到的问题是 RecyclerView
无法平滑滚动,导致卡住。
这是因为,出于某种原因,我忘记了我已将我的 RecyclerView
放在 NestedScrollView
中。
这是一个愚蠢的错误,但我花了一段时间才弄明白...
这里已经有一些非常流行的解决方案,但在试用它们之后,我想出了一个相当简单的解决方案,对我来说效果很好。我的解决方案还确保 AppBarLayout
仅在可滚动内容到达顶部时展开,这是优于此处其他解决方案的优势。
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;
});
接受的答案对我不起作用,因为我在 SwipeRefreshLayout
和 ViewPager
中有 RecyclerView
。这是在层次结构中寻找 RecyclerView
的改进版本,应该适用于任何布局:
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;
}
}
在此处添加另一个答案,因为上述答案要么没有完全满足我的需求,要么效果不佳。这个部分是基于这里传播的想法。
那么这个有什么作用呢?
情景向下一掷:
如果 AppBarLayout 被折叠,它会让 RecyclerView 自行移动而不做任何事情。否则,它会折叠 AppBarLayout 并阻止 RecyclerView 执行其投掷操作。一旦它被折叠(达到给定速度要求的点)并且如果还有速度,RecyclerView 就会以原始速度减去 AppBarLayout 刚刚消耗的折叠速度。
场景向上一甩:
如果 RecyclerView 的滚动偏移不为零,它会以原始速度甩动。一旦完成并且仍然有速度剩余(即 RecyclerView 滚动到位置 0),AppBarLayout 将扩展到原始速度减去刚刚消耗的需求的点。
否则,AppBarLayout 会扩展到原始速度要求的点。
据我所知,这是预期的行为。
其中涉及很多反思,而且很习惯。不过还没有发现问题。
也是用Kotlin写的,不过看懂应该没问题。
您可以使用 IntelliJ Kotlin 插件将其编译为字节码 -> 并将其反编译回 Java。
要使用它,将它放在 android.support.v7.widget 包中,并在代码中将其设置为 AppBarLayout 的 CoordinatorLayout.LayoutParams' 行为(或添加 xml 适用的构造函数或其他东西)
/*
* 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
}
}
自支持设计 26.0.0 以来已修复。
compile 'com.android.support:design:26.0.0'
是对的。
如果 recyclerview 低于阈值并滚动, 不起作用。您必须比较 recyclerview 的 offset
和 velocity to the distance
,而不是项目位置。
我参考julian的kotlin代码,减去反射做了java版本
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));
}
}
我已找到 Eniz Bilgin
的修复程序
此存储库中的库已解决该问题。
(https://developer.android.com/topic/libraries/support-library/setup.html)
allprojects {
repositories {
jcenter()
maven {
url "https://maven.google.com"
}
}
}
回答:已在支持库 v26 中修复
但是 v26 在 flinging 方面有一些问题。有时,即使不太用力,AppBar 也会再次弹回。
如果您在更新以支持 v26 时遇到同样的问题,请查看此 的摘要。
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.
参考Google issue tracker,已用Android 26.0.0-beta2版本的支持库修复
请更新您的 Android 支持库版本 26.0.0-beta2。
如果任何问题仍然存在,请报告 Google issue tracker 他们将重新开放进行检查。
这是我在项目中的解决方案。
获取 Action_Down
时停止 mScroller
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
对于 androidx,
如果您的清单文件有 android:hardwareAccelerated="false" 行,请将其删除。
我将新的 CoordinatorLayout 与 AppBarLayout 和 CollapsingToolbarLayout 一起使用。在 AppBarLayout 下面,我有一个带有内容列表的 RecyclerView。
我已验证当我上下滚动列表时快速滚动在 RecyclerView 上有效。但是,我也希望 AppBarLayout 在扩展期间能够平滑滚动。
向上滚动展开CollaspingToolbarLayout时,一旦手指离开屏幕,滚动立即停止。如果快速向上滚动,有时 CollapsingToolbarLayout 也会重新折叠。 RecyclerView 的这种行为似乎与使用 NestedScrollView 时的行为有很大不同。
我曾尝试在 recyclerview 上设置不同的滚动属性,但我一直无法解决这个问题。
这是一个显示一些滚动问题的视频。 https://youtu.be/xMLKoJOsTAM
这是一个显示 RecyclerView (CheeseDetailActivity) 问题的示例。 https://github.com/tylerjroach/cheesesquare
这是使用 Chris Banes 的 NestedScrollView 的原始示例。 https://github.com/chrisbanes/cheesesquare
我在 AppBarLayout 中添加了一个高度为 1dp 的视图,然后效果就好多了。这是我的布局。
<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" />
这是我的布局和滚动条它正在正常工作。
<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>
似乎 v23
更新还没有修复它。
我发现了一种破解方法,可以通过向下滑动来修复它。诀窍是如果 ScrollingView 的顶部子项接近 Adapter 中数据的开头,则重新使用 fling 事件。
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);
}
}
像这样在您的布局中使用它:
<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>
编辑: Fling 事件重新消费现在基于 verticalScrollOffset
而不是 RecyclerView
.
EDIT2: 检查目标为 ScrollingView
接口实例而不是 RecyclerView
。 RecyclerView
和 NestedScrollingView
都实现了它。
主要问题是 RecyclerView 有时会给出不正确的投掷方向,因此如果您将以下代码添加到他的答案中,它就会正常工作:
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;
}
}
希望对您有所帮助。
这是一个 recyclerview 错误。它应该在 v23.1.0 中修复。
看https://code.google.com/p/android/issues/detail?id=177729
这是Google流畅版支持设计AppBarLayout。如果您使用的是 AppBarLayout,您就会知道它存在 fling 问题。
compile "me.henrytao:smooth-app-bar-layout:<latest-version>"
在此处查看图书馆.. https://github.com/henrytao-me/smooth-app-bar-layout
我通过将 OnScrollingListener 应用于 recyclerView 找到了解决方法。现在它工作得很好。问题是 recyclerview 提供了错误的消耗值,并且当 recyclerview 滚动到顶部时行为不知道。
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);
}
}
}
}
我目前的解决方案基于
它并不完全完美。现在我不知道如何重新计算有效速度以避免奇怪的效果:appbar 可以比滚动速度更快地扩展。 但是无法达到扩展应用栏和滚动回收视图的状态。
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;
}
}
}
在我的例子中,我遇到的问题是 RecyclerView
无法平滑滚动,导致卡住。
这是因为,出于某种原因,我忘记了我已将我的 RecyclerView
放在 NestedScrollView
中。
这是一个愚蠢的错误,但我花了一段时间才弄明白...
这里已经有一些非常流行的解决方案,但在试用它们之后,我想出了一个相当简单的解决方案,对我来说效果很好。我的解决方案还确保 AppBarLayout
仅在可滚动内容到达顶部时展开,这是优于此处其他解决方案的优势。
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;
});
接受的答案对我不起作用,因为我在 SwipeRefreshLayout
和 ViewPager
中有 RecyclerView
。这是在层次结构中寻找 RecyclerView
的改进版本,应该适用于任何布局:
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;
}
}
在此处添加另一个答案,因为上述答案要么没有完全满足我的需求,要么效果不佳。这个部分是基于这里传播的想法。
那么这个有什么作用呢?
情景向下一掷: 如果 AppBarLayout 被折叠,它会让 RecyclerView 自行移动而不做任何事情。否则,它会折叠 AppBarLayout 并阻止 RecyclerView 执行其投掷操作。一旦它被折叠(达到给定速度要求的点)并且如果还有速度,RecyclerView 就会以原始速度减去 AppBarLayout 刚刚消耗的折叠速度。
场景向上一甩: 如果 RecyclerView 的滚动偏移不为零,它会以原始速度甩动。一旦完成并且仍然有速度剩余(即 RecyclerView 滚动到位置 0),AppBarLayout 将扩展到原始速度减去刚刚消耗的需求的点。 否则,AppBarLayout 会扩展到原始速度要求的点。
据我所知,这是预期的行为。
其中涉及很多反思,而且很习惯。不过还没有发现问题。 也是用Kotlin写的,不过看懂应该没问题。 您可以使用 IntelliJ Kotlin 插件将其编译为字节码 -> 并将其反编译回 Java。 要使用它,将它放在 android.support.v7.widget 包中,并在代码中将其设置为 AppBarLayout 的 CoordinatorLayout.LayoutParams' 行为(或添加 xml 适用的构造函数或其他东西)
/*
* 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
}
}
自支持设计 26.0.0 以来已修复。
compile 'com.android.support:design:26.0.0'
offset
和 velocity to the distance
,而不是项目位置。
我参考julian的kotlin代码,减去反射做了java版本
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));
}
}
我已找到 Eniz Bilgin
的修复程序此存储库中的库已解决该问题。
(https://developer.android.com/topic/libraries/support-library/setup.html)
allprojects {
repositories {
jcenter()
maven {
url "https://maven.google.com"
}
}
}
回答:已在支持库 v26 中修复
但是 v26 在 flinging 方面有一些问题。有时,即使不太用力,AppBar 也会再次弹回。
如果您在更新以支持 v26 时遇到同样的问题,请查看此
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.
参考Google issue tracker,已用Android 26.0.0-beta2版本的支持库修复
请更新您的 Android 支持库版本 26.0.0-beta2。
如果任何问题仍然存在,请报告 Google issue tracker 他们将重新开放进行检查。
这是我在项目中的解决方案。
获取 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
对于 androidx,
如果您的清单文件有 android:hardwareAccelerated="false" 行,请将其删除。