一篇文章搞定《CoordinatorLayout完成电商首页》

前言

Android中为我们提供了一些解决嵌套滑动的方式方法,本篇文章利用这些方式方法来实现和处理一下嵌套滑动,作为实战的实例。
先铺垫一下NestedScrollingParent和NestedScrollingChild,后面利用CoordinatorLayout完成电商首页。

NestedScroll

上篇文章也有说到,其实在一些已经把嵌套滑动解决的控件中。
比如RecyclerView

public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {
    
    

NestedScrollView

public class NestedScrollView extends FrameLayout implements NestedScrollingParent3,
        NestedScrollingChild3, ScrollingView {
    
    

CoordinatorLayout

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2,
        NestedScrollingParent3 {
    
    

可以看到在一些滑动的组件中都继承了NestedScrollingChild或者NestedScrollingParent来对嵌套滑动进行了处理。所以大家在使用这些组件进行嵌套时,会发现没什么嵌套滑动的问题。
那我们来看看NestedScrollingChild、NestedScrollingParent都有什么因为我们下面要使用CoordinatorLayout

NestedScrollingParent

什么时候去继承:可滑动的ViewGroup作为父容器的时候,那么就需要实现这个接口。
实现这个接口主要是想去重写他的几个方法,来帮助我们解决滑动冲突,所以这块不用太纠结。把这几个方法了解了就行:

public interface NestedScrollingParent {
    
    
    /**
     * React to a descendant view initiating a nestable scroll operation, claiming the
     * nested scroll operation if appropriate.

     * 当子View调用startNestedScroll方法的时候,父容器会在这个方法中获取回调
     */
    boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes);

    /**
     * React to the successful claiming of a nested scroll operation.
     * 在onStartNestedScroll调用之后,就紧接着调用这个方法
     */
    void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes);

    /**
     * React to a nested scroll operation ending.
     * 当子View调用 stopNestedScroll方法的时候回调
     */
    void onStopNestedScroll(@NonNull View target);

    /**
     * React to a nested scroll in progress.
     *
     */
    void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
    int dxUnconsumed, int dyUnconsumed);

    /**
     * React to a nested scroll in progress before the target view consumes a portion of the scroll.
     * 在子View调用dispatchNestedPreScroll之后,这个方法拿到了回调
     *
     */
    void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);

    /**
     * Request a fling from a nested scroll.
     *
     */
    boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);

    /**
     * React to a nested fling before the target view consumes it.
     *
     */
    boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);

    /**
     * Return the current axes of nested scrolling for this NestedScrollingParent.
     * 返回当前滑动的方向
     */
    @ScrollAxis
    int getNestedScrollAxes();
}

NestedScrollingChild

什么时候去继承:作为可滑动的子View,那么就需要实现NestedScrollingChild接口。

public interface NestedScrollingChild {
    
    
    /**
     * Enable or disable nested scrolling for this view.
     *
     * 启动或者禁用嵌套滑动,如果返回ture,那么说明当前布局存在嵌套滑动的场景,反之没有
     * 使用场景:NestedScrollingParent嵌套NestedScrollingChild
     * 在此接口中的方法,都是交给NestedScrollingChildHelper代理类实现
     */
    void setNestedScrollingEnabled(boolean enabled);

    /**
     * Returns true if nested scrolling is enabled for this view.
     * 其实就是返回setNestedScrollingEnabled中设置的值
     */
    boolean isNestedScrollingEnabled();

    /**
     * Begin a nestable scroll operation along the given axes.
     * 表示view开始滚动了,一般是在ACTION_DOWN中调用,如果返回true则表示父布局支持嵌套滚动。
     * 一般也是直接代理给NestedScrollingChildHelper的同名方法即可。这个时候正常情况会触发Parent的onStartNestedScroll()方法
     */
    boolean startNestedScroll(@ScrollAxis int axes);

    /**
     * Stop a nested scroll in progress.
     * 停止嵌套滚动,一般在UP或者CANCEL事件中执行,告诉父容器已经停止了嵌套滑动
     */
    void stopNestedScroll();

    /**
     * Returns true if this view has a nested scrolling parent.
     * 判断当前View是否存在嵌套滑动的Parent
     */
    boolean hasNestedScrollingParent();

    /**
     * 当前View消费滑动事件之后,滚动一段距离之后,把剩余的距离回调给父容器,父容器知道当前剩余距离
     * dxConsumed:x轴滚动的距离
     * dyConsumed:y轴滚动的距离
     * dxUnconsumed:x轴未消费的距离
     * dyUnconsumed:y轴未消费的距离
     * 这个方法是嵌套滑动的时候调用才有用,返回值 true分发成功;false 分发失败
     */
    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
    int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);

    /**
     * Dispatch one step of a nested scroll in progress before this view consumes any portion of it.
     * 在子View消费滑动距离之前,将滑动距离传递给父容器,相当于把消费权交给parent
     * dx:当前水平方向滑动的距离
     * dy:当前垂直方向滑动的距离
     * consumed:输出参数,会将Parent消费掉的距离封装进该参数consumed[0]代表水平方向,consumed[1]代表垂直方向
     * @return true:代表Parent消费了滚动距离
     */
    boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
    @Nullable int[] offsetInWindow);

    /**
     * Dispatch one step of a nested scroll in progress.
     * 处理惯性事件,与dispatchNestedScroll类似,也是在消费事件之后,将消费和未消费的距离都传递给父容器
     */
    boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    /**
     * Dispatch a fling to a nested scrolling parent before it is processed by this view.
     * 与dispatchNestedPreScroll类似,在消费之前首先会传递给父容器,把优先处理权交给父容器
     */
    boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

通过这两个接口,我们大概就能够明白,其实嵌套滑动机制完全是子View在做主导,通过子View能够决定Parent是否能够优先消费事件(dispatchNestedPreScroll)。

NestedScrollingChildHelper 和 NestedScrollingParentHelper

NestedScrollingChildHelper 和 NestedScrollingParentHelper 类的作用:主要是帮助内部View和外部View实现交互逻辑。
举个例子就知道了:
在实现了NestedScrollingChild就需要声明NestedScrollingChildHelper进行

public class NestedChildView extends View implements NestedScrollingChild {
    
    
    ......
    private NestedScrollingChildHelper mScrollingChildHelper;
    
    private void init() {
    
    
        mScrollingChildHelper = new NestedScrollingChildHelper(this);
    }
    .....
    
    /**
     * 设置是否允许嵌套滑动
     */
     @Overridepublic void setNestedScrollingEnabled(boolean enabled) {
    
    
         //通过mScrollingChildHelper进行通信
         mScrollingChildHelper.setNestedScrollingEnabled(enabled);
     }
     
     /**
     * 是否允许嵌套滑动
     */
     @Overridepublic boolean isNestedScrollingEnabled() {
    
    
         return mScrollingChildHelper.isNestedScrollingEnabled();
     }
     .....

知道干啥的了吧,不用整那么细,把上面的方法都解读一下。了解有什么作用,用的时候再去细看就行。都是事件分发那一套。

CoordinatorLayout

CoordinatorLayout翻译为协调者布局,是用来协调其子View们之间动作的一个容器,他是一个超级强大的FrameLayout,结合AppBarLayout、 CollapsingToolbarLayout等可产生各种炫酷的效果。
下面用一个例子来讲解
以下面的图片为例:
在这里插入图片描述
我们按照这样的架构实现下面的交互功能:
1、上面为一个headerView模块我们以一个图片模块代替
2、中间为一个TabLayout,有多个Table结合下面的ViewPage
3、下面为ViewPage中的Fragment,其中用RecyclerView展示我们的商品
4、TabLayout可以吸顶
5、当headerView没有完全隐藏的情况下在RecyclerView上滑动时,优先滑动headerView直至隐藏后RecyclerView继续滑动。
6、在headView上向下fling,会将剩余的惯性传递到RecyclerView。
下面是小编实现的一个效果:

在这里插入图片描述
接下来看看都是怎么实现的一步一步进行。
首先实现前三步:(这是比较常规的代码了)
1、上面为一个headerView模块我们以一个图片模块代替
2、中间为一个TabLayout,有多个Table结合下面的ViewPage
3、下面为ViewPage中的Fragment,其中用RecyclerView展示我们的商品

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <ImageView
        android:id="@+id/head_view"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:scaleType="fitXY"
        android:src="@mipmap/image" />

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:background="@color/pick"/>

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/view_page"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/black"/>
</LinearLayout>

首先实现这三个模块(内部代码自己写吧后面我会把代码地址给出来)
在这里插入图片描述
大概就是这个样子。
之后大家运行一下发现,几乎跟我们上面实现的效果不搭边。 那我们就继续实现:
4、TabLayout可以吸顶
5、当headerView没有完全隐藏的情况下在RecyclerView上滑动时,优先滑动headerView直至隐藏后RecyclerView继续滑动。
6、在headView上向下fling,会将剩余的惯性传递到RecyclerView。
这时候就要用到CoordinatorLayout布局了。

CoordinatorLayout知识点讲解

1、CoordinatorLayout是什么布局?
他是协调者布局,本身是个FrameLayout,是继承与NestedScrollingParent2和NestedScrollingParent3来解决滑动冲突。

CoordinatorLayout is a super-powered FrameLayout. //API中的解释

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2,
        NestedScrollingParent3 {
    
    

2、协调员是谁?
协调员就是我们的父布局CoordinatorLayout
3、协调谁?
CoordinatorLayout的子女们
4、通过谁来协调?
通过Behavior来萧条
5、协调什么内容?
代理儿女的事件流程、相应兄弟的变化、代理儿女的绘制

实现后续功能

4、TabLayout可以吸顶
5、当headerView没有完全隐藏的情况下在RecyclerView上滑动时,优先滑动headerView直至隐藏后RecyclerView继续滑动。
其中这两项功能通过我们的现有API代码就可以实现。这里我先将代码给大家,再进一步讲解。

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appbar_layout"
        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$Behavior"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:id="@+id/head_view"
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:scaleType="fitXY"
                android:src="@mipmap/image" />
        </com.google.android.material.appbar.CollapsingToolbarLayout>

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tab_layout"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:background="@color/pick"/>
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/view_page"
        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

我们只需要改变我们的XML就可以实现这个效果。不需要改动任何之前实现的代码。
那这里是怎么处理的呢?
这里是通过:
a、AppBarLayout实现吸顶、并配合CollapsingToolbarLayout实现head的ImageView进行折叠的效果。这里不清楚的单独去查一下AppBarLayout和CollapsingToolbarLayout的作用,毕竟是一个布局这里就不细说了。来实现我们的吸顶
b、通过AppBarLayout S c r o l l i n g V i e w B e h a v i o r 将滑动先传递给 A p p B a r L a y o u t ScrollingViewBehavior将滑动先传递给AppBarLayout ScrollingViewBehavior将滑动先传递给AppBarLayoutBehavior来实现。当headerView没有完全隐藏的情况下在RecyclerView上滑动时,优先滑动headerView直至隐藏后RecyclerView继续滑动。

这样我的4、和5也完成了

6、在headView上向下Fling,会将剩余的惯性传递到RecyclerView
这个需要我们稍微的麻烦一些,通过自定义的Behavior来实现效果。在下面哦,先铺垫一下Behavior的知识。

通过自定义Behavior实现Fling效果

认识Behavior

1、首先Behavior是什么?
Behavior是一个可以控制子View间互动的类
2、Behavior能做什么?
可以用来实现类似于嵌套的ScrollView顶部悬浮的效果,或者实现联动效果,例如当一个View移动时,其他的View随之进行相应的调整。它可以通过实现Behavior类或者继承已有的Behavior类来自定义Behavior,实现在不同的交互场景下子View的动态协调。
3、有几种常见的Behavior

Behavior 功能作用
AppBarLayout.ScrollingViewBehavior 用于将可滚动视图与AppBarLayout关联,实现当可滚动视图滚动时,AppBarLayout随之滚动,实现顶部标题栏随着内容的滚动而发生变化。
BottomSheetBehavior 用于在屏幕的底部展示一个可滑动的特定区域,一般用来显示应用程序的底部菜单或者对话框。
SwipeDismissBehavior 用于给View添加滑动关闭的功能,类似于Android系统自带的Notification 消息滑动删除功能。
HeaderScrollingViewBehavior 用于将一个可滑动控件(如RecyclerView、NestedScrollView)中的headerView与一个具有特殊行为的顶部控件(如Toolbar)关联,实现当滚动视图滚动时,顶部控件也会同步进行动画效果。
FloatingActionButton.Behavior 用于让FloatingActionButton随着滚动事件而出现或者消失。
总体而言,在Android开发中,使用Behavior时要根据实际情况进行选择,不同Behavior之间具有不同的效果,选择合适的Behavior能够大大提升用户体验。

自定义Behavior

想实现Fling的效果还是需要我们来自定义的
先看思路:
第一步:要知道他是Fling这里用ViewConfiguration.get(context).scaledTouchSlop获取并利用ACTION_MOVE判断
第二步:利用onInterceptTouchEvent中ACTION_MOVE判断Fling之后拦截到本层ViewGroup处理事件
第三步:怎么处理事件? 就是将HeaderView的AppBarLayout.Behavior事件交给ViewPage的AppBarLayout.ScrollingViewBehavior事件进行处理。
第四步:(可以说应该是第一步)获取HeaderView的AppBarLayout.Behavior事件
下面代码的中心思想就是围绕着上面的步骤开展的。

class CustomBehavior : AppBarLayout.Behavior {
    
    
    constructor() : super()
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
    
    
        if (mTouchSlop < 0) {
    
    
            mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
        }
    }

    private var mTouchSlop: Int = -1
    private lateinit var mScrollingViewBehaviorView: View
    private var mIsBeginDragged = false
    private var mNeedDispatcher = true
    private var mLastMotionY = 0
    private var mActivityPointId = -1
    private var mCurrentEvent: MotionEvent? = null

    //确认滑动去拦截
    override fun onInterceptTouchEvent(
        parent: CoordinatorLayout,
        child: AppBarLayout,
        ev: MotionEvent
    ): Boolean {
    
    
        Log.e("onInterceptTouchEvent", "onInterceptTouchEvent: " + ev.action)
        when (ev.action) {
    
    
            MotionEvent.ACTION_DOWN -> {
    
    
                mIsBeginDragged = false
                mNeedDispatcher = true
                val x = ev.x
                val y = ev.y.toInt()
                if (parent.isPointInChildBounds(child, x.toInt(), y)) {
    
    
                    mLastMotionY = y
                    mActivityPointId = ev.getPointerId(0)
                    mCurrentEvent?.recycle()
                    mCurrentEvent = MotionEvent.obtain(ev)
                }
            }

            MotionEvent.ACTION_MOVE -> {
    
    
                val activityId = mActivityPointId
                if (activityId != 0 && ev.findPointerIndex(activityId) != -1) {
    
    
                    val moveY = ev.y
                    val diffY = abs(moveY - mLastMotionY)
                    if (diffY > mTouchSlop) {
    
     //说明是滑动动作
                        mIsBeginDragged = true
                    }
                }
            }

            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
    
    
                mIsBeginDragged = false
                mNeedDispatcher = true
                mActivityPointId = -1
            }
        }
        if (!mIsBeginDragged) {
    
    
            return super.(parent, child, ev)
        }
        return true
    }

    //去处理将AppBarLayout.Behavior事件交给AppBarLayout.ScrollingViewBehavior
    override fun onTouchEvent(
        parent: CoordinatorLayout,
        child: AppBarLayout,
        ev: MotionEvent
    ): Boolean {
    
    
        Log.e("onInterceptTouchEvent", "onTouchEvent: " + ev.action)
        var mIsTouchEvent = false
        when (ev.action) {
    
    
            MotionEvent.ACTION_DOWN -> {
    
    
                mIsTouchEvent = false
            }

            MotionEvent.ACTION_MOVE -> {
    
    
                val moveY = ev.y
                val diffY = abs(moveY - mLastMotionY)
                if (diffY > mTouchSlop) {
    
     //说明是滑动动作
                    mIsBeginDragged = true
                }
                if (mIsBeginDragged) {
    
    
                    //位移到下方View
                    val offset = child.height - child.bottom
                    if (mNeedDispatcher) {
    
    
                        mNeedDispatcher = false
                        mCurrentEvent?.offsetLocation(0F, offset.toFloat())
                        //传递下去 进行移交事件
                        mScrollingViewBehaviorView.dispatchTouchEvent(mCurrentEvent)
                    } else {
    
    
                        ev.offsetLocation(0F, offset.toFloat())
                        mScrollingViewBehaviorView.dispatchTouchEvent(ev)
                        mIsTouchEvent = true
                    }
                }
            }

            MotionEvent.ACTION_UP -> {
    
    
                if (mIsBeginDragged) {
    
    
                    ev.offsetLocation(0F, (child.height - child.bottom).toFloat())
                    mScrollingViewBehaviorView.dispatchTouchEvent(ev)
                    mIsTouchEvent = true
                }
            }
        }
        if (!mIsTouchEvent) {
    
    
            return super.onTouchEvent(parent, child, ev)
        }
        return true
    }

    override fun onLayoutChild(
        parent: CoordinatorLayout,
        abl: AppBarLayout,
        layoutDirection: Int
    ): Boolean {
    
    
        val onLayoutChild = super.onLayoutChild(parent, abl, layoutDirection)
        val childCount: Int = parent.childCount
        for (i in 0 until childCount) {
    
    
            val childView: View = parent.getChildAt(i)
            val behavior: CoordinatorLayout.Behavior<AppBarLayout> =
                (childView.layoutParams as CoordinatorLayout.LayoutParams).behavior as CoordinatorLayout.Behavior<AppBarLayout>
            if (behavior is AppBarLayout.ScrollingViewBehavior) {
    
    
                mScrollingViewBehaviorView = childView
            }
        }
        return onLayoutChild
    }
}

总结

虽然Android API提供给我们很多的方式方法去解决滑动嵌套的问题。但是万变不离其宗,还是要清楚的知道事件分发的流程和具体的原因。
不然就算你去使用了API处理了滑动嵌套的问题。一旦出了问题,依旧会一头雾水。

猜你喜欢

转载自blog.csdn.net/weixin_45112340/article/details/131206163
今日推荐