Android 一步一步教你使用ViewDragHelper

在自定义viewgroup的时候 要重写onInterceptTouchEvent和onTouchEvent 这2个方法 是非常麻烦的事情,好在谷歌后来推出了ViewDragHelper这个类。可以极大方便我们自定义viewgroup.先看一个简单效果 一个layout里有2个图片 其中有一个可以滑动 一个不能滑:
这里写图片描述

这个效果其实还蛮简单的(原谅我让臭脚不能动 让BABY动)
布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.example.administrator.viewdragertestapp.DragLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/iv1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:src="@drawable/a1"></ImageView>

        <ImageView
            android:id="@+id/iv2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:src="@drawable/a2"></ImageView>


    </com.example.administrator.viewdragertestapp.DragLayout>

</LinearLayout>

然后我们看一下自定义的layout 如何实现2个子view 一个可以滑动 一个不能滑动的:

package com.example.administrator.viewdragertestapp;

import android.content.Context;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

/**
 * Created by Administrator on 2015/8/12.
 */
public class DragLayout extends LinearLayout {

    private ViewDragHelper mDragger;

    private ViewDragHelper.Callback callback;

    private ImageView iv1;
    private ImageView iv2;

    @Override
    protected void onFinishInflate() {
        iv1 = (ImageView) this.findViewById(R.id.iv1);
        iv2 = (ImageView) this.findViewById(R.id.iv2);
        super.onFinishInflate();

    }

    public DragLayout(Context context) {
        super(context);

    }

    public DragLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        callback = new DraggerCallBack();
        //第二个参数就是滑动灵敏度的意思 可以随意设置
        mDragger = ViewDragHelper.create(this, 1.0f, callback);
    }

    class DraggerCallBack extends ViewDragHelper.Callback {

        //这个地方实际上函数返回值为true就代表可以滑动 为false 则不能滑动
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            if (child == iv2) {
                return false;
            }
            return true;
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return left;
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            return top;
        }
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //决定是否拦截当前事件
        return mDragger.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //处理事件
        mDragger.processTouchEvent(event);
        return true;
    }


}

然后再完善一下这个layout,刚才滑动的时候我们的view 出了屏幕的边界很不美观 现在我们修改2个函数 让滑动的范围在这个屏幕之内(准确的说是在这个layout之内,因为我们的布局文件layout充满了屏幕 所以看上去是在屏幕内):

//这个地方实际上left就代表 你将要移动到的位置的坐标。返回值就是最终确定的移动的位置。
        // 我们要让view滑动的范围在我们的layout之内
        //实际上就是判断如果这个坐标在layout之内 那我们就返回这个坐标值。
        //如果这个坐标在layout的边界处 那我们就只能返回边界的坐标给他。不能让他超出这个范围
        //除此之外就是如果你的layout设置了padding的话,也可以让子view的活动范围在padding之内的.

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            //取得左边界的坐标
            final int leftBound = getPaddingLeft();
            //取得右边界的坐标
            final int rightBound = getWidth() - child.getWidth() - leftBound;
            //这个地方的含义就是 如果left的值 在leftbound和rightBound之间 那么就返回left
            //如果left的值 比 leftbound还要小 那么就说明 超过了左边界 那我们只能返回给他左边界的值
            //如果left的值 比rightbound还要大 那么就说明 超过了右边界,那我们只能返回给他右边界的值
            return Math.min(Math.max(left, leftBound), rightBound);
        }

        //纵向的注释就不写了 自己体会
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            final int topBound = getPaddingTop();
            final int bottomBound = getHeight() - child.getHeight() - topBound;
            return Math.min(Math.max(top, topBound), bottomBound);
        }

我们看下效果:
这里写图片描述

然后我们可以再加上一个回弹的效果,就是你把babay拉倒一个位置 然后松手他会自动回弹到初始位置其实思路很简单 就是你松手的时候 回到初始的坐标位置即可。

package com.example.administrator.viewdragertestapp;

import android.content.Context;
import android.graphics.Point;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

/**
 * Created by Administrator on 2015/8/12.
 */
public class DragLayout extends LinearLayout {

    private ViewDragHelper mDragger;

    private ViewDragHelper.Callback callback;

    private ImageView iv1;
    private ImageView iv2;

    private Point initPointPosition = new Point();

    @Override
    protected void onFinishInflate() {
        iv1 = (ImageView) this.findViewById(R.id.iv1);
        iv2 = (ImageView) this.findViewById(R.id.iv2);
        super.onFinishInflate();

    }

    public DragLayout(Context context) {
        super(context);

    }

    public DragLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        callback = new DraggerCallBack();
        //第二个参数就是滑动灵敏度的意思 可以随意设置
        mDragger = ViewDragHelper.create(this, 1.0f, callback);
    }

    class DraggerCallBack extends ViewDragHelper.Callback {

        //这个地方实际上函数返回值为true就代表可以滑动 为false 则不能滑动
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            if (child == iv2) {
                return false;
            }
            return true;
        }


        //这个地方实际上left就代表 你将要移动到的位置的坐标。返回值就是最终确定的移动的位置。
        // 我们要让view滑动的范围在我们的layout之内
        //实际上就是判断如果这个坐标在layout之内 那我们就返回这个坐标值。
        //如果这个坐标在layout的边界处 那我们就只能返回边界的坐标给他。不能让他超出这个范围
        //除此之外就是如果你的layout设置了padding的话,也可以让子view的活动范围在padding之内的.

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            //取得左边界的坐标
            final int leftBound = getPaddingLeft();
            //取得右边界的坐标
            final int rightBound = getWidth() - child.getWidth() - leftBound;
            //这个地方的含义就是 如果left的值 在leftbound和rightBound之间 那么就返回left
            //如果left的值 比 leftbound还要小 那么就说明 超过了左边界 那我们只能返回给他左边界的值
            //如果left的值 比rightbound还要大 那么就说明 超过了右边界,那我们只能返回给他右边界的值
            return Math.min(Math.max(left, leftBound), rightBound);
        }

        //纵向的注释就不写了 自己体会
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            final int topBound = getPaddingTop();
            final int bottomBound = getHeight() - child.getHeight() - topBound;
            return Math.min(Math.max(top, topBound), bottomBound);
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            //松手的时候 判断如果是这个view 就让他回到起始位置
            if (releasedChild == iv1) {
                //这边代码你跟进去去看会发现最终调用的是startScroll这个方法 所以我们就明白还要在computeScroll方法里刷新
                mDragger.settleCapturedViewAt(initPointPosition.x, initPointPosition.y);
                invalidate();
            }
        }
    }

    @Override
    public void computeScroll() {
        if (mDragger.continueSettling(true)) {
            invalidate();
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        //布局完成的时候就记录一下位置
        initPointPosition.x = iv1.getLeft();
        initPointPosition.y = iv1.getTop();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //决定是否拦截当前事件
        return mDragger.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //处理事件
        mDragger.processTouchEvent(event);
        return true;
    }


}

看下效果:
这里写图片描述

到这里有人会发现 这样做的话imageview就无法响应点击事件了。继续修改这个代码让iv可以响应点击事件并且可以响应滑动事件。首先修改xml 把click属性设置为true 这个代码就不上了,然后修改我们的代码 其实就是增加2个函数:

@Override
        public int getViewHorizontalDragRange(View child) {
            return getMeasuredWidth() - child.getMeasuredWidth();
        }

        @Override
        public int getViewVerticalDragRange(View child) {
            return getMeasuredHeight()-child.getMeasuredHeight();
        }

然后看下效果:
这里写图片描述

这个地方 如果你学过android 事件传递的话很好理解,因为如果你子view可以响应点击事件的话,那说明你消费了这个事件。如果你消费了这个事件话 就会先走dragger的 onInterceptTouchEvent这个方法。我们跟进去看看这个方法:

case MotionEvent.ACTION_MOVE: {
                if (mInitialMotionX == null || mInitialMotionY == null) break;

                // First to cross a touch slop over a draggable view wins. Also report edge drags.
                final int pointerCount = MotionEventCompat.getPointerCount(ev);
                for (int i = 0; i < pointerCount; i++) {
                    final int pointerId = MotionEventCompat.getPointerId(ev, i);
                    final float x = MotionEventCompat.getX(ev, i);
                    final float y = MotionEventCompat.getY(ev, i);
                    final float dx = x - mInitialMotionX[pointerId];
                    final float dy = y - mInitialMotionY[pointerId];

                    final View toCapture = findTopChildUnder((int) x, (int) y);
                    final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
                    if (pastSlop) {
                        // check the callback's
                        // getView[Horizontal|Vertical]DragRange methods to know
                        // if you can move at all along an axis, then see if it
                        // would clamp to the same value. If you can't move at
                        // all in every dimension with a nonzero range, bail.
                        final int oldLeft = toCapture.getLeft();
                        final int targetLeft = oldLeft + (int) dx;
                        final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
                                targetLeft, (int) dx);
                        final int oldTop = toCapture.getTop();
                        final int targetTop = oldTop + (int) dy;
                        final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
                                (int) dy);
                        final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
                                toCapture);
                        final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
                        if ((horizontalDragRange == 0 || horizontalDragRange > 0
                                && newLeft == oldLeft) && (verticalDragRange == 0
                                || verticalDragRange > 0 && newTop == oldTop)) {
                            break;
                        }
                    }
                    reportNewEdgeDrags(dx, dy, pointerId);
                    if (mDragState == STATE_DRAGGING) {
                        // Callback might have started an edge drag
                        break;
                    }

                    if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
                        break;
                    }
                }
                saveLastMotion(ev);
                break;
            }

注意看29行到末尾你会发现,只有当horizontalDragRange 和verticalDragRange大于0的时候 对应的move事件才会捕获。否则就是丢弃直接丢给子view自己处理了。另外,还有一个效果就是 假如我们的 baby被拉倒了边界处,我们的手指不需要拖动baby这个iv,手指直接在边界的其他地方拖动此时也能把这个iv拖走。这个效果其实也可以实现,无非就是捕捉你手指在边界处的动作 然后传给你要拖动的view即可。代码非常简单 两行即可再重写一个回调函数 然后加个监听:

@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
    mDragger.captureChildView(iv1, pointerId);
}

mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);      

这个效果在模拟器上不知道为啥 鼠标拖不动,GIF图片我就不上了大家可以自己在手机里跑一下就可以。上面的那些效果实际上都是DrawerLayout 等类似抽屉效果里经常用到的函数。

ViewDragHelper让我们很容易实现一个类似于YouTube视频浏览效果的控件,效果如下:
这里写图片描述

代码中的关键点:
1.tryCaptureView返回了唯一可以被拖动的header view;
2.拖动范围drag range的计算是在onLayout中完成的;
3.注意在onInterceptTouchEvent和onTouchEvent中使用的ViewDragHelper的若干方法;
4.在computeScroll中使用continueSettling方法(因为ViewDragHelper使用了scroller)
5.smoothSlideViewTo方法来完成拖动结束后的惯性操作。
需要注意的是代码仍然有很大改进空间。

activity_main.xml:

<FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    <ListView
            android:id="@+id/listView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:tag="list"
            />
    <com.example.vdh.YoutubeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/youtubeLayout"
            android:orientation="vertical"
            android:visibility="visible">
        <TextView
                android:id="@+id/viewHeader"
                android:layout_width="match_parent"
                android:layout_height="128dp"
                android:fontFamily="sans-serif-thin"
                android:textSize="25sp"
                android:tag="text"
                android:gravity="center"
                android:textColor="@android:color/white"
                android:background="#AD78CC"/>
        <TextView
                android:id="@+id/viewDesc"
                android:tag="desc"
                android:textSize="35sp"
                android:gravity="center"
                android:text="Loreum Loreum"
                android:textColor="@android:color/white"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="#FF00FF"/>
    </com.example.vdh.YoutubeLayout>
</FrameLayout>

YoutubeLayout.java:

public class YoutubeLayout extends ViewGroup {
private final ViewDragHelper mDragHelper;
private View mHeaderView;
private View mDescView;
private float mInitialMotionX;
private float mInitialMotionY;
private int mDragRange;
private int mTop;
private float mDragOffset;
public YoutubeLayout(Context context) {
  this(context, null);
}
public YoutubeLayout(Context context, AttributeSet attrs) {
  this(context, attrs, 0);
}
@Override
protected void onFinishInflate() {
    mHeaderView = findViewById(R.id.viewHeader);
    mDescView = findViewById(R.id.viewDesc);
}
public YoutubeLayout(Context context, AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);
  mDragHelper = ViewDragHelper.create(this, 1f, new DragHelperCallback());
}
public void maximize() {
    smoothSlideTo(0f);
}
boolean smoothSlideTo(float slideOffset) {
    final int topBound = getPaddingTop();
    int y = (int) (topBound + slideOffset * mDragRange);
    if (mDragHelper.smoothSlideViewTo(mHeaderView, mHeaderView.getLeft(), y)) {
        ViewCompat.postInvalidateOnAnimation(this);
        return true;
    }
    return false;
}
private class DragHelperCallback extends ViewDragHelper.Callback {
  @Override
  public boolean tryCaptureView(View child, int pointerId) {
        return child == mHeaderView;
  }
    @Override
  public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
      mTop = top;
      mDragOffset = (float) top / mDragRange;
        mHeaderView.setPivotX(mHeaderView.getWidth());
        mHeaderView.setPivotY(mHeaderView.getHeight());
        mHeaderView.setScaleX(1 - mDragOffset / 2);
        mHeaderView.setScaleY(1 - mDragOffset / 2);
        mDescView.setAlpha(1 - mDragOffset);
        requestLayout();
  }
  @Override
  public void onViewReleased(View releasedChild, float xvel, float yvel) {
      int top = getPaddingTop();
      if (yvel > 0 || (yvel == 0 && mDragOffset > 0.5f)) {
          top += mDragRange;
      }
      mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top);
  }
  @Override
  public int getViewVerticalDragRange(View child) {
      return mDragRange;
  }
  @Override
  public int clampViewPositionVertical(View child, int top, int dy) {
      final int topBound = getPaddingTop();
      final int bottomBound = getHeight() - mHeaderView.getHeight() - mHeaderView.getPaddingBottom();
      final int newTop = Math.min(Math.max(top, topBound), bottomBound);
      return newTop;
  }
}
@Override
public void computeScroll() {
  if (mDragHelper.continueSettling(true)) {
      ViewCompat.postInvalidateOnAnimation(this);
  }
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
  final int action = MotionEventCompat.getActionMasked(ev);
  if (( action != MotionEvent.ACTION_DOWN)) {
      mDragHelper.cancel();
      return super.onInterceptTouchEvent(ev);
  }
  if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
      mDragHelper.cancel();
      return false;
  }
  final float x = ev.getX();
  final float y = ev.getY();
  boolean interceptTap = false;
  switch (action) {
      case MotionEvent.ACTION_DOWN: {
          mInitialMotionX = x;
          mInitialMotionY = y;
            interceptTap = mDragHelper.isViewUnder(mHeaderView, (int) x, (int) y);
          break;
      }
      case MotionEvent.ACTION_MOVE: {
          final float adx = Math.abs(x - mInitialMotionX);
          final float ady = Math.abs(y - mInitialMotionY);
          final int slop = mDragHelper.getTouchSlop();
          if (ady > slop && adx > ady) {
              mDragHelper.cancel();
              return false;
          }
      }
  }
  return mDragHelper.shouldInterceptTouchEvent(ev) || interceptTap;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
  mDragHelper.processTouchEvent(ev);
  final int action = ev.getAction();
    final float x = ev.getX();
    final float y = ev.getY();
    boolean isHeaderViewUnder = mDragHelper.isViewUnder(mHeaderView, (int) x, (int) y);
    switch (action & MotionEventCompat.ACTION_MASK) {
      case MotionEvent.ACTION_DOWN: {
          mInitialMotionX = x;
          mInitialMotionY = y;
          break;
      }
      case MotionEvent.ACTION_UP: {
          final float dx = x - mInitialMotionX;
          final float dy = y - mInitialMotionY;
          final int slop = mDragHelper.getTouchSlop();
          if (dx * dx + dy * dy < slop * slop && isHeaderViewUnder) {
              if (mDragOffset == 0) {
                  smoothSlideTo(1f);
              } else {
                  smoothSlideTo(0f);
              }
          }
          break;
      }
  }
  return isHeaderViewUnder && isViewHit(mHeaderView, (int) x, (int) y) || isViewHit(mDescView, (int) x, (int) y);
}
private boolean isViewHit(View view, int x, int y) {
    int[] viewLocation = new int[2];
    view.getLocationOnScreen(viewLocation);
    int[] parentLocation = new int[2];
    this.getLocationOnScreen(parentLocation);
    int screenX = parentLocation[0] + x;
    int screenY = parentLocation[1] + y;
    return screenX >= viewLocation[0] && screenX < viewLocation[0] + view.getWidth() &&
            screenY >= viewLocation[1] && screenY < viewLocation[1] + view.getHeight();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
    int maxHeight = MeasureSpec.getSize(heightMeasureSpec);
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
            resolveSizeAndState(maxHeight, heightMeasureSpec, 0));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
  mDragRange = getHeight() - mHeaderView.getHeight();
    mHeaderView.layout(
            0,
            mTop,
            r,
            mTop + mHeaderView.getMeasuredHeight());
    mDescView.layout(
            0,
            mTop + mHeaderView.getMeasuredHeight(),
            r,
            mTop  + b);
}

参考博客:
Android 一步一步教你使用ViewDragHelper
ViewDragHelper详解

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值