ViewDragHelper通常是在自定义的ViewGroup中使用,通过ViewDragHelper,我们可以很方便的实现ViewGroup中子View的滑动。
ViewDragHelper有几个常见的方法:
ViewDragHelper dragHelper=ViewDragHelper.create(this, new ViewDragHelper.Callback() {
/*
* child - Child the user is attempting to capture
* pointerId - ID of the pointer attempting the capture
* 该方法在ViewGroup中的子View被点击时会调用一次
* 第一个参数child是被点击的子View
* 如果返回值为true,表示响应,后面的所有方法才能执行
* 如果直接 return true,则表示,所有的子View都响应
* 可以进行判断,如果是指定的View则返回true
*/
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
return false;
}
/*
* 水平滑动响应
* 该方法只响应水平滑动,在滑动过程中持续响应。
* 注意:该方法调用时,View位置还未改变
* child 滑动的View
* left 滑动后View左边距与父View左边距的距离
* dx 滑动的距离
* 返回值为滑动View的最终left
* 如:子View child,child的原本left为20
* 手指在其上向右滑动30,此时调用该方法,参数left=20+30,dx=30。
* 如return left,则child滑到left为50处。如果return 20,则不滑动。
*
*/
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
return super.clampViewPositionHorizontal(child, left, dx);
}
/*
* 垂直滑动响应,与水平滑动类似
*/
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
return super.clampViewPositionVertical(child, top, dy);
}
/*
* 与上面的垂直水平滑动类似,不同之处在于:
* 该方法调用时,View位置已经发生改变。
* 如果View已经滑动到无法滑动,其参数值固定不变的
* */
@Override
public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
/*
* 松开View时调用
* xvel - X velocity of the pointer as it left the screen in pixels per second.
* yvel - Y velocity of the pointer as it left the screen in pixels per second.
* 貌似是手指松开时的滑动速度
* */
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
}
});
接下来开始实现类似QQ的消息滑动效果,下面是效果图
首先是XML代码
<?xml version="1.0" encoding="utf-8"?>
<com.example.m.viewdraghelpertest.VDHLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/colorAccent"
android:id="@+id/front">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher_background"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="Hello World!"
android:textSize="16sp"
android:gravity="center" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:id="@+id/back"
android:background="@color/colorPrimary">
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textSize="16sp"
android:layout_gravity="center"
android:text="置顶"
android:gravity="center"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textSize="16sp"
android:layout_gravity="center"
android:text="删除"
android:gravity="center"/>
</LinearLayout>
</com.example.m.viewdraghelpertest.VDHLayout>
最外层的自定义类VDCLayout继承了FrameLayout,通过ViewDragHelper的帮助,实现了上述效果。
首先,因为VDCLayout继承了FrameLayout,所以front和back这两个LinearLayout是重叠在一起的。所以一开始我们需要让back的位置在front的左边,也就是屏幕外。
VDCLayout中,重写onSizeChanged()方法,获取back和front的宽度。然后在重写的onLayout方法中,通过back的layout方法,将back放在front右边。
back和front对象的获取在onFinishInflate()中完成
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mFront=findViewById(R.id.front);
mBack=findViewById(R.id.back);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//参数为各边距离父控件的距离
mBack.layout(mFrontWidth,0,mFrontWidth+mBackWidth,getMeasuredHeight());
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mBackWidth=mBack.getMeasuredWidth();
mFrontWidth=mFront.getMeasuredWidth();
super.onSizeChanged(w, h, oldw, oldh);
}
然后我们需要初始化一下ViewDragHelper,并且重写上面提到的除了垂直滑动的所有方法。在重写这些方法之前,我们还需要重写VDCLayout里的方法onInterceptTouchEvent()和OnTouchEvent()。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mDragHelper.processTouchEvent(event);
return true; }
这个总的意思就是拦截触摸事件,将触摸事件交给mDragHelper处理。获得了触摸事件后,就可以开始重写ViewDragHelper的方法
mDragHelper=ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {//第二个参数是响应的敏感程度,1.0f是正常
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
return true;
}
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
//front滑动,left不能小于-mBackWidth,不能大于0
if (child==mFront){
if (left<=-mBackWidth)
return -mBackWidth;
else if (left>=0)
return 0;
return left;
}else {//back滑动,left不能小于mFrontWidth-mBackWidth,不能大于mFrontWidth
if (left>=mFrontWidth)
return mFrontWidth;
else if (left<=mFrontWidth-mBackWidth)
return mFrontWidth-mBackWidth;
return left;
}
}
//为了不卡在中间,要么完全显示,要么完全隐藏
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
if (mFront.getLeft()<=-mBackWidth/2){
mDragHelper.smoothSlideViewTo(mFront,-mBackWidth,mFront.getTop());
}else if (mFront.getLeft()<0&&mFront.getLeft()>-mBackWidth/2){
mDragHelper.smoothSlideViewTo(mFront,0,mFront.getTop());
}
invalidate();
}
@Override
public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
//如果front滑动了,back的位置也要随之变化,反之亦然
if (changedView==mFront){
mBack.offsetLeftAndRight(dx);
}else if (changedView==mBack){
mFront.offsetLeftAndRight(dx);
}
}
});
这里要注意,mDragHelper.smoothSlideViewTo()中调用了Scroller的startScroll()方法,所以我们需要重写computeScroll()方法。
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)){
invalidate();
}
}
完整的代码如下:
public class VDHLayout extends FrameLayout{
private ViewDragHelper mDragHelper;
private View mFront;
private View mBack;
private int mBackWidth;
private int mFrontWidth;
public VDHLayout(Context context) {
super(context);
init();
}
public VDHLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public VDHLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public VDHLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//参数为距离父控件的上下左右距离
mBack.layout(mFrontWidth,0,mFrontWidth+mBackWidth,getMeasuredHeight());
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mBackWidth=mBack.getMeasuredWidth();
mFrontWidth=mFront.getMeasuredWidth();
super.onSizeChanged(w, h, oldw, oldh);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mDragHelper.processTouchEvent(event);
return true;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mFront=findViewById(R.id.front);
mBack=findViewById(R.id.back);
}
private void init(){
mDragHelper=ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
//会在触碰到View的时候调用一次,松开,再触碰才会调用第二次
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
return true;
}
//left:控件的X坐标 dx:水平滑动的增量
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
if (child==mFront){
if (left<=-mBackWidth)
return -mBackWidth;
else if (left>=0)
return 0;
return left;
}else {
if (left>=mFrontWidth)
return mFrontWidth;
else if (left<=mFrontWidth-mBackWidth)
return mFrontWidth-mBackWidth;
return left;
}
}
//top:控件的Y坐标 dy:垂直滑动的增量
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
return super.clampViewPositionVertical(child, top, dy);
}
//触碰到边缘的时候调用一次,松开,再触碰才会调用第二次
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
}
//手指松开的时候调用
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
Log.v("松开","xvel:"+xvel+" yvel:"+yvel);
if (mFront.getLeft()<=-mBackWidth/2){
mDragHelper.smoothSlideViewTo(mFront,-mBackWidth,mFront.getTop());
}else if (mFront.getLeft()<0&&mFront.getLeft()>-mBackWidth/2){
mDragHelper.smoothSlideViewTo(mFront,0,mFront.getTop());
}
invalidate();
}
@Override
public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
if (changedView==mFront){
mBack.offsetLeftAndRight(dx);
}else if (changedView==mBack){
mFront.offsetLeftAndRight(dx);
}
}
});
}
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)){
invalidate();
}
}
}