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
我的新浪

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容