CoordinatorLayout调用原理源码解析

获取Behavior

CoordinatorLayout中子view关联的动作主要由Behavior实现。
系统控件中往往把behavior作为内部类,自己实现,比如AppbarLayout。
当然我们也可以自己实现自定义的Behavior来完成我们需要的动作。
自定义Behavior时,我们需要在xml代码中实现相应属性,例如:

app:layout_behavior="com.wuba.views.CustomAppbarBehavior"

这个就是我们首页动画实现的自定义的behavior。
在xml中实现之后,必然会在代码中获取这个behavior:

public static class LayoutParams extends MarginLayoutParams {
    CoordinatorLayout.Behavior mBehavior;

    LayoutParams(Context context, AttributeSet attrs) {
    super(context, attrs);
    TypedArray a = context.obtainStyledAttributes(attrs, styleable.CoordinatorLayout_LayoutParams);
    this.mBehaviorResolved = a.hasValue(styleable.CoordinatorLayout_LayoutParams_layout_behavior);
    if(this.mBehaviorResolved) {
        this.mBehavior = CoordinatorLayout.parseBehavior(context, attrs, a.getString(styleable.CoordinatorLayout_LayoutParams_layout_behavior));//这个方法获取到behavior的实例
    }
    a.recycle();
}

可以看到,获取behavior的方式和很多自定义view的方式是一样的,通过自定义属性来获取。我们具体看一下parseBehavior()方法是怎么实现的:

static CoordinatorLayout.Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
    if(TextUtils.isEmpty(name)) {
        return null;
    } else {
    String fullName;
    if(name.startsWith(".")) {//如果以.开始,就拼接上包名
        fullName = context.getPackageName() + name;
    } else if(name.indexOf(46) >= 0) {//这里我猜46是类名的最大长度,大于46由没有以.开始,说明它本身就是完整包名
        fullName = name;
    } else {
        fullName = WIDGET_PACKAGE_NAME + '.' + name;//否则就是包名.类名
    }

    try {
        Object e = (Map)sConstructors.get();
        if(e == null) {
            e = new HashMap();
            sConstructors.set(e);
        }

        Constructor c = (Constructor)((Map)e).get(fullName);
        if(c == null) {
            Class clazz = Class.forName(fullName, true, context.getClassLoader());//用反射获取类
            c = clazz.getConstructor(CONSTRUCTOR_PARAMS);//通过构造方法获取实例
           ((Map)e).put(fullName, c);
        }

        return (CoordinatorLayout.Behavior)c.newInstance(new Object[]{context, attrs});
    } catch (Exception var7) {
        throw new RuntimeException("Could not inflate Behavior subclass " + fullName, var7);
    }
}

可以看到这个方法,通过拼接路径,然后用反射的方式获取到了behavior的实例。这样View就成功获取到了behavior。

通用动作的实现

Behavior中有很多可以实现的接口。在我看来关于动作的方法大概可以分成两类。先说第一类;

public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) 
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) 

这两个方法主要实现通用的的关联动作,各种view的改变都可以使用。
下面看一下他们的作用以及调用关系。
前两个方法和主要是在dispatchOnDependentViewChanged方法中调用,我们先看看在哪里调用了这个方法。

class OnPreDrawListener implements android.view.ViewTreeObserver.OnPreDrawListener {
    OnPreDrawListener() {
    }

    public boolean onPreDraw() {
        CoordinatorLayout.this.dispatchOnDependentViewChanged(false);
        return true;
    }
}

主要是在这个listener中,再看一下哪里注册了这个监听:

public void onAttachedToWindow() {
    super.onAttachedToWindow();
    this.resetTouchBehaviors();
    if(this.mNeedsPreDrawListener) {
        if(this.mOnPreDrawListener == null) {
            this.mOnPreDrawListener = new CoordinatorLayout.OnPreDrawListener();
    }

    ViewTreeObserver vto = this.getViewTreeObserver();
    vto.addOnPreDrawListener(this.mOnPreDrawListener);
    }

    this.mIsAttachedToWindow = true;
}

主要是在onAttachedToWindow里。也就是说在coordinatorlayout绑定到窗口是,就会直接注册这个监听,每次要绘制view之前都会i调用dispatchOnDependentViewChanged方法。现在再来看一下刚才提到的dispatchOnDependentViewChanged的具体实现:

void dispatchOnDependentViewChanged(boolean fromNestedScroll) {
    int layoutDirection = ViewCompat.getLayoutDirection(this);
    int childCount = this.mDependencySortedChildren.size();

    for(int i = 0; i < childCount; ++i) {
        View child = (View)this.mDependencySortedChildren.get(i);
        CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)child.getLayoutParams();

        for(int oldRect = 0; oldRect < i; ++oldRect) {
        View newRect = (View)this.mDependencySortedChildren.get(oldRect);
        if(lp.mAnchorDirectChild == newRect) {
            this.offsetChildToAnchor(child, layoutDirection);
            }
        }

        Rect var14 = this.mTempRect1;
        Rect var15 = this.mTempRect2;
        this.getLastChildRect(child, var14);
        this.getChildRect(child, true, var15);
        if(!var14.equals(var15)) {
            this.recordLastChildRect(child, var15);

            for(int j = i + 1; j < childCount; ++j) {
                View checkChild = (View)this.mDependencySortedChildren.get(j);
                CoordinatorLayout.LayoutParams checkLp = (CoordinatorLayout.LayoutParams)checkChild.getLayoutParams();
                CoordinatorLayout.Behavior b = checkLp.getBehavior();
                if(b != null && b.layoutDependsOn(this, checkChild, child)) {
                    if(!fromNestedScroll && checkLp.getChangedAfterNestedScroll()) {
                        checkLp.resetChangedAfterNestedScroll();
                    } else {
                        boolean handled = b.onDependentViewChanged(this, checkChild, child);
                        if(fromNestedScroll) {
                        checkLp.setChangedAfterNestedScroll(handled);
                        }
                    }
                }
            }
        }
    }
}

只看我们关心的部分,也就是最下边的for循环,可以看出coordinatorlayout会循环获取每个子view的behavior。然后在if中看到了b.layoutDependsOn,if里面有b.onDependentViewChanged。
也就是说,当view改变时,如果通过符合我们自己实现的某些条件(这个条件在layoutDependsOn中定义),就会调用onDependentViewChanged方法,实现我们需要的动作。

滚动动作的实现

下面就是第二类,专门针对滚动动作的接口

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) 

可以看到有很多方法,针对各种不同的滚动以及不同的阶段。
这个单在coordinatorlayout中很难看清楚如何调用,因为它涉及到了,接口,以及其他的view。就从事件的源头看,也就是NestedScrollView。
首先看NestedScrollView,它实现了NestedScrollingChild接口,这个接口有很多方法,我们就找startNestedScroll这个方法,这个从名称来看,应该跟上边的onStartNestedScroll方法对应。

@Override
public boolean startNestedScroll(int axes) {
    return mChildHelper.startNestedScroll(axes);
}

先看看哪里调用了这个方法,果然是touch事件触发的:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {   
    final int action = ev.getAction();
    switch (action & MotionEventCompat.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN: {
            startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
            break;
        }
    }
    return mIsBeingDragged;
}

再跟进这个方法,可以看到是在NestedScrollingChildHelper中实现的

public boolean startNestedScroll(int axes) {
    if (hasNestedScrollingParent()) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        while (p != null) {
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                mNestedScrollingParent = p;
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

这个方法主要是循环网上找parent,直到符合条件,条件就是ViewParentCompat.onStartNestedScroll(p, child, mView, axes),具体看一下这个方法:

public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
    int nestedScrollAxes) {
    return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
}

再往下点:

static final ViewParentCompatImpl IMPL;
static {
    final int version = Build.VERSION.SDK_INT;
    if (version >= 21) {
        IMPL = new ViewParentCompatLollipopImpl();
    } else if (version >= 19) {
        IMPL = new ViewParentCompatKitKatImpl();
    } else if (version >= 14) {
        IMPL = new ViewParentCompatICSImpl();
    } else {
        IMPL = new ViewParentCompatStubImpl();
    }
}   

这里是一个版本判断。我们app支持的最小版本是16,所以肯定看第三个:

@Override
public boolean onStartNestedScroll(ViewParent parent, View child, View target,int nestedScrollAxes) {
    if (parent instanceof NestedScrollingParent) {
        return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,nestedScrollAxes);
    }
    return false;
}

所这里可以看到,如果想要实现滚动的关联,要求父view必须实现NestedScrollingParent接口,然后调用这个接口的onStartNestedScroll方法。
好了,现在再返回头来看:

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent 

CoordinatorLayout 刚好实现了这个接口,所以如果它的子view如果实现了NestedScrollingChild接口就可以实现滚动动作的联动了
再来看下这个方法的具体实现:

public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    boolean handled = false;
    int childCount = this.getChildCount();

    for(int i = 0; i < childCount; ++i) {
        View view = this.getChildAt(i);
        CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)view.getLayoutParams();
        CoordinatorLayout.Behavior viewBehavior = lp.getBehavior();
        if(viewBehavior != null) {
            boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target, nestedScrollAxes);
            handled |= accepted;
            lp.acceptNestedScroll(accepted);
        } else {
            lp.acceptNestedScroll(false);
        }
    }

    return handled;
}

可以看到,最终还是通过调用其behavior中的onStartNestedScroll实现了具体的动作。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值