[Digging] 支付宝首页交互三部曲 1 CoordinatorLayout和Behavior

《[Digging] 支付宝首页交互三部曲 1 CoordinatorLayout和Behavior》 cover_1

博客原文:kyleduo.com

前言

这个系列源自前几天看到一篇使用CoordinatorLayout实现支付宝首页效果的文章,下载看了效果和源码,不敢苟同,所以打算自己动手。实现的过程有点曲折,但也发现了一些有意思的事情,用三篇文章来记录并分享给大家。

  • CoordinatorLayout和Behavior
  • 自定义CoordinatorLayout.Behavior
  • 支付宝首页效果实现

文中:CoL代表CoordinatorLayout,ABL表示AppBarLayout,CTL表示CollapsingToolbarLayout,SRL表示SwipeRefreshLayout,RV表示RecyclerView。

源码:Github

第一篇文章主要讨论Behavior的结构、CoordinatorLayout的实现以及CoordinatorLayout和Behavior之间的通信。除了Behavior相关的内容,CoordinatorLayout作为官方实现的一个ViewGroup,也有一些在自定义ViewGroup时可以借鉴的内容,这些也穿插在这篇文章中。

Behavior结构

使用CoordinatorLayout结合ABL,CTL,SRL/RV可以方便的实现各种MaterialDesign ToolBar效果,还有FloatingActionButton,SnackBar等控件,可以直接使用。CoL的主要功能是为其直接子View提供 协调滚动 的统一接口,让子View可以方便的实现诸如嵌套滚动,跟随滚动等效果,让界面更加灵动;而这个统一接口,就是Behavior。

我们先来看一下Behavior提供的方法,大致可以分为4组:

布局相关,这类方法用来重载child的Measure、Layout相关回调。

public boolean onMeasureChild(CoordinatorLayout parent, V child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed);
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection);
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency);
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency);
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency);

Touch事件相关,这组方法用来拦截和处理Touch事件传递。

public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev);
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev);
public boolean blocksInteractionBelow(CoordinatorLayout parent, V child);

NestedScrolling相关,这组方法用来响应NestedScrolling,更多关于NestedScrolling的讨论可以查看另一篇博客

public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes);
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target);
public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed);
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY, boolean consumed);
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY);

其他辅助方法

// 关联/取消关联LayoutParams的时候回调
public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params);
public void onDetachedFromLayoutParams();
public WindowInsetsCompat onApplyWindowInsets(CoordinatorLayout coordinatorLayout, V child, WindowInsetsCompat insets);
// 控制Scrim效果,只有当getScrimOpacity返回值不为0时才绘制。
public int getScrimColor(CoordinatorLayout parent, V child);
public float getScrimOpacity(CoordinatorLayout parent, V child);
// 暂时没发现用到的地方
public static void setTag(View child, Object tag);
public static Object getTag(View child);
public boolean onRequestChildRectangleOnScreen(CoordinatorLayout coordinatorLayout, V child, Rect rectangle, boolean immediate);
public void onRestoreInstanceState(CoordinatorLayout parent, V child, Parcelable state);
public Parcelable onSaveInstanceState(CoordinatorLayout parent, V child);
// 防止CoL子View间出现遮挡,获取child应避免遮挡部分的Rect。
public boolean getInsetDodgeRect(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull Rect rect);

多数情况下我们只需要关注前三组方法。通过这些方法我们可以看到Behavior可以做的事情不仅仅是“依赖某个View的变化并且在其变化之后进行响应”这么单一,Behavior实际上可以控制child在CoL中的measure,layout以及拦截toush事件,支持NestedScrolling等等,基本上是除了Draw之外的全部自定义View需要关注的内容了。

Behavior的使用和自定义我们下一篇文章进行讨论,这篇文章我们继续关注CoL如何操作Behavior。

设置Behavior

给一个View设置Behavior有两种方法:在布局文件中指定;使用@CoordinatorLayout.DefaultBehavior注解。

第一种方法适用于多数情况,需要注意的是,如果使用自定义Behavior,需要覆写2个参数的构造方法:

public Behavior(Context context, AttributeSet attrs);

因为CoL在解析时是通过反射调用Behavior的这个构造方法创建Behavior对象的:

static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
    // {...}
  
    try {
        Map<String, Constructor<Behavior>> constructors = sConstructors.get();
        if (constructors == null) {
            constructors = new HashMap<>();
            sConstructors.set(constructors);
        }
        Constructor<Behavior> c = constructors.get(fullName);
        if (c == null) {
            final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
                    context.getClassLoader());
            c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
            c.setAccessible(true);
            constructors.put(fullName, c);
        }
        return c.newInstance(context, attrs);
    } catch (Exception e) {
        throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
    }
}

第二种方法,适用于自定义View并且自定义Behavior的情况,比如AppBarLayout:

@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
  // {...}
}

如果在布局文件中不另外指定,这里将调用Behavior的无参构造方法创建对象。

事件分发

上面看到的Behavior的各个方法,其调用者基本都是CoL。CoL在自己的回调方法中通过调用子View Behavior的相关方法,将事件向下分发。

在讨论分发方法之前,有一点需要注意:Behavior虽然影响的是子View的布局和行为,但实际是对CoL本身事件处理的代理

基本的模式大家可以想到,就是遍历子View,获取Behavior,然后调用子Behavior对应的方法。这里对几个有意思的地方进行讨论:

Touch事件分发

Touch事件分发分两个方法onInterceptTouchEvent和onTouchEvent。CoL的实现中,这两个方法都调用performIntercept方法将是否拦截事件的判断交给Behavior处理。每个CoordinatorLayout的子View都有机会拦截事件并响应,注意这里子View并不是在自己的onTouch相关方法中进行处理,而是Behavior子类,有机会代理CoL对事件进行拦截并处理。

出于篇幅考虑这里不贴源码了,关键的地方这里解释一下:

  • 在遍历子View之前,使用getTopSortedChildren(topmostChildList);获取按照显示顺序由上至下排序过的子View列表。ViewGroup可以覆写getChildDrawingOrder自定义子View的绘制顺序(这篇文章中有对这个方法的解释),getTopSortedChildren方法会按照绘制顺序获取子View;在5.0及以上版本中,还要考虑Z轴次序,也就是elevation,会再进行一次排序,最终得到真实可靠的自顶之下的子View分发顺序。这对让子View合理响应Touch事件很重要,如果自定义Group需要有类似功能,可以参考CoL的实现。
  • Behavior通过覆写onInterceptTouchEvent或者onTouchEvent并返回true来声明拦截事件,CoL会将该View缓存到mBehaviorTouchView属性,后续事件将直接分发到该View。直到该View的onTouchEvent方法返回false。
  • 在确定mBehaviorTouchView之后,CoL会将该View(Z轴)下面View的事件流终止,具体操作是向这些View分发一个CANCEL事件。

Behavior可以通过覆写blocksInteractionBelow方法block下方View的事件。在自己不需要处理事件但同时不希望子View处理事件时,可以简单的覆写这个方法。

默认实现逻辑是判断getScrimOpacity的值>0

NestedScrolling

NestedScrolling是Behavior实现滑动的重要支撑。前文提到Behavior是对CoL自身事件的代理,所以Behavior对NestedScrolling的支持,就是在代理CoL的NestedScrollingParent接口方法。

更多NestedScrolling相关信息参见更早的博客:Android Nested Scrolling

需要注意的是关于NestedScrolling机制中“消费量”的处理

@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    int xConsumed = 0;
    int yConsumed = 0;
    boolean accepted = false;

    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View view = getChildAt(i);
        if (view.getVisibility() == GONE) {
            // If the child is GONE, skip...
            continue;
        }

        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
        if (!lp.isNestedScrollAccepted()) {
            continue;
        }

        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            mTempIntPair[0] = mTempIntPair[1] = 0;
            viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);

            xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
                    : Math.min(xConsumed, mTempIntPair[0]);
            yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
                    : Math.min(yConsumed, mTempIntPair[1]);

            accepted = true;
        }
    }

    consumed[0] = xConsumed;
    consumed[1] = yConsumed;

    if (accepted) {
        onChildViewsChanged(EVENT_NESTED_SCROLL);
    }
}

注意这里是取了所有Behavior消费掉偏移量的最大值。因为Behavior是代理的角色,而各个代理的消费对于NestedScrolling机制来说,都会被看做是CoL这个NestedScrollingParent的消费。各Behavior之间是同级的,所以他们对事件的消费是“重叠”的(可以重复消费),所以这里返回的consumed是取最大值。

LayoutDependence

这里是其他博客讲的比较多的地方,确定View依赖的Dependence,当Dependence变化之后,会将变化广播给所有依赖这个View的兄弟View。我要说的有两点:1. onDependentViewChanged回调的调用时机。2. 依赖关系的存储。

  1. onDependentViewChanged在某个View的大小或者位置发生变化的时候都会进行回调。并且是真正变化之后才会进行回调。
  2. 子View之间的依赖关系通过非循环有向图数据结构进行存储。具体到结构上就是通过一个Map<Node, List<Node>>存储(这个Map并不是JDK的实现,感兴趣的可以看下源码)。
  3. 既然存在依赖关系,那么在涉及到对子View遍历的时候,就要考虑到子View之前的依赖关系。CoL的实现中prepareChildren方法构建依赖图并根据依赖图进行DFS搜索得到依赖链列表,这个列表用在了分发布局、NestedScrolling、LayoutDependentChange的过程中。

自定义LayoutParams

自定义ViewGroup很常见,但是多数情况下用不到自定义LayoutParams。LayoutParams正如其名,用了设置布局参数,也就是控制ViewGroup如何measure和layout子View。如果一个自定义ViewGroup提供了额外的布局参数,那就需要自定义LayoutParams了。自定义LayoutParams并没有多么复杂,这里列几个需要注意的地方。

基类

如果自定义LayoutParams需要支持margin,继承自ViewGroup.MarginLayoutParams即可,默认的ViewGroup.LayoutParams并不支持margin。

构造方法

LayoutParams默认有多个不同参数的构造方法:

  • LayoutParams(Context c, AttributeSet attrs) 适用于解析布局文件时生成LayoutParams,layout相关的xml属性,就是在这个构造方法里面解析的。
  • LayoutParams(int width, int height) 代码构建时只传入宽高。
  • LayoutParams(LayoutParams source) LayoutParams转换
  • LayoutParams() 无参构造函数

自定义LayoutParams也要覆写这些构造方法并做响应的转换。

ViewGroup方法

使用自定义LayoutParams的ViewGroup,也需要实现几个相关方法,主要是在解析、addView的时候生成适合的LayoutParams。

protected LayoutParams generateDefaultLayoutParams();
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p);
public LayoutParams generateLayoutParams(AttributeSet attrs);
// 检查LayoutParams是否是自定义的LayoutParams类型
protected boolean checkLayoutParams(ViewGroup.LayoutParams p);

具体实现可以参考CoL。

CoordinatorLayout.LayoutParams

CoL.LP主要实现了基于anchorView的布局和keylines(纵向基准线,子View可以对齐到keyline)的布局、保存Behavior、储存滑动过程中的标记位,具体实现这里就不展开了,逻辑比较简单。

    原文作者:kyleduo
    原文地址: https://www.jianshu.com/p/3a9fd369f0e4
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞