SwipeDismissBehavior用法及實現原理

jopen 8年前發布 | 7K 次閱讀 Android開發 移動開發

引文

無意間發現design兼容庫中有一個叫做SwipeDismissBehavior的類,顧名思義它就是用來實現滑動刪除的了。莫非現在滑動刪除又有更簡單的解決辦法了?鑒于之前RecyclerView中已經有ItemTouchHelper,而且也非常簡單,所以很好奇到底有何不同,于是決定研究研究,看看它的實現原理以及應用場景:真的能替代其他的(不管是第三方還是RecyclerView自帶的ItemTouchHelper)滑動刪除嗎?。

很不幸SwipeDismissBehavior現在的文檔還很少,只有stackoverfolw上有點價值的討論。

先來直接從API的角度使用SwipeDismissBehavior,然后再講解SwipeDismissBehavior的原理。從而說明為什么SwipeDismissBehavior只能和CoordinatorLayout一起使用?為什么SwipeDismissBehavior對CoordinatorLayout中RecyclerView的item不起作用。


SwipeDismissBehavior的用法

SwipeDismissBehavior的用法非常簡單。

第一步:引入design庫:

compile 'com.android.support:appcompat-v7:23.1.0'
compile 'com.android.support:design:23+'

第二步:把要滑動刪除的View放在CoordinatorLayout中:

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"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/coordinatorLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <TextView
        android:id="@+id/swip"
        android:layout_width="match_parent"
        android:layout_height="200dip"
        android:background="#32CD32"
        android:text="別刪我"
        android:textSize="20dip"
        android:gravity="center"
        />

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

第三步:在MainActivity中為View設置一個SwipeDismissBehavior對象:

package com.jcodecraeer.swipedismissbehaviordemo;

import android.os.Bundle;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.SwipeDismissBehavior;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView swipeView = (TextView)findViewById(R.id.swip);
        final SwipeDismissBehavior<View> swipe
                = new SwipeDismissBehavior();

        swipe.setSwipeDirection(
                SwipeDismissBehavior.SWIPE_DIRECTION_ANY);

        swipe.setListener(
                new SwipeDismissBehavior.OnDismissListener() {
                    @Override public void onDismiss(View view) {

                    }

                    @Override
                    public void onDragStateChanged(int state) {}
                });

        CoordinatorLayout.LayoutParams coordinatorParams =
                (CoordinatorLayout.LayoutParams) swipeView.getLayoutParams();

        coordinatorParams.setBehavior(swipe);
    }
    
}


然后運行就能得到如下效果:

Untitled.gif

就是這么簡單。

下面來講講SwipeDismissBehavior的原理。

要講SwipeDismissBehavior,得先講講CoordinatorLayout.Behavior。因為

SwipeDismissBehavior類是如下定義的。

public class SwipeDismissBehavior<V extends View> extends CoordinatorLayout.Behavior<V>

從這個定義可以看出SwipeDismissBehavior繼承自CoordinatorLayout.Behavior。


CoordinatorLayout與Behavior

Behavior是CoordinatorLayout的一個內部類

public static abstract class Behavior<V extends View>

它只定義了一些抽象方法,其中最主要的當屬下面兩個(與本文相關):

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

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

CoordinatorLayout會在自己的onInterceptTouchEvent()方法中調用Behavior的

onInterceptTouchEvent:

b.onInterceptTouchEvent(this, child, ev);

把自己(this)、與此Behavior對象相關的子view(child)以及MotionEvent ev傳遞過去。這三個參數對于實現一個Behavior都至關重要。 

CoordinatorLayout遍歷子view,判斷子view的mLayoutParam變量中是否有Behavior成員,如果有則調用Behavior的onInterceptTouchEvent和onTouchEvent方法。上面的代碼中,swipeView通過

CoordinatorLayout.LayoutParams coordinatorParams =
        (CoordinatorLayout.LayoutParams) swipeView.getLayoutParams();

coordinatorParams.setBehavior(swipe);

給自己設置了一個類型為SwipeDismissBehavior的Behavior,而且它又是CoordinatorLayout的子view,因此當CoordinatorLayout遍歷到了這個cardview的時候,會嘗試從這個swipeView獲得Behavior:

final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior b = lp.getBehavior();

注:這里的LayoutParams是CoordinatorLayout.LayoutParams類,跟ViewGroup的還是有所區別,他是CoordinatorLayout的內部類。其實任何一個布局比如LinearLayout都有自己的LayoutParams類型,也都是定義在布局類的內部。

如果檢測到這個Behavior不為空,就調用它的onInterceptTouchEvent和onTouchEvent方法。

if (!intercepted && b != null) {
    switch (type) {
        case TYPE_ON_INTERCEPT:
            intercepted = b.onInterceptTouchEvent(this, child, ev);
            break;
        case TYPE_ON_TOUCH:
            intercepted = b.onTouchEvent(this, child, ev);
            break;
    }
    if (intercepted) {
        mBehaviorTouchView = child;
    }
}

自此CoordinatorLayout的任務完成,因為我們設置的這個Behavior是SwipeDismissBehavior對象,所以接下來該怎么處理就交給SwipeDismissBehavior了。

以上過程基本都在CoordinatorLayout的performIntercept(MotionEvent ev, final int type)方法里。

SwipeDismissBehavior

那么我們看看SwipeDismissBehavior的onInterceptTouchEvent和onTouchEvent方法到底做了什么呢?

public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
    switch(MotionEventCompat.getActionMasked(event)) {
    case 1:
    case 3:
        if(this.mIgnoreEvents) {
            this.mIgnoreEvents = false;
            return false;
        }
        break;
    default:
        this.mIgnoreEvents = !parent.isPointInChildBounds(child, (int)event.getX(), (int)event.getY());
    }

    if(this.mIgnoreEvents) {
        return false;
    } else {
        this.ensureViewDragHelper(parent);
        return this.mViewDragHelper.shouldInterceptTouchEvent(event);
    }
}

public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
    if(this.mViewDragHelper != null) {
        this.mViewDragHelper.processTouchEvent(event);
        return true;
    } else {
        return false;
    }
}

在onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) 中,調用了ensureViewDragHelper(parent):

private void ensureViewDragHelper(ViewGroup parent) {
    if(this.mViewDragHelper == null) {
        this.mViewDragHelper = this.mSensitivitySet?ViewDragHelper.create(parent, this.mSensitivity, this.mDragCallback):ViewDragHelper.create(parent, this.mDragCallback);
    }

}


可以看到這里其實是用parent參數創建了一個ViewDragHelper,根據前面的分析,這里的parent其實就是CoordinatorLayout對象。如果你熟悉ViewDragHelper,那么基本上都能猜到SwipeDismissBehavior要做些什么了。

SwipeDismissBehavior就是根據 MotionEvent和parent創建了一個實現了滑動刪除的ViewDragHelper。

具體實現的代碼請看SwipeDismissBehavior的mDragCallback變量。

private final Callback mDragCallback = new Callback() {
    private int mOriginalCapturedViewLeft;

    public boolean tryCaptureView(View child, int pointerId) {
        this.mOriginalCapturedViewLeft = child.getLeft();
        return true;
    }

    public void onViewDragStateChanged(int state) {
        if(SwipeDismissBehavior.this.mListener != null) {
            SwipeDismissBehavior.this.mListener.onDragStateChanged(state);
        }

    }

    public void onViewReleased(View child, float xvel, float yvel) {
        int childWidth = child.getWidth();
        boolean dismiss = false;
        int targetLeft;
        if(this.shouldDismiss(child, xvel)) {
            targetLeft = child.getLeft() < this.mOriginalCapturedViewLeft?this.mOriginalCapturedViewLeft - childWidth:this.mOriginalCapturedViewLeft + childWidth;
            dismiss = true;
        } else {
            targetLeft = this.mOriginalCapturedViewLeft;
        }

        if(SwipeDismissBehavior.this.mViewDragHelper.settleCapturedViewAt(targetLeft, child.getTop())) {
            ViewCompat.postOnAnimation(child, SwipeDismissBehavior.this.new SettleRunnable(child, dismiss));
        } else if(dismiss && SwipeDismissBehavior.this.mListener != null) {
            SwipeDismissBehavior.this.mListener.onDismiss(child);
        }

    }

    private boolean shouldDismiss(View child, float xvel) {
        if(xvel != 0.0F) {
            boolean distance1 = ViewCompat.getLayoutDirection(child) == 1;
            return SwipeDismissBehavior.this.mSwipeDirection == 2?true:(SwipeDismissBehavior.this.mSwipeDirection == 0?(distance1?xvel < 0.0F:xvel > 0.0F):(SwipeDismissBehavior.this.mSwipeDirection == 1?(distance1?xvel > 0.0F:xvel < 0.0F):false));
        } else {
            int distance = child.getLeft() - this.mOriginalCapturedViewLeft;
            int thresholdDistance = Math.round((float)child.getWidth() * SwipeDismissBehavior.this.mDragDismissThreshold);
            return Math.abs(distance) >= thresholdDistance;
        }
    }

    public int getViewHorizontalDragRange(View child) {
        return child.getWidth();
    }

    public int clampViewPositionHorizontal(View child, int left, int dx) {
        boolean isRtl = ViewCompat.getLayoutDirection(child) == 1;
        int min;
        int max;
        if(SwipeDismissBehavior.this.mSwipeDirection == 0) {
            if(isRtl) {
                min = this.mOriginalCapturedViewLeft - child.getWidth();
                max = this.mOriginalCapturedViewLeft;
            } else {
                min = this.mOriginalCapturedViewLeft;
                max = this.mOriginalCapturedViewLeft + child.getWidth();
            }
        } else if(SwipeDismissBehavior.this.mSwipeDirection == 1) {
            if(isRtl) {
                min = this.mOriginalCapturedViewLeft;
                max = this.mOriginalCapturedViewLeft + child.getWidth();
            } else {
                min = this.mOriginalCapturedViewLeft - child.getWidth();
                max = this.mOriginalCapturedViewLeft;
            }
        } else {
            min = this.mOriginalCapturedViewLeft - child.getWidth();
            max = this.mOriginalCapturedViewLeft + child.getWidth();
        }

        return SwipeDismissBehavior.clamp(min, left, max);
    }

    public int clampViewPositionVertical(View child, int top, int dy) {
        return child.getTop();
    }

    public void onViewPositionChanged(View child, int left, int top, int dx, int dy) {
        float startAlphaDistance = (float)this.mOriginalCapturedViewLeft + (float)child.getWidth() * SwipeDismissBehavior.this.mAlphaStartSwipeDistance;
        float endAlphaDistance = (float)this.mOriginalCapturedViewLeft + (float)child.getWidth() * SwipeDismissBehavior.this.mAlphaEndSwipeDistance;
        if((float)left <= startAlphaDistance) {
            ViewCompat.setAlpha(child, 1.0F);
        } else if((float)left >= endAlphaDistance) {
            ViewCompat.setAlpha(child, 0.0F);
        } else {
            float distance = SwipeDismissBehavior.fraction(startAlphaDistance, endAlphaDistance, (float)left);
            ViewCompat.setAlpha(child, SwipeDismissBehavior.clamp(0.0F, 1.0F - distance, 1.0F));
        }

    }
};

問題

那么我們的問題來了?SwipeDismissBehavior可以替代RecyclerView的ItemTouchHelper或者其他列表滑動刪除庫嗎?

答案是不能。

因為CoordinatorLayout遍歷子View的時候,只遍歷了第一層view,而列表的滑動刪除對象是在RecyclerView的里面,不是CoordinatorLayout的直接子view。

再者,既然是RecyclerView的item,那么它的LayoutParams就是RecyclerView.LayoutParams 它無法強制轉換成CoordinatorLayout.LayoutParams,所以運行的時候會報錯:

java.lang.ClassCastException: android.support.v7.widget.RecyclerView$LayoutParams cannot be cast to android.support.design.widget.CoordinatorLayout$LayoutParams

因此SwipeDismissBehavior只適合本文開始的那種用法。






來自: http://www.jcodecraeer.com//a/anzhuokaifa/androidkaifa/2015/1103/3650.html

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