android.support.design 學習筆記 1

dim.red
在appcompat 22 的時候,google帶來了Support Design,成為實現MD的利器,最近因為要開始使用這個庫,稍微過了下庫的內容.

這次主要通過講解當前界面是怎么實現的.來學習這個庫.
布局

布局

看看這個界面的實現,我們主要通過3個方面來了解,

  1. 子控件的寬高的測量
  • 子控件的位置擺放
  • 子控件的事件傳遞

1 測量:

因為它們的根控件是CoordinatorLayout .所以我們重點是放在
CoordinatorLayout 的onMeasure方法里面:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    /**
     * 省略N多代碼
     */
        final Behavior b = lp.getBehavior();
        if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                childHeightMeasureSpec, 0)) {
            onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0);
        }

    /**
     * 省略N多代碼
     */
         
    setMeasuredDimension(width, height);
}

子控件的測量交給他們的Behavior,Behavior 不處理,交給CoordinatorLayout處理 ,Behavior 可以在attr中指定. 可以看出ViewPager的Behavior 是AppBarLayout$ScrollingViewBehavior
,我們進入ScrollingViewBehavior 中的onMeasureChild方法中



@Override
public boolean onMeasureChild(CoordinatorLayout parent, View child,
        int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
        int heightUsed) {
    final int childLpHeight = child.getLayoutParams().height;
    if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
            || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
     /**
     * 省略N多代碼
     */
 

    if (availableHeight == 0) {
       // If the measure spec doesn't specify a size, use the         current height
         availableHeight = parent.getHeight();
     }
     final int height = availableHeight - header.getMeasuredHeight()
                    + getScrollRange(header);
      final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,
                    childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
                            ? View.MeasureSpec.EXACTLY
                            : View.MeasureSpec.AT_MOST);

            // Now measure the scrolling menu with the correct height
      parent.onMeasureChild(child, parentWidthMeasureSpec,
                    widthUsed, heightMeasureSpec, heightUsed);

            return true;
        }
    }
    return false;

}

可以看出來當你的ViewPager的高度不設置固定的值得話,他的高度會被ScrollingViewBehavior重新賦值,高度為CoordinatorLayout的高度減去AppBarLayout的可滑動范圍.(既getTotalScrollRange())

可以看出:當前的ViewPager 的高度比我們當前屏幕上看的要高一點.

AppBarLayout 里面有3個范圍比較有意思.
getTotalScrollRange():表示總共可以滑動的范圍
它是計算所有layout_scrollFlags標有scroll 的View 的高度減去所有同時標有scroll 和 exitUntilCollapsed 的 View 的最小高度.

getDownNestedPreScrollRange():表示當向下滑動可以滑動的范圍.
它計算了所有layout_scrollFlags同時標記scroll 和 enterAlways 同時不標記 enterAlwaysCollapsed的View 的高度 加上既標記了scroll 和 enterAlways又標記了enterAlwaysCollapsed 的最小高度.
產生的效果是:在下滑的過程中AppBarLayout殘留在屏幕上的最小高度為 AppBarLayout本身的高度減去getDownNestedPreScrollRange()的高度.

getUpNestedPreScrollRange():表示當向上滑動可以滑動的范圍.
這里返回的是getTotalScrollRange().
產生的效果是:在上滑的過程中AppBarLayout殘留在屏幕上的最小高度為 AppBarLayout本身的高度減去getUpNestedPreScrollRange()的高.

而這三種范圍構成了 AppBarLayout 在 RecyclerView 滑動事件的滑動效果.

主意點:
  1. exitUntilCollapsed只有和scroll一起組合才會有效果;
  • enterAlwaysCollapsed 要和scroll 和enterAlways一起使用才有效果.
  • 官方說要把帶有scroll flag的view放在前面,這樣收回的view才能讓正常退出,而固定的view繼續留在頂部。
    那是因為AppBarLayout 是一個 LinearLayout 布局.最后留在屏幕上的東西是 AppBarLayout 的底部,所以需要把要固定的 View 放在最后.
  • 這里所有的 View 都是 AppBarLayout 的一級 View.二級不太考慮當中,

下面放出幾個例子來加深大家對layout_scrollFlags和3中范圍的理解.
第一中 正常情況(scroll):

<android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="#f00"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
        app:layout_scrollFlags="scroll" />

    <android.support.design.widget.TabLayout
        android:id="@+id/tabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</android.support.design.widget.AppBarLayout>
demo_0.gif

第2種(minHeight +scroll +exitUntilCollapsed)

<android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="#f00"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
        android:minHeight="20dp"
        app:layout_scrollFlags="scroll|exitUntilCollapsed" />

    <android.support.design.widget.TabLayout
        android:id="@+id/tabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</android.support.design.widget.AppBarLayout>
demo_1.gif

第3種(minHeight +scroll +enterAlways+enterAlwaysCollapsed)


<android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="#f00"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
        android:minHeight="20dp"
        app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed" />

    <android.support.design.widget.TabLayout
        android:id="@+id/tabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</android.support.design.widget.AppBarLayout>
demo_2.gif

2 位置擺放

同樣進入CoordinatorLayout 的onLayout方法


@Override
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);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior behavior = lp.getBehavior();

        if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
            onLayoutChild(child, layoutDirection);
        }
    }
}

同樣可以看到它也是先讓Behavior處理.不處理才是CoordinatorLayout自身去處理.
同樣我們為了查看ViewPager 的擺放,我們進入ScrollingViewBehavior 中的onLayoutChild方法中.

@Override
public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
    // First lay out the child as normal
    super.onLayoutChild(parent, child, layoutDirection);

    // Now offset us correctly to be in the correct position. This is important for things
    // like activity transitions which rely on accurate positioning after the first layout.
    final List<View> dependencies = parent.getDependencies(child);
    for (int i = 0, z = dependencies.size(); i < z; i++) {
        if (updateOffset(parent, child, dependencies.get(i))) {
            // If we updated the offset, break out of the loop now
            break;
        }
    }
    return true;
}

先調用的父類的onLayoutChild 的方法.然后根據dependencies (其實就是AppBarLayout)的getTopBottomOffsetForScrollingSibling(),其實就是把ViewPager放在AppBarLayout的下方.

3 事件傳遞

Touch事件的話

CoordinatorLayout是會在onInterceptTouchEvent 對所有的攜帶Behavior的第一級View 發送通知.如果被哪一個Behavior的onInterceptTouchEvent 的攔截,所以的后續的 Touch動作都分發給這個Behavior.

7BE0A9A6-CA47-4FD4-9CFF-6BE1790B86B6.png
注意點:

能接受到事件只有第一級的并且攜帶Behavior的控件.
同時這個事件是通知給所有的攜帶Behavior的控件,也就是說當你的點擊事件不在這個 View 的上方,只要這個View 有攜帶 Behavior 都會收到通知,就是說不管你是點擊屏幕上的1還是2,AppBarLayout 都會收到onInterceptTouchEvent事件,所以在復寫 Behavior 的onInterceptTouchEvent 要特別注意到這個情況.

比如說界面一開始往上滑動. 這個時候點擊事件是被AppBarLayout的Behavior 攔截的. AppBarLayout的Behavior事件會設置AppBarLayout的setTopAndBottomOffset ,使AppBarLayout產生了往上偏移,所以你可以看到AppBarLayout 往上偏移,那么ViewPager 為啥也向上偏移.因為ViewPager的ScrollingViewBehavior 中

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    // We depend on any AppBarLayouts
    return dependency instanceof AppBarLayout;
}

對AppBarLayout 進行關聯,當AppBarLayout 有變化的時候會通知給
ScrollingViewBehavior 的onDependentViewChanged 方法中.
通過在這個方法中進行對ViewPager的位置也進行偏移.使他們一起往上偏移.所以看起來想兩個一起往上偏移,這個也是酷酷的.

Scroll 事件

當Touch 事件在ViewPager中. 因為ViewPager中的使用的RecyclerView控件,而RecyclerView 是使用Nest來和其他控件一起處理Scroll事件.RecyclerView 的Nest的事件會一層一層的上傳Scroll 事件,被最近的NestedScrollingParent 接受,這里是CoordinatorLayout ,CoordinatorLayout是一個協調者的角色,他將Nest的事件分發給子控件的View的Behavior處理.
在這里都會被AppBarLayout的Behavior接受.它會根據getTotalScrollRange,getDownNestedPreScrollRange,getUpNestedPreScrollRange來進行想對應的偏移. 效果在上面已經講了.

關于Nest 來處理 Scroll 事件:

當 NestedScrollingChild(下面用Child代替) 要開始滑動的時候會發送 onStartNestedScroll 請求給最近的NestedScrollingParent(下面用Parent代替). 當onStartNestedScroll 返回 true 表示同意一起處理 Scroll 事件的時候時候Child會發送onNestedScrollAccepted 通知 讓Parent去做一些準備動作,當Child 要開始滑動的時候,會先發送onNestedPreScroll 請求給Parent ,告訴它我現在要滑動多少米了,你覺得行不行,這時候Parent 根據實際情況告訴Child 現在只允許你滑動多少.然后 Child 根據 onNestedPreScroll 中傳遞回來的信息對滑動距離做相對應的調整.在滑動的過程中 Child 會發送onNestedScroll通知告知Parent 當前 Child 的滑動情況. 當要進行滑行的時候,會先發送onNestedFling 請求給Parent,告訴它 我現在要滑行了,你說行不行, 這時候Parent會根據情況告訴 Child 你是否可以滑行. Child 通過onNestedFling 返回的 Boolean 值來覺得是否進行滑行.如果要滑行的話,會在滑行的時候發送onNestedFling 通知告知 Parent 滑行情況.當滑動事件結束就會發送onStopNestedScroll 通知 Parent 去做相關操作.

主意點:
  1. Parent 告知 Child 現在允許你滑動多少是通過
    onNestedPreScroll中的數組int[] consumed ,consumed[0]表示 Parent 在 X 軸消耗的量, 所以 Child 滑動距離是請求X軸的滑動距離上面減少consumed[0],consumed[1]表示 Y軸上面的消耗.
    因為consumed是數組,所以Child可以完成可以拿到數據,而不需要onNestedPreScroll 的返回值.
  • 重點注意講解中的請求和通知.

尾巴

詳情界面我也大概看了一遍..機制差不多,其實就是多了CollapsingToolbarLayout這個的好玩的控件.所以這個學習筆記不一定有2.呵呵

同時我會在最近的寫一些有意思的 Behavior 出來.

歡迎大家關注 我的github,我的微博.

github
我的新浪

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容