ViewDragHelper详解

一、基本介绍

前辈们已经总结的很多了,所以从别人的博客里直接复制的比较多,用到的源码我也会经过修改。用到的博客我会在下方注释。

2013年谷歌i/o大会上介绍了两个新的layout: SlidingPaneLayout和DrawerLayout,现在这俩个类被广泛的运用,其实研究他们的源码你会发现这两个类都运用了ViewDragHelper来处理拖动。ViewDragHelper是framework中不为人知却非常有用的一个工具

ViewDragHelper解决了android中手势处理过于复杂的问题,在DrawerLayout出现之前,侧滑菜单都是由第三方开源代码实现的,其中著名的当属MenuDrawer(现在用android.support.v4.widget.DrawerLayout)。

其实ViewDragHelper并不是第一个用于分析手势处理的类,gesturedetector也是,但是在和拖动相关的手势分析方面gesturedetector只能说是勉为其难。

关于ViewDragHelper有如下几点:

   1、ViewDragHelper.Callback是连接ViewDragHelper与view之间的桥梁(这个view一般是指拥子view的容器即parentView);

   2、ViewDragHelper的实例是通过静态工厂方法创建的;

   3、你能够指定拖动的方向;

   4、ViewDragHelper可以检测到是否触及到边缘;

   5、ViewDragHelper并不是直接作用于要被拖动的View,而是使其控制的视图容器中的子View可以被拖动,如果要指定某个子view的行为,需要在Callback中想办法;

   6、ViewDragHelper的本质其实是分析onInterceptTouchEventonTouchEvent的MotionEvent参数,然后根据分析的结果去改变一个容器中被拖动子View的位 通过offsetTopAndBottom(int offset)和offsetLeftAndRight(int offset)方法 ),他能在触摸的时候判断当前拖动的是哪个子View;

   7、虽然ViewDragHelper的实例方法 ViewDragHelper create(ViewGroup forParent, Callback cb) 可以指定一个被ViewDragHelper处理拖动事件的对象 ,但ViewDragHelper类的设计决定了其适用于被包含在一个自定义ViewGroup之中,而不是对任意一个布局上的视图容器使用ViewDragHelper

注意:ViewDragHelper是作用在一个ViewGroup上,也就是说他不能直接作用到被拖拽的view, 其实这也很好理解,因为view在布局中的位置是父ViewGroup决定的。

如何使用ViewGroup实现一个可以拖动的view?

1、获取ViewDragHelper的实例,注意,这里不能直接new,而是使用ViewDragHelper的一个静态方法:

ViewDragHelper.create(ViewGroup forParent, float sensitivity, ViewDragHelper.Callback cb);

参数1: 一个ViewGroup, 也就是ViewDragHelper将要用来拖拽谁下面的子view

参数2:灵敏度,一般设置为1.0f就行

参数3:一个回调,用来处理拖动到位置

2、在Callback对象里面,下面几个方法非常重要。

 (1) public int clampViewPositionHorizontal(View child, int left, int dx)

          这个是返回被横向移动的子控件child的左坐标left,和移动距离dx,我们可以根据这些值来返回child的新的left。

          返回值该child现在的位置,  这个方法必须重写,要不然就不能移动了。

(2)public int clampViewPositionVertical(View child, int top, int dy) 

         这个和上面的方法一个意思,就是换成了垂直方向的移动和top坐标。

         如果有垂直移动,这个也必须重写,要不默认返回0,也不能移动了。

(3)public abstract boolean tryCaptureView(View child, int pointerId) 

表示尝试捕获子view,这里一定要返回true, 返回true表示允许。           

这个方法用来返回可以被移动的View对象,我们可以通过判断child与我们想移动的View是的相等来控制谁能移动。

(4)public int getViewVerticalDragRange(View child)

          这个用来控制垂直移动的边界范围,单位是像素。

(5)public int getViewHorizontalDragRange(View child) 

          和上面一样,就是是横向的边界范围。

(6)public void onViewReleased(View releasedChild, float xvel, float yvel) 

           当releasedChild被释放的时候,xvel和yvel是x和y方向的加速度

(7)public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) 

这个是当changedView的位置发生变化时调用,我们可以在这里面控制View的显示位置和移动。

    我们前面虽然获取了ViewDragHelper的对象,但是现在我们还是不能接收到事件的,我们需要在onTouch()和onInterceptTouchEvent()里面,将触摸事件传入到ViewDragHelper里面,才能进行处理,就像下面这样,注意需要在clampViewPositionHorizontal和clampViewPositionHorizontal中做一点工作,以防止view超出了边界。

 

@Override  
public boolean onInterceptTouchEvent(MotionEvent ev) {  
  
    …  
  
    return mDragHelper.shouldInterceptTouchEvent(ev);  
} 

 

public boolean onTouchEvent(MotionEvent event) {  
      
    …  
    mDragHelper.processTouchEvent(event);  
    return true;  
}


   传递给ViewDragHelper之后,我们就可以在Callback的各个事件里面进行处理了。


二、实战1


例1:

public class CustomView extends LinearLayout {
	private ViewDragHelper mDragHelper;

	public CustomView(Context context) {
		super(context);
		init();
	}

	public CustomView(Context context, AttributeSet attrs) {
		super(context, attrs);
		init();
	}

	public CustomView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		init();
	}

	private void init() {
		/**
		 * @params ViewGroup forParent 必须是一个ViewGroup
		 * @params float sensitivity 灵敏度
		 * @params Callback cb 回调
		 */
		mDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragCallback());
	}
	
	private class ViewDragCallback extends ViewDragHelper.Callback {
		/**
		 * 尝试捕获子view,一定要返回true
		 * @param View child 尝试捕获的view
		 * @param int pointerId 指示器id? 
		 * 这里可以决定哪个子view可以拖动
		 */
		@Override
		public boolean tryCaptureView(View view, int pointerId) {
//			return mCanDragView == view;
			return true;
		}
		
		/**
		 * 处理水平方向上的拖动
		 * @param View child 被拖动到view
		 * @param int left 移动到达的x轴的距离
		 * @param int dx 建议的移动的x距离
		 */
		@Override
		public int clampViewPositionHorizontal(View child, int left, int dx) {
			System.out.println("left = " + left + ", dx = " + dx);
			
			// 两个if主要是为了让viewViewGroup里
			if(getPaddingLeft() > left) {
				return getPaddingLeft();
			}
			
			if(getWidth() - child.getWidth() < left) {
				return getWidth() - child.getWidth();
			}
			
			return left;
		}
		
		/**
		 *  处理竖直方向上的拖动
		 * @param View child 被拖动到view
		 * @param int top 移动到达的y轴的距离
		 * @param int dy 建议的移动的y距离
		 */
		@Override
		public int clampViewPositionVertical(View child, int top, int dy) {
			// 两个if主要是为了让viewViewGroup里
			if(getPaddingTop() > top) {
				return getPaddingTop();
			}
			
			if(getHeight() - child.getHeight() < top) {
				return getHeight() - child.getHeight();
			}
			
			return top;
		}
		
		/**
		 * 当拖拽到状态改变时回调
		 * @params 新的状态
		 */
		@Override
		public void onViewDragStateChanged(int state) {
			switch (state) {
			case ViewDragHelper.STATE_DRAGGING:  // 正在被拖动
				break;
			case ViewDragHelper.STATE_IDLE:  // view没有被拖拽或者 正在进行fling/snap
				break;
			case ViewDragHelper.STATE_SETTLING: // fling完毕后被放置到一个位置
				break;
			}
			super.onViewDragStateChanged(state);
		}
	}
	
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		switch (ev.getAction()) {
		case MotionEvent.ACTION_CANCEL:
		case MotionEvent.ACTION_DOWN:
			mDragHelper.cancel(); // 相当于调用 processTouchEvent收到ACTION_CANCEL
			break;
		}
		
		/**
		 * 检查是否可以拦截touch事件
		 * 如果onInterceptTouchEvent可以return true 则这里return true
		 */
		return mDragHelper.shouldInterceptTouchEvent(ev);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		/**
		 * 处理拦截到的事件
		 * 这个方法会在返回前分发事件
		 */
		mDragHelper.processTouchEvent(event);
		return true;
	}
}

layout:

<span style="color:#999999;"><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <com.bluemor.reddotface.view.CustomView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <View
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="#FFFF0000" />
    </com.bluemor.reddotface.view.CustomView>

</RelativeLayout></span>


三、实战2

 

DragLayout.java

public class DragLayout extends FrameLayout {
	private Context context;
	private GestureDetectorCompat gestureDetector;
	private ViewDragHelper dragHelper;
	private DragListener dragListener;

	/** 水平可以滚动的范围 */
	private int horizontalRange;
	/** 垂直可以滚动的范围 */
	private int verticalRange;
	/** 默认滚动式水平的 */
	private Orientation orientation = Orientation.Horizontal;

	private int viewWidth;
	private int viewHeight;
	private int distanceLeft;
	private int distanceTop;

	private ViewGroup layoutMenu;
	private ViewGroup layoutContent;

	private Status status = Status.Close;

	public DragLayout(Context context) {
		this(context, null);
	}

	public DragLayout(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
		this.context = context;
	}

	public DragLayout(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		gestureDetector = new GestureDetectorCompat(context,
				new XYScrollDetector());
		dragHelper = ViewDragHelper.create(this, dragHelperCallback);
	}

	class XYScrollDetector extends SimpleOnGestureListener {
		@Override
		public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx,
				float dy) {
			if (orientation == Orientation.Vertical) {
				return Math.abs(dy) >= Math.abs(dx);
			}
			return Math.abs(dy) <= Math.abs(dx);
		}
	}

	private ViewDragHelper.Callback dragHelperCallback = new ViewDragHelper.Callback() {

		// 这个是返回被横向移动的子控件child的左坐标left,和移动距离dx,我们可以根据这些值来返回child的新的left。
		// 这个方法必须重写,要不然就不能移动了。
		// 返回横向坐标左右边界值
		@Override
		public int clampViewPositionHorizontal(View child, int left, int dx) {

			if (orientation == Orientation.Vertical)
				return 0;
			if (distanceLeft + dx < 0) {
				// 右边界
				return 0;
			} else if (distanceLeft + dx > horizontalRange) {
				// 左边界
				return horizontalRange;
			} else {
				// 左右边界范围内
				return left;
			}
		}

		// 这个方法用来返回可以被移动的View对象,我们可以通过判断child与我们想移动的View是的相等来控制谁能移动。
		@Override
		public boolean tryCaptureView(View child, int pointerId) {
			return true;
		}

		// 横向的边界范围
		@Override
		public int getViewHorizontalDragRange(View child) {
			return horizontalRange;
		}

		public int clampViewPositionVertical(View child, int top, int dy) {

			if (orientation == Orientation.Horizontal)
				return 0;

			if (distanceTop + dy < 0) {
				return 0;
			} else if (distanceTop + dy > verticalRange) {
				return verticalRange;
			} else {
				return top;
			}
		}

		public int getViewVerticalDragRange(View child) {

			return verticalRange;

		};

		// ACTION_UP事件后调用其方法
		// 当releasedChild被释放的时候,xvel和yvel是x和y方向的加速度
		@Override
		public void onViewReleased(View releasedChild, float xvel, float yvel) {
			super.onViewReleased(releasedChild, xvel, yvel);
			if (orientation == Orientation.Vertical) {
				if (releasedChild == layoutMenu)
					return;
				if (yvel > 0) {
					// 加速度向下
					open();
				} else if (yvel < 0) {
					// 加速度向上
					close();
				} else if (releasedChild == layoutContent
						&& distanceTop > verticalRange * 0.3) {
					// 如果释放时,手指在内容区且内容区离左边的距离是range * 0.3
					open();
				} else {
					close();
				}

			} else {
				if (xvel > 0) {
					// 加速度向
					open();
				} else if (xvel < 0) {
					// 加速度向左
					close();
				} else if (releasedChild == layoutContent
						&& distanceLeft > horizontalRange * 0.3) {
					// 如果释放时,手指在内容区且内容区离左边的距离是range * 0.3
					open();
				} else if (releasedChild == layoutMenu
						&& distanceLeft > horizontalRange * 0.7) {
					// 如果释放时,手指在菜单区且内容区离左边的距离是range * 0.7
					open();
				} else {
					close();
				}
			}

		}

		// view在拖动过程坐标发生变化时会调用此方法,包括两个时间段:手动拖动和自动滚动
		@Override
		public void onViewPositionChanged(View changedView, int left, int top,
				int dx, int dy) {

			if (orientation == Orientation.Horizontal) {
				if (changedView == layoutContent) {
					distanceLeft = left;
				} else {
					distanceLeft = distanceLeft + left;
				}
				if (distanceLeft < 0) {
					distanceLeft = 0;
				} else if (distanceLeft > horizontalRange) {
					distanceLeft = horizontalRange;
				}
				layoutMenu.layout(0, 0, viewWidth, viewHeight);
				layoutContent.layout(distanceLeft, 0, distanceLeft + viewWidth,
						viewHeight);
				dispatchDragEvent(distanceLeft);
			} else {
				distanceTop = top;
				if (distanceTop < 0) {
					distanceTop = 0;
				} else if (distanceTop > verticalRange) {
					distanceTop = verticalRange;
				}
				layoutMenu.layout(0, 0, viewWidth, viewHeight);
				layoutContent.layout(0, distanceTop, viewWidth, distanceTop
						+ viewHeight);
				dispatchDragEvent(distanceTop);
			}
		}
	};

	public interface DragListener {
		/** 已经打开 */
		public void onOpen();

		/** 已经关闭 */
		public void onClose();

		/** 真在拖拽 */
		public void onDrag(float percent);
	}

	public void setDragListener(DragListener dragListener) {
		this.dragListener = dragListener;
	}

	@Override
	protected void onFinishInflate() {
		super.onFinishInflate();
		layoutMenu = (ViewGroup) getChildAt(0);
		layoutContent = (ViewGroup) getChildAt(1);
		layoutMenu.setClickable(true);
		layoutContent.setClickable(true);
	}

	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);
		viewWidth = layoutMenu.getMeasuredWidth();
		viewHeight = layoutMenu.getMeasuredHeight();
		horizontalRange = (int) (viewWidth * 0.7);
		verticalRange = (int) (viewHeight * 0.9);
	}

	@Override
	protected void onLayout(boolean changed, int left, int top, int right,
			int bottom) {
		layoutMenu.layout(0, 0, viewWidth, viewHeight);
		if (orientation == Orientation.Horizontal) {
			layoutContent.layout(distanceLeft, 0, distanceLeft + viewWidth,
					viewHeight);
		} else {
			layoutContent.layout(0, distanceTop, viewWidth, distanceTop
					+ viewHeight);
		}

	}

	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {

		if (orientation == Orientation.Vertical) {
			if ((Status.Open == getStatus() && ev.getY() < verticalRange)) {
				return false;
			}
		}
		return dragHelper.shouldInterceptTouchEvent(ev)
				&& gestureDetector.onTouchEvent(ev);

	}

	@Override
	public boolean onTouchEvent(MotionEvent e) {
		try {

			// 在processTouchEvent中对ACTION_DOWN、ACTION_MOVE和ACTION_UP事件进行了处理:
			// 1.在ACTION_DOWN中调用回调接口中的tryCaptureView方法,看当前touch的view是否允许拖动
			// 在此项目中的是直接return true,两个view都是允许拖动的
			// 2.在ACTION_MOVE中,view的坐标发生改变,调用回调接口中的onViewPositionChanged方法,
			// 根据坐标信息对view进行layout,通过ViewHelper这个类中的setScaleX、setScaleY方法,实现在
			// 拖动的过程中view在XY坐标上进行相应比例的缩放;
			// 3.在ACTION_UP后调用回调接口中的onViewReleased方法,此方法中一个重要的任务是在ACTION_UP事件
			dragHelper.processTouchEvent(e);
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return false;
	}

	private void dispatchDragEvent(int mainLeft) {
		float percent;
		if (orientation == Orientation.Horizontal) {
			percent = mainLeft / (float) horizontalRange;
			animateView(percent);
		} else {
			percent = mainLeft / (float) verticalRange;
		}

		Status lastStatus = status;
		if (dragListener == null)
			return;
		dragListener.onDrag(percent);
		if (lastStatus != getStatus() && status == Status.Close) {
			dragListener.onClose();
		} else if (lastStatus != getStatus() && status == Status.Open) {
			dragListener.onOpen();
		}
	}

	private void animateView(float percent) {
		float f1 = 1 - percent * 0.3f;

		ViewHelper.setScaleX(layoutContent, f1);
		ViewHelper.setScaleY(layoutContent, f1);

		ViewHelper.setTranslationX(
				layoutMenu,
				-layoutMenu.getWidth() / 2.3f
						+ layoutMenu.getWidth() / 2.3f * percent);
		ViewHelper.setScaleX(layoutMenu, 0.5f + 0.5f * percent);
		ViewHelper.setScaleY(layoutMenu, 0.5f + 0.5f * percent);
		ViewHelper.setAlpha(layoutMenu, percent);

		getBackground().setColorFilter(
				evaluate(percent, Color.BLACK, Color.TRANSPARENT),
				Mode.SRC_OVER);
	}

	private Integer evaluate(float fraction, Object startValue, Integer endValue) {
		int startInt = (Integer) startValue;
		int startA = (startInt >> 24) & 0xff;
		int startR = (startInt >> 16) & 0xff;
		int startG = (startInt >> 8) & 0xff;
		int startB = startInt & 0xff;
		int endInt = (Integer) endValue;
		int endA = (endInt >> 24) & 0xff;
		int endR = (endInt >> 16) & 0xff;
		int endG = (endInt >> 8) & 0xff;
		int endB = endInt & 0xff;
		return (int) ((startA + (int) (fraction * (endA - startA))) << 24)
				| (int) ((startR + (int) (fraction * (endR - startR))) << 16)
				| (int) ((startG + (int) (fraction * (endG - startG))) << 8)
				| (int) ((startB + (int) (fraction * (endB - startB))));
	}

	@Override
	public void computeScroll() {
		if (dragHelper.continueSettling(true)) {
			ViewCompat.postInvalidateOnAnimation(this);
		}
	}

	public enum Status {
		Drag, Open, Close
	}

	public enum Orientation {
		Horizontal, Vertical;
	}

	public Status getStatus() {
		if (orientation == Orientation.Horizontal) {
			if (distanceLeft == 0) {
				status = Status.Close;
			} else if (distanceLeft == horizontalRange) {
				status = Status.Open;
			} else {
				status = Status.Drag;
			}
		} else {
			if (distanceTop == 0) {
				status = Status.Close;
			} else if (distanceTop == verticalRange) {
				status = Status.Open;
			} else {
				status = Status.Drag;
			}
		}

		return status;
	}

	public ViewGroup getlayoutContent() {
		return layoutContent;
	}

	public ViewGroup getlayoutMenu() {
		return layoutMenu;
	}

	public void setOrientation(Orientation orientation) {
		this.orientation = orientation;
	}

	public void open() {
		open(true);
	}

	public void open(boolean animate) {
		if (animate) {
			if (orientation == Orientation.Horizontal) {
				if (dragHelper.smoothSlideViewTo(layoutContent,
						horizontalRange, 0)) {
					ViewCompat.postInvalidateOnAnimation(this);
				}
			} else {
				if (dragHelper.smoothSlideViewTo(layoutContent, 0,
						verticalRange)) {
					ViewCompat.postInvalidateOnAnimation(this);
				}
			}

		} else {
			layoutContent.layout(horizontalRange, 0, horizontalRange
					+ viewWidth, viewHeight);
			dispatchDragEvent(horizontalRange);
		}
	}

	public void close() {
		close(true);
	}

	public void close(boolean animate) {
		if (animate) {

			if (dragHelper.smoothSlideViewTo(layoutContent, 0, 0)) {
				ViewCompat.postInvalidateOnAnimation(this);
			}

		} else {
			layoutContent.layout(0, 0, viewWidth, viewHeight);
			dispatchDragEvent(0);
		}
	}

}
layout.xml

<com.bluemor.reddotface.view.DragLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/dl"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/bg" >

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="30dp"
        android:paddingLeft="30dp"
        android:onClick="true"
        android:paddingTop="50dp" >

        <LinearLayout
            android:id="@+id/ll1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal" >

            <ImageView
                android:id="@+id/iv_bottom"
                android:layout_width="70dp"
                android:layout_height="70dp"
                android:src="@drawable/ic_launcher" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="20dp"
                android:text="BlueMor"
                android:textColor="#ffffff"
                android:textSize="25sp" />
        </LinearLayout>

        <ListView
            android:id="@+id/lv"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/ll1"
            android:layout_marginBottom="30dp"
            android:layout_marginTop="20dp"
            android:cacheColorHint="#00000000"
            android:divider="@null"
            android:textColor="#ffffff" />
    </RelativeLayout>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#eeeeee" >

        <RelativeLayout
            android:id="@+id/rl_title"
            android:layout_width="match_parent"
            android:layout_height="55dp"
            android:background="#009990" >

            <ImageView
                android:id="@+id/iv_icon"
                android:layout_width="42dp"
                android:layout_height="42dp"
                android:layout_centerVertical="true"
                android:layout_marginLeft="10dp"
                android:scaleType="centerCrop"
                android:src="@drawable/ic_launcher" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:text="系统相册"
                android:textColor="#ffffff"
                android:textSize="20sp" />
        </RelativeLayout>

        <GridView
            android:id="@+id/gv_img"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/rl_title"
            android:cacheColorHint="#00000000"
            android:numColumns="4"
            android:verticalSpacing="20dp" >
        </GridView>

    </RelativeLayout>

</com.bluemor.reddotface.view.DragLayout>

activity

		dl = (DragLayout) findViewById(R.id.dl);
		dl.setOrientation(Orientation.Vertical);
//		dl.setDragListener(new DragListener() {
//			@Override
//			public void onOpen() {
//				lv.smoothScrollToPosition(new Random().nextInt(30));
//			}
//
//			@Override
//			public void onClose() {
//				
//			}
//
//			@Override
//			public void onDrag(float percent) {
//				
//			}
//		});




四、实战3

例3:

ViewDragHelper一般用在一个自定义ViewGroup的内部,比如下面自定义了一个继承于LinearLayout的DragLayout,DragLayout内部有一个子viewmDragView作为成员变量:

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public class DragLayout extends LinearLayout {  
  2. private final ViewDragHelper mDragHelper;  
  3. private View mDragView;  
  4. public DragLayout(Context context) {  
  5.   this(context, null);  
  6. }  
  7. public DragLayout(Context context, AttributeSet attrs) {  
  8.   this(context, attrs, 0);  
  9. }  
  10. public DragLayout(Context context, AttributeSet attrs, int defStyle) {  
  11.   super(context, attrs, defStyle);  
  12. }  

创建一个带有回调接口的 ViewDragHelper

 

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public DragLayout(Context context, AttributeSet attrs, int defStyle) {  
  2.   super(context, attrs, defStyle);  
  3.   mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());  
  4. }  

其中1.0f是敏感度参数参数越大越敏感。第一个参数为this,表示该类生成的对象,他是ViewDragHelper的拖动处理对象,必须为ViewGroup

要让ViewDragHelper能够处理拖动需要将触摸事件传递给ViewDragHelper,这点和gesturedetector是一样的:

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2. public boolean onInterceptTouchEvent(MotionEvent ev) {  
  3.   final int action = MotionEventCompat.getActionMasked(ev);  
  4.   if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {  
  5.       mDragHelper.cancel();  
  6.       return false;  
  7.   }  
  8.   return mDragHelper.shouldInterceptTouchEvent(ev);  
  9. }  
  10. @Override  
  11. public boolean onTouchEvent(MotionEvent ev) {  
  12.   mDragHelper.processTouchEvent(ev);  
  13.   return true;  
  14. }  

接下来,你就可以在回调中处理各种拖动行为了。


2.拖动行为的处理

处理横向的拖动:

DragHelperCallback中实现clampViewPositionHorizontal方法, 并且返回一个适当的数值就能实现横向拖动效果,clampViewPositionHorizontal的第二个参数是指当前拖动子view应该到达的x坐标。所以按照常理这个方法原封返回第二个参数就可以了,但为了让被拖动的view遇到边界之后就不在拖动,对返回的值做了更多的考虑。

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2. public int clampViewPositionHorizontal(View child, int left, int dx) {  
  3.   Log.d("DragLayout", "clampViewPositionHorizontal " + left + "," + dx);  
  4.   final int leftBound = getPaddingLeft();  
  5.   final int rightBound = getWidth() - mDragView.getWidth();  
  6.   final int newLeft = Math.min(Math.max(left, leftBound), rightBound);  
  7.   return newLeft;  
  8. }  

同上,处理纵向的拖动:

DragHelperCallback中实现clampViewPositionVertical方法,实现过程同clampViewPositionHorizontal

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2. public int clampViewPositionVertical(View child, int top, int dy) {  
  3.   final int topBound = getPaddingTop();  
  4.   final int bottomBound = getHeight() - mDragView.getHeight();  
  5.   final int newTop = Math.min(Math.max(top, topBound), bottomBound);  
  6.   return newTop;  
  7. }  

clampViewPositionHorizontal 和 clampViewPositionVertical必须要重写,因为默认它返回的是0。事实上我们在这两个方法中所能做的事情很有限。 个人觉得这两个方法的作用就是给了我们重新定义目的坐标的机会。

通过DragHelperCallback的tryCaptureView方法的返回值可以决定一个parentview中哪个子view可以拖动,现在假设有两个子views (mDragView1和mDragView2)  ,如下实现tryCaptureView之后,则只有mDragView1是可以拖动的。

1
2
3
4
@Override
public boolean tryCaptureView(View child, int pointerId) {
   return child == mDragView1;
}

滑动边缘:

分为滑动左边缘还是右边缘:EDGE_LEFT和EDGE_RIGHT,下面的代码设置了可以处理滑动左边缘:

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);  

假如如上设置,onEdgeTouched方法会在左边缘滑动的时候被调用,这种情况下一般都是没有和子view接触的情况。

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2. public void onEdgeTouched(int edgeFlags, int pointerId) {  
  3.     super.onEdgeTouched(edgeFlags, pointerId);  
  4.     Toast.makeText(getContext(), "edgeTouched", Toast.LENGTH_SHORT).show();  
  5. }  

如果你想在边缘滑动的时候根据滑动距离移动一个子view,可以通过实现onEdgeDragStarted方法,并在onEdgeDragStarted方法中手动指定要移动的子View

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2. public void onEdgeDragStarted(int edgeFlags, int pointerId) {  
  3.     mDragHelper.captureChildView(mDragView2, pointerId);  
  4. }  

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


代码中的关键点:

1.tryCaptureView返回了唯一可以被拖动的header view;

2.拖动范围drag range的计算是在onLayout中完成的;

3.注意在onInterceptTouchEvent和onTouchEvent中使用的ViewDragHelper的若干方法;

4.在computeScroll中使用continueSettling方法(因为ViewDragHelper使用了scroller)

5.smoothSlideViewTo方法来完成拖动结束后的惯性操作。

需要注意的是代码仍然有很大改进空间。

activity_main.xml

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. <FrameLayout  
  2.         xmlns:android="http://schemas.android.com/apk/res/android"  
  3.         android:layout_width="match_parent"  
  4.         android:layout_height="match_parent">  
  5.     <ListView  
  6.             android:id="@+id/listView"  
  7.             android:layout_width="match_parent"  
  8.             android:layout_height="match_parent"  
  9.             android:tag="list"  
  10.             />  
  11.     <com.example.vdh.YoutubeLayout  
  12.             android:layout_width="match_parent"  
  13.             android:layout_height="match_parent"  
  14.             android:id="@+id/youtubeLayout"  
  15.             android:orientation="vertical"  
  16.             android:visibility="visible">  
  17.         <TextView  
  18.                 android:id="@+id/viewHeader"  
  19.                 android:layout_width="match_parent"  
  20.                 android:layout_height="128dp"  
  21.                 android:fontFamily="sans-serif-thin"  
  22.                 android:textSize="25sp"  
  23.                 android:tag="text"  
  24.                 android:gravity="center"  
  25.                 android:textColor="@android:color/white"  
  26.                 android:background="#AD78CC"/>  
  27.         <TextView  
  28.                 android:id="@+id/viewDesc"  
  29.                 android:tag="desc"  
  30.                 android:textSize="35sp"  
  31.                 android:gravity="center"  
  32.                 android:text="Loreum Loreum"  
  33.                 android:textColor="@android:color/white"  
  34.                 android:layout_width="match_parent"  
  35.                 android:layout_height="match_parent"  
  36.                 android:background="#FF00FF"/>  
  37.     </com.example.vdh.YoutubeLayout>  
  38. </FrameLayout>  

YoutubeLayout.java
[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public class YoutubeLayout extends ViewGroup {  
  2. private final ViewDragHelper mDragHelper;  
  3. private View mHeaderView;  
  4. private View mDescView;  
  5. private float mInitialMotionX;  
  6. private float mInitialMotionY;  
  7. private int mDragRange;  
  8. private int mTop;  
  9. private float mDragOffset;  
  10. public YoutubeLayout(Context context) {  
  11.   this(context, null);  
  12. }  
  13. public YoutubeLayout(Context context, AttributeSet attrs) {  
  14.   this(context, attrs, 0);  
  15. }  
  16. @Override  
  17. protected void onFinishInflate() {  
  18.     mHeaderView = findViewById(R.id.viewHeader);  
  19.     mDescView = findViewById(R.id.viewDesc);  
  20. }  
  21. public YoutubeLayout(Context context, AttributeSet attrs, int defStyle) {  
  22.   super(context, attrs, defStyle);  
  23.   mDragHelper = ViewDragHelper.create(this, 1f, new DragHelperCallback());  
  24. }  
  25. public void maximize() {  
  26.     smoothSlideTo(0f);  
  27. }  
  28. boolean smoothSlideTo(float slideOffset) {  
  29.     final int topBound = getPaddingTop();  
  30.     int y = (int) (topBound + slideOffset * mDragRange);  
  31.     if (mDragHelper.smoothSlideViewTo(mHeaderView, mHeaderView.getLeft(), y)) {  
  32.         ViewCompat.postInvalidateOnAnimation(this);  
  33.         return true;  
  34.     }  
  35.     return false;  
  36. }  
  37. private class DragHelperCallback extends ViewDragHelper.Callback {  
  38.   @Override  
  39.   public boolean tryCaptureView(View child, int pointerId) {  
  40.         return child == mHeaderView;  
  41.   }  
  42.     @Override  
  43.   public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {  
  44.       mTop = top;  
  45.       mDragOffset = (float) top / mDragRange;  
  46.         mHeaderView.setPivotX(mHeaderView.getWidth());  
  47.         mHeaderView.setPivotY(mHeaderView.getHeight());  
  48.         mHeaderView.setScaleX(1 - mDragOffset / 2);  
  49.         mHeaderView.setScaleY(1 - mDragOffset / 2);  
  50.         mDescView.setAlpha(1 - mDragOffset);  
  51.         requestLayout();  
  52.   }  
  53.   @Override  
  54.   public void onViewReleased(View releasedChild, float xvel, float yvel) {  
  55.       int top = getPaddingTop();  
  56.       if (yvel > 0 || (yvel == 0 && mDragOffset > 0.5f)) {  
  57.           top += mDragRange;  
  58.       }  
  59.       mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top);  
  60.   }  
  61.   @Override  
  62.   public int getViewVerticalDragRange(View child) {  
  63.       return mDragRange;  
  64.   }  
  65.   @Override  
  66.   public int clampViewPositionVertical(View child, int top, int dy) {  
  67.       final int topBound = getPaddingTop();  
  68.       final int bottomBound = getHeight() - mHeaderView.getHeight() - mHeaderView.getPaddingBottom();  
  69.       final int newTop = Math.min(Math.max(top, topBound), bottomBound);  
  70.       return newTop;  
  71.   }  
  72. }  
  73. @Override  
  74. public void computeScroll() {  
  75.   if (mDragHelper.continueSettling(true)) {  
  76.       ViewCompat.postInvalidateOnAnimation(this);  
  77.   }  
  78. }  
  79. @Override  
  80. public boolean onInterceptTouchEvent(MotionEvent ev) {  
  81.   final int action = MotionEventCompat.getActionMasked(ev);  
  82.   if (( action != MotionEvent.ACTION_DOWN)) {  
  83.       mDragHelper.cancel();  
  84.       return super.onInterceptTouchEvent(ev);  
  85.   }  
  86.   if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {  
  87.       mDragHelper.cancel();  
  88.       return false;  
  89.   }  
  90.   final float x = ev.getX();  
  91.   final float y = ev.getY();  
  92.   boolean interceptTap = false;  
  93.   switch (action) {  
  94.       case MotionEvent.ACTION_DOWN: {  
  95.           mInitialMotionX = x;  
  96.           mInitialMotionY = y;  
  97.             interceptTap = mDragHelper.isViewUnder(mHeaderView, (int) x, (int) y);  
  98.           break;  
  99.       }  
  100.       case MotionEvent.ACTION_MOVE: {  
  101.           final float adx = Math.abs(x - mInitialMotionX);  
  102.           final float ady = Math.abs(y - mInitialMotionY);  
  103.           final int slop = mDragHelper.getTouchSlop();  
  104.           if (ady > slop && adx > ady) {  
  105.               mDragHelper.cancel();  
  106.               return false;  
  107.           }  
  108.       }  
  109.   }  
  110.   return mDragHelper.shouldInterceptTouchEvent(ev) || interceptTap;  
  111. }  
  112. @Override  
  113. public boolean onTouchEvent(MotionEvent ev) {  
  114.   mDragHelper.processTouchEvent(ev);  
  115.   final int action = ev.getAction();  
  116.     final float x = ev.getX();  
  117.     final float y = ev.getY();  
  118.     boolean isHeaderViewUnder = mDragHelper.isViewUnder(mHeaderView, (int) x, (int) y);  
  119.     switch (action & MotionEventCompat.ACTION_MASK) {  
  120.       case MotionEvent.ACTION_DOWN: {  
  121.           mInitialMotionX = x;  
  122.           mInitialMotionY = y;  
  123.           break;  
  124.       }  
  125.       case MotionEvent.ACTION_UP: {  
  126.           final float dx = x - mInitialMotionX;  
  127.           final float dy = y - mInitialMotionY;  
  128.           final int slop = mDragHelper.getTouchSlop();  
  129.           if (dx * dx + dy * dy < slop * slop && isHeaderViewUnder) {  
  130.               if (mDragOffset == 0) {  
  131.                   smoothSlideTo(1f);  
  132.               } else {  
  133.                   smoothSlideTo(0f);  
  134.               }  
  135.           }  
  136.           break;  
  137.       }  
  138.   }  
  139.   return isHeaderViewUnder && isViewHit(mHeaderView, (int) x, (int) y) || isViewHit(mDescView, (int) x, (int) y);  
  140. }  
  141. private boolean isViewHit(View view, int x, int y) {  
  142.     int[] viewLocation = new int[2];  
  143.     view.getLocationOnScreen(viewLocation);  
  144.     int[] parentLocation = new int[2];  
  145.     this.getLocationOnScreen(parentLocation);  
  146.     int screenX = parentLocation[0] + x;  
  147.     int screenY = parentLocation[1] + y;  
  148.     return screenX >= viewLocation[0] && screenX < viewLocation[0] + view.getWidth() &&  
  149.             screenY >= viewLocation[1] && screenY < viewLocation[1] + view.getHeight();  
  150. }  
  151. @Override  
  152. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  153.     measureChildren(widthMeasureSpec, heightMeasureSpec);  
  154.     int maxWidth = MeasureSpec.getSize(widthMeasureSpec);  
  155.     int maxHeight = MeasureSpec.getSize(heightMeasureSpec);  
  156.     setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0),  
  157.             resolveSizeAndState(maxHeight, heightMeasureSpec, 0));  
  158. }  
  159. @Override  
  160. protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  161.   mDragRange = getHeight() - mHeaderView.getHeight();  
  162.     mHeaderView.layout(  
  163.             0,  
  164.             mTop,  
  165.             r,  
  166.             mTop + mHeaderView.getMeasuredHeight());  
  167.     mDescView.layout(  
  168.             0,  
  169.             mTop + mHeaderView.getMeasuredHeight(),  
  170.             r,  
  171.             mTop  + b);  
  172. }  

代码下载地址: https://github.com/flavienlaurent/flavienlaurent.com


不管是menudrawer 还是本文实现的DragLayout都体现了一种设计哲学,即可拖动的控件都是封装在一个自定义的Layout中的,为什么这样做?为什么不直接将ViewDragHelper.create(this, 1f, new DragHelperCallback())中的this替换成任何已经布局好的容器,这样这个容器中的子View就能被拖动了,而往往是单独定义一个Layout来处理?个人认为如果在一般的布局中去拖动子view并不会出现什么问题,只是原本规则的世界被打乱了,而单独一个Layout来完成拖动,无非是说,他本来就没有什么规则可言,拖动一下也无妨。


参考:http://www.xuebuyuan.com/2225442.html

           http://blog.csdn.net/pi9nc/article/details/39583377

           https://software.intel.com/zh-cn/blogs/2015/03/05/android-zlistview-listview-0


评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值