[Digging] 支付寶首頁交互三部曲 1 CoordinatorLayout和Behavior

cover_1

博客原文:kyleduo.com

前言

這個系列源自前幾天看到一篇使用CoordinatorLayout實現(xiàn)支付寶首頁效果的文章励幼,下載看了效果和源碼,不敢茍同,所以打算自己動手斤儿。實現(xiàn)的過程有點曲折,但也發(fā)現(xiàn)了一些有意思的事情,用三篇文章來記錄并分享給大家往果。

  • CoordinatorLayout和Behavior
  • 自定義CoordinatorLayout.Behavior
  • 支付寶首頁效果實現(xiàn)

文中:CoL代表CoordinatorLayout疆液,ABL表示AppBarLayout,CTL表示CollapsingToolbarLayout棚放,SRL表示SwipeRefreshLayout,RV表示RecyclerView馅闽。

源碼:Github

第一篇文章主要討論Behavior的結(jié)構(gòu)飘蚯、CoordinatorLayout的實現(xiàn)以及CoordinatorLayout和Behavior之間的通信。除了Behavior相關(guān)的內(nèi)容福也,CoordinatorLayout作為官方實現(xiàn)的一個ViewGroup局骤,也有一些在自定義ViewGroup時可以借鑒的內(nèi)容,這些也穿插在這篇文章中暴凑。

Behavior結(jié)構(gòu)

使用CoordinatorLayout結(jié)合ABL峦甩,CTL,SRL/RV可以方便的實現(xiàn)各種MaterialDesign ToolBar效果现喳,還有FloatingActionButton凯傲,SnackBar等控件,可以直接使用嗦篱。CoL的主要功能是為其直接子View提供 協(xié)調(diào)滾動 的統(tǒng)一接口冰单,讓子View可以方便的實現(xiàn)諸如嵌套滾動,跟隨滾動等效果灸促,讓界面更加靈動诫欠;而這個統(tǒng)一接口,就是Behavior浴栽。

我們先來看一下Behavior提供的方法荒叼,大致可以分為4組:

布局相關(guān),這類方法用來重載child的Measure典鸡、Layout相關(guān)回調(diào)被廓。

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事件相關(guān),這組方法用來攔截和處理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相關(guān)伊者,這組方法用來響應NestedScrolling,更多關(guān)于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);

其他輔助方法

// 關(guān)聯(lián)/取消關(guān)聯(lián)LayoutParams的時候回調(diào)
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);
// 暫時沒發(fā)現(xiàn)用到的地方
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間出現(xiàn)遮擋汁尺,獲取child應避免遮擋部分的Rect法精。
public boolean getInsetDodgeRect(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull Rect rect);

多數(shù)情況下我們只需要關(guān)注前三組方法。通過這些方法我們可以看到Behavior可以做的事情不僅僅是“依賴某個View的變化并且在其變化之后進行響應”這么單一,Behavior實際上可以控制child在CoL中的measure搂蜓,layout以及攔截toush事件狼荞,支持NestedScrolling等等,基本上是除了Draw之外的全部自定義View需要關(guān)注的內(nèi)容了帮碰。

Behavior的使用和自定義我們下一篇文章進行討論相味,這篇文章我們繼續(xù)關(guān)注CoL如何操作Behavior。

設置Behavior

給一個View設置Behavior有兩種方法:在布局文件中指定殉挽;使用@CoordinatorLayout.DefaultBehavior注解丰涉。

第一種方法適用于多數(shù)情況,需要注意的是斯碌,如果使用自定義Behavior一死,需要覆寫2個參數(shù)的構(gòu)造方法:

public Behavior(Context context, AttributeSet attrs);

因為CoL在解析時是通過反射調(diào)用Behavior的這個構(gòu)造方法創(chuàng)建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 {
  // {...}
}

如果在布局文件中不另外指定投慈,這里將調(diào)用Behavior的無參構(gòu)造方法創(chuàng)建對象。

事件分發(fā)

上面看到的Behavior的各個方法冠骄,其調(diào)用者基本都是CoL伪煤。CoL在自己的回調(diào)方法中通過調(diào)用子View Behavior的相關(guān)方法,將事件向下分發(fā)凛辣。

在討論分發(fā)方法之前带族,有一點需要注意:Behavior雖然影響的是子View的布局和行為,但實際是對CoL本身事件處理的代理蟀给。

基本的模式大家可以想到蝙砌,就是遍歷子View,獲取Behavior跋理,然后調(diào)用子Behavior對應的方法择克。這里對幾個有意思的地方進行討論:

Touch事件分發(fā)

Touch事件分發(fā)分兩個方法onInterceptTouchEvent和onTouchEvent。CoL的實現(xiàn)中前普,這兩個方法都調(diào)用performIntercept方法將是否攔截事件的判斷交給Behavior處理肚邢。每個CoordinatorLayout的子View都有機會攔截事件并響應,注意這里子View并不是在自己的onTouch相關(guān)方法中進行處理拭卿,而是Behavior子類骡湖,有機會代理CoL對事件進行攔截并處理。

出于篇幅考慮這里不貼源碼了峻厚,關(guān)鍵的地方這里解釋一下:

  • 在遍歷子View之前响蕴,使用getTopSortedChildren(topmostChildList);獲取按照顯示順序由上至下排序過的子View列表。ViewGroup可以覆寫getChildDrawingOrder自定義子View的繪制順序(這篇文章中有對這個方法的解釋)惠桃,getTopSortedChildren方法會按照繪制順序獲取子View浦夷;在5.0及以上版本中辖试,還要考慮Z軸次序,也就是elevation劈狐,會再進行一次排序罐孝,最終得到真實可靠的自頂之下的子View分發(fā)順序。這對讓子View合理響應Touch事件很重要肥缔,如果自定義Group需要有類似功能莲兢,可以參考CoL的實現(xiàn)。
  • Behavior通過覆寫onInterceptTouchEvent或者onTouchEvent并返回true來聲明攔截事件续膳,CoL會將該View緩存到mBehaviorTouchView屬性改艇,后續(xù)事件將直接分發(fā)到該View。直到該View的onTouchEvent方法返回false姑宽。
  • 在確定mBehaviorTouchView之后遣耍,CoL會將該View(Z軸)下面View的事件流終止闺阱,具體操作是向這些View分發(fā)一個CANCEL事件炮车。

Behavior可以通過覆寫blocksInteractionBelow方法block下方View的事件。在自己不需要處理事件但同時不希望子View處理事件時酣溃,可以簡單的覆寫這個方法瘦穆。

默認實現(xiàn)邏輯是判斷getScrimOpacity的值>0

NestedScrolling

NestedScrolling是Behavior實現(xiàn)滑動的重要支撐。前文提到Behavior是對CoL自身事件的代理赊豌,所以Behavior對NestedScrolling的支持扛或,就是在代理CoL的NestedScrollingParent接口方法。

更多NestedScrolling相關(guān)信息參見更早的博客:Android Nested Scrolling

需要注意的是關(guān)于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回調(diào)的調(diào)用時機。2. 依賴關(guān)系的存儲磷脯。

  1. onDependentViewChanged在某個View的大小或者位置發(fā)生變化的時候都會進行回調(diào)蛾找。并且是真正變化之后才會進行回調(diào)。
  2. 子View之間的依賴關(guān)系通過非循環(huán)有向圖數(shù)據(jù)結(jié)構(gòu)進行存儲赵誓。具體到結(jié)構(gòu)上就是通過一個Map<Node, List<Node>>存儲(這個Map并不是JDK的實現(xiàn)腋粥,感興趣的可以看下源碼)晦雨。
  3. 既然存在依賴關(guān)系,那么在涉及到對子View遍歷的時候隘冲,就要考慮到子View之前的依賴關(guān)系闹瞧。CoL的實現(xiàn)中prepareChildren方法構(gòu)建依賴圖并根據(jù)依賴圖進行DFS搜索得到依賴鏈列表,這個列表用在了分發(fā)布局展辞、NestedScrolling奥邮、LayoutDependentChange的過程中。

自定義LayoutParams

自定義ViewGroup很常見罗珍,但是多數(shù)情況下用不到自定義LayoutParams洽腺。LayoutParams正如其名,用了設置布局參數(shù)覆旱,也就是控制ViewGroup如何measure和layout子View蘸朋。如果一個自定義ViewGroup提供了額外的布局參數(shù),那就需要自定義LayoutParams了扣唱。自定義LayoutParams并沒有多么復雜藕坯,這里列幾個需要注意的地方。

基類

如果自定義LayoutParams需要支持margin噪沙,繼承自ViewGroup.MarginLayoutParams即可炼彪,默認的ViewGroup.LayoutParams并不支持margin。

構(gòu)造方法

LayoutParams默認有多個不同參數(shù)的構(gòu)造方法:

  • LayoutParams(Context c, AttributeSet attrs) 適用于解析布局文件時生成LayoutParams正歼,layout相關(guān)的xml屬性辐马,就是在這個構(gòu)造方法里面解析的。
  • LayoutParams(int width, int height) 代碼構(gòu)建時只傳入寬高局义。
  • LayoutParams(LayoutParams source) LayoutParams轉(zhuǎn)換
  • LayoutParams() 無參構(gòu)造函數(shù)

自定義LayoutParams也要覆寫這些構(gòu)造方法并做響應的轉(zhuǎn)換喜爷。

ViewGroup方法

使用自定義LayoutParams的ViewGroup,也需要實現(xiàn)幾個相關(guān)方法萄唇,主要是在解析檩帐、addView的時候生成適合的LayoutParams。

protected LayoutParams generateDefaultLayoutParams();
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p);
public LayoutParams generateLayoutParams(AttributeSet attrs);
// 檢查LayoutParams是否是自定義的LayoutParams類型
protected boolean checkLayoutParams(ViewGroup.LayoutParams p);

具體實現(xiàn)可以參考CoL穷绵。

CoordinatorLayout.LayoutParams

CoL.LP主要實現(xiàn)了基于anchorView的布局和keylines(縱向基準線轿塔,子View可以對齊到keyline)的布局、保存Behavior仲墨、儲存滑動過程中的標記位勾缭,具體實現(xiàn)這里就不展開了,邏輯比較簡單目养。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末俩由,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子癌蚁,更是在濱河造成了極大的恐慌幻梯,老刑警劉巖兜畸,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異碘梢,居然都是意外死亡咬摇,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門煞躬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肛鹏,“玉大人,你說我怎么就攤上這事恩沛≡谌牛” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵雷客,是天一觀的道長芒珠。 經(jīng)常有香客問我,道長搅裙,這世上最難降的妖魔是什么皱卓? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮呈宇,結(jié)果婚禮上好爬,老公的妹妹穿的比我還像新娘局雄。我一直安慰自己甥啄,他們只是感情好,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布炬搭。 她就那樣靜靜地躺著蜈漓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪宫盔。 梳的紋絲不亂的頭發(fā)上融虽,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機與錄音灼芭,去河邊找鬼有额。 笑死,一個胖子當著我的面吹牛彼绷,可吹牛的內(nèi)容都是我干的巍佑。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼寄悯,長吁一口氣:“原來是場噩夢啊……” “哼萤衰!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起猜旬,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤脆栋,失蹤者是張志新(化名)和其女友劉穎倦卖,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體椿争,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡怕膛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了秦踪。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嘉竟。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖洋侨,靈堂內(nèi)的尸體忽然破棺而出舍扰,到底是詐尸還是另有隱情,我是刑警寧澤希坚,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布边苹,位于F島的核電站,受9級特大地震影響裁僧,放射性物質(zhì)發(fā)生泄漏个束。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一聊疲、第九天 我趴在偏房一處隱蔽的房頂上張望茬底。 院中可真熱鬧,春花似錦获洲、人聲如沸阱表。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽最爬。三九已至,卻和暖如春门岔,著一層夾襖步出監(jiān)牢的瞬間爱致,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工寒随, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留糠悯,地道東北人。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓妻往,卻偏偏與公主長得像互艾,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蒲讯,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容