美文网首页
Android协调滚动的几种实现方式

Android协调滚动的几种实现方式

作者: 小城哇哇 | 来源:发表于2022-11-25 21:27 被阅读0次

    那在一些细度化的操作中,如我们需要一些控件随着滚动布局做一些粒度比较小的动画、移动等操作,那么我们就需要监听滚动,然后改变当前控件的属性。

    如何实现这种协调滚动的布局呢?我们使用 CoordinatorLayout + AppBarLayout 或者 CoordinatorLayout + Behavior 实现,另一种方案是 MotionLayout。我们看看都是怎么实现的吧。

    一、CoordinatorLayout + Behavior

    CoordinatorLayout 顾名思义是协调布局,其原理很简单,在onMeasure()的时候保存childView,通过 PreDrawListener监听childView的变化,最终通过双层for循环找到对应的Behavior,分发任务即可。CoordinatorLayout实现了NestedScrollingParent2,那么在childView实现了NestedScrollingChild方法时候也能解决滑动冲突问题。

    而Behavior就是一个应用于View的观察者模式,一个View跟随者另一个View的变化而变化,或者说一个View监听另一个View。

    在Behavior中,被观察View 也就是事件源被称为denpendcy,而观察View,则被称为child。

    一般自定义Behavior来说分两种情况:

    1. 监听另一个view的状态变化,例如大小、位置、显示状态等
    2. 监听CoordinatorLayout里的滑动状态

    这里我们以之前的效果为主来实现自定义的Behavior,先设置NestedScrollView在ImageView下面:

    public class MyScrollBehavior extends ViewOffsetBehavior<NestedScrollView> {
        private int topImgHeight;
        private int topTextHeight;
    
        public MyScrollBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull NestedScrollView child,
                                       @NonNull View dependency) {
            return dependency instanceof ImageView ;
        }
    
        @Override
        protected void layoutChild(CoordinatorLayout parent, NestedScrollView child, int layoutDirection) {
            super.layoutChild(parent, child, layoutDirection);
            if (topImgHeight == 0) {
                final List<View> dependencies = parent.getDependencies(child);
    
                for (int i = 0, z = dependencies.size(); i < z; i++) {
                    View view = dependencies.get(i);
                    if (view instanceof ImageView) {
                        topImgHeight = view.getMeasuredHeight();
                    } 
                }
            }
    
            child.setTop(topImgHeight);
            child.setBottom(child.getBottom() + topImgHeight);
        }
    
    }
    

    然后设置监听CoordinatorLayout里的滑动状态,ImageView做同样的滚动

    public class MyImageBehavior extends CoordinatorLayout.Behavior<View> {
    
        private int topBarHeight = 0;  //负图片高度
        private int downEndY = 0;   //默认为0
    
        public MyImageBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                           @NonNull View child, @NonNull View directTargetChild,
                                           @NonNull View target, int axes, int type) {
            //监听垂直滚动
            return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
        }
    
        @Override
        public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
                                      @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
    
            if (topBarHeight == 0) {
                topBarHeight = -child.getMeasuredHeight();
            }
    
            float transY = child.getTranslationY() - dy;
    
            //处理上滑
            if (dy > 0) {
                if (transY >= topBarHeight) {
                    translationByConsume(child, transY, consumed, dy);
                    translationByConsume(target, transY, consumed, dy);
                } else {
                    translationByConsume(child, topBarHeight, consumed, (child.getTranslationY() - topBarHeight));
                    translationByConsume(target, topBarHeight, consumed, (child.getTranslationY() - topBarHeight));
                }
            }
    
            if (dy < 0 && !target.canScrollVertically(-1)) {
                //处理下滑
                if (transY >= topBarHeight && transY <= downEndY) {
                    translationByConsume(child, transY, consumed, dy);
                    translationByConsume(target, transY, consumed, dy);
                } else {
                    translationByConsume(child, downEndY, consumed, (downEndY - child.getTranslationY()));
                    translationByConsume(target, downEndY, consumed, (downEndY - child.getTranslationY()));
                }
            }
        }
    
        @Override
        public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, float velocityX, float velocityY, boolean consumed) {
            return super.onNestedFling(coordinatorLayout, child, target, velocityX,
                    velocityY, consumed);
        }
    
        private void translationByConsume(View view, float translationY, int[] consumed, float consumedDy) {
            consumed[1] = (int) consumedDy;
            view.setTranslationY(translationY);
        }
    
    }
    

    分别为ImageView和NestedScrollView设置对应的 Behavior。

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 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="@color/white"
        android:orientation="vertical">
    
        <com.guadou.lib_baselib.view.titlebar.EasyTitleBar
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:Easy_title="CoordinatorLayout+Behavior" />
    
        <androidx.coordinatorlayout.widget.CoordinatorLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <ImageView
                android:layout_width="150dp"
                android:layout_height="150dp"
                app:layout_behavior="com.google.android.material.appbar.MyImageBehavior"
                android:layout_gravity="center_horizontal"
                android:contentDescription="我是测试的图片"
                android:src="@mipmap/ic_launcher" />
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:background="#ccc"
                android:gravity="center"
                android:text="我是测试的分割线"
                android:visibility="gone" />
    
            <androidx.core.widget.NestedScrollView
                android:id="@+id/nestedScroll"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:layout_behavior="com.google.android.material.appbar.MyScrollBehavior">
    
                <TextView
                    android:id="@+id/nestedScrollLayout"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="@string/scroll_content" />
    
            </androidx.core.widget.NestedScrollView>
    
        </androidx.coordinatorlayout.widget.CoordinatorLayout>
    
    </LinearLayout>
    
    

    我们先把TextView隐藏先不处理TextView。效果如下:

    这样我们就实现了自定义 Behavior 监听滚动的实现。那么我们加上TextView 的 Behavior 监听ImageView的滚动,做对应的滚动。

    先修改 MyScrollBehavior 让他在ImageView和TextView下面

    public class MyScrollBehavior extends ViewOffsetBehavior<NestedScrollView> {
        private int topImgHeight;
        private int topTextHeight;
    
        public MyScrollBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull NestedScrollView child,
                                       @NonNull View dependency) {
            return dependency instanceof ImageView || dependency instanceof TextView ;
        }
    
        @Override
        protected void layoutChild(CoordinatorLayout parent, NestedScrollView child, int layoutDirection) {
            super.layoutChild(parent, child, layoutDirection);
            if (topImgHeight == 0) {
                final List<View> dependencies = parent.getDependencies(child);
    
                for (int i = 0, z = dependencies.size(); i < z; i++) {
                    View view = dependencies.get(i);
                    if (view instanceof ImageView) {
                        topImgHeight = view.getMeasuredHeight();
                    } else if (view instanceof TextView) {
                        topTextHeight = view.getMeasuredHeight();
                        view.setTop(topImgHeight);
                        view.setBottom(view.getBottom() + topImgHeight);
                    }
                }
            }
    
            child.setTop(topImgHeight + topTextHeight);
            child.setBottom(child.getBottom() + topImgHeight + topTextHeight);
        }
    
    }
    

    然后设置监听ImageView的滚动:

    public class MyTextBehavior extends CoordinatorLayout.Behavior<View> {
    
        private int imgHeight;
    
        public MyTextBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
            return dependency instanceof ImageView;
        }
    
        @Override
        public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
            //跟随ImageView滚动,ImageView滚动多少我滚动多少
            float translationY = dependency.getTranslationY();
    
            if (imgHeight == 0) {
                imgHeight = dependency.getHeight();
            }
    
            float offsetTranslationY = imgHeight + translationY;
    
            child.setTranslationY(offsetTranslationY);
    
            return true;
        }
    }
    

    xml修改如下:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 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="@color/white"
        android:orientation="vertical">
    
        <com.guadou.lib_baselib.view.titlebar.EasyTitleBar
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:Easy_title="CoordinatorLayout+Behavior" />
    
        <androidx.coordinatorlayout.widget.CoordinatorLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <ImageView
                android:layout_width="150dp"
                android:layout_height="150dp"
                app:layout_behavior="com.google.android.material.appbar.MyImageBehavior"
                android:layout_gravity="center_horizontal"
                android:contentDescription="我是测试的图片"
                android:src="@mipmap/ic_launcher" />
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:background="#ccc"
                app:layout_behavior="com.google.android.material.appbar.MyTextBehavior"
                android:gravity="center"
                android:text="我是测试的分割线"
                android:visibility="visible" />
    
            <androidx.core.widget.NestedScrollView
                android:id="@+id/nestedScroll"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:layout_behavior="com.google.android.material.appbar.MyScrollBehavior">
    
                <TextView
                    android:id="@+id/nestedScrollLayout"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="@string/scroll_content" />
    
            </androidx.core.widget.NestedScrollView>
    
        </androidx.coordinatorlayout.widget.CoordinatorLayout>
    
    </LinearLayout>
    

    看到上面的示例,我们把常用的几种 Behavior 都使用了一遍,系统的ViewOffsetBehavior 和监听滚动的 Behavior 监听View的 Behavior。

    但是如果只是简单实现上面的效果,我们可以用 AppBarLayout + 内部自带的 Behavior 也能实现类似的效果,AppBarLayout内部已经封装并使用了 Behavior 。我们看看如何实现。

    二、CoordinatorLayout + AppBarLayout

    其实内部也是基于 Behavior 实现的,内部实现为 HeaderBehavior 和 HeaderScrollingViewBehavior 。

    对一些场景使用进行了封装,滚动效果,吸顶效果,折叠效果等。我们看看同样的效果,使用 AppBarLayout 如何实现吧:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 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="@color/white"
        android:orientation="vertical">
    
        <com.guadou.lib_baselib.view.titlebar.EasyTitleBar
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:Easy_title="CoordinatorLayout+AppBarLayout" />
    
        <androidx.coordinatorlayout.widget.CoordinatorLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <com.google.android.material.appbar.AppBarLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:elevation="0dp"
                android:background="@color/white"
                android:orientation="vertical">
    
                <ImageView
                    android:layout_width="match_parent"
                    android:layout_height="150dp"
                    android:contentDescription="我是测试的图片"
                    android:src="@mipmap/ic_launcher"
                    app:layout_scrollFlags="scroll" />
    
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#ccc"
                    android:gravity="center"
                    android:text="我是测试的分割线"
                    app:layout_scrollFlags="noScroll" />
    
            </com.google.android.material.appbar.AppBarLayout>
    
            <androidx.core.widget.NestedScrollView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:layout_behavior="@string/appbar_scrolling_view_behavior">
    
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="@string/scroll_content" />
    
            </androidx.core.widget.NestedScrollView>
    
        </androidx.coordinatorlayout.widget.CoordinatorLayout>
    
    </LinearLayout>
    

    类似的效果我们都能使用 AppbarLayout 来实现,比如一些详情页面顶部图片,下面列表或ViewPager的都可以使用这种方式,更加的便捷。

    三、MotionLayout

    不管怎么说,AppbarLayout 只能实现一些简单的效果,如果想要一些粒度比较细的效果,我们还得使用自定义 Behavior 来实现,但是它的实现确实是有点复杂,2019年谷歌推出了 MotionLayout 。

    淘宝的出现可以说让世上没有难做的生意,那么 MotionLayout 的出现可以说让 Android 没有难实现的动画了。不管是动画效果,滚动效果,MotionLayout 绝杀!能用 Behavior 实现的 MotionLayout 几乎是都能做。

    使用 MotionLayout 我们只需要定义起始点和结束点就行了,我们这里不需要根据百分比Fram进行别的操作,所以只定义最简单的使用。

    我们看看如何用 MotionLayout 实现同样的效果:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:background="@color/white"
        android:orientation="vertical">
    
        <com.guadou.lib_baselib.view.titlebar.EasyTitleBar
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:Easy_title="MotionLayout的动作" />
    
        <androidx.constraintlayout.motion.widget.MotionLayout
            android:layout_width="match_parent"
            android:layout_weight="1"
            app:layoutDescription="@xml/scene_scroll_13"
            android:layout_height="0dp">
    
            <ImageView
                android:id="@+id/iv_img"
                android:layout_width="150dp"
                android:layout_height="150dp"
                android:scaleType="centerCrop"
                android:contentDescription="我是测试的图片"
                android:src="@mipmap/ic_launcher" />
    
            <TextView
                android:id="@+id/tv_message"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:background="#ccc"
                android:gravity="center"
                android:text="我是测试的分割线"
                tools:layout_editor_absoluteY="150dp" />
    
            <androidx.core.widget.NestedScrollView
                android:id="@+id/nestedScroll"
                android:layout_width="match_parent"
                android:layout_height="wrap_content">
    
                <TextView
                    android:id="@+id/nestedScrollLayout"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="@string/scroll_content" />
    
            </androidx.core.widget.NestedScrollView>
    
        </androidx.constraintlayout.motion.widget.MotionLayout>
    
    </LinearLayout>
    

    定义的scene_scroll_13.xml

    <?xml version="1.0" encoding="utf-8"?>
    <MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:motion="http://schemas.android.com/apk/res-auto">
    
        <Transition
            motion:constraintSetEnd="@+id/end"
            motion:constraintSetStart="@+id/start">
    
            <OnSwipe
                motion:dragDirection="dragUp"
                motion:touchAnchorId="@id/nestedScroll" />
    
        </Transition>
    
        <ConstraintSet android:id="@+id/start">
    
            <Constraint
                android:id="@id/iv_img"
                android:layout_width="150dp"
                android:layout_height="150dp"
                android:translationY="0dp"
                motion:layout_constraintLeft_toLeftOf="parent"
                motion:layout_constraintRight_toRightOf="parent"
                motion:layout_constraintTop_toTopOf="parent" />
    
            <Constraint
                android:id="@id/tv_message"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                motion:layout_constraintTop_toBottomOf="@id/iv_img" />
    
            <Constraint
                android:id="@id/nestedScroll"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintTop_toBottomOf="@id/tv_message" />
    
        </ConstraintSet>
    
        <ConstraintSet android:id="@+id/end">
    
            <Constraint
                android:id="@id/iv_img"
                android:layout_width="150dp"
                android:layout_height="150dp"
                android:translationY="-150dp"
                motion:layout_constraintLeft_toLeftOf="parent"
                motion:layout_constraintRight_toRightOf="parent"
                motion:layout_constraintTop_toTopOf="parent" />
    
            <Constraint
                android:id="@id/tv_message"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                motion:layout_constraintLeft_toLeftOf="parent"
                motion:layout_constraintRight_toRightOf="parent"
                motion:layout_constraintTop_toTopOf="parent" />
    
            <Constraint
                android:id="@id/nestedScroll"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintTop_toBottomOf="@id/tv_message" />
    
        </ConstraintSet>
    
    </MotionScene>
    

    效果:

    非常的简单,效果很流畅,性能也很好。有时候都不得不感慨一句,有了 MotionLayout 要你 Behavior 何用。

    来自:https://juejin.cn/post/7109291118910504996

    相关文章

      网友评论

          本文标题:Android协调滚动的几种实现方式

          本文链接:https://www.haomeiwen.com/subject/zalqfdtx.html