CoordinatorLayout三部曲學習之二:CoordinateLayout源碼學習

參考文章:php

http://androidwing.net/index.php/70java

https://www.jianshu.com/p/f7989a2a3ec2 (能夠看這篇文章)node

今天記錄下CoordinateLayout源碼學習的過程,下週又要在CoordinateLayout上面作文章了,實現暫時也還沒啥思路,不看源碼也不行了。android

關於CoordinateLayout的使用,這裏就不詳細說明了,不懂得能夠查閱這篇文章app

這裏研究的是排除anchor做用的源碼ide

這裏先放上個一個簡單的例子吧:函數

<android.support.design.widget.CoordinatorLayout
    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:id="@+id/coordinator"
    tools:context=".photo.TestActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:id="@+id/appbar"
        android:layout_height="220dp"
        android:background="#ffffff">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_scrollFlags="scroll"
            android:orientation="vertical">
        <View
            android:layout_width="match_parent"
            android:id="@+id/edit"
            android:background="#e29de3"
            android:layout_height="50dp">

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

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="500dp"
        android:background="#1d9d29"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <View
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:layout_margin="20dp"
                android:background="#d9ee33">

            </View>

            <View
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:layout_margin="20dp"
                android:background="#2277dd">

            </View>

            <View
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:layout_margin="20dp"
                android:background="#2277dd">

            </View>

            <View
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:layout_margin="20dp"
                android:background="#2277dd">

            </View>

            <View
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:layout_margin="20dp"
                android:background="#2277dd">

            </View>

            <View
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:layout_margin="20dp"
                android:background="#2277dd">

            </View>

            <View
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:layout_margin="20dp"
                android:background="#2277dd">

            </View>

            <View
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:layout_margin="20dp"
                android:background="#dd2288">

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

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

實現的效果以下:學習

代碼很簡單,經過一個appbar_scrolling_view_behavior就可以直接實現聯動的效果,那麼首先就介紹一下解耦父View和子View的關鍵類:Behavior吧。ui

Behavior定義在CoordinateLayout內部:this

public static abstract class Behavior<V extends View> {

        ...
      //在子View獲取觸摸事件以前調用
        public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
            return false;
        }
		//在Behavior開始時候調用
        public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
            return false;
        }
		//依賴選擇
        public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }

       //依賴選擇後的變化回調
        public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }

       ...
 		//底下的方法跟NestedScrollingParent中的解釋一致
        public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                @ScrollAxis int axes, @NestedScrollType int type) {
            if (type == ViewCompat.TYPE_TOUCH) {
                return onStartNestedScroll(coordinatorLayout, child, directTargetChild,
                        target, axes);
            }
            return false;
        }

  
        @Deprecated
        public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                @ScrollAxis int axes) {
            // Do nothing
        }

   
        public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                @ScrollAxis int axes, @NestedScrollType int type) {
            if (type == ViewCompat.TYPE_TOUCH) {
                onNestedScrollAccepted(coordinatorLayout, child, directTargetChild,
                        target, axes);
            }
        }

      
        @Deprecated
        public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target) {
            // Do nothing
        }

  
        public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, @NestedScrollType int type) {
            if (type == ViewCompat.TYPE_TOUCH) {
                onStopNestedScroll(coordinatorLayout, child, target);
            }
        }

    
        @Deprecated
        public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
                @NonNull View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed) {
            // Do nothing
        }

        public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
                @NonNull View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type) {
            if (type == ViewCompat.TYPE_TOUCH) {
                onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
                        dxUnconsumed, dyUnconsumed);
            }
        }

        @Deprecated
        public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed) {
            // Do nothing
        }
        public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed,
                @NestedScrollType int type) {
            if (type == ViewCompat.TYPE_TOUCH) {
                onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
            }
        }

        public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, float velocityX, float velocityY,
                boolean consumed) {
            return false;
        }

        public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, float velocityX, float velocityY) {
            return false;
        }

        ....
    }

上述的方法中主要重要的爲兩個,layoutDependsOn()和onDependentViewChanged(),這裏詳細拿出來講一下吧。借用wing神寫的文章的話:

其實Behavior就是一個應用於View的觀察者模式,一個View跟隨者另外一個View的變化而變化,或者說一個View監聽另外一個View。在Behavior中,被觀察View 也就是事件源被稱爲denpendcy,而觀察View,則被稱爲child。

layoutDependsOn() 表明尋找被觀察View.

onDependentViewChanged() 被觀察View變化的時候回調用的方法.

這裏舉個例子,分別定義一個TextView,Button,EditText和ImageButton,讓ImageButton跟着EditText移動,EditText跟着Button移動,Button跟着TextView,因爲全部Behavior都基本同樣,惟一區別就是layoutDependsOn()中的判斷,爲了節省篇幅,貼一個Button跟着TextView移動的Behavior:

public class TextBehavior extends CoordinatorLayout.Behavior<View> {
...
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency.getId==R.id.textView;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        child.setY(dependency.getY());
        return true;
    }
}

xml文件以下,省略無關代碼:

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

    <TextView
        android:id="@+id/textView"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:background="#22ee22"
        android:gravity="center"
        android:text="TextView"
        />

    <Button
        app:layout_behavior=".TextBehavior"/>

    <EditText
        app:layout_behavior=".ButtonBehavior"/>

    <ImageButton
        app:layout_behavior=".EditBehavior"
/>
</android.support.design.widget.CoordinatorLayout>

layoutDependsOn中dependency爲TextView的時候爲true,即咱們移動了TextView時候就會觸發onDependentViewChanged()方法,而後再onDependentViewChanged()操做Button的座標Y,而後依次觸發後續的View的移動。效果以下:

在CoordinateLayout中設置Behavior有兩種方式,一種是經過LayoutParam調用setBehavior()直接設置,一種是經過反射的方式,即經過在子View的xml中的layout_behavior來獲取對應的Behavior,這裏也都不敘述了。

OK,Behavior的介紹就到這,下面咱們就研究一下CoordinateLayout源碼吧, 構造函數中沒什麼關鍵信息,直接看onAttachTowindow(),在onAttachToWindow()中初始化了OnPreDrawListener類,在其回調onPreDraw()實現了onChildViewsChanged(EVENT_PRE_DRAW),這裏因爲onPreDraw()回調當視圖樹將要被繪製時,因此咱們先放着,接着繼續往下看onMeasure()方法:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        prepareChildren();
        ensurePreDrawListener();
		...//肯定寬高
		 final Behavior b = lp.getBehavior();
		 //是否由child自主測量寬高
         if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0)) {
                onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                        childHeightMeasureSpec, 0);
            }
			...
		}

在測量方法中主要看prepareChildren(),這裏面會實例化一個的實例變量:mDependencySortedChildren

private void prepareChildren() {
        mDependencySortedChildren.clear();
        mChildDag.clear();
		//a
        for (int i = 0, count = getChildCount(); i < count; i++) {
            final View view = getChildAt(i);

            final LayoutParams lp = getResolvedLayoutParams(view);
            lp.findAnchorView(this, view);
			//1
            mChildDag.addNode(view);

			//b
            for (int j = 0; j < count; j++) {
                if (j == i) {
                    continue;
                }
                final View other = getChildAt(j);
				//2
                if (lp.dependsOn(this, view, other)) {
                    if (!mChildDag.contains(other)) {
                        // Make sure that the other node is added
                        mChildDag.addNode(other);
                    }
                    // Now add the dependency to the graph
                    mChildDag.addEdge(other, view);
                }
            }
        }

        //3
        mDependencySortedChildren.addAll(mChildDag.getSortedList());
        // We also need to reverse the result since we want the start of the list to contain
        // Views which have no dependencies, then dependent views after that
        Collections.reverse(mDependencySortedChildren);
    }

首先整理一下Demo中的對應關係:

分析該段代碼,咱們使用上面的自定義聯動的四個View的Demo進行分析,在a循環代碼段執行完畢後,在mChildDrag中的Map會依次記錄下各個View的依賴關係:

能夠看下mChildDrag的結構,以及對應方法:

public final class DirectedAcyclicGraph<T> {
    private final Pools.Pool<ArrayList<T>> mListPool = new Pools.SimplePool<>(10);
    private final SimpleArrayMap<T, ArrayList<T>> mGraph = new SimpleArrayMap<>();
	...
	public void addNode(@NonNull T node) {
        if (!mGraph.containsKey(node)) {
            mGraph.put(node, null);
        }
    }
	...
	public void addEdge(@NonNull T node, @NonNull T incomingEdge) {
       ...
        ArrayList<T> edges = mGraph.get(node);
        if (edges == null) {
            // If edges is null, we should try and get one from the pool and add it to the graph
            edges = getEmptyList();
            mGraph.put(node, edges);
        }
        // Finally add the edge to the list
        edges.add(incomingEdge);
    }
	...
}

prepareChildren()執行完畢後,mDependencySortedChildren按依賴關係排序,被依賴者排在前面,在上述Demo中即TextView,Button,EditText,ImageButton這樣排序。

看完onMeasure(....)後再看下onLayout(...)方法:

protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            if (child.getVisibility() == GONE) {
                // If the child is GONE, skip...
                continue;
            }

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Behavior behavior = lp.getBehavior();
			//是否代理給Behavior進行layout
            if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {				//由CoordinatorLayout進行layout
                onLayoutChild(child, layoutDirection);
            }
        }
    }

layout方法比較簡單,經過咱們在onMeasure()方法中獲取到的mDependencySortedChildren來進行layout。

接下來再說一下NestScrolling的問題吧,CoordinateLayout中實現了NestedScrollingParent2接口,意味着能夠進行聯動實現,在CoordinatorLayout源碼中有關於NestedScrollingParent2的方法皆由Behavior進行實現,Behavior也實現了NestedScrollingParent2接口,至關於CoordinatorLayout是Behavior的代理,這裏就不貼源碼了,因爲上一篇文章中分析過NestedScrollingParent和NestedScrollingChild的源碼。

再回來從新看下OnPreDrawListener類,在其回調onPreDraw()實現了onChildViewsChanged(EVENT_PRE_DRAW)的方法:

final void onChildViewsChanged(@DispatchChangeEvent final int type) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        final int childCount = mDependencySortedChildren.size();
        final Rect inset = acquireTempRect();
        final Rect drawRect = acquireTempRect();
        final Rect lastDrawRect = acquireTempRect();
		
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);//獲取對應的View
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
          ...
			if (type != EVENT_VIEW_REMOVED) {//若是當前View所在視圖沒有變化,則進入下次循環
                // Did it change? if not continue
                getLastChildRect(child, lastDrawRect);
                if (lastDrawRect.equals(drawRect)) {
                    continue;
                	}
                recordLastChildRect(child, drawRect);
            	}
			...
            // Update any behavior-dependent views for the change
            for (int j = i + 1; j < childCount; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                final Behavior b = checkLp.getBehavior();
				//通知依賴於的View進行對應的回調
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    ...
                    final boolean handled;
					//回調Behavior對應的方法
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            // EVENT_VIEW_REMOVED means that we need to dispatch
                            // onDependentViewRemoved() instead
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            // Otherwise we dispatch onDependentViewChanged()
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }

                    if (type == EVENT_NESTED_SCROLL) {
                        // If this is from a nested scroll, set the flag so that we may skip
                        // any resulting onPreDraw dispatch (if needed)
                        checkLp.setChangedAfterNestedScroll(handled);
                    }
                }
            }
        }

        releaseTempRect(inset);
        releaseTempRect(drawRect);
        releaseTempRect(lastDrawRect);
    }

上面就能夠看見咱們在Behavior中可能會重寫的方法layoutDependsOn()以及onDependentViewChanged()了。

總結一下Behavior的做用,對於Behavior而言,總共有兩個做用:

1.監聽CoordinatorLayout內部子View的滑動,從而進行滑動處理,即NestScrollingParent2以及NestScrollingChild聯動,主要由Behavior中的NestScrollingParent2接口實現。

2.一個子View監聽CoordinatorLayout內部另外一個子View的位置,大小等變換,主要由layoutDependsOn()以及onDependentViewChanged()實現。

關於觸摸事件,在CoordinatorLayout中的onInterceptEvent(..)以及onTouchEvent(..)中也都是交由被Behavior處理,CoordinatorLayout中自己並無進行額外的處理,這裏也就不詳細分析了,有興趣能夠本身看下源碼。

有個問題,從上面的分析來看CoordinatorLayout至關於一個FrameLayout,並無進行什麼滑動的操做,那麼爲何AppBarLayout能夠滑動呢?這個問題一開始看完源碼後就直接出如今腦子裏面了,後來一想能夠經過Behavior委託給AppbarLayout進行滑動的處理啊,後面看AppbarLayout源碼的時候也證明這點。

那麼CoordinatorLayout源碼整體的思路大概清楚了,經過委託給Behavior的方式,CoordinatorLayout自己不作什麼處理,全部操做都由子View的自定義Behavior進行實現,充分解耦子View和CoordinatorLayout的邏輯,不得不佩服google大牛的擼代碼手法與思路。下篇文章須要分析一下AppBarLayout跟CoordinatorLayout的關係,理清對應滑動的流程。

相關文章
相關標籤/搜索