1.1 实现最简单的拖拽
- 1.1.1 自定义一个 DragLayout
- 1.1.2 串连构造方法
- 1.1.3 ViewDragHelper 简介
- 1.1.4 创建 ViewDragHelper
- 1.1.5 触摸事件转交
- 1.1.6 处理回调事件
- 1.1.7 DragLayout 布局到 xml
1.2 限定拖拽范围
效果:
1.1 实现最简单的拖拽
1.1.1 自定义一个 DragLayout
在创建 DragLayout 时,继承 FrameLayout,这里需要注意两个问题
1、 为什么不继承 ViewGroup,因为继承 ViewGroup 需要重写 onMeasure()和实现 onLayout()方法, 自己实现子 view 的测量和摆放, 在这里我们不需要自己去做测量和摆放, 而 FrameLayout 已经对这两个方法进行了具体实现,所以继承 FrameLayout 更加简单省事
2、 为什么不继承 RelativeLayout,因为这里我们只需要层级关系, 不需要相对关系, 继承 RelativeLayout界面效果是一样的,但 RelativeLayout 对 FrameLayout 多了相对关系的计算,效率会低一些,所以选择继承 FrameLayout
package com.fighting.qqview;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
/**
* 描述:
* 作者 mjd
* 日期:2016/1/26 19:10
*/
public class DragLayout extends FrameLayout {
public DragLayout(Context context) {
super(context);
}
public DragLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
1.1.2 串联构造方法
DragLayout 实例化时需要做一些初始化操作, 如果我们定义一个 init()方法, 则我们需要在三个构造方法中都调用 init()方法,这样非常麻烦,我们可以通过串连三个构造方法的方式实现只调用一次 init()方法这样无论是代码创建还是布局在 xml 中都能调用到我们的初始化代码
package com.fighting.qqview;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
/**
* 描述:
* 作者 mjd
* 日期:2016/1/26 19:10
*/
public class DragLayout extends FrameLayout {
public DragLayout(Context context) {
//代码创建时调用
this(context, null);
}
public DragLayout(Context context, AttributeSet attrs) {
//布局在 xml 中,实例化时调用
this(context, attrs, 0);
}
public DragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
1.1.3 ViewDragHelper 简介
我们要实现拖拽的效果,则需要自己去解析 Touch 事件的 ACTION_DOWN,ACTION_MOVEACTION_UP,相当的麻烦。所以 Google 在 2013 年的 IO 大会上发布了 ViewDragHelper 这个类,用来解决滑动拖拽问题,用这个类可以非常简单的实现 view 的拖拽
1.1.4 创建 ViewDragHelper
我们只需要在第三个构造方法中实现 ViewDragHelper 的实例即可
public DragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 在这里初始化
// 1.forParent 父类容器 2.sensitivity 敏感度,越大越敏感, 1.0f 是默认值 3.Callback 回调事件
//1. 通静态方法创建拖拽辅助类
viewDragHelper = ViewDragHelper.create(this, 1.0f, mCallback);
}
ViewDragHelper 三个参数的创建的方法源码中的 mTouchSlop 表示触摸的最小敏感范围,越小越敏感即在界面拖动的瞬间变化量大于 mTouchSlop 时才可以成功触发拖拽事件
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;
}
1.1.5 触摸事件转交
ViewDragHelper 创建成功了,但它和 DragLayout 并没有任何关系,我们需要让它们建立关系
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//return super.onInterceptTouchEvent(ev);
//由 ViewDragHelper 判断是否拦截
return viewDragHelper.shouldInterceptTouchEvent(ev);
}
重写 onInterceptTouchEvent 方法, 将触摸事件交给 ViewDragHelper 判断是否拦截, 这样它们就建立了关系,事件拦截后,还需要对拦截到的事件进行处理,注意返回值必须是 true
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
viewDragHelper.processTouchEvent(event);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
1.1.6 处理回调事件
ViewDragHelper 在处理触摸事件时会通过传入的 callback 给我们反馈, 通过对回调方法的处理即可实现简单的拖拽
//3. 处理回调事件
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
//返回值决定了 child 是否可以被拖拽
@Override
public boolean tryCaptureView(View child, int pointerId) {
//child 被用户拖拽的孩子 pointerId 多点触摸的手指 id
return true;
}
//修正子 view 水平方向上的位置,此时还没有真正的移动,返回值决定 view 将移动到的位置
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//left 建议移动到的位置
return left;
}
};
1.1.7 DragLayout 布局到 xml 中
<?xml version="1.0" encoding="utf-8"?>
<com.fighting.qqview.DragLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#66ff0000">
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00ff00">
</LinearLayout>
</com.fighting.qqview.DragLayout>
给左面板和主面板设置不同的背景颜色便于拖拽时观察效果,运行工程,即可实现简单的拖拽
1.2 限定拖拽范围
现在左面板和主面板可以任意拖动,本节要实现左面板不动,拖动时,主面板在一定范围内拖动
1.2.1 OnFinishInflate()介绍
onFinishInflate()在控件 inflate 完成时会被调用,可以在这个方法中查找子控件
1.可以通过 findViewById()的方式查找子控件
2.可以通过子 view 索引的方式查找子控件
这里采用第二种方式
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//增强代码的健壮性
if (getChildCount() < 2) {
//必须有两个子 view
throw new IllegalStateException("Your viewGroup must have two children.");
}
if (!(getChildAt(0) instanceof ViewGroup) || !(getChildAt(1) instanceof ViewGroup)) {
//子 view 必须是 viewGroup 的子类
throw new IllegalStateException("The child must an instance of viewGroup.");
}
mLeftContent = getChildAt(0);
mMainContent = getChildAt(1);
}
1.2.2 获取控件宽高
在 onMeasure()方法中可以获取到控件的宽高, 也可以在 onSizeChanged()方法中去获取宽高, onMeasure方法调用后会检测宽高值有没有变化,有变化才调用 onSizeChanged()方法,无变化则不调用,所以onSizeChanged()调用的次数比 onMeasure()少,在这里我们在 onSizeChanged()方法中去获取宽高,同时计算出拖拽范围为宽度的 60%
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewWidth = getMeasuredWidth();
viewHeight = getMeasuredHeight();
range = (int) (viewWidth * 0.6f);
Log.e(TAG, "viewWidth = " + viewWidth + ", viewHeight = " + viewHeight + ", range = " + range);
}
1.2.3 限定主面板的拖动范围
对 callback 中的其它几个方法进行重写
//3. 处理回调事件
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
//返回值决定了 child 是否可以被拖拽
@Override
public boolean tryCaptureView(View child, int pointerId) {
//child 被用户拖拽的孩子 pointerId 多点触摸的手指 id
return true;
}
@Override
public int getViewHorizontalDragRange(View child) {
return super.getViewHorizontalDragRange(child);
}
//修正子 view 水平方向上的位置,此时还没有真正的移动,返回值决定 view 将移动到的位置
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//left 建议移动到的位置
return left;
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
}
};
回调方法中的 getViewHorizontalDragRange(View child)方法返回拖拽的范围,但不会真正限定这个范围,只要返回一个大于零的值即可。
在 ViewDragHelper 源码中, computeSettleDuration()会调用这个返回值来计算动画执行的时长,checkTouchSlop()方法会调用这个返回值检查左面板,主面板是否可以被滑动,所以需要返回一个大于 0的值才能实现拖动。
如果返回值为 0, 左面板, 主面板中不能有子 view 或子 view 没有对 touch 事件做处理, 最后触摸还是会交给 ViewDragHelper 处理,所以也能实现拖动
//返回拖拽的范围,返回一个大于零的值,计算动画执行的时长,水平方向是否可以被滑开
@Override
public int getViewHorizontalDragRange(View child) {
//computeSettleDuration 计算动画执行的时长
//checkTouchSlop 检查是否可以被滑动(没有孩子处理触摸事件, 最后返回给 DragLayout 处理)
return range;
}
限定主面板的拖拽范围,当建议的值 left 小于 0 时,让 left 等于 0,大于 range时等于 range,然后再将 left 返回
//修正子 view 水平方向上的位置,此时还没有真正的移动,返回值决定 view 将移动到的位置
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//child 被用户拖拽的孩子 left 建议移动到的位置 dx 新的位置与旧的位置的差值
int oldLeft = mainContent.getLeft();
Log.e(TAG, "clamp: left:" + left + " oldLeft:" + oldLeft + " dx:" + dx);
if (child == mainContent) {
left = fixLeft(left);
}
return left;
}
/**
* 修正左边的位置,限定拖拽范围在 0 到 range 间变化
*/
private int fixLeft(int left) {
if (left < 0) {
left = 0;
} else if (left > range) {
left = range;
}
return left;
}
当控件位置变化时会调用 onViewPositionChanged()方法,可以在此方法中做伴随动画,状态更新,事件回调, left 表示最新的水平位置, dx 表示刚刚发生的水平变化量。
此时左面板还可以任意拖动, 为了实现拖动左面板时界面表现为拖动主面板, 可以对 changedView 进行判断,如果 changedView 是左面板,则通过 layout()把左面板放回到原来的位置,然后把变化量 dx 累加给主面板,再通过 layout()方法来移动主面板
// 当控件位置变化时调用,可以做伴随动画,状态更新,事件回调
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
// left 最新的水平位置 dx 刚刚发生的水平变化量
Log.e(TAG, "onViewPositionChanged: left:" + left + " dx:" + dx);
if (changedView == leftContent) {
// 如果滑动的是左面板
// 1. 放回到原来的位置
leftContent.layout(0, 0, viewWidth, viewHeight);
// 2. 把变化量传递给主面板, 主面板旧的值+变化量
int newLeft = mainContent.getLeft() + dx;
// 需要修正左边值
newLeft = fixLeft(newLeft);
mainContent.layout(newLeft, 0, newLeft + viewWidth, viewHeight);
}
// offsetLeftAndRight 在低版本中没有重绘界面,手动调用重绘
invalidate();
}
注意:由于 onViewPositionChanged()方法调用前调用了 offsetLeftAndRight()方法,此方法在低版本中没有重绘界面, 并且在高版本中也有一个 bug, 最后一帧没有被绘制, 所以需要手动调用一次 invalidate(),否则在低版本中无法实现拖拽效果