CollapsingToolbarLayout 无法识别滚动投掷

Posted

技术标签:

【中文标题】CollapsingToolbarLayout 无法识别滚动投掷【英文标题】:CollapsingToolbarLayout doesn't recognize scroll fling 【发布时间】:2015-10-26 01:02:51 【问题描述】:

我创建了一个简单的 CollapsingToolbarLayout,它就像一个魅力。我的问题是,如果我尝试在 nestedscrollview 上使用快速滚动,它会在我松开手指时停止。正常的滚动就像它应该的那样工作。

我的活动代码是 unchanged => auto 生成的空活动。 (我只是点击了在 android studio 中创建新的空活动并编辑了 XML)。

我在这里读到,图像视图本身的滚动手势有问题,但不是,滚动本身有问题:see here。

我尝试通过 java 代码激活“平滑滚动”。似乎如果我滚动得足够远以至于图像视图不再可见,那么就会识别出投掷手势。

TLDR: 为什么只要 imageview 可见,fling 手势就不起作用? 我的 XML 代码如下所示:

    <android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_
    android:layout_
    android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/profile_app_bar_layout"
        android:layout_
        android:layout_
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        android:fitsSystemWindows="true">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/profile_collapsing_toolbar_layout"
            android:layout_
            android:layout_
            app:layout_scrollFlags="scroll|exitUntilCollapsed"
            app:contentScrim="?attr/colorPrimary"
            app:expandedTitleMarginStart="48dp"
            app:expandedTitleMarginEnd="64dp"
            android:fitsSystemWindows="true">

            <ImageView
                android:id="@+id/image"
                android:layout_
                android:layout_
                android:scaleType="centerCrop"
                android:fitsSystemWindows="true"
                android:src="@drawable/headerbg"
                android:maxHeight="192dp"

                app:layout_collapseMode="parallax"/>

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_
                android:layout_
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
                app:layout_collapseMode="pin" />

        </android.support.design.widget.CollapsingToolbarLayout>

    </android.support.design.widget.AppBarLayout>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        app:layout_anchor="@id/profile_app_bar_layout"
        app:layout_anchorGravity="bottom|right|end"
        android:layout_
        android:layout_
        app:elevation="2dp"
        app:pressedTranslationZ="12dp"
        android:layout_marginRight="8dp"
        android:layout_marginEnd="8dp"/>

    <android.support.v4.widget.NestedScrollView
        android:id="@+id/profile_content_scroll"
        android:layout_
        android:layout_
        android:clipToPadding="false"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:layout_gravity="fill_vertical"
        android:minHeight="192dp"
        android:overScrollMode="ifContentScrolls"
        >

        <RelativeLayout
            android:layout_
            android:layout_>

            <TextView
                android:layout_
                android:layout_
                android:text="@string/LoremIpsum"/>

        </RelativeLayout>

    </android.support.v4.widget.NestedScrollView>

</android.support.design.widget.CoordinatorLayout>

【问题讨论】:

有趣的是,我在受影响的投掷期间记录了嵌套滚动视图上的触摸事件。它得到ACTION_DOWN y=98 -&gt; ACTION_MOVE y=-40 -&gt; ACTION_MOVE y=-33 -&gt; ACTION_UP y=97。看起来最后一个触摸事件误报为在第一个触摸事件旁边。 您使用的是哪个版本的设计支持库? 您是否覆盖了任何触摸事件?尝试将 nestedScrollView.getParent().requestDisallowInterceptTouchEvent(true); 设置为嵌套滚动视图 Android Support Library 26.0.0-beta2 fixes this issue 【参考方案1】:

我遇到了与 CollapsingToolbarLayout 完全相同的问题,其中包含 ImageViewNestedScrollView。松开手指时,滑动滚动停止。

但是,我注意到了一些奇怪的事情。如果您从具有 OnClickListener(例如 Button)的视图中开始用手指滚动,则可以完美地滚动。

因此我用一个奇怪的解决方案修复了它。在 NestedScrollView 的直接子级上设置 OnClickListener(什么都不做)。然后就完美运行了!

<android.support.v4.widget.NestedScrollView 
   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_
   android:layout_
   app:layout_behavior="@string/appbar_scrolling_view_behavior">

  <LinearLayout
      android:id="@+id/content_container"
      android:layout_
      android:layout_
      android:orientation="vertical">

    <!-- Page Content -->

  </LinearLayout>

</android.support.v4.widget.NestedScrollView>

给直接子(LinearLayout)一个id并在Activity中设置OnClickListener

ViewGroup mContentContainer = (ViewGroup) findViewById(R.id.content_container);    
mContentContainer.setOnClickListener(this);

@Override
public void onClick(View view) 
    int viewId = view.getId();

注意事项:

使用支持设计库 25.0.1 测试

CollapsingToolbarLayout with scrollFlags="scroll|enterAlwaysCollapsed"

【讨论】:

AOSP 应该用这个很棒的解决方案来修补:D 应该得到更多的投票,哈哈。顺便说一句,这使得 CollapsingToolbarLayout 对滚动非常敏感,但比当前的损坏行为要好。 这太好了,令人难以置信,所以我尝试了它,但它不适合我 这是迄今为止我在 SO 中尝试过的最疯狂的解决方案。 :D 很棒的观察@jinang !!【参考方案2】:

我知道这个问题是在一年前提出的,但支持/设计库中似乎仍未解决此问题。您可以star this 发出问题,使其在优先级队列中更靠前。

也就是说,我尝试了大多数已发布的解决方案,包括 patrick-iv 的解决方案,但均未成功。如果在onPreNestedScroll() 中检测到一组特定条件,我能够开始工作的唯一方法是模仿并以编程方式调用它。在我调试的几个小时内,我注意到onNestedFling() 从未被调用向上(向下滚动),并且似乎过早地被消耗掉了。我不能 100% 肯定地说这将适用于 100% 的实现,但它对于我的使用来说已经足够好了,所以我最终还是接受了这个,尽管它很老套而且绝对不是我想做的。

public class NestedScrollViewBehavior extends AppBarLayout.Behavior 

    // Lower value means fling action is more easily triggered
    static final int MIN_DY_DELTA = 4;
    // Lower values mean less velocity, higher means higher velocity
    static final int FLING_FACTOR = 20;

    int mTotalDy;
    int mPreviousDy;
    WeakReference<AppBarLayout> mPreScrollChildRef;

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                                  View target, int dx, int dy, int[] consumed) 
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        // Reset the total fling delta distance if the user starts scrolling back up
        if(dy < 0) 
            mTotalDy = 0;
        
        // Only track move distance if the movement is positive (since the bug is only present
        // in upward flings), equal to the consumed value and the move distance is greater
        // than the minimum difference value
        if(dy > 0 && consumed[1] == dy && MIN_DY_DELTA < Math.abs(mPreviousDy - dy)) 
            mPreScrollChildRef = new WeakReference<>(child);
            mTotalDy += dy * FLING_FACTOR;
        
        mPreviousDy = dy;
    

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
                                       View directTargetChild, View target, int nestedScrollAxes) 
        // Stop any previous fling animations that may be running
        onNestedFling(parent, child, target, 0, 0, false);
        return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
    

    @Override
    public void onStopNestedScroll(CoordinatorLayout parent, AppBarLayout abl, View target) 
        if(mTotalDy > 0 && mPreScrollChildRef != null && mPreScrollChildRef.get() != null) 
            // Programmatically trigger fling if all conditions are met
            onNestedFling(parent, mPreScrollChildRef.get(), target, 0, mTotalDy, false);
            mTotalDy = 0;
            mPreviousDy = 0;
            mPreScrollChildRef = null;
        
        super.onStopNestedScroll(parent, abl, target);
    

并将其应用到 AppBar

AppBarLayout scrollView = (AppBarLayout)findViewById(R.id.appbar);
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams)scrollView.getLayoutParams();
params.setBehavior(new NestedScrollViewBehavior());

CheeseSquare 演示: Before After

【讨论】:

确实聊胜于无,但与有经验的 Android 用户所期望的不太一样。感谢您链接问题,我已加注星标。 必须删除 enterAlways layout_ScrollFlag 才能正常工作,但现在工作正常【参考方案3】:

我尝试了 Floofer 的解决方案,但对我来说仍然不够好。所以我想出了一个更好的版本他的行为。 AppBarLayout 现在可以在投掷时平滑地展开和折叠。

注意:我使用反射来破解这个问题,因此它可能无法完美地适用于不同于 25.0.0 的 Android 设计库版本。

public class SmoothScrollBehavior extends AppBarLayout.Behavior 
    private static final String TAG = "SmoothScrollBehavior";
    //The higher this value is, the faster the user must scroll for the AppBarLayout to collapse by itself
    private static final int SCROLL_SENSIBILITY = 5;
    //The real fling velocity calculation seems complex, in this case it is simplified with a multiplier
    private static final int FLING_VELOCITY_MULTIPLIER = 60;

    private boolean alreadyFlung = false;
    private boolean request = false;
    private boolean expand = false;
    private int velocity = 0;
    private int nestedScrollViewId;

    public SmoothScrollBehavior(int nestedScrollViewId) 
        this.nestedScrollViewId = nestedScrollViewId;
    

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                                  View target, int dx, int dy, int[] consumed) 
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        if(Math.abs(dy) >= SCROLL_SENSIBILITY) 
            request = true;
            expand = dy < 0;
            velocity = dy * FLING_VELOCITY_MULTIPLIER;
         else 
            request = false;
        
    

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
                                       View directTargetChild, View target, int nestedScrollAxes) 
        request = false;
        return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
    

    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout appBarLayout, View target) 
        if(request) 
            NestedScrollView nestedScrollView = (NestedScrollView) coordinatorLayout.findViewById(nestedScrollViewId);
            if (expand) 
                //No need to force expand if it is already ready expanding
                if (nestedScrollView.getScrollY() > 0) 
                    int finalY = getPredictedScrollY(nestedScrollView);
                    if (finalY <= 0) 
                        //since onNestedFling does not work to expand the AppBarLayout, we need to manually expand it
                        expandAppBarLayoutWithVelocity(coordinatorLayout, appBarLayout, velocity);
                    
                
             else 
                //onNestedFling will collapse the AppBarLayout with an animation time relative to the velocity
                onNestedFling(coordinatorLayout, appBarLayout, target, 0, velocity, true);

                if(!alreadyFlung) 
                    //TODO wait for AppBarLayout to be collapsed before scrolling for even smoother visual
                    nestedScrollView.fling(velocity);
                
            
        
        alreadyFlung = false;
        super.onStopNestedScroll(coordinatorLayout, appBarLayout, target);
    

    private int getPredictedScrollY(NestedScrollView nestedScrollView) 
        int finalY = 0;
        try 
            //With reflection, we can get the ScrollerCompat from the NestedScrollView to predict where the scroll will end
            Field scrollerField = nestedScrollView.getClass().getDeclaredField("mScroller");
            scrollerField.setAccessible(true);
            Object object = scrollerField.get(nestedScrollView);
            ScrollerCompat scrollerCompat = (ScrollerCompat) object;
            finalY = scrollerCompat.getFinalY();
         catch (Exception e ) 
            e.printStackTrace();
            //If the reflection fails, it will return 0, which means the scroll has reached top
            Log.e(TAG, "Failed to get mScroller field from NestedScrollView through reflection. Will assume that the scroll reached the top.");
        
        return finalY;
    

    private void expandAppBarLayoutWithVelocity(CoordinatorLayout coordinatorLayout, AppBarLayout appBarLayout, float velocity) 
        try 
            //With reflection, we can call the private method of Behavior that expands the AppBarLayout with specified velocity
            Method animateOffsetTo = getClass().getSuperclass().getDeclaredMethod("animateOffsetTo", CoordinatorLayout.class, AppBarLayout.class, int.class, float.class);
            animateOffsetTo.setAccessible(true);
            animateOffsetTo.invoke(this, coordinatorLayout, appBarLayout, 0, velocity);
         catch (Exception e) 
            e.printStackTrace();
            //If the reflection fails, we fall back to the public method setExpanded that expands the AppBarLayout with a fixed velocity
            Log.e(TAG, "Failed to get animateOffsetTo method from AppBarLayout.Behavior through reflection. Falling back to setExpanded.");
            appBarLayout.setExpanded(true, true);
        
    

    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY) 
        alreadyFlung = true;
        return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
    

要使用它,请为您的 AppBarLayout 设置一个新 Behavior。

AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.app_bar);
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
params.setBehavior(new SmoothScrollBehavior(R.id.nested_scroll_view));

【讨论】:

你的类在其构造函数中需要一个 int,但在代码中你什么也不发送给构造函数 我的错,我加了。 这看起来不错,它使滚动流畅,但我有一个问题,一旦 AppBarLayout 到达顶部,是否可以让 NestedScrollView 滚动到 AppBarLayout 中,并且当我向下滚动时, AppBarLayout 最后出现,当 NestedScrollView 完全滚动出来时,AppBarLayout 开始展开。 @ZijianWang 请解释一下“滚动到AppBarLayout”是什么意思,我也不懂你的第二个问题,你能改一下吗?【参考方案4】:

This answer 为我解决了这个问题。像这样创建一个自定义AppBarLayout.Behavior

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;
    

并将其添加到AppBarLayout,如下所示:

<android.support.design.widget.AppBarLayout
        android:layout_
        android:layout_
        ...
        app:layout_behavior="com.example.test.FlingBehavior">

【讨论】:

没有工作,因为另一个问题中的问题是这里没有使用的 RecyclerView。【参考方案5】:

我只是在这里发布这个,以便其他人不会在评论中错过它。 Jinang 的答案效果很好,但感谢AntPachon 指出了一种更简单的方法。与其以编程方式在Child of the NestedScrollView 上实现OnClick 方法,更好的方法是在子级的xml 中设置clickable=true

(使用与Jinang's相同的示例)

<android.support.v4.widget.NestedScrollView 
   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_
   android:layout_
   app:layout_behavior="@string/appbar_scrolling_view_behavior">

  <LinearLayout
      android:id="@+id/content_container"
      android:layout_
      android:layout_
      android:orientation="vertical"
      android:clickable="true" >                  <!-- new -->

    <!-- Page Content -->

  </LinearLayout>

</android.support.v4.widget.NestedScrollView>

【讨论】:

【参考方案6】:

在代码中:https://android.googlesource.com/platform/frameworks/support/+/master/core-ui/java/android/support/v4/widget/NestedScrollView.java#834

       case MotionEvent.ACTION_UP:
            if (mIsBeingDragged) 
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker,
                        mActivePointerId);
                if ((Math.abs(initialVelocity) > mMinimumVelocity)) 
                    flingWithNestedDispatch(-initialVelocity);
                 else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
                        getScrollRange())) 
                    ViewCompat.postInvalidateOnAnimation(this);
                
            
            mActivePointerId = INVALID_POINTER;
            endDrag();
            break;

当我在 NestedScrollView 上使用 fling 滚动时,有时“mIsBeingDragged = false”,因此 NestedScrollView 不会调度 fling 事件。

当我删除if (mIsBeingDragged) 语句时。

 case MotionEvent.ACTION_UP:
        //if (mIsBeingDragged) 
            final VelocityTracker velocityTracker = mVelocityTracker;
            velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
            int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker,
                    mActivePointerId);
            if ((Math.abs(initialVelocity) > mMinimumVelocity)) 
                flingWithNestedDispatch(-initialVelocity);
             else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
                    getScrollRange())) 
                ViewCompat.postInvalidateOnAnimation(this);
            
        //
        mActivePointerId = INVALID_POINTER;
        endDrag();
        break;

不会有问题的。但是不知道还会引起什么严重的问题

【讨论】:

添加更多细节以使答案易于理解,您写道 When i remove the if... from where you are talk to remove if ?

以上是关于CollapsingToolbarLayout 无法识别滚动投掷的主要内容,如果未能解决你的问题,请参考以下文章

如何更改 CollapsingToolbarLayout 字体和大小?

以编程方式折叠或展开 CollapsingToolbarLayout

Android:CollapsingToolbarLayout 和 SwipeRefreshLayout 卡住

CollapsingToolbarLayout 无法识别滚动投掷

检测 AppBarLayout/CollapsingToolbarLayout 何时完全展开

设置 CollapsingToolbarLayout 的起始高度