1. 程式人生 > >Android 自定義Behavior

Android 自定義Behavior

一、Behavior的介紹

1、什麼是Behavior

上篇文章我們講到CoordinatorLayout 配合AppBarLayoutCollapsingToolbarLayout實現了Toolbar的隱藏和摺疊,但他們之間能夠進行互動,其實就是通過一個介質CoordinatorLayout.Behavior 實現的。BehaviorCoordinatorLayout用來和各個子View通訊用的代理類,用來協調CoordinatorLayout的Child Views之間的互動行為,但使用的前提是Behavior只有是CoordinatorLayout的直接子View才有意義。

2、引用Behavior的兩種方式

  • app:layout_behavior佈局屬性
    在佈局中設定,值為自定義Behavior類的名字字串(包含路徑)
  • @CoordinatorLayout.DefaultBehavior類註解
    在需要使用Behavior的控制元件原始碼定義中新增該註解,然後通過反射機制獲取

二、Behavior 的兩種機制

1、Dependent 機制

1.1 Dependent 介紹

這種機制主要是用來描述 兩個Child View 之間的繫結依賴關係,設定Behavior 屬性的Child View 跟隨依賴物件Dependency View 的大小位置改變而發生改變,因此它用於一個View 監聽另一個View 的狀態變化。我們需要通過兩個方法,來實現繫結:

    /**
     * 確定要依賴的物件
     */
     @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return super.layoutDependsOn(parent, child, dependency);
    }

    /**
     * 當依賴的物件發生變化時會自動回撥這個方法
     */
    @Override
    public boolean onDependentViewChanged
(CoordinatorLayout parent, View child, View dependency) { return super.onDependentViewChanged(parent, child, dependency); }

1.2、舉例說明 Dependent 機制


可以看到,我對紅色方塊 進行拖動的時候,綠色的方塊也發生了位置變化。
先自定義一個DependentBehavior,程式碼如下:

public class DependentBehavior extends CoordinatorLayout.Behavior<View> {
    /**
     * 構造方法
     *
     * @param context
     * @param attrs
     */
    public DependentBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 確定要依賴的物件
     *
     * @param parent
     * @param child
     * @param dependency
     * @return
     */
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency.getId() == R.id.btn_first;
    }

    /**
     * 當依賴的物件發生變化時會自動回撥這個方法
     *
     * @param parent
     * @param child
     * @param dependency
     * @return
     */
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        // 根據要依賴的x座標,更改當前控制元件的X座標
        child.setX(parent.getWidth() - dependency.getX() - dependency.getWidth());
        child.setY(dependency.getY());
        return super.onDependentViewChanged(parent, child, dependency);
    }
}

activity_dependent.xml 程式碼:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/coordinator"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/btn_first"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="100dp"
        android:background="@android:color/holo_red_light" />

    <Button
        android:id="@+id/btn_two"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_gravity="right"
        android:layout_marginRight="20dp"
        android:layout_marginTop="100dp"
        android:background="@android:color/holo_green_light"
        app:layout_behavior=".DependentBehavior" />
</android.support.design.widget.CoordinatorLayout>

主介面程式碼:

public class DependentActivity extends AppCompatActivity implements View.OnTouchListener {

    private Button mbtnFirst;
    private int lastX, lastY;    //儲存手指點下的點的座標
    private int mWidth, mHeight;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_dependent);
        mbtnFirst = (Button) findViewById(R.id.btn_first);
        //設定螢幕觸控事件
        mbtnFirst.setOnTouchListener(this);
    }

    /**
     * 當activity 的焦點發生改變時(View 已經繪製完成,可以獲得寬高)
     *
     * @param hasFocus
     */
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        mWidth = mbtnFirst.getWidth();
        mHeight = mbtnFirst.getHeight();
    }

    /**
     * 觸屏事件處理
     *
     * @param view
     * @param event
     * @return
     */
    @Override
    public boolean onTouch(View view, MotionEvent event) {
        // 處理多點觸控的ACTION_POINTER_DOWN和ACTION_POINTER_UP事件
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                //將點下的點的座標儲存
                lastX = (int) event.getRawX();
                lastY = (int) event.getRawY();
                break;

            case MotionEvent.ACTION_MOVE:
                //計算出需要移動的距離
                int dx = (int) event.getRawX() - lastX;
                int dy = (int) event.getRawY() - lastY;
                //將移動距離加上,現在本身距離邊框的位置
                int left = view.getLeft() + dx;
                int top = view.getTop() + dy;
                //獲取到layoutParams然後改變屬性,在設定回去
                CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) view
                        .getLayoutParams();
                layoutParams.height = mHeight;
                layoutParams.width = mWidth;
                layoutParams.leftMargin = left;
                layoutParams.topMargin = top;
                view.setLayoutParams(layoutParams);

                //記錄最後一次移動的位置
                lastX = (int) event.getRawX();
                lastY = (int) event.getRawY();
                break;
        }
        return true;
    }
}

2、Nested機制

2.1 Nested 介紹

Nested機制要求CoordinatorLayout包含了一個實現了NestedScrollingChild介面的滾動檢視控制元件,比如v7包中的RecyclerView,設定Behavior屬性的Child View會隨著這個控制元件的滾動而發生變化,涉及到的方法有:

onStartNestedScroll(View child, View target, int nestedScrollAxes)
onNestedPreScroll(View target, int dx, int dy, int[] consumed)
onNestedPreFling(View target, float velocityX, float velocityY)
onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
onNestedFling(View target, float velocityX, float velocityY, boolean consumed)
onStopNestedScroll(View target)

下面我將簡單描述下四個方法:

    /**
     *  會遍歷每一個 子View,詢問它們是否對滾動列表的滾動事件感興趣,若 Behavior.onStartNestedScroll 方法返回 true,
     *  則表示感興趣,那麼滾動列表後續的滾動事件都會分發到該 子View的Behavior
     */
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int axes, int type) {
        return true;
    }

    /**
     *  處理 子View 的滾動事件
     */
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed, int type) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
    }

    /**
     * 檢測滾動距離的最終消費情況,可以繼續處理 滾動事件
     */
    @Override
    public void onNestedScroll( CoordinatorLayout coordinatorLayout,  View child,  View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
    }

    /**
     * 檢測到滾動事件的結束
     */
    @Override
    public void onStopNestedScroll( CoordinatorLayout coordinatorLayout,  View child,  View target, int type) {
        super.onStopNestedScroll(coordinatorLayout, child, target, type);
    }

2.2、舉例說明 Nested 機制


可以看到,當下拉列表時,紅色佈局就會消失,向上滑動時,紅色佈局就會出現。
先建立一個NestedBehavior,程式碼如下:

public class NestedBehavior extends CoordinatorLayout.Behavior<View> {

    int offsetTotal = 0;

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

    /**
     *  會遍歷每一個 子View,詢問它們是否對滾動列表的滾動事件感興趣,若 Behavior.onStartNestedScroll 方法返回 true,
     *  則表示感興趣,那麼滾動列表後續的滾動事件都會分發到該 子View的Behavior
     */
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int axes, int type) {
        return true;
    }

    /**
     *  處理 子View 的滾動事件
     */
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed, int type) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
        offset(child, dy);
    }

    public void offset(View child, int dy) {
        // 上次儲存的位置
        int old = offsetTotal;
        // 當前的位置
        int curr = offsetTotal - dy;
        // 保證子控制元件的位置一直在 0-控制元件高度之間
        curr = Math.max(curr, -child.getHeight());
        curr = Math.min(curr, 0);
        offsetTotal = curr;
        if (old == offsetTotal) {
            return;
        }
        // 原來的位置 - 當前的位置 = 要移動的位置
        int delta = old - offsetTotal;
        child.offsetTopAndBottom(delta);
    }

}

activity_nested.xml 程式碼:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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.support.v7.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </android.support.v7.widget.RecyclerView>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:layout_gravity="bottom"
        android:background="@android:color/holo_red_light"
        android:gravity="center"
        app:layout_behavior=".NestedBehavior">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="自定義Behavior"
            android:textColor="@android:color/white"
            android:textSize="20sp" />
    </LinearLayout>
</android.support.design.widget.CoordinatorLayout>

結尾:以上就是對自定義Behavior 的簡單的介紹