本篇博客讲解的是自定义View之侧滑面板,应用场景:QQ,知乎,效果图如下
1. 内容摘要
- 了解ViewDragHelper 的产生及解决的问题
- 掌握ViewDragHelper 的使用步骤
- 掌握属性动画的使用
- 掌握状态更新及事件回调的用法
2. 实现最简单的拖拽
2.1 实现最简单的拖拽
在创建DragLayout 时,继承FrameLayout,这里需要注意两个问题
为什么不继承ViewGroup,因为继承ViewGroup 需要重写onMeasure()和实现onLayout()方法,自己实现子view 的测量和摆放,在这里我们不需要自己去做测量和摆放,而FrameLayout 已经对这两个方法进行了具体实现,所以继承FrameLayout 更加简单省事
为什么不继承RelativeLayout,因为这里我们只需要层级关系,不需要相对关系,继承RelativeLayout界面效果是一样的,但RelativeLayout 对FrameLayout 多了相对关系的计算,效率会低一些,所以选择继承FrameLayout
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 defStyle) {
super(context, attrs, defStyle);
}
}
2.2 串联构造方法
DragLayout 实例化时需要做一些初始化操作,如果我们定义一个init()方法,则我们需要在三个构造方法中都调用init()方法,这样非常麻烦,我们可以通过串连三个构造方法的方式实现只调用一次init()方法这样无论是代码创建还是布局在xml 中都能调用到我们的初始化代码
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 defStyle) {
super(context, attrs, defStyle);
//在这里初始化
}
}
2.3 ViewDragHelper 简介
我们要实现拖拽的效果,则需要自己去解析Touch 事件的ACTION_DOWN,ACTION_MOVE,ACTION_UP,相当的麻烦。所以Google 在2013 年的IO 大会上发布了ViewDragHelper 这个类,用来解决滑动拖拽问题,用这个类可以非常简单的实现view 的拖拽
2.4 创建ViewDragHelper
由于eclipse 创建项目时,为我们添加的android-support-v4.jar 没有包含ViewDragHelper,我们需要将最新的android-support-v4.jar 拷贝到libs 下面,然后clean 一下工程。
在这里我们需要关联android-support-v4.jar 的源码,通过配置文件的方法来关联源码
在libs 下面创建一个android-support-v4.jar.properties 的文件
android-support-v4.jar.properties 中的内容为src = V4 包源码路径
我们只需要在第三个构造方法中实现ViewDragHelper 的实例即可
public DragLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// 在这里初始化
// forParent 父类容器
// sensitivity 敏感度,越大越敏感,1.0f 是默认值
// Callback 回调事件
//1.通静态方法创建拖拽辅助类
mViewDragHelper = 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;
}
2.5 触摸事件转交
ViewDragHelper 创建成功了,但它和DragLayout 并没有任何关系,我们需要让它们建立关系
//2.转交触摸事件
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
//由ViewDragHelper 判断是否拦截
return mViewDragHelper.shouldInterceptTouchEvent(event);
};
重写onInterceptTouchEvent 方法,将触摸事件交给ViewDragHelper 判断是否拦截,这样它们就建立了关系,事件拦截后,还需要对拦截到的事件进行处理,注意返回值必须是true
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
//由ViewDragHelper 处理拦截的事件
mViewDragHelper.processTouchEvent(event);
} catch (Exception e) {}
//事件已被处理,所以需要返回true
return true;
};
2.6 处理回调事件
ViewDragHelper 在处理触摸事件时会通过传入的callback 给我们反馈,通过对回调方法的处理即可实现简单的拖拽
//3.处理回调事件
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
@Override
//返回值决定了child 是否可以被拖拽
public boolean tryCaptureView(View child, int pointerId) {
//child 被用户拖拽的孩子
//pointerId 多点触摸的手指id
return true;
}
@Override
//修正子view 水平方向上的位置,此时还没有真正的移动,返回值决定view 将移动到的位置
public int clampViewPositionHorizontal(View child, int left, int dx) {
//left 建议移动到的位置
return left;
}
};
2.7 DragLayout 布局到xml 中
给左面板和主面板设置不同的背景颜色便于拖拽时观察效果,运行工程,即可实现简单的拖拽
<com.example.draglayout.widget.DragLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg">
<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.example.draglayout.widget.DragLayout>
3. 限定拖拽范围
现在左面板和主面板可以任意拖动,本节要实现左面板不动,拖动时,主面板在一定范围内拖动
3.1 OnFinishInflate()介绍
onFinishInflate()在控件inflate 完成时会被调用,可以在这个方法中查找子控件
- 可以通过findViewById()的方式查找子控件
- 可以通过子view 索引的方式查找子控件
这里采用第二种方式
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//增强代码的健壮性
if(getChildCount() < 2){
//必须有两个子view
throw new IllegalStateException("Your viewgroup must have two children.");
}
if(!(getChildAt(0)instanceofViewGroup)||!(getChildAt(1)instanceof ViewGroup)){
//子view 必须是viewgroup 的子类
throw new IllegalStateException("The child must an instance of viewgroup.");
}
mLeftContent = getChildAt(0);
mMainContent = getChildAt(1);
};
3.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);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
//拖拽的范围
mRange = (int) (mWidth * 0.6f);
System.out.println("mWidth:"+mWidth+" mHeight:"+mHeight +" mRange:"+mRange);
}
3.3 限定主面板的拖动范围
对callback 中的其它几个方法进行重写
//3.处理回调事件
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
@Override
//返回值决定了child 是否可以被拖拽
public boolean tryCaptureView(View child, int pointerId) {
//child 被用户拖拽的孩子
//pointerId 多点触摸的手指id
return true;
}
@Override
public int getViewHorizontalDragRange(View child) {
return super.getViewHorizontalDragRange(child);
}
@Override
//修正子view 水平方向上的位置,此时还没有真正的移动,返回值决定view 将移动到的位置
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 getViewVerticalDragRange(View child) {
//computeSettleDuration 计算动画执行的时长
//checkTouchSlop 检查是否可以被滑动(没有孩子处理触摸事件,最后返回给DragLayout 处理)
return mRange;
}
限定主面板的拖拽范围,当建议的值left 小于0 时,让left 等于0,大于mRange 时等于mRange,然后再将left 返回
@Override
// 修正子view 水平方向上的位置,此时还没有真正的移动,返回值决定view 将移动到的位置
public int clampViewPositionHorizontal(View child, int left, int dx) {
// child 被用户拖拽的孩子
// left 建议移动到位置
// dx 新的位置与旧的位置的差值
int oldLeft = mMainContent.getLeft();
System.out.println("clamp: left:" + left + " oldLeft:"<