攔截一切的CoordinatorLayout Behavior

RosIrvin 8年前發布 | 30K 次閱讀 Android開發 移動開發

來自: http://www.jcodecraeer.com//a/anzhuokaifa/androidkaifa/2016/0224/3991.html


原文:Intercepting everything with CoordinatorLayout Behaviors。   

如果沒有深入CoordinatorLayout?,你注定無法在探索Android Design Support Library的路上走多遠 - Design Library中的許多view都需要一個CoordinatorLayout。但是為什么呢?CoordinatorLayout本身并沒有做太多事情:和標準的framework視圖一起使用時,它就跟一個普通的FrameLayout差不多。那么它的神奇之處來自于哪里呢?答案就是CoordinatorLayout.Behavior通過為CoordinatorLayout的直接子view設置一個Behavior,就可以攔截touch events, window insets, measurement, layout, 和 nested scrolling等動作。Design Library大量利用了Behaviors來實現你所看到的功能。

blob.png

創建一個Behavior

創建一個behavior很簡單:繼承Behavior即可。

public class FancyBehavior<V extends View>
    extends CoordinatorLayout.Behavior<V> {
  /**
   * Default constructor for instantiating a FancyBehavior in code.
   */
  public FancyBehavior() {
  }
  /**
   * Default constructor for inflating a FancyBehavior from layout.
   *
   * @param context The {@link Context}.
   * @param attrs The {@link AttributeSet}.
   */
  public FancyBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);
    // Extract any custom attributes out
    // preferably prefixed with behavior_ to denote they
    // belong to a behavior
  }
}

注意這個類設置的是普通View,這意味著你可以把FancyBehavior設置給任何View類。但是,如果你只允許讓Behavior設置給一個特定類型的View,則需要這樣寫:

public class FancyFrameLayoutBehavior
    extends CoordinatorLayout.Behavior<FancyFrameLayout>

這可以省去把回調方法中收到的view參數轉換成正確類型的步驟-效率第一嘛。

可以使用Behavior.setTag()/Behavior.getTag() 來保存臨時數據,還可以使用onSaveInstanceState()/onRestoreInstanceState()來保存跟Behavior相關的實例的狀態。我建議讓Behaviors盡可能的輕,但是這些方法讓狀態化Behaviors成為可能。

設置Behavior

當然了,Behaviors并不會對自身做任何事情-它們需要被設置在一個CoordinatorLayout的子view上之后才會被實際調用。設置Behaviors主要有三種方式:程序中動態設置,xml布局文件設置使用注解設置

在程序中設置Behavior

當你認為Behavior是一個被設置在CoordinatorLayout每個子view上的附加數據時,你就不會對Behavior其實是保存在每個view的LayoutParam中感到奇怪了( 如果你已經閱讀了我們  關于布局的文章 )- 這也是為什么Behaviors需要聲明在CoordinatorLayout的直接子View上的原因,因為只有那些子View才存有CoordinatorLayout.LayoutParams(根據自己的理解翻譯的)。

FancyBehavior fancyBehavior = new FancyBehavior();
CoordinatorLayout.LayoutParams params =
    (CoordinatorLayout.LayoutParams) yourView.getLayoutParams();
params.setBehavior(fancyBehavior);

這里你會發現我們使用的是默認的無參構造函數。但這并不是說你就不能使用任何參數 - 如果你想,代碼里面,萬事皆有可能。

在xml里設置Behavior

當然,每次都在代碼里面把所有事情做完會顯得有點亂。就跟多數自定義的LayoutParam一樣,這里也有相應的layout_ attribute 與之對應。那就是layout_behavior 屬性:

<FrameLayout
  android:layout_height=”wrap_content”
  android:layout_width=”match_parent”
  app:layout_behavior=”.FancyBehavior” />

這里與前面不同的是,被調用的構造函數總是FancyBehavior(Context context, AttributeSet attrs)。因此,你可以在xml屬性中聲明你想要的其他自定義屬性。如果你想讓開發者能夠通過xml自定義Behavior的功能,這點是很重要的。

注意:類似于由父類負責解析和解釋的layout_  屬性命名規則,使用behavior_ prefix來指定被專門Behavior使用的某個屬性。

例子(譯者結合評論做的補充):

<FrameLayout
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  app:layout_behavior=".MaxWidthBehavior"
  app:behavior_maxWidth="400dp" />

自動設置一個Behavior

如果你正在創建一個需要一個自定義Behavior的自定義View(就如Design Library中的許多控件那樣),那么你很可能希望view默認就設置了那個Behavior,而不需要每次都通過xml或者代碼去手動指定。為此,你只需在自定義View類的最上面設置一個簡單的注解:

@CoordinatorLayout.DefaultBehavior(FancyFrameLayoutBehavior.class)
public class FancyFrameLayout extends FrameLayout {
}

你會發現你的Behavior會隨著默認的構造函數被調用,這非常類似于與通過程序設置Behavior。注意任何 layout_behavior屬性所代表的Behavior都會重寫 DefaultBehavior

攔截 Touch Events

一旦你設置好了所有的behavior,你就該準備做點實際工作了。Behavior能做的事情之一就是攔截觸摸事件。

如果沒有CoordinatorLayout,我們通常會被牽涉進 ViewGroup的子類中,就像 Managing Touch Events training一文所討論的那樣。但是如果有了CoordinatorLayout,CoordinatorLayout就會把它onInterceptTouchEvent() 中的參數(主要是MotionEvent)和調用傳遞到Behavior的onInterceptTouchEvent(),讓你的Behavior有一次攔截觸摸事件的機會。如果返回true,你的Behavior則會通過onTouchEvent()?收到所有的后續觸摸事件-而View完全不知道發生了什么事情。這也是SwipeDismissBehavior 在view上的工作原理。

ps:我以前專門分析過SwipeDismissBehavior,和這段話基本一致。另外CoordinatorLayout其實是遍歷了一遍自己的直接子View,一個一個的調用子view中的Behavior,見:SwipeDismissBehavior用法及實現原理 。

不過還有一個更粗暴的觸摸攔截:攔截所有的交互。只需在 blocksInteractionBelow() 里返回true即可(我們這個視圖下的其他視圖將獲取不到任何Touch事件)。當然,你可能希望在交互被阻止的情況下能有一些視覺效果  - 這就是為什么blocksInteractionBelow()實際上默認依賴 getScrimOpacity()?的值 - 返回一個非零將在View之上繪制一層overlay顏色并且屏蔽所有的交互。

攔截Window Insets

假設你讀了Why would I want to fitsSystemWindows? blog。那里深入討論了fitsSystemWindows到底干什么的,但是它歸納為:window insets 需要避免在 system windows(比如status bar 和 navigation bar)的下面繪制。

Behaviors在這里也有攔截的機會 - 如果你的View是fitsSystemWindows=“true”的,那么任何依附著的Behavior都將得到onApplyWindowInsets()調用,且優先級高于View自身。

注意:如果你的Behavior并沒有消費掉整個 window insets,它應該通過 ViewCompat.dispatchApplyWindowInsets() 傳遞insets,以確保任何子view都能有機會看到這個WindowInsets。

攔截Measurement 和 layout

測量與布局(Measurement and layout)是 安卓如何繪制View的關鍵組成部分。因此對于能夠攔截一切的Behavior來說,它應該能在第一時間攔截測量和布局才是合情合理的。 這要通過onMeasureChild() 和 onLayoutChild() 回調來完成。

比如, 我們找來任意一個普通的ViewGroup,并向它添加一個maxWidth:

/*
 * Copyright 2015 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
 package com.example.behaviors;

import android.content.Context;
import android.content.res.TypedArray;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.view.ViewGroup;

import static android.view.View.MeasureSpec;

/**
 * Behavior that imposes a maximum width on any ViewGroup.
 *
 * <p />Requires an attrs.xml of something like
 *
 * <pre>
 * &lt;declare-styleable name="MaxWidthBehavior_Params"&gt;
 *     &lt;attr name="behavior_maxWidth" format="dimension"/&gt;
 * &lt;/declare-styleable&gt;
 * </pre>
 */
public class MaxWidthBehavior<V extends ViewGroup> extends CoordinatorLayout.Behavior<V> {
    private int mMaxWidth;

    public MaxWidthBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.MaxWidthBehavior_Params);
        mMaxWidth = a.getDimensionPixelSize(
                R.styleable.MaxWidthBehavior_Params_behavior_maxWidth, 0);
        a.recycle();
    }
    
    @Override
    public boolean onMeasureChild(CoordinatorLayout parent, V child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        if (mMaxWidth <= 0) {
            // No max width means this Behavior is a no-op
            return false;
        }
        int widthMode = MeasureSpec.getMode(parentWidthMeasureSpec);
        int width = MeasureSpec.getSize(parentWidthMeasureSpec);
        
        if (widthMode == MeasureSpec.UNSPECIFIED || width > mMaxWidth) {
            // Sorry to impose here, but max width is kind of a big deal
            width = mMaxWidth;
            widthMode = MeasureSpec.AT_MOST;
            parent.onMeasureChild(child,
                    MeasureSpec.makeMeasureSpec(width, widthMode), widthUsed,
                    parentHeightMeasureSpec, heightUsed);
            // We've measured the View, so CoordinatorLayout doesn't have to
            return true;
        }

        // Looks like the default measurement will work great
        return false;
    }
}

 

寫一個通用的Behavior固然有用,但我們需要知道的是有時候如果你想讓你的app簡單一點的話完全可以把Behavior的相關功能寫在自定義View的內部,沒必要為了使用Behavior而是用它。

理解View之間的依賴

以上的所有功能都只需要一個View。但是Behaviors的強大之處在于在View之間建立依賴關系-當另一個View改變的時候,你的Behavior會得到一個callback,根據外部條件改變它的功能。

Behaviors依賴于View有兩種形式:當它的View錨定于另外一個View(一種隱式的依賴)或者,當你在layoutDependsOn()中明確的返回true。

錨定發生于你使用了CoordinatorLayout的layout_anchor 屬性之時。它和layout_anchorGravity 屬性結合,可以讓你有效的把兩個View捆綁在一起。比如,你可以把一個FloatingActionButton錨定在一個AppBarLayout上,那么如果AppBarLayout滾動出屏幕,FloatingActionButton.Behavior將使用隱式的依賴去隱藏FAB。

不管什么形式,當一個依賴的View被移除的時候你的Behavior會得到回調 onDependentViewRemoved() ,當依賴的View發生變化的時候(比如:調整大小或者重置自己的position),得到回調 onDependentViewChanged()

這個把View綁定在一起的能力正是Design Library那些酷炫功能的工作原理 -以FloatingActionButton與Snackbar之間的交互為例。FAB的 Behavior依賴于被添加到CoordinatorLayout的Snackbar,然后它使用onDependentViewChanged()  callback來將FAB向上移動,以避免和Snackbar重疊。

注意:如果你添加了一個依賴,不管child的順序如何,你的View將總是在所依賴的View放置之后才會被放置。

嵌套滾動

啊哈,嵌套滾動。在這篇博客中,我只會點到為止。記住幾點:

  1. 你不需要在嵌套滾動的View上面定義依賴。CoordinatorLayout的每個child都有機會接收到嵌套滾動事件。

  2. 嵌套滾動不僅可以開始于CoordinatorLayout的直接child,還可以開始于任何child(比如CoordinatorLayout的child的child)。

  3. 雖然我叫它嵌套滾動,但其實它包含滾動(scrolling)和劃動(flinging)兩種。

那么讓我們使用onStartNestedScroll()來定義你所感興趣的嵌套滾動(方向)。你將收到滾動的軸(比如橫向或者縱向-讓它可以輕易的忽略某個方向上的滾動)并且為了接收那個方向上的后續滾動事件必須返回true。

當你在onStartNestedScroll()中返回了true之后,嵌套滾動進入兩個階段:

  • onNestedPreScroll() 會在scrolling View獲得滾動事件前調用,它允許你消費部分或者全部的事件信息。

  • onNestedScroll() 會在scrolling View做完滾動后調用,通過回調可以知道scrolling view滾動了多少和它沒有消耗的滾動事件。

同樣,fling操作也有與之相對應的方法(雖然e pre-fling callback 必須消費完或者完全不消費fling - 沒有消費部分的情況)。

當嵌套滾動(或者flinging)結束,你將得到一個onStopNestedScroll()回調。這標志著滾動的結束 - 迎接在下一個滾動之前的onStartNestedScroll() 調用。 

比如,當向下滾動的時候隱藏FloatingActionButton,向上滾動的時候顯示FloatingActionButton- 這只牽涉到重寫onStartNestedScroll() 和 onNestedScroll(),就如在ScrollAwareFABBehavior中所看到的那樣。

這只是開始

Behavior每個單獨的部分都很有趣,當他們結合起來就會發生很神奇的事情。為了了解更多的高級behavior,我強烈鼓勵你去查看Design Library的源碼-Android SDK Search Chrome extension是我探索AOSP源碼時最喜歡的資源(雖然包含在 <android-sdk>/extras/android/m2repository中的源碼總是最新的)。

在了解Behavior能做哪些事情這點上打下了堅實的基礎后,讓我知道你們是如何使用它們創建更優秀的app的。

要了解更多,請參與在 Google+ post  上的討論并關注 Android Development Patterns Collection !


 本文由用戶 RosIrvin 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!