前言: 前面写了两篇关于ViewDragHelper的博客了,自我感觉收获了挺多知识的,光说不敲还是不行的,所以准备结合项目的例子写一篇关于VDH的实战篇。
项目需求: 做到跟ios一样的效果,滑动Activity的左边距的时候可以退出当前Activity,也就是右滑退出的效果,如果我表述不太清楚的话,可以想象下侧滑菜单,当我们隐藏菜单的时候,菜单消失,底部Activity显示,在碰到此需求的时候我第一个定位的工具就是VDH,然后就到网上搜了一下,发现在很久以前别人就写出了这么个东西,而且适配度还很高,我又何必去造轮子呢?(^__^) 嘻嘻……
先附上项目的github链接:
https://github.com/ikew0ng/SwipeBackLayout
(ps:看了下项目创建的时间,3 years ago,悲哀啊!我与世界差距太大了,不管咋滴,加油吧,骚年!!!)
先看下运行的效果:
如果对VDH的api不了解的话,可以去参考下我的前两篇博客:
思路:
1、创建一个ViewGroup,然后把Activity所在window的contentView
加入到我们创建的ViewGroup中。
2、通过VDH对自定义ViewGroup中的contentView进行边界检查、滑动
的处理。
3、根据滑动的距离添加背景、判断滑动的距离后finishActivity。
思路是很简单的,但是做起来还不是那么容易的,接下来我们一步一步实现下这个很牛掰的控件。
attrs.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SwipeBackLayout">
<!--可触碰边界的大小-->
<attr name="edge_size" format="dimension"/>
<!--滑动边界的位置为:左、右、下、全部-->
<attr name="edge_flag" format="enum">
<enum name="left" value="0"/>
<enum name="right" value="1"/>
<enum name="bottom" value="2"/>
<enum name="all" value="3"/>
</attr>
<!--左边的背影-->
<attr name="shadow_left" format="reference"/>
<!--右边的背景-->
<attr name="shadow_right" format="reference"/>
<!--下边的背景-->
<attr name="shadow_bottom" format="reference"/>
</declare-styleable>
</resources>
SwipeBackLayout.xml:
package com.cisetech.demo.demo;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import com.cisetech.demo.R;
import com.cisetech.demo.ViewDragHelper;
/**
* author:yinqingy
* date:2016-11-07 15:24
* blog:http://blog.csdn.net/vv_bug
* desc:SwipeBack
*/
public class SwipeBackLayout extends FrameLayout {
/**
* 从左边开始滑动
*/
public static final int EDGE_LEFT= ViewDragHelper.EDGE_LEFT;
/**
* 从右边开始滑动
*/
public static final int EDGE_RIGHT= ViewDragHelper.EDGE_RIGHT;
/**
* 从底部开始滑动
*/
public static final int EDGE_BOTTOM= ViewDragHelper.EDGE_BOTTOM;
/**
* 所有的方向可以滑动
*/
public static final int EDGE_ALL= EDGE_LEFT|EDGE_RIGHT|EDGE_BOTTOM;
private static final int[]EDGE_FLAGS={
EDGE_LEFT,EDGE_RIGHT,EDGE_BOTTOM,EDGE_ALL
};
/**
* 滑动的方向
*/
private int mEdgeFlag;
private ViewDragHelper mDragHelper;
private Drawable mShadowLeft;
private Drawable mShadowRight;
private Drawable mShadowBottom;
public SwipeBackLayout(Context context) {
this(context,null);
}
public SwipeBackLayout(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public SwipeBackLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDragHelper=ViewDragHelper.create(this,new MyCallBack());
obtainStyleAttrs(context,attrs,defStyleAttr);
}
/**
* 获取参数属性
* @param context
* @param attrs
* @param defStyleAttr
*/
private void obtainStyleAttrs(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SwipeBackLayout, defStyleAttr,0);
int count = a.getIndexCount();
for (int i = 0; i <count; i++) {
int attr=a.getIndex(i);
switch (attr){
case R.styleable.SwipeBackLayout_edge_size:
int edgeSize=a.getDimensionPixelSize(attr,-1);
if(edgeSize>0)
break;
case R.styleable.SwipeBackLayout_edge_flag:
int mode = EDGE_FLAGS[a.getInt(R.styleable.SwipeBackLayout_edge_flag, 0)];
setEdgeTrackingEnabled(mode);
break;
case R.styleable.SwipeBackLayout_shadow_left:
int shadowLeft = a.getResourceId(R.styleable.SwipeBackLayout_shadow_left,
R.drawable.shadow_left);
setShadow(shadowLeft, EDGE_LEFT);
break;
case R.styleable.SwipeBackLayout_shadow_right:
int shadowRight = a.getResourceId(R.styleable.SwipeBackLayout_shadow_right,
R.drawable.shadow_right);
setShadow(shadowRight, EDGE_RIGHT);
break;
case R.styleable.SwipeBackLayout_shadow_bottom:
int shadowBottom = a.getResourceId(R.styleable.SwipeBackLayout_shadow_bottom,
R.drawable.shadow_bottom);
setShadow(shadowBottom, EDGE_BOTTOM);
break;
}
}
a.recycle();
}
/**
* ViewDragHelper回调
*/
private class MyCallBack extends ViewDragHelper.Callback{
@Override
public boolean tryCaptureView(View child, int pointerId) {
return false;
}
}
/**
* 设置滑动方向
* @param edgeFlags
*/
public void setEdgeTrackingEnabled(int edgeFlags) {
mEdgeFlag = edgeFlags;
mDragHelper.setEdgeTrackingEnabled(mEdgeFlag);
}
/**
* 设置边缘的背影
* @param resId
* @param edgeFlag
*/
public void setShadow(int resId, int edgeFlag) {
setShadow(getResources().getDrawable(resId), edgeFlag);
}
public void setShadow(Drawable shadow, int edgeFlag) {
if ((edgeFlag & EDGE_LEFT) != 0) {
mShadowLeft = shadow;
} else if ((edgeFlag & EDGE_RIGHT) != 0) {
mShadowRight = shadow;
} else if ((edgeFlag & EDGE_BOTTOM) != 0) {
mShadowBottom = shadow;
}
invalidate();
}
}
重点说一下VDH的回调:
/**
* ViewDragHelper回调
*/
private class MyCallBack extends ViewDragHelper.Callback{
@Override
public boolean tryCaptureView(View child, int pointerId) {
return false;
}
}
1、我们不能直接拖动,当在ViewGroup边缘拖动的时候开始处理拖动。
利用mDragHelper中的isEdgeTouched方法先判断是否是边界触碰,
@Override
public boolean tryCaptureView(View child, int pointerId) {
//判断是否是边缘触碰
boolean ret = mDragHelper.isEdgeTouched(mEdgeFlag, pointerId);
if (ret) {
//判断是哪个方向的触碰
if (mDragHelper.isEdgeTouched(EDGE_LEFT, pointerId)) {
mTrackingEdge = EDGE_LEFT;
} else if (mDragHelper.isEdgeTouched(EDGE_RIGHT, pointerId)) {
mTrackingEdge = EDGE_RIGHT;
} else if (mDragHelper.isEdgeTouched(EDGE_BOTTOM, pointerId)) {
mTrackingEdge = EDGE_BOTTOM;
}
}
//防止斜着滑动的那种情况
boolean directionCheck = false;
if (mEdgeFlag == EDGE_LEFT || mEdgeFlag == EDGE_RIGHT) {
//如果是左右模式,不满足垂直可以滑动的时候
directionCheck = !mDragHelper.checkTouchSlop(ViewDragHelper.DIRECTION_VERTICAL, pointerId);
} else if (mEdgeFlag == EDGE_BOTTOM) {
//如果是底部滑动的时候,不满足水平滑动的时候
directionCheck = !mDragHelper
.checkTouchSlop(ViewDragHelper.DIRECTION_HORIZONTAL, pointerId);
} else if (mEdgeFlag == EDGE_ALL) {
directionCheck = true;
}
//是边缘滑动并且满足可以滑动的时候,contentview才可以拖动
return ret & directionCheck;
}
2、防止contentView默认具有点击事件而消费掉事件无法滑动的情况,我们要重写getViewHorizontalDragRange跟getViewVerticalDragRange。
@Override
public int getViewHorizontalDragRange(View child) {
//返回值必须>0,所以只要是EDGE_LEFT或者EDGE_RIGHT模式,就返回1
return mEdgeFlag & (EDGE_LEFT | EDGE_RIGHT);
}
@Override
public int getViewVerticalDragRange(View child) {
//返回值必须>0,所以只要是EDGE_BOTTOM,就返回1
return mEdgeFlag & EDGE_BOTTOM;
}
具体原因我就不去详细解析了,可以去看一下我前面两篇博客(^__^) 嘻嘻……
3、重写onViewPositionChanged,根据contentView位置变换做处理。
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
//获取一个滑动位置的百分比参数
if ((mTrackingEdge & EDGE_LEFT) != 0) {
//如果是左边滑动的情况,mScrollPercent=拖动过后的left/(contentView的宽度+左边阴影的宽度)
mScrollPercent = Math.abs((float) left
/ (mContentView.getWidth() + mShadowLeft.getIntrinsicWidth()));
} else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
//如果是右边滑动的情况,获取到的left为赋值,所以需要取绝对值
mScrollPercent = Math.abs((float) left
/ (mContentView.getWidth() + mShadowRight.getIntrinsicWidth()));
} else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
//如果是底部滑动的情况,mScrollPercent=(top/(contentView的高度+底部阴影的高度))
mScrollPercent = Math.abs((float) top
/ (mContentView.getHeight() + mShadowBottom.getIntrinsicHeight()));
}
//获取拖动过后的left跟top,然后调用invalidate会重新绘制ViewGroup,在onLayout方法中重新摆放子控件位置
mContentLeft = left;
mContentTop = top;
invalidate();
/**
* 当滑动的距离百分比》=1的时候,结束掉当前Activity
*/
if (mScrollPercent >= 1) {
if (!mActivity.isFinishing()) {
mActivity.finish();
mActivity.overridePendingTransition(0, 0);
}
}
}
4、当手指抬起的时候,如果滑动的距离>=我们设置的Activty关闭的距离后,自动滑动退出,否则就回到初始位置。
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
final int childWidth = releasedChild.getWidth();
final int childHeight = releasedChild.getHeight();
int left = 0, top = 0;
//当为左边滑动的时候
if ((mTrackingEdge & EDGE_LEFT) != 0) {
//x轴上滑动的速度>=0并且滑动的距离到达总距离自定的0.3f的时候,left即为(content的宽度+左阴影的宽度+超出滑动距离的offset)
left = xvel > 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? childWidth
+ mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE : 0;
} else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
left = xvel < 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? -(childWidth
+ mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE) : 0;
} else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
top = yvel < 0 || yvel == 0 && mScrollPercent > mScrollThreshold ? -(childHeight
+ mShadowBottom.getIntrinsicHeight() + OVERSCROLL_DISTANCE) : 0;
}
//自动滚动到指定的left跟top位置
mDragHelper.settleCapturedViewAt(left, top);
invalidate();
}
既然有settleCapturedViewAt方法自动回到自定位置,那么肯定会有:
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
5、最后因为有边界限制的问题,所以要重写clampViewPositionHorizontal跟clampViewPositionVertical方法
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
int ret = 0;
//如果为左边界滑动,边界限制为(最小值为0,最大值为contentview的宽度)
//如果为右边界滑动,边界限制为(最小值为-contentView的宽度,最大值为0)
if ((mTrackingEdge & EDGE_LEFT) != 0) {
ret = Math.min(child.getWidth(), Math.max(left, 0));
} else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
ret = Math.min(0, Math.max(left, -child.getWidth()));
}
return ret;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
int ret = 0;
//如果为底部滑动,边界限制为(最小值为-contentView的高度,最大值为0)
if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
ret = Math.min(0, Math.max(top, -child.getHeight()));
}
return ret;
}
6、重写onLayout方法,摆置ContentView的位置:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
mInLayout = true;
if (mContentView != null)
mContentView.layout(mContentLeft, mContentTop,
mContentLeft + mContentView.getMeasuredWidth(),
mContentTop + mContentView.getMeasuredHeight());
mInLayout = false;
}
6、最后把事件的拦截跟处理给VDH
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (!mEnable) {
return false;
}
try {
return mDragHelper.shouldInterceptTouchEvent(event);
} catch (ArrayIndexOutOfBoundsException e) {
// FIXME: handle exception
// issues #9
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mEnable) {
return false;
}
mDragHelper.processTouchEvent(event);
return true;
}
对SwipeBackLayout简单的进行了下解析说明,具体代码,小伙伴们可以直接下载一个SwipeBackLayout,还有一些其他的功能跟封装我就不详细解析了,反正我是跟着敲了很多遍(^__^) 嘻嘻……
好啦!!VDH就暂时告一段落了,总算是解决掉了,继续努力啊!!!!