项目中有个需求,就是防QQ的消息列表侧滑删除。在网上找了一些资料后,弄明白了原理。无非就是利用ViewDragHelper去拖动控件。
我的想法是,在一个FrameLayout中有两个控件,一个是主控件,另一个就是删除控件。主控件宽高充满布局,删除控件在FrameLayout的右侧,并且被主控件遮挡住。
当滑动主控件的时候,从而让删除控件显示出来(这个方法,并不会让删除控件移动)。
当然,也可以将主控件跟删除控件都放到一个LinearLayout中,然后纵向布局,拖动的时候删除控件也会一并移动。
弄清了原理,接下来就是敲代码。
先上代码。
public class SlideLinearLayout extends FrameLayout {
private ViewDragHelper mViewDragHelper;
/**删除控件*/
private View mDeleteView;
/**主控件*/
private View mMainView;
/**主控件可以移动的最大值跟最小值*/
private int mMaxWidth, mMinWidth;
/**主控件当前移动的位移*/
private int mMoveLength;
/**主View的初始X坐标跟Y坐标*/
private int mOriginalX,mOriginalY;
/**删除控件的中心坐标*/
private int mPrivX,mPrivY;
public SlideLinearLayout(Context context) {
super(context);
init();
}
public SlideLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SlideLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mViewDragHelper = ViewDragHelper.create(this, new Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
if (child == mMainView) {
return true;
} else {
return false;
}
}
/**
* 控件移动的范围
* 默认是为0,但是这里需要设置。因为当为0的时候,会把触摸事件传递给子控件,自己不再使用,这样就无法触发onToucher,也就无法拖动。
* 如果大于0,自己依然会触发onToucher
* @param child
* @return
*/
@Override
public int getViewHorizontalDragRange(View child) {
return mDeleteView.getWidth();
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
/**拖动距离有效性验证*/
left = left < mMinWidth ? mMinWidth : left;
left = left > mMaxWidth ? mMaxWidth : left;
mMoveLength = Math.abs(left);
/**拖动的时候为了触发删除控件的动画,调用了此方法*/
invalidate();
return left;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
/***
* 当手拖动View,并且释放的时候,根据拖动的距离判断滑动向哪一方
*/
if(releasedChild == mMainView) {
if(mMoveLength > (mDeleteView.getWidth()/2) ){
mViewDragHelper.settleCapturedViewAt(mOriginalX - mDeleteView.getWidth(), mOriginalY);
}else{
mViewDragHelper.settleCapturedViewAt(mOriginalX, mOriginalY);
}
invalidate();
}
}
});
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
/**
* 对删除控件进行Y坐标缩放动画
*/
if(child == mDeleteView){
/**
* 根据移动的位移以及删除控件的宽来获取缩放比例。
* 之所以用mMainView.getLeft(),而不是mMoveLength
* 是因为当拖动的时候mMoveLength是有效的,但是动画自动滑动的时候,
* MoveLength就没有发生改变,所以使用mMainView.getLeft()
*
*/
float scale = Math.abs( (float) mMainView.getLeft() /(float) mDeleteView.getWidth() );
/**有效性验证*/
scale = scale < 0 ? 0:scale;
scale = scale > 1 ? 1:scale;
canvas.save();
/**开始缩放*/
canvas.scale(1,scale,mPrivX,mPrivY);
boolean result = super.drawChild(canvas, child, drawingTime);
canvas.restore();
return result;
}
return super.drawChild(canvas, child, drawingTime);
}
@Override
public void computeScroll() {
if(mViewDragHelper.continueSettling(true)){
invalidate();
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);
return true;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
/**
* 第一个是删除控件,第二个是主控件。在XML放置的时候要注意
*/
mMainView = getChildAt(1);
mDeleteView = getChildAt(0);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
/**
* 初始化坐标跟移动范围
*/
mOriginalX = mMainView.getLeft();
mOriginalY = mMainView.getTop();
mMinWidth = -mDeleteView.getWidth();
mMaxWidth = 0;
/***
* 删除控件的中心坐标
*/
mPrivX = mDeleteView.getLeft() + mDeleteView.getWidth()/2;
mPrivY = mDeleteView.getTop() + mDeleteView.getHeight()/2;
}
首先是init()方法。
这个方法是初始化了ViewDragHlper。创建该实例需要传个Context跟一个接口。这个接口才是重要的。先来看看这个接口有哪些重要的方法。
@Override
public boolean tryCaptureView(View child, int pointerId) {
if (child == mMainView) {
return true;
} else {
return false;
}
}
这个方法是什么意思呢?API的说明是这样的。
/**
* Capture a specific child view for dragging within the parent. The callback will be notified
* but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to
* capture this view.
*
* @param childView Child view to capture
* @param activePointerId ID of the pointer that is dragging the captured child view
*/
简单的来说,就是当你触摸屏幕,移动的时候,就会回调这个方法。它会返回两个参数。第一个参数,就是你触摸的那个控件。第二个就是ID。
返回值又代表什么呢?返回ture,就是代笔允许拖动这个控件。返回false就代表不允许拖动这个控件.。这里我只允许拖动主控件。
接下来是
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
/**拖动距离有效性验证*/
left = left < mMinWidth ? mMinWidth : left;
left = left > mMaxWidth ? mMaxWidth : left;
mMoveLength = Math.abs(left);
/**拖动的时候为了触发删除控件的动画,调用了此方法*/
invalidate();
return left;
}
这个方法,是拖动距离的时候X方向回调。第一个参数是拖动的控件。第二个是拖动的总距离。第三个参数是此次拖动跟上次拖动之间的距离。
为了防止越界,这里做了个拖动范围。范围是负删除控件宽度 - 0,
同理还有Y方向的回调()。
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return super.clampViewPositionVertical(child, top, dy);
}
这里我只需要X方向的拖动,Y方向就没有管.
初始化完ViewDragHelper后,还需要做两件事。
能够拖动控件的原理是获取到触摸事件MotionEvent ,然后根据触摸事件的移动,来动态改变控件的位置。既然需要触摸事件,那么我们就需要给ViewDragHelper设置触摸事件。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);
return true;
}复写了父控件的拦截方法跟触摸方法,分别设置给ViewDragHelper进行处理。
这样,就可以实现了拖动。
接下来的问题,就是发现无法点击删除按钮,也无法出发Item的点击事件。这是为什么呢。
原因就在于,ViewDragHelper拦截了事件,事件并没有传递给子控件,包括主控件跟删除控件。自然也就无法触发点击事件了。
网上查了下,说是要复写getViewHorizontalDragRange()方法,使其返回值不返回0即可。我这里返回的是删除控件的宽度。
至于为什么不返回0就可以,我看了源码,没看懂。技术有限。
到这里一个侧滑控件就初步搞定了。