学习资料:
- Android开发群英传
- 鸿洋大神的Android ViewDragHelper完全解析 自定义ViewGroup神器
ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number of useful operations and state tracking for allowing a user to drag and reposition views within their parent ViewGroup.
ViewDragHelper
是一个自定义ViewGroup
的工具类。内部提供了一系列属性和用户拖动状态,并且支持恢复
ViewDragHelper
是解决各种滑动的终极绝招,几乎可以实现各种不同的的滑动、拖动
1. 简单使用
简单需求:
在屏幕上,一个TextView
可以拖动,并且当拖动的距离位于屏幕上半部分1/2
区域内,可以自己恢复原始位置
ViewDragHelper
使用也有一个固定的模式
- 初始化
ViewDragHer
实例,并创建所需要的回调接口 - 处理事件拦截和事件的消费
代码:
public class DragView extends LinearLayout {
private ViewDragHelper mViewDragHelper;
public DragView(Context context, AttributeSet attrs) {
super(context, attrs);
initDragHelper();
}
private void initDragHelper() {
mViewDragHelper = ViewDragHelper.create(DragView.this, 1.0f, mDragCallback);
}
/**
* ViewDragHelper回调接口
*/
private ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {//可以用来指定哪一个childView可以拖动
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 onInterceptHoverEvent(MotionEvent event) {//拦截事件
return mViewDragHelper.shouldInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {//消费事件
//将触摸事件传递给`ViewDragHelper`,必不可少
mViewDragHelper.processTouchEvent(event);
return true;
}
}
布局文件:
布局文件很简单,就是包含一个TextView
在DragView
内,TextView
就可以任意拖动
在创建ViewDragHelper
对象时,create()
方法有两种形式
//方式 1
public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
return new ViewDragHelper(forParent.getContext(), forParent, cb);
}
//方式2
public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
final ViewDragHelper helper = create(forParent, cb);
helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
return helper;
}
方式2
多了第二个参数值,代表了灵敏度。sensitivity
越大,helper.mTouchSlop
越小,一般写为1.0f
在ViewDragHelper.Callback mDragCallback
内,重写了3个方法
@Override
public boolean tryCaptureView(View child, int pointerId) {
return true;
}
这个方法可以用来指定哪一个childView
可以进行拖动,通过重写onFinishInflate()
来获取childView
子控件,然后进行childView
判断
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {// 水平拖动
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {//竖直拖动
return top;
}
这两个方法默认返回值都为0
left
代表在水平方向上,childView
即将在x
轴移动到目标坐标位置,dx
代表较前一次的增量,left = child.getLeft()+dx
,top
同理,代表垂直方向上childView
即将在y
轴移动到目标坐标位置
虽然上面的代码实现了childView
的拖动,但有些问题需要考虑优化
2.简单进行Padding优化
在简单使用
使用图中,首先一个问题便是TextView
超出了屏幕范围,导致内容都无法显示完全。需要对left
和top
进行修改
修改代码
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {// 水平拖动
final int leftPadding = getPaddingLeft();
final int rightPadding = getWidth() - child.getWidth() - getPaddingRight();
return Math.min(Math.max(left, leftPadding), rightPadding);
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {//竖直拖动
final int topPadding = getPaddingTop();
final int bottomPadding = getHeight() - child.getHeight() - getPaddingBottom();
return Math.min(Math.max(top, topPadding), bottomPadding);
}
可以移动的区域就是灰色区域,水平范围便是
paddingLeft <= target <= getWidth()-child.getWidth()-getPaddingRight()
,垂直方向同理。
padding
四边的值不一定相同。
3.恢复到默认位置
当在竖直方向上,拖动不超过DragView
高度的一半,就会回弹到默认位置
完整代码
public class DragView extends LinearLayout {
private ViewDragHelper mViewDragHelper;
private Point initPoint;
private View autoTextView;
public DragView(Context context, AttributeSet attrs) {
super(context, attrs);
initDragHelper();
}
private void initDragHelper() {
mViewDragHelper = ViewDragHelper.create(DragView.this, 1.0f, mDragCallback);
initPoint = new Point();
}
/**
* ViewDragHelper回调接口
*/
private ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return true;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {// 水平拖动
final int leftPadding = getPaddingLeft();
final int rightPadding = getWidth() - child.getWidth() - leftPadding;
final int newLeft = Math.min(Math.max(left, leftPadding), rightPadding);
return newLeft;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {//竖直拖动
final int topPadding = getPaddingTop();
final int bottomPadding = getHeight() - child.getHeight() - topPadding;
final int newTop = Math.min(Math.max(top, topPadding), bottomPadding);
return newTop;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {//拖动结束后
super.onViewReleased(releasedChild, xvel, yvel);
if (releasedChild == autoTextView && releasedChild.getTop() < (getHeight()/2)){
mViewDragHelper.smoothSlideViewTo(releasedChild,initPoint.x,initPoint.y);//平滑移动
ViewCompat.postInvalidateOnAnimation(DragView.this);
}
}
};
@Override
public boolean onInterceptHoverEvent(MotionEvent event) {//拦截事件
return mViewDragHelper.shouldInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {//消费事件
mViewDragHelper.processTouchEvent(event);
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
if (mViewDragHelper.continueSettling(true)) {//不停计算位置后,自动移动
ViewCompat.postInvalidateOnAnimation(DragView.this);//重新绘制
}
}
/**
* 完成解析布局xml文件
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
autoTextView = getChildAt(0);
}
/**
* 布局
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
initPoint.x = autoTextView.getLeft();
initPoint.y = autoTextView.getTop();
}
}
利用onLayout()
方法,拿到TextView
起始点一开始的初始化坐标( initPoint.x,initPoint.y)
,在onViewReleased()
方法中,进行结束拖动后的处理
4.处理Button
在DragView
中,添加一个Button
或者给TextView
添加android:clickable="true",android:longClickable="true"
,便不能进行拖动处理
原因根据前面学过的,Button
的clickable
默认为ture
,事件被消费了,DragView
便不会再处理ACTION_MOVE,ACTION_UP
事件,Button
的clickable
设置为false
后,便可以拖动,但此时Button
却不在可以点击
想要实现Button
既可以点击又又可以拖动,需要在ViewDragHelper.Callback mDragCallback
重写两个方法
@Override
public int getViewHorizontalDragRange(View child) {
return getMeasuredWidth() - child.getMeasuredWidth();
}
@Override
public int getViewVerticalDragRange(View child) {
return getMeasuredHeight() - child.getMeasuredHeight();
}
注意:
因为Button
是可以点击的,当ACTION_DOWN
事件发生时(也就是手指落在按钮上),之后的ACTION_MOVE,ACTION_UP
便会由Button
处理,需要从Button
外的区域滑到Button
内后,Button
再才会跟随手指动作被拖动
5. 边缘拖动
在使用一些侧滑的控件时,有些可以从手机屏幕最左侧边缘滑出,ViewDragHelper.Callback mDragCallback
提供了回调方法,使用有两个步骤
- 第1步:指定边缘拖动目标控件
边缘拖动代码:
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
super.onEdgeDragStarted(edgeFlags, pointerId);
mViewDragHelper.captureChildView(autoTextView,pointerId);
}
使用captureChildView()
方法来指定childView
主动进行边缘拖动回调方法操作
- 第2步:在初始化
ViewDragHelper
时,指定边缘方向
mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT);
方向共有5个值:
ViewDragHelper.EDGE_ALL 四周都可以
ViewDragHelper.EDGE_LEFT 左边缘
ViewDragHelper.EDGE_RIGHT 右边缘
ViewDragHelper.EDGE_TOP 顶部
ViewDragHelper.EDGE_BOTTOM 底部
6.Callback中的其他回调方法
方法 | 作用 |
---|---|
onViewDragStateChanged(int state) |
ViewDragHelper拖动状态发生改变,STATE_IDLE ,STATE_DRAGGING ,STATE_SETTLING[自动滚动] ,分别对应0,1,2 |
onViewPositionChanged() |
拖动目标childView 位置发生改变 |
onViewCaptured(View capturedChild, int activePointerId) |
调用captureChildView 确定拖动目标时,回调此方法 |
onEdgeTouched(int edgeFlags, int pointerId) |
触摸ViewGroup 边缘 |
onEdgeLock(int edgeFlags) |
true的时候会锁住当前的边界,false则unLock |
getOrderedChildIndex(int index) |
默认返回传入的index,可以重写将控件重新排序 |
加上上面用过的方法,Callback
的回调方法就这些
7. ViewDragHelper常用方法
方法 | 作用 |
---|---|
cancel() |
取消拖动 |
abort() |
取消拖动的过程,直接将控件移动了指定位置 |
captureChildView(View childView, int activePointerId) |
将指定的子控件移动到指定位置 |
continueSettling(boolean deferCallbacks) |
自动不断计算位置后移动控件 |
smoothSlideViewTo(View child, int finalLeft, int finalTop) |
将child 平滑移动到指定的位置 |
settleCapturedViewAt(int finalLeft, int finalTop) |
以手指离开时的速度为初速度,将控件移动到指定的位置 |
shouldInterceptTouchEvent(MotionEvent ev) |
判断父容器是否应该拦截事件 |
processTouchEvent(MotionEvent ev) |
处理触摸事件由父视图接收 |
其他的以后用到了再学习补充
8.最后
都是方法的简单调用
本人很菜,有错误请指出
共勉 : )