【问题标题】:Flinging with RecyclerView + AppBarLayout使用 RecyclerView + AppBarLayout
【发布时间】:2023-04-10 23:43:01
【问题描述】:

我正在使用带有 AppBarLayout 和 CollapsingToolbarLayout 的新 CoordinatorLayout。在 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

【问题讨论】:

  • 我遇到了同样的问题(我正在使用 RecyclerView)。如果您查看任何应用程序的 google play 商店列表,它似乎表现正确,所以肯定有解决方案......
  • 嘿,Aneem,我知道这不是最好的解决方案,但我开始尝试使用这个库:github.com/ksoichiro/Android-ObservableScrollView。尤其是在这个活动中实现了我需要的结果:FlexibleSpaceWithImageRecyclerViewActivity.java。很抱歉在编辑之前拼错了您的名字。自动更正..
  • 同样的问题,我最终避开了 AppBarLayout。
  • 是的。我最终从 OvservableScrollView 库中得到了我需要的东西。我相信它会在未来的版本中得到修复。
  • fling 有问题,an issue 已被提出(并被接受)。

标签: android android-layout android-design-library android-appcompat


【解决方案1】:

Kirill Boyarshinov 的答案几乎是正确的。

主要问题是 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;
    }
}

我希望这会有所帮助。

【讨论】:

  • 你拯救了我的一天!似乎工作得很好!为什么您的回答不被接受?
  • 如果您使用 SwipeRefreshLayout 作为 recyclerview 的父级,只需在 if (target instanceof RecyclerView &amp;&amp; velocityY &lt; 0) { 之前添加此代码:if (target instanceof SwipeRefreshLayout &amp;&amp; velocityY &lt; 0) { target = ((SwipeRefreshLayout) target).getChildAt(0); }
  • + 1 分析这个修复,我不明白为什么谷歌还没有修复这个。代码看起来很简单。
  • 您好,如何使用 appbarlayout 和 Nestedscrollview 实现相同的功能...提前致谢..
  • 对我不起作用 =/ 顺便说一下,你不需要将类移动到支持包中来实现它,你可以在构造函数中注册一个 DragCallback。
【解决方案2】:

看来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 接口实例而不是RecyclerViewRecyclerViewNestedScrollingView 都实现了它。

【讨论】:

  • layout_behavior错误不允许获取字符串类型
  • 我测试过,效果更好!但是 TOP_CHILD_FLING_THRESHOLD 的目的是什么?为什么是 3?
  • @Julio_oa TOP_CHILD_FLING_THRESHOLD 表示如果回收站视图滚动到位置低于此阈值的元素,则将重新使用投掷事件。顺便说一句,我更新了答案以使用更通用的verticalScrollOffset。现在,当recyclerView 滚动到顶部时,将重新使用 fling 事件。
  • 您好,如何使用 appbarlayout 和 Nestedscrollview 实现相同的功能...提前致谢..
  • @Hardeep 将target instanceof RecyclerView 更改为target instanceof NestedScrollView,或将一般情况下的更多更改为target instanceof ScrollingView。我更新了答案。
【解决方案3】:

我通过将 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);
            }
        }
    }
}

【讨论】:

  • 感谢您的帖子。我已经尝试了此页面上的所有答案,根据我的经验,这是最有效的答案。但是,如果我没有用足够的力量滚动 RecyclerView,我的布局中的 RecylerView 在 AppBarLayout 滚动出屏幕之前会在内部滚动。换句话说,当我用足够的力滚动 RecyclerView 时,AppBar 会滚动到屏幕外,而 RecyclerView 不会在内部滚动,但是当我没有用足够的力滚动 RecyclerView 时,RecyclerView 在 AppbarLayout 滚动到屏幕之外之前会在内部滚动。你知道是什么原因造成的吗?
  • recyclerview仍然接收触摸事件,这就是它仍然滚动的原因,onnestedfling的行为将动画同时滚动appbarlayout。也许您可以尝试在行为中覆盖 onInterceptTouch 来改变这一点。对我来说,目前的行为是可以接受的。 (不确定我们是否看到相同的东西)
  • @MakSing 它对CoordinatorLayoutViewPager 设置非常有帮助,非常感谢这个期待已久的解决方案。请为此编写一个 GIST,以便其他开发人员也可以从中受益。我也在分享这个解决方案。再次感谢。
  • @MakSing 关闭所有解决方案,这对我来说效果最好。我将传递给 onNestedFling 的速度调整了一点速度 * 0.6f ... 似乎给它一个更好的流动。
  • 为我工作。 @MakSing 在 onScrolled 方法中是否必须调用 AppBarLayout.Behavior 的 onNestedFling 而不是 RecyclerViewAppBarBehavior ?对我来说似乎有点奇怪。
【解决方案4】:

自支持设计 26.0.0 以来已修复。

compile 'com.android.support:design:26.0.0'

【讨论】:

  • 这需要向上移动。这是here 的描述,以防有人对细节感兴趣。
  • 现在状态栏似乎有问题,当您向下滚动时,状态栏会随着滚动而下降一点……超级烦人!
  • @Xiaozou 我正在使用 26.1.0,但仍然遇到问题。快速投掷有时会导致相反的运动(在 onNestedFling 方法中可以看到运动的速度是相反的/错误的)。在小米红米 Note 3 和 Galaxy S3 中重现
  • @dor506 stackoverflow.com/a/47298312/782870 当你说相反的运动结果时,我不确定我们是否有同样的问题。但我在这里发布了一个答案。希望它有所帮助:)
【解决方案5】:

这是 Google Support Design AppBarLayout 的流畅版本。如果您使用的是 AppBarLayout,您会知道它有问题。

compile "me.henrytao:smooth-app-bar-layout:<latest-version>"

在此处查看图书馆.. https://github.com/henrytao-me/smooth-app-bar-layout

【讨论】:

    【解决方案6】:

    这是一个 recyclerview 错误。它应该在 v23.1.0 中修复。

    https://code.google.com/p/android/issues/detail?id=177729

    【讨论】:

    • v23.4.0 - 仍未修复
    • 在 v25.1.0 中仍未修复
    • v25.3.1,看起来还是很糟糕。
    • 终于在v26.0.1修复了!
    【解决方案7】:

    这是我的布局和滚动它正在正常工作。

    <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>
    

    【讨论】:

      【解决方案8】:

      到目前为止,我的解决方案基于 Mak SingManolo Garcia 的答案。

      它并不完全完美。现在我不知道如何重新计算有效速度以避免奇怪的效果: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(截至25.1.0)的当前速度:Field viewFlingerField = recyclerView.getClass().getDeclaredField("mViewFlinger"); viewFlingerField.setAccessible(true); Object flinger = viewFlingerField.get(recyclerView); Field scrollerField = flinger.getClass().getDeclaredField("mScroller"); scrollerField.setAccessible(true); ScrollerCompat scroller = (ScrollerCompat) scrollerField.get(flinger); velocity = Math.signum(mVelocity) * Math.abs(scroller.getCurrVelocity());
      【解决方案9】:

      在我的情况下,我遇到了RecyclerView 无法顺利滚动的问题,导致卡住。

      这是因为,出于某种原因,我忘记了我将 RecyclerView 放入了 NestedScrollView

      这是一个愚蠢的错误,但我花了一段时间才弄清楚......

      【讨论】:

        【解决方案10】:

        这里已经有一些非常流行的解决方案,但在使用它们之后,我想出了一个相当简单的解决方案,对我来说效果很好。我的解决方案还确保 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;
            });
        

        【讨论】:

        • 什么是 mPrevDy
        • @ARR.s 我认为这是'mPreviousDy'
        【解决方案11】:

        我在 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" />
        

        【讨论】:

        • 只有向上滚动才有效。不是当你向下滚动时
        • 对我来说,这两个方向都很好。您是否在 appbarlayout 中添加了 1dp 视图?我只在 android lollipop 和 kitkat 中测试过。
        • 好吧,我也在使用包装工具栏的 CollapsingToolbarLayout。我把 1dp 视图放在里面。有点像这样 AppBarLayout ->CollapsingToolbarLayout ->Toolbar + 1dp view
        • 我不知道它是否适用于 CollapsingToolbarLayout。我只用这段代码测试过。您是否尝试将 1dp 视图放在 CollapsingToolbarLayout 之外?
        • 是的。向上滚动有效,向下滚动不展开工具栏。
        【解决方案12】:

        接受的答案对我不起作用,因为我在 SwipeRefreshLayoutViewPager 中有 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;
            }
        }
        

        【讨论】:

          【解决方案13】:

          答案:已在支持库 v26 中修复

          但是 v26 有一些问题。有时,AppBar 会再次反弹,即使弹跳不是太用力。

          How do I remove the bouncing effect on appbar?

          如果您在更新以支持 v26 时遇到同样的问题,这里是此answer 的摘要。

          解决方案:扩展 AppBar 的默认 Behavior 并阻止调用 AppBar.Behavior 的 onNestedPreScroll() 和 AppBar 时的 onNestedScroll() 在 NestedScroll 尚未停止时被触摸。

          【讨论】:

            【解决方案14】:

            Julian Os 是对的。

            Manolo Garcia's answer 如果 recyclerview 低于阈值并滚动,则不起作用。你必须比较recyclerview的offsetvelocity to the distance,而不是item位置。

            我参考了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));
                }
            
            }
            

            【讨论】:

            • 无法回复BaseApplication
            • @ARR.s 抱歉,您只需将其替换为您的上下文,如下所示。
            • YOUR_CONTEXT.getResources().getDisplayMetrics().density * 160.0f;
            【解决方案15】:

            我找到了 Eniz Bilgin https://stackoverflow.com/a/45090239/7639018

            的修复程序

            此存储库中的库已解决问题。

            (https://developer.android.com/topic/libraries/support-library/setup.html)

            allprojects {
                repositories {
                    jcenter()
                    maven {
                        url "https://maven.google.com"
                    }
                }
            }
            

            【讨论】:

              【解决方案16】:

              参考Google issue tracker,已修复Android 26.0.0-beta2版本的支持库

              请更新您的 Android 支持库版本 26.0.0-beta2。

              如果任何问题仍然存在,请通过Google issue tracker 报告,他们将重新打开进行检查。

              【讨论】:

                【解决方案17】:

                在此处添加另一个答案,因为上述答案要么不能完全满足我的需求,要么效果不佳。这部分是基于这里传播的想法。

                那么这个有什么作用呢?

                向下抛的情景: 如果 AppBarLayout 被折叠,它会让 RecyclerView 自己扔掉而不做任何事情。否则,它会折叠 AppBarLayout 并阻止 RecyclerView 进行它的投掷。一旦它被折叠(达到给定速度所需的点)并且如果还剩下速度,RecyclerView 就会以原始速度减去 AppBarLayout 刚刚消耗的折叠速度。

                向上抛的情景: 如果 RecyclerView 的滚动偏移量不为零,它会以原始速度被抛掷。一旦完成并且如果仍然存在速度(即 RecyclerView 滚动到位置 0),AppBarLayout 就会扩展到原始速度减去刚刚消耗的需求的点。 否则,AppBarLayout 会扩展到原始速度所需的点。

                AFAIK,这是默认行为。

                其中涉及很多反射,而且非常习惯。虽然还没有发现问题。 它也是用 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
                    }
                
                }
                

                【讨论】:

                • 如何设置?
                【解决方案18】:

                这是我在项目中的解决方案。
                获取 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
                

                【讨论】:

                  【解决方案19】:

                  对于 androidx,

                  如果您的清单文件有 android:hardwareAccelerated="false" 行,请将其删除。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2020-04-11
                    • 2017-05-31
                    相关资源
                    最近更新 更多