SwipeDismissBehavior用法及实现原理

jopen 8年前

引文

无意间发现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