CoordinatorLayout使用(二):Behavior流程 和 事件流

上一篇,我們大體理解了 Behavior簡單理解
具體代碼可以見 https://github.com/2954722256/use_little_demo
對應 coordinator 的 Module


簡單使用

知道大體作用以后,我們可以參考一下別人的文章
自己簡單搜索后,找一篇自己覺得很好的文章
例如:
http://www.lxweimin.com/p/a506ee4afecb
(大體講解Behavior以及對應的反射注解實現)
其中,有一個View一直在另一個View的下方
是通過自定義View,傳遞id來實現的

http://www.lxweimin.com/p/39fbc9f4f0c6
一些總結感覺寫得挺好
比如,一些寫法,一些分類
雖然沒有什么圖示


自己簡單實現

先寫對應的Behavior
這里簡單一點,不用 自定義屬性,傳遞id了
直接寫死對應的 dependencyView
只需要把這個View的Y值, 設置為 dependencyView的Y值 + dependencyView的高度 即可

DodoBelowBehavior 類
package com.aohuan.dodo.coordinator.utils;

import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;

import com.aohuan.dodo.coordinator.view.DodoMoveView;

/**
 * Created by dodo on 2016/11/1.
 * qq: 2390183798
 *
 *
 * 在Main View 下方
 *      原理也簡單, 只要是 Main View 為 DodoMoveView,  就設置 一起動的View的Y值為 自己的Y值 + MainView的Height
 */
public class DodoBelowBehavior extends CoordinatorLayout.Behavior<View> {

    public DodoBelowBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }


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

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency instanceof DodoMoveView;
    }

}

對應的layout為:

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <android.support.design.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.aohuan.dodo.coordinator.view.DodoMoveView
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:background="#666666"
            android:gravity="center"
            android:layout_gravity="top|right"
            android:text="Main" />

        <Button
            android:id="@+id/btn"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:background="#888888"
            android:text="  Dodo Below  "
            app:layout_behavior="com.aohuan.dodo.coordinator.utils.DodoBelowBehavior" />

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

</LinearLayout>

layout中,將需要上下關聯的View放在一個 CoordinatorLayout 中
在給跟著動的View設置Behavior即可

我們看一下效果:


簡單總結

對應的總結,參考文章:
http://www.lxweimin.com/p/39fbc9f4f0c6

前提: 上面只是簡單使用了一部分,總結后,后續文章再一起探索

markdown 的二級和代碼解析,有一些沖突, 和之前wiki排版差不多,由于不重要,所以自己不花時間去整理了, 有時間找到解決方法后,再做修改


自定義Behavior的通用流程
通常分為:

  • 重寫構造方法
  • 綁定到View
  • 判斷依賴對象

事件流
通常分為:(為了好記,自己名字可能不太一樣)

  • 觸摸事件
  • 計算和布局事件
  • CoordinatorLayout關聯事件
  • 嵌套滑動事件

自定義Behavior的通用流程

對應的Behavior的路程大體為:

  • 1. 重寫構造方法

public class CustomBehavior extends CoordinatorLayout.Behavior {

public CustomBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);
}

}


- **2. 綁定到View**
  - 一定要重寫這個構造方法,因為當你在XML中設置該Behavior時, 
  - 在 CoordinatorLayout中會反射調用該方法,并生成該 Behavior 實例。
  - **綁定的方法有三種:**
    - 在 XML 文件中,設置任意 View 的屬性
      - ```
app:layout_behavior="你的Behavior的包路徑和類名"
- 或者在代碼中:
  - ```

(CoordinatorLayout.LayoutParams)child.getLayoutParams().setBehavior();

    - 或者在你的自定義View類上添加@DefaultBehavior(你的Behavior.class)
      - ```
@DefaultBehavior(CustomBehavior.class)
public class CustomView extends View {}
  • 3. 判斷依賴對象
    • 過程:
      • 當 CoordinatorLayout 收到某個 view 的變化或者嵌套滑動事件時
      • CoordinatorLayout就會嘗試把事件下發給Behavior
      • 綁定了該 Behavior 的 view 就會對事件做出響應
    • 判斷關系的幾種方式
      • 根據id

public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
·   return dependency.getId() == R.id.xxx;
}
- **根據類型**
  - 
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    ·   return dependency instanceof CustomView;
}
- **自定義View的id傳遞**
  - 自定義屬性
    - 
<declare-styleable name="Follow">
    <attr name="target" format="reference"/>
</declare-styleable>
  - layout中傳遞id
    - 
<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:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".MainActivity">
    <View
        android:id="@+id/first"
        android:layout_width="match_parent"
        android:layout_height="128dp"
        android:background="@android:color/holo_blue_light"/>
    <View
        android:id="@+id/second"
        android:layout_width="match_parent"
        android:layout_height="128dp"
        app:layout_behavior=".FollowBehavior"
        app:target="@id/first"
        android:background="@android:color/holo_green_light"/>
</android.support.design.widget.CoordinatorLayout>
  - Behavior中獲取對象
    - 
public class FollowBehavior extends CoordinatorLayout.Behavior {
    private int targetId;
    public FollowBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Follow);
        for (int i = 0; i < a.getIndexCount(); i++) {
            int attr = a.getIndex(i);
            if(a.getIndex(i) == R.styleable.Follow_target){
                targetId = a.getResourceId(attr, -1);
            }
        }
        a.recycle();
    }
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        return true;
    }
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency.getId() == targetId;
    }
}

事件流

為了便于記憶,對應的事件,大體分為下面幾種

  • 觸摸事件
    • 我們知道View的事件分發中有這2個方法:(dispatchEvent先忽略)

public boolean onInterceptTouchEvent(MotionEvent ev)
public boolean onTouchEvent(MotionEvent ev)
  • CoordinatorLayout 會嘗試調用其 Child View 擁有的 Behavior 中的同名方法

public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev)
public boolean onTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev)
  • 如果 Behavior 對觸摸事件進行了攔截,就不會再分發到 Child View 自身擁有的觸摸事件中
    • 這就意味著:在不知道具體View的情況下,就可以重寫它的觸摸事件
    • onTouch事件是CoordinatorLayout分發下來的,所以這里的onTouchEvent并不是我們控件自己的onTouch事件,也就是說,你假如手指不在我們的控件上滑動,也會觸發onTouchEvent
    • 需要在onTouchEvent 方法中的 MotionEvent.ACTION_DOWN 下添加:

ox = ev.getX();
oy = ev.getY();
if (oy < child.getTop() || oy > child.getBottom() || ox < child.getLeft() || ox > child.getRight()) { 
·   return true;
}
  - 手勢過濾,以后自己再單獨找資料學習
  • 計算和布局事件
    • View的計算和布局有這2個方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
protected void onLayout(boolean changed, int l, int t, int r, int b)
  • CoordinatorLayout 也會嘗試調用其 Child View 擁有的 Behavior 中對應的同名方法

public boolean onMeasureChild(CoordinatorLayout parent, V child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection)  
- 同樣地,CoordinatorLayout 會優先處理 Behavior 中所重寫的布局事件
  • CoordinatorLayout關聯事件
    • 這個上面也用過部分類似的,應該會有點印象
    • 這個關聯事件 是指 View 的位置、尺寸發生了變化
    • CoordinatorLayoutonDraw 方法中,會遍歷全部的 Child View 嘗試尋找是否有相互關聯的對象
    • 確定是否關聯的方式有兩種:
      • 1. Behavior中定義
        • 通過 Behavior 的 layoutDependsOn 方法來判斷是否有依賴關系
          • 這個前面的例子中,已經用過很多次了
            • 判斷是dependency是否是當前behavior需要的對象
            • parent CoordinatorLayout
            • child 該Behavior對應的那個View
            • dependency dependency 要檢查的View
            • return true 依賴, false 不依賴
          • 大體常見的3種方式,可以參考前面的說明
        • 如果有就繼續調用 onDependentViewChanged
          • 這個上面也詳細說明過
          • 當改變dependency的尺寸或者位置時被調用
            • parent CoordinatorLayout
            • child 該Behavior對應的那個View
            • dependency child依賴dependency
            • return true 處理了, false 沒處理
        • 在layoutDependsOn返回true的基礎上之后,onDependentViewRemoved通知dependency被移除了
          • parent CoordinatorLayout
          • child 該Behavior對應的那個View
          • dependency child依賴dependency
      • 2. XML中設置屬性
        • 通過 XML 中設置的 layout_anchor
        • 關聯設置 layout_anchor 的 Child View 與 layout_anchor
        • dependency View隨后調用 offsetChildToAnchor(child, layoutDirection)
        • 其實就是調整兩者的位置,讓它們可以一起變化
app:layout_anchor="@id/dependencyView.id"
  • 嵌套滑動事件
    • 大體分為 子控件 和 父控件, 也就是 被觸發的 和 觸發的

      • 具體大致就是 NestedScrollingChildNestedScrollingParentBehavior子類
      • 一些理解,可以參考 鴻洋的一篇博客:
      • 自己的理解:
        • 實現NestedScrollingChild接口,獲得事件,準備傳遞給 NestedScrollingParent
        • 實現NestedScrollingParent接口,獲取傳遞的事件,消費或者傳遞給Behavior子類消費
        • 繼承抽象類Behavior,獲得事件,進行消費。完成對應View動作
    • 1. 實現NestedScrollingChild

      • 如果一個View想向外界傳遞滑動事件,即通知 NestedScrollingParent ,就必須實現此接口
      • 而 Child 與 Parent 的具體交互邏輯, NestedScrollingChildHelper 輔助類基本已經幫我們封裝好了,所以我們只需要調用對應的方法即可
      • NestedScrollingChild接口的一般實現:
public class CustomNestedScrollingChildView extends View implements NestedScrollingChild {
    private NestedScrollingChildHelper mChildHelper = new NestedScrollingChildHelper(this);
    /**
     * 設置當前View能否滑動
     * @param enabled
     */
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        mChildHelper.setNestedScrollingEnabled(enabled);
    }
    /**
     * 判斷當前View能否滑動
     * @return
     */
    @Override
    public boolean isNestedScrollingEnabled() {
        return mChildHelper.isNestedScrollingEnabled();
    }
    /**
     * 啟動嵌套滑動事件流
     * 1. 尋找可以接收 NestedScroll 事件的 parent view,即實現了 NestedScrollingParent 接口的 ViewGroup
     * 2. 通知該 parent view,現在我要把滑動的參數傳遞給你
     * @param axes
     * @return
     */
    @Override
    public boolean startNestedScroll(int axes) {
        return mChildHelper.startNestedScroll(axes);
    }
    /**
     * 停止嵌套滑動事件流
     */
    @Override
    public void stopNestedScroll() {
        mChildHelper.stopNestedScroll();
    }
    /**
     * 是否存在接收 NestedScroll 事件的 parent view
     * @return
     */
    @Override
    public boolean hasNestedScrollingParent() {
        return mChildHelper.hasNestedScrollingParent();
    }
    /**
     * 在滑動之后,向父view匯報滾動情況,包括child view消費的部分和child view沒有消費的部分。
     * @param dxConsumed x方向已消費的滑動距離
     * @param dyConsumed y方向已消費的滑動距離
     * @param dxUnconsumed x方向未消費的滑動距離
     * @param dyUnconsumed y方向未消費的滑動距離
     * @param offsetInWindow 如果parent view滑動導致child view的窗口發生了變化(child View的位置發生了變化)
     *                       該參數返回x(offsetInWindow[0]) y(offsetInWindow[1])方向的變化
     *                       如果你記錄了手指最后的位置,需要根據參數offsetInWindow計算偏移量,
     *                       才能保證下一次的touch事件的計算是正確的。
     * @return 如果parent view接受了它的滾動參數,進行了部分消費,則這個函數返回true,否則為false。
     */
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
                                        int dyUnconsumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                offsetInWindow);
    }
    /**
     * 在滑動之前,先問一下 parent view 是否需要滑動,
     * 即child view的onInterceptTouchEvent或onTouchEvent方法中調用。
     * 1. 如果parent view滑動了一定距離,你需要重新計算一下parent view滑動后剩下給你的滑動距離剩余量,
     *      然后自己進行剩余的滑動。
     * 2. 該方法的第三第四個參數返回parent view消費掉的滑動距離和child view的窗口偏移量,
     *      如果你記錄了手指最后的位置,需要根據第四個參數offsetInWindow計算偏移量,
     *      才能保證下一次的touch事件的計算是正確的。
     * @param dx x方向的滑動距離
     * @param dy y方向的滑動距離
     * @param consumed 如果不是null, 則告訴child view現在parent view滑動的情況,
     *                 consumed[0]parent view告訴child view水平方向滑動的距離(dx)
     *                 consumed[1]parent view告訴child view垂直方向滑動的距離(dy)
     * @param offsetInWindow 可選 length=2 的數組,
     *                       如果parent view滑動導致child View的窗口發生了變化(子View的位置發生了變化)
     *                       該參數返回x(offsetInWindow[0]) y(offsetInWindow[1])方向的變化
     *                       如果你記錄了手指最后的位置,需要根據參數offsetInWindow計算偏移量,
     *                       才能保證下一次的touch事件的計算是正確的。
     * @return 如果parent view對滑動距離進行了部分消費,則這個函數返回true,否則為false。
     */
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }
    /**
     * 在嵌套滑動的child view快速滑動之后再調用該函數向parent view匯報快速滑動情況。
     * @param velocityX 水平方向的速度
     * @param velocityY 垂直方向的速度
     * @param consumed true 表示child view快速滑動了, false 表示child view沒有快速滑動
     * @return true 表示parent view快速滑動了, false 表示parent view沒有快速滑動
     */
    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }
    /**
     * 在嵌套滑動的child view快速滑動之前告訴parent view快速滑動的情況。
     * @param velocityX 水平方向的速度
     * @param velocityY 垂直方向的速度
     * @return true 表示parent view快速滑動了, false 表示parent view沒有快速滑動
     */
    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }
}
  • 2. 實現NestedScrollingParent
    • 如果一個View Group想接收來自 NestedScrollingChild 的滑動事件,就需要實現該接口。
    • 同樣有一個 NestedScrollingParentHelper 輔助類,幫我們封裝好了 parent viewchild view之間的具體交互邏輯。
    • NestedScrollingChild 主動發出滑動事件傳遞給 NestedScrollingParentNestedScrollingParent 做出響應
    • 之間的調用關系如下表所示:
Child View Parent View
startNestedScroll onStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScroll onNestedPreScroll
dispatchNestedScroll onNestedScroll
stopNestedScroll onStopNestedScroll
dispatchNestedFling onNestedFling
dispatchNestedPreFling onNestedPreFling
  • 3. Behavior子類獲得事件,對應View變化
    • Parent View 自身并不會消費滑動距離,都是傳遞給 Behavior
    • 擁有這個 BehaviorChild View 才是真正消費滑動距離的實例
    • Behavior 擁有與 NestedScrollingParent 接口完全同名的方法。在每一個 NestedScrollingParent 的方法中都會調用 Behavior 中的同名方法
    • 特殊方法的說明:
      • public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes)
        • 開始嵌套滑動的時候被調用
        • 需要判斷滑動的方向是否是我們需要的
          • nestedScrollAxes == ViewCompat.SCROLL_AXIS_HORIZONTAL 表示是水平方向的滑動
          • nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL 表示是豎直方向的滑動
        • 對應的返回值:
          • 返回 true 表示繼續接收后續的滑動事件,
          • 返回 false 表示不再接收后續滑動事件
      • public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
        • 滑動中調用
        • 1 正在上滑: dyConsumed > 0 && dyUnconsumed == 0
        • 2 已經到頂部了還在上滑: dyConsumed == 0 && dyUnconsumed > 0
        • 3 正在下滑: dyConsumed < 0 && dyUnconsumed == 0
        • 4 已經打底部了還在下滑: dyConsumed == 0 && dyUnconsumed < 0
      • public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY, boolean consumed)
        • 快速滑動中調用

事件流總結

前面有寫,通常分為:

  • 事件來自外部父view
    • 觸摸事件
      • BehavioronInterceptTouchEvent + onTouchEvent
    • 計算和布局事件
      • BehavioronMeasureChild + onLayoutChild
  • 事件來自內部子view
    • CoordinatorLayout關聯事件
      • BehaviorlayoutDependsOn + onDependentViewChanged + onDependentViewRemoved
    • 嵌套滑動事件
      • BehavioronStartNestedScroll + onNestedScrollAccepted + onStopNestedScroll + onNestedScroll + onNestedPreScroll + onNestedFling + onNestedPreFling

其他參考


簡單回顧

最開始的demo和后面的流程關系不大
開始的demo大體也可以理解成:

  • 自己定義的View,相當于NestedScrollingChild,獲得事件,傳遞給 NestedScrollingParent
  • CoordinatorLayout 實現NestedScrollingParent接口,獲取傳遞的事件,傳遞給Behavior子類消費
  • 自定義的Behavior繼承抽象類Behavior,獲得事件,進行消費。完成對應View動作

這篇文章內容很少,也很多
只有1個demo,但是總結的很多
其他的內容,后續一起學習

具體代碼,可以見
https://github.com/2954722256/use_little_demo
對應 coordinator 的 Module

其他總結,后續的文章,接著參考分析,和大家一起學習。


下一篇我們可以了解
NestedScrollView & 嵌套滑動事件

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

推薦閱讀更多精彩內容