ViewDragHelper
它主要用于处理ViewGroup中对子View的拖拽处理,本质是对触摸事件的解析类。
ViewDrager是简化view拖拽操作的帮助类,ViewDragHelper解决了android中手势处理过于复杂的问题。
1.ViewDragHelper在高版本的v4包(android4.4以上的v4)中
2.它主要用于处理ViewGroup中对子View的拖拽处理
3.它是Google在2013年开发者大会提出的
4.它主要封装了对View的触摸位置,触摸速度,移动距离等的检测和Scroller,通过接口回调的
方式告诉我们;只需要我们指定是否需要移动,移动多少等;5.本质是对触摸事件的解析类;
View移动的相关方法总结
1.通过改变view在父View的layout位置来移动,但是只能移动指定的View:
view.layout(l,t,r,b);
view.offsetLeftAndRight(offset);//同时改变left和right
view.offsetTopAndBottom(offset);//同时改变top和bottom
2.通过改变scrollX和scrollY来移动,但是可以移动所有的子View;
scrollTo(x,y)表示移动到一个具体的坐标点(x,y),而scrollBy(x,y)表示移动的增量为dx,dy。
scrollTo(x,y);
scrollBy(xOffset,yOffset);
3.通过改变Canvas绘制的位置来移动View的内容:
canvas.drawBitmap(bitmap, left, top, paint)
4.LayoutParams
//必须获取父View的LayoutParams
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams)getLayoutParams();
layoutParams.leftMargin = getLeft() + dx;
layoutParams.topMargin = getTop() + dy;
setLayoutParams(layoutParams);
案例
接下来就实现一个内部View随意拖动的demo
如上图简单呈现出两个方块后,提出一个需求:
1.拖动方块时,方块(即子View)可以跟随手指移动。
2.一个方块移动时,另一个方块可以跟随移动。
3.将方块移动到左边区域(右边区域)后放开(即手指离开屏幕),它会自动移动到左边界(右边界)。
4.移动的时候给方块加点动画 。
自定义一个ViewGroup继承FrameLayout:
在自定义ViewGroup的时候,如果对子View的测量没有特殊的需求,那么可以继承系统已有的
布局(比如FrameLayout),目的是为了让已有的布局帮我们实行onMeasure;
package com.xiaoyehai.viewdragerhelper.widget;
import android.content.Context;
import android.graphics.Color;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import com.nineoldandroids.view.ViewHelper;
import com.xiaoyehai.viewdragerhelper.ColorUtil;
/**
* 继承FrameLayout
* 在自定义ViewGroup的时候,如果对子View的测量没有特殊的需求,那么可以继承系统已有的
* 布局(比如FrameLayout),目的是为了让已有的布局帮我们实行onMeasure;
* <p>
* Created by xiaoyehai on 2018/8/3 0003.
*/
public class DragLayout extends FrameLayout {
/**
* ViewDragHelper:它主要用于处理ViewGroup中对子View的拖拽处理,
* 本质是对触摸事件的解析类;
*/
private ViewDragHelper mViewDragHelper;
private View redView;
private View blueView;
public DragLayout(Context context) {
this(context, null);
}
public DragLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DragLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
/**
* 初始化ViewDragHelper
*/
private void init() {
mViewDragHelper = ViewDragHelper.create(this, callback);
}
/**
* 当DragLayout的布局文件的结束标签读取完成后会执行该方法,此时会知道自己有几个子控件
* 一般用来初始化子控件的引用
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
redView = getChildAt(0);
blueView = getChildAt(1);
}
/**
* 确定控件位置
*
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//放在左上角
int left = getPaddingLeft();
int top = getPaddingTop();
//放在水平居中
//int left = getPaddingLeft() + getMeasuredWidth() / 2 - redView.getMeasuredWidth() / 2;
//摆放在左上角
redView.layout(left, top, left + redView.getMeasuredWidth(),
top + redView.getMeasuredHeight());
//摆放在redView下面
blueView.layout(left, redView.getBottom(), left + blueView.getMeasuredWidth(),
redView.getBottom() + blueView.getMeasuredHeight());
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 让ViewDragHelper帮我们判断是否应该拦截
boolean result = mViewDragHelper.shouldInterceptTouchEvent(ev);
return result;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 将触摸事件交给ViewDragHelper来解析处理
mViewDragHelper.processTouchEvent(event);
return true;
}
/**
* 回调类
*/
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
/**
* 用于判断是否捕获当前child的触摸事件
* @param child 当前触摸的子View
* @param pointerId
* @return true:捕获并解析 false:不处理
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == blueView || child == redView;
}
/**
* 当view被开始捕获和解析的回调
* @param capturedChild 当前被捕获的子view
* @param activePointerId
*/
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
}
/**
* 获取view水平方向的拖拽范围,但是目前不能限制边界,返回的值目前用在手指抬起的时候
* view缓慢移动的动画时间的计算; 最好不要返回0
* @param child
* @return
*/
@Override
public int getViewHorizontalDragRange(View child) {
return getMeasuredWidth() - child.getMeasuredWidth();
}
/**
* 获取view垂直方向的拖拽范围,目前不能限制边界,最好不要返回0
* @param child
* @return
*/
@Override
public int getViewVerticalDragRange(View child) {
return getMeasuredHeight() - child.getMeasuredHeight();
}
/**
* 控制child在水平方向的移动
* @param child 当前触摸的子View
* @param left 当前child的即将移动到的位置,left=chile.getLeft()+dx
* @param dx 本次child水平方向移动的距离
* @return 表示你真正想让child的left变成的值
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
if (left < 0) {
left = 0; //限制左边界
} else if (left > getMeasuredWidth() - child.getMeasuredWidth()) {
left = getMeasuredWidth() - child.getMeasuredWidth(); //限制右边界
}
// return left-dx; //不能移动
return left;
}
/**
* 控制child在垂直方向的移动
* @param child
* @param top top=child.getTop()+dy
* @param dy
* @return
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
if (top < 0) {
top = 0; //限制上边界
} else if (top > getMeasuredHeight() - child.getMeasuredHeight()) {
top = getMeasuredHeight() - child.getMeasuredHeight(); //限制下边界
}
return top;
}
/**
* 当child的位置改变的时候执行,一般用来做其他子View跟随该view移动
* @param changedView 当前位置改变的child
* @param left child当前最新的left
* @param top child当前最新的top
* @param dx 本次水平移动的距离
* @param dy 本次垂直移动的距离
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
//让redView跟着blueView移动
if (changedView == blueView) {
redView.layout(redView.getLeft() + dx, redView.getTop() + dy,
redView.getRight() + dx, redView.getBottom() + dy);
} else if (changedView == redView) {
//让blueView跟着redView移动
blueView.layout(blueView.getLeft() + dx, blueView.getTop() + dy,
blueView.getRight() + dx, blueView.getBottom() + dy);
}
//1.计算view移动百分比:当前位置/总长
float fraction = changedView.getLeft() * 1f / (getMeasuredWidth() - changedView.getMeasuredWidth());
//2.执行伴随动画
excuteAnim(fraction);
}
/**
* 手指抬起的执行该方法
* @param releasedChild 当前抬起的view
* @param xvel x方向的移动速度有 正:向右移动
* @param yvel 方向的移动速度
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
int centerLeft = getMeasuredWidth() / 2 - releasedChild.getMeasuredWidth() / 2;
if (releasedChild.getLeft() < centerLeft) {
//控件在左半边,向左缓慢移动
//ViewDragHelper封装了Scroller
mViewDragHelper.smoothSlideViewTo(releasedChild, 0, releasedChild.getTop());
ViewCompat.postInvalidateOnAnimation(DragLayout.this); //刷新
} else {
//控件在右半边,向右缓慢移动
mViewDragHelper.smoothSlideViewTo(releasedChild,
getMeasuredWidth() - releasedChild.getMeasuredWidth(), releasedChild.getTop());
ViewCompat.postInvalidateOnAnimation(DragLayout.this); //刷新
}
}
};
@Override
public void computeScroll() {
//如果动画还没结束
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(DragLayout.this); //刷新
}
}
/**
* 执行移动过程中的伴随动画
*
* @param fraction 移动百分比
*/
private void excuteAnim(float fraction) {
//缩放
// redView.setScaleX(1 + 0.5f * fraction);
// redView.setScaleY(1 + 0.5f * fraction);
//ViewHelper.setScaleX(redView, 1 + 0.5f * fraction);
//ViewHelper.setScaleX(blueView, 1 + 0.5f * fraction);
//ViewHelper.setScaleY(redView, 1 + 0.5f * fraction);
//旋转
// ViewHelper.setRotation(redView, 720 * fraction); //围绕z抽转
ViewHelper.setRotationX(redView, 360 * fraction); //围绕x抽转
ViewHelper.setRotationX(blueView, 360 * fraction); //围绕x抽转
//ViewHelper.setRotationX(redView, 360 * fraction);
// ViewHelper.setRotationY(redView,720 * fraction); //围绕y抽转
//平移
// ViewHelper.setTranslationX(redView,80 * fraction);
//透明度
// ViewHelper.setAlpha(redView,1 - fraction);
//设置 redView过度颜色的渐变
//redView.setBackgroundColor((Integer) ColorUtil.evaluateColor(fraction, Color.RED, Color.GREEN));
//设置该控件过度颜色的渐变
//setBackgroundColor((Integer) ColorUtil.evaluateColor(fraction, Color.RED, Color.GREEN));
}
}
使用:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
tools:context="com.xiaoyehai.viewdragerhelper.MainActivity">
<com.xiaoyehai.viewdragerhelper.widget.DragLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#f00" />
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#00f" />
</com.xiaoyehai.viewdragerhelper.widget.DragLayout>
</android.support.constraint.ConstraintLayout>
package com.xiaoyehai.viewdragerhelper;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
/**
* ViewDragHelper:它主要用于处理ViewGroup中对子View的拖拽处理,
* 本质是对触摸事件的解析类.
*/
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
ColorUtil:
package com.xiaoyehai.viewdragerhelper;
public class ColorUtil {
public static Object evaluateColor(float fraction, Object startValue,
Object 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))));
}
}
继承 ViewGroup也可实现
getHeight和getMeasuredHeight的区别:
getMeasuredHeight:只要view执行完onMeasure方法就能够获取到值;
getHeight:只有view执行完layout才能获取到值;
package com.xiaoyehai.viewdragerhelper.widget;
import android.content.Context;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import com.nineoldandroids.view.ViewHelper;
/**
* getHeight和getMeasuredHeight的区别:
* getMeasuredHeight:只要view执行完onMeasure方法就能够获取到值;
* getHeight:只有view执行完layout才能获取到值;
* <p>
* Created by xiaoyehai on 2018/8/3 0003.
*/
public class DragLayout2 extends ViewGroup {
private View redView;
private View blueView;
/**
* ViewDragHelper:它主要用于处理ViewGroup中对子View的拖拽处理,
* 本质是对触摸事件的解析类;
*/
private ViewDragHelper mViewDragHelper;
public DragLayout2(Context context) {
this(context, null);
}
public DragLayout2(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DragLayout2(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
/**
* 初始化ViewDragHelper
*/
private void init() {
mViewDragHelper = ViewDragHelper.create(this, callback);
}
/**
* 当DragLayout的布局文件的结束标签读取完成后会执行该方法,此时会知道自己有几个子控件
* 一般用来初始化子控件的引用
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
redView = getChildAt(0);
blueView = getChildAt(1);
}
/**
* 测量控件大小::宽度和高度
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//要测量自己的子View
// View view = getChildAt(0); //获取子view,不这样用,在onFinishInflate()方法中获取
//int size = (int) getResources().getDimension(R.dimen.width); //100dp
int size = redView.getLayoutParams().width;
//测量规则,宽和高相同,用同一个测量规则
int measureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
//父view测量完子view可填(0,0),现在是正要测量子View,不能写(0,0)
redView.measure(measureSpec, measureSpec); //宽,高
blueView.measure(measureSpec, measureSpec);
//如果说没有特殊的对子View的测量需求,可以用如下方法测量比较简单
//measureChild(redView, widthMeasureSpec, heightMeasureSpec);
// measureChild(blueView, widthMeasureSpec, heightMeasureSpec);
}
/**
* 确定控件位置
*
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//放在左上角
int left = getPaddingLeft();
int top = getPaddingTop();
//放在水平居中
//int left = getPaddingLeft() + getMeasuredWidth() / 2 - redView.getMeasuredWidth() / 2;
//摆放在左上角
redView.layout(left, top, left + redView.getMeasuredWidth(),
top + redView.getMeasuredHeight());
//摆放在redView下面
blueView.layout(left, redView.getBottom(), left + blueView.getMeasuredWidth(),
redView.getBottom() + blueView.getMeasuredHeight());
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 让ViewDragHelper帮我们判断是否应该拦截
boolean result = mViewDragHelper.shouldInterceptTouchEvent(ev);
return result;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 将触摸事件交给ViewDragHelper来解析处理
mViewDragHelper.processTouchEvent(event);
return true;
}
/**
* 回调类
*/
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
/**
* 用于判断是否捕获当前child的触摸事件
* @param child 当前触摸的子View
* @param pointerId
* @return true:捕获并解析 false:不处理
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == blueView || child == redView;
}
/**
* 当view被开始捕获和解析的回调
* @param capturedChild 当前被捕获的子view
* @param activePointerId
*/
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
}
/**
* 获取view水平方向的拖拽范围,但是目前不能限制边界,返回的值目前用在手指抬起的时候
* view缓慢移动的动画时间的计算; 最好不要返回0
* @param child
* @return
*/
@Override
public int getViewHorizontalDragRange(View child) {
return getMeasuredWidth() - child.getMeasuredWidth();
}
/**
* 获取view垂直方向的拖拽范围,目前不能限制边界,最好不要返回0
* @param child
* @return
*/
@Override
public int getViewVerticalDragRange(View child) {
return getMeasuredHeight() - child.getMeasuredHeight();
}
/**
* 控制child在水平方向的移动
* @param child 当前触摸的子View
* @param left 当前child的即将移动到的位置,left=chile.getLeft()+dx
* @param dx 本次child水平方向移动的距离
* @return 表示你真正想让child的left变成的值
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
if (left < 0) {
left = 0; //限制左边界
} else if (left > getMeasuredWidth() - child.getMeasuredWidth()) {
left = getMeasuredWidth() - child.getMeasuredWidth(); //限制右边界
}
// return left-dx; //不能移动
return left;
}
/**
* 控制child在垂直方向的移动
* @param child
* @param top top=child.getTop()+dy
* @param dy
* @return
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
if (top < 0) {
top = 0; //限制上边界
} else if (top > getMeasuredHeight() - child.getMeasuredHeight()) {
top = getMeasuredHeight() - child.getMeasuredHeight(); //限制下边界
}
return top;
}
/**
* 当child的位置改变的时候执行,一般用来做其他子View跟随该view移动
* @param changedView 当前位置改变的child
* @param left child当前最新的left
* @param top child当前最新的top
* @param dx 本次水平移动的距离
* @param dy 本次垂直移动的距离
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
//让redView跟着blueView移动
if (changedView == blueView) {
redView.layout(redView.getLeft() + dx, redView.getTop() + dy,
redView.getRight() + dx, redView.getBottom() + dy);
} else if (changedView == redView) {
//让blueView跟着redView移动
blueView.layout(blueView.getLeft() + dx, blueView.getTop() + dy,
blueView.getRight() + dx, blueView.getBottom() + dy);
}
//1.计算view移动百分比:当前位置/总长
float fraction = changedView.getLeft() * 1f / (getMeasuredWidth() - changedView.getMeasuredWidth());
//2.执行伴随动画
excuteAnim(fraction);
}
/**
* 手指抬起的执行该方法
* @param releasedChild 当前抬起的view
* @param xvel x方向的移动速度有 正:向右移动
* @param yvel 方向的移动速度
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
int centerLeft = getMeasuredWidth() / 2 - releasedChild.getMeasuredWidth() / 2;
if (releasedChild.getLeft() < centerLeft) {
//控件在左半边,向左缓慢移动
//ViewDragHelper封装了Scroller
mViewDragHelper.smoothSlideViewTo(releasedChild, 0, releasedChild.getTop());
ViewCompat.postInvalidateOnAnimation(DragLayout2.this); //刷新
} else {
//控件在右半边,向右缓慢移动
mViewDragHelper.smoothSlideViewTo(releasedChild,
getMeasuredWidth() - releasedChild.getMeasuredWidth(), releasedChild.getTop());
ViewCompat.postInvalidateOnAnimation(DragLayout2.this); //刷新
}
}
};
@Override
public void computeScroll() {
//如果动画还没结束
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(DragLayout2.this); //刷新
}
}
/**
* 执行移动过程中的伴随动画
*
* @param fraction 移动百分比
*/
private void excuteAnim(float fraction) {
//缩放
// redView.setScaleX(1 + 0.5f * fraction);
// redView.setScaleY(1 + 0.5f * fraction);
//ViewHelper.setScaleX(redView, 1 + 0.5f * fraction);
//ViewHelper.setScaleX(blueView, 1 + 0.5f * fraction);
//ViewHelper.setScaleY(redView, 1 + 0.5f * fraction);
//旋转
// ViewHelper.setRotation(redView, 720 * fraction); //围绕z抽转
ViewHelper.setRotationX(redView, 360 * fraction); //围绕x抽转
ViewHelper.setRotationX(blueView, 360 * fraction); //围绕x抽转
//ViewHelper.setRotationX(redView, 360 * fraction);
// ViewHelper.setRotationY(redView,720 * fraction); //围绕y抽转
//平移
// ViewHelper.setTranslationX(redView,80 * fraction);
//透明度
// ViewHelper.setAlpha(redView,1 - fraction);
//设置 redView过度颜色的渐变
//redView.setBackgroundColor((Integer) ColorUtil.evaluateColor(fraction, Color.RED, Color.GREEN));
//设置该控件过度颜色的渐变
//setBackgroundColor((Integer) ColorUtil.evaluateColor(fraction, Color.RED, Color.GREEN));
}
}
代码链接