Android 進階——Material Design新控件之利用CoordinatorLayout協同多控件交互(七)

引言

前面系列文章總結了Material Design 兼容庫提供大部分新控件的使用,Android L之前,如果希望一個ViewGroup裏的獨立的控件互相關聯和交互,比如說側滑菜單、可滑動刪除的UI元素等效果,需要自己去實現交互邏輯,而在引入Material Design 兼容庫之後就十分簡單了,CoodinatorLayout就提供交互邏輯,系列文章鏈接:

一、CoordinatorLayout概述

CoordinatorLayout繼承自ViewGroup,可以看成是一個加強版的“FrameLayout”,在Android開發中提供的核心功能主要有:

  • 佈局文件當中作爲最頂層的佈局容器控件
  • CoordinatorLayout中的直接子控件提供特定的交互功能,相當於是給CoordinatorLayout裏的直接子控件建立依賴關係,使得原本相對獨立的控件,產生“依賴”聯繫,可以實現一個目標控件改變時,另一個也隨着改變。

簡而言之,CoordinatorLayout可以使得其直接子控件之間產生“依賴”聯繫,具體是通過變形的“觀察者模式”實現的

  • 主題(被觀察者角色)——配置了Behavior的子View(在佈局文件中使用Behavior的全類名字符串(也可以@string/R.string.xxx形式引入)來配置View的app:layout_behavior屬性
  • 觀察者角色—— 在綁定的Behavior的layoutDependsOn方法返回true時的dependency view

觀察者View(位置、大小)改變時,就會觸發Behavior的onDependentViewChanged方法(只有Dependency View 改變時纔會觸發)。

無論是主題還是觀察者View,都必須是CoordinatorLayout內的直接子View

二、CoordinatorLayout.LayoutParams概述

CoordinatorLayout.LayoutParams是CoordinatorLayout的內部類,和其他ViewGroup功能類似,在CoordinatorLayout的generateLayoutParams方法中直接調用構造方法進行初始化且在CoordinatorLayout.LayoutParams構造方法內部調用CoordinatorLayout的parseBehavior根據配置的Behavior的類名反射創建Behavior並賦值到mBehavior字段,然後再通過Behavior的onAttachedToLayoutParams方法Called when the Behavior has been attached to a LayoutParams instance.,所以除了保存CoordinatorLayout內的子控件的佈局信息之外,還保存着對應的Behavior對象引用 mBehavior

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2 {
   ...
   public static class LayoutParams extends MarginLayoutParams {
   		...
        Behavior mBehavior;
        
		LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
		    super(context, attrs);
		    final TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CoordinatorLayout_Layout);
		    this.gravity = a.getInteger(R.styleable.CoordinatorLayout_Layout_android_layout_gravity,Gravity.NO_GRAVITY);
		    mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_Layout_layout_anchor,View.NO_ID);
		    this.anchorGravity = a.getInteger(R.styleable.CoordinatorLayout_Layout_layout_anchorGravity,Gravity.NO_GRAVITY);
		    this.keyline = a.getInteger(R.styleable.CoordinatorLayout_Layout_layout_keyline,-1);
		
		    insetEdge = a.getInt(R.styleable.CoordinatorLayout_Layout_layout_insetEdge, 0);
		    dodgeInsetEdges = a.getInt(R.styleable.CoordinatorLayout_Layout_layout_dodgeInsetEdges, 0);
		    mBehaviorResolved = a.hasValue(R.styleable.CoordinatorLayout_Layout_layout_behavior);
		    if (mBehaviorResolved) {
		        mBehavior = parseBehavior(context, attrs, a.getString(R.styleable.CoordinatorLayout_Layout_layout_behavior));
		    }
		    a.recycle();
		    if (mBehavior != null) {
		        // If we have a Behavior, dispatch that it has been attached
		        mBehavior.onAttachedToLayoutParams(this);
		    }
		}
}
...
}

三、CoordinatorLayout.Behavior

1、Behavior概述

CoordinatorLayout.Behavior是CoordinatorLayout的抽象泛型內部類,Behvaior 本身並不具備具體的業務功能,本質上就只是爲了進行解耦的而封裝的一個交互接口集合類,而CoordinatorLayout可以藉助Behavior使得獨立的子View可以產生交互,是因爲CoordinatorLayout內部把事件分發至Behavior,讓Behavior具有可以控制其他子View的效果了,也是CoordinatorLayout中核心的設計,也正是因爲這個CoordinatorLayout.Behavior使得CoordinatorLayout中的直接子控件間可以產生聯繫,CoordinatorLayout.Behavior可以理解爲事件分發的傳送渠道(並不負責具體的任務),只是負責調用對應子View的相關方法parseBehavior方法根據配置的Behavior的類名反射創建Behavior並賦值到mBehavior字段,這是繼承Behavior時必須要重寫兩個參數的構造方法的原因。通俗來說,Behavior 設置在誰身上,就可以通過Behavior來改變它對應的狀態,觀察者改變時,主題也跟着改變

2、CoordinatorLayout.Behavior核心方法

CoordinatorLayout.Behavior中最核心的方法只有三個:layoutDependsOn方法、onDependentViewChanged方法和onDependentViewRemoved方法,通過這三個方法就可以實現直接子View之間的交互,至於其他方法是處理到其他業務情況的時候,比如說嵌套滑動、重新佈局等等。

2.1、layoutDependsOn方法

當進行Layout請求的時候就會觸發執行,給CoordinatorLayout中的直接子控件設置了對應的Behavior之後,繪製時至少會執行一次,表示是否給配置了Behavior 的CoordinatorLayout直接子View 指定一個作爲觀察者角色的子View,返回true則表示主題角色child view的觀察者是dependency view, 當觀察者角色View狀態(大小、位置)發生變化時,不管被觀察View 的順序怎樣,被觀察的View也可監聽到並回調對應的方法;反之則兩者之間沒有建立聯繫。簡而言之,這個方法的作用是配置了Behavior的主題子控件被符合哪些條件邏輯的子控件觀察的(即作爲主題的觀察者之一)(Determine whether the supplied child view has another specific sibling view as a layout dependency)。

/**
 * 用於給配置了Behavior的View(主題) 指定一個觀察者角色的View,返回true則dependency 爲主題的觀察者
 * @param parent child和dependency view的外層父佈局
 * @param child 綁定behavior 的View   (觀察者)
 * @param dependency   被觀察者的view (主題)
 * @return 如果child 是觀察者觀察的View 返回true,否則返回false
 */
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    Log.e("------>","layoutDependsOn:child= "+child.getClass().getName()
            +"  dependency= "+dependency.getClass().getName());
    //依次判斷dependency 是否符合條件,
    if(dependency instanceof Button){
        return true;
    }
    return super.layoutDependsOn(parent, child, dependency);
}

比如在CoordinatorLayout裏的某一個子View配置了Behavior之後,CoordinatorLayout在佈局時會循環去查找其所有的直接子View,逐一去判斷這個子View是否可以作爲主題的觀察者,所以當CoordinatorLayout裏有2個直接子控件時layoutDependsOn方法會觸發3次(觸發時機是處理layout request時,包含第一次繪製時和View改變時觸發),如果打印dependency view的名稱,你會看到CoordinatorLayout裏所有的直接子View都會打印一遍(除了主題View)。

在這裏插入圖片描述

2.2、onDependentViewChanged方法

當且僅當Dependency View 狀態(位置、大小等)改變時就會觸發,返回true則表示Behavior改變了主題的狀態,可能會執行多次,當然第一次繪製到佈局上也算是狀態改變時,所以自然也會觸發,至於當監聽到改變之後,如何去實現什麼樣的效果則由我們自己去開發實現。

/**
 * 當被觀察者的View 狀態(如:位置、大小)發生變化時就會觸發執行
 * @return true if the Behavior changed the child view's size or position, false otherwise
 */
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    Log.e("------>","onDependentViewChanged:child= "+child.getClass().getName()
            +"  dependency= "+dependency.getClass().getName());
    return super.onDependentViewChanged(parent, child, dependency);
}

可以在View改變時及時得知,以下爲部分運行日誌:

在這裏插入圖片描述

2.3、onDependentViewRemoved方法

當依賴的Dependency View被移除時觸發回調(Respond to a child’s dependent view being removed.)

/**
 * Respond to a child's dependent view being removed.
 * @param parent the parent view of the given child
 * @param child the child view to manipulate
 * @param dependency the dependent view that has been removed
 */
public void onDependentViewRemoved(@NonNull CoordinatorLayout parent, @NonNull V child,
        @NonNull View dependency) {
}

2.4、onInterceptTouchEvent方法設置是否攔截觸摸事件

設置是否攔截觸摸事件,返回true則表示當前Behavior會攔截觸摸事件,不會分發到CoordinatorLayout內的子View下了。(Respond to CoordinatorLayout touch events before they are dispatched to child views.)

public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child,
       @NonNull MotionEvent ev) {
   return false;
}

2.5、onTouchEvent方法處理觸摸事件

public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child,
       @NonNull MotionEvent ev) {
   return false;
}

2.6、onMeasureChild方法測量使用Behavior的View尺寸

/**
 * Called when the parent CoordinatorLayout is about to measure the given child view.
 * @param child the child to measure
 * @return true if the Behavior measured the child view, false if the CoordinatorLayout
 *         should perform its default measurement
 */
public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull V child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    return false;
}

2.7、onLayoutChild方法重新佈局使用Behavior的View

/**
 * Called when the parent CoordinatorLayout is about the lay out the given child view.
 * @return true if the Behavior performed layout of the child view, false to request default layout behavior
 */
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull V child,
        int layoutDirection) {
    return false;
}

三、CoordinatorLayout的簡單使用

1、引入CoordinatorLayout 依賴

dependencies {
    def coordinatorlayout_version = "1.0.0"
    implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorlayout_version"
}

2、繼承CoordinatorLayout.Behavior實現自己的Behavior

  • 必須重寫形參列表爲(Context context, AttributeSet attrs)的構造方法
  • 重寫layoutDependsOn方法
  • 重寫onDependentViewChanged方法
/**
 * 此Behavior 將要被綁定到TextView上,用於改變TextView的UI效果
 * @author : Crazy.Mo
 */
public class TextColorBehavior extends CoordinatorLayout.Behavior<TextView> {
	//避免因爲CoordinatorLayout的onLayout執行時,一開始就調用了Behavior的onLayout造成,還未開始交互就一直執行了onDependentViewChanged
    private boolean isFirst=true;
    /**
     * 必須重寫,否則肯定報錯
     * @param context
     * @param attrs
     */
    public TextColorBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 這個方法是響應layout請求的,將至少調用一次,返回true時,child和dependency建立了依賴聯繫
     * 即child是依賴dependency的
     * @param parent
     * @param child
     * @param dependency
     * @return
     */
    @Override
    public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull TextView child, @NonNull View dependency) {
        //確定什麼樣的dependency 纔有資格做child的觀察者,邏輯由你自己決定
        if(dependency instanceof Button){
            return true;
        }
        return false;
    }

    /**
     * 當dependency 的狀態改變時就會觸發
     * @param parent
     * @param child
     * @param dependency
     * @return
     */
    @Override
    public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull TextView child, @NonNull View dependency) {
        ///當dependency 的狀態改變時,就會觸發這個方法執行,在這個方法你可以拿到主題View和觀察者View,所以想要實現什麼樣的效果,完全由你決定,徹徹底底地解耦
        if(!isFirst){
            if(child.getId()==R.id.txt_demo2) {
                child.setX(dependency.getX() + 20);
                child.setY(dependency.getY() + 150);
                child.setTextColor(Color.GREEN);
            }else if(child.getId()==R.id.txt_demo){
                child.setX(dependency.getX() + 20);
                child.setY(dependency.getY() + 350);
                child.setTextColor(Color.BLUE);
            }
//當然你可以同時改變dependency的狀態
//            dependency.setX(child.getX()+20);
//            dependency.setY(child.getY()+150);
           /// dependency.setBackgroundColor(Color.RED);
        }
        isFirst=false;
        return true;
    }
}

在CoordinatorLayout中一般對於一個主題View來說一次只能設置綁定一個Behavior(且綁定的Behavior並不是被獨享,其他主題View也可以綁定同一個Behavior),但可以被多個觀察者所監聽,而一個觀察者可以同時監聽多個主題View

package com.crazymo.coordinator;

import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.coordinatorlayout.widget.CoordinatorLayout;

/**
 * @author : Crazy.Mo
 */
public class TextBehavior extends CoordinatorLayout.Behavior<TextView> {
    private boolean isFirst=true;
    /**
     * 必須重寫,否則肯定報錯
     * @param context
     * @param attrs
     */
    public TextBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 這個方法是響應layout請求的,將至少調用一次,返回true時,child和dependency建立了依賴聯繫
     * 即child是依賴dependency的
     * @param parent
     * @param child
     * @param dependency
     * @return
     */
    @Override
    public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull TextView child, @NonNull View dependency) {
        if(dependency instanceof Button){
            return true;
        }
        return false;
    }

    @Override
    public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull TextView child, @NonNull View dependency) {
        if(!isFirst){
            child.setX(dependency.getX()+20);
            child.setY(dependency.getY()+250);
            child.setTextColor(Color.RED);
        }
        isFirst=false;
        return true;
    }
}

上面定義了兩個Behavior。

3、使用CoordinatorLayout作爲頂層佈局,同時給對應的View配置Behavior使之變成主題View

使用CoordinatorLayout一定要給對應的主題View配置app:layout_behavior屬性,否則就沒有必要使用CoordinatorLayout。

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/txt_demo"
        app:layout_behavior=".TextColorBehavior"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="top"
        android:text="我是主題View(被觀察者)" />

    <TextView
        android:id="@+id/txt_demo2"
        app:layout_behavior=".TextColorBehavior"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left|center"
        android:text="我是主題View2(被觀察者)" />

    <TextView
        android:id="@+id/txt_demo3"
        app:layout_behavior=".TextBehavior"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right|center_horizontal"
        android:text="我是主題View3(被觀察者)" />

    <Button
        android:id="@+id/btn_demo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:text="觀察者" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

4、在MainActivity中觸發dependency View狀態改變進行測試

此處模擬的是當dependency View狀態改變時,其他主題View跟隨者改變的簡單效果,當觀察者移動時主題TextView緊跟其下方移動。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.btn_demo).setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if(event.getAction()==MotionEvent.ACTION_MOVE){
                    v.setX(event.getRawX()-v.getWidth()/2);
                    v.setY(event.getRawY()-v.getHeight()/2);
                }
                return true;
            }
        });
    }
}

如下圖所示當我按着觀察者Button移動時候,三個主題TextView都顯示在Button的下方且跟着移動:
在這裏插入圖片描述
通俗總結就是:CoordinatorLayout裏的任何直接子View具有隨時監聽到對方的狀態改變的能力

四、CoordinatorLayout的核心流程解析

未完待續,篇幅問題見下篇文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章