自定义控件:侧滑面板

本篇博客讲解的是自定义View之侧滑面板,应用场景:QQ,知乎,效果图如下

侧滑面板

内容摘要

  • 了解ViewDragHelper 的产生及解决的问题
  • 掌握ViewDragHelper 的使用步骤
  • 掌握属性动画的使用
  • 掌握状态更新及事件回调的用法

实现最简单的拖拽

实现最简单的拖拽

在创建DragLayout 时,继承FrameLayout,这里需要注意两个问题

为什么不继承ViewGroup,因为继承ViewGroup 需要重写onMeasure()和实现onLayout()方法,自己实现子view 的测量和摆放,在这里我们不需要自己去做测量和摆放,而FrameLayout 已经对这两个方法进行了具体实现,所以继承FrameLayout 更加简单省事

为什么不继承RelativeLayout,因为这里我们只需要层级关系,不需要相对关系,继承RelativeLayout界面效果是一样的,但RelativeLayout 对FrameLayout 多了相对关系的计算,效率会低一些,所以选择继承FrameLayout

1
2
3
4
5
6
7
8
9
10
11
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);
}
}

串联构造方法

DragLayout 实例化时需要做一些初始化操作,如果我们定义一个init()方法,则我们需要在三个构造方法中都调用init()方法,这样非常麻烦,我们可以通过串连三个构造方法的方式实现只调用一次init()方法这样无论是代码创建还是布局在xml 中都能调用到我们的初始化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
//在这里初始化
}
}

ViewDragHelper 简介

我们要实现拖拽的效果,则需要自己去解析Touch 事件的ACTION_DOWN,ACTION_MOVE,ACTION_UP,相当的麻烦。所以Google 在2013 年的IO 大会上发布了ViewDragHelper 这个类,用来解决滑动拖拽问题,用这个类可以非常简单的实现view 的拖拽

创建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 的实例即可

1
2
3
4
5
6
7
8
9
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 时才可以成功触发拖拽事件

1
2
3
4
5
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;
}

触摸事件转交

ViewDragHelper 创建成功了,但它和DragLayout 并没有任何关系,我们需要让它们建立关系

1
2
3
4
5
6
//2.转交触摸事件
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
//由ViewDragHelper 判断是否拦截
return mViewDragHelper.shouldInterceptTouchEvent(event);
};

重写onInterceptTouchEvent 方法,将触摸事件交给ViewDragHelper 判断是否拦截,这样它们就建立了关系,事件拦截后,还需要对拦截到的事件进行处理,注意返回值必须是true

1
2
3
4
5
6
7
8
9
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
//由ViewDragHelper 处理拦截的事件
mViewDragHelper.processTouchEvent(event);
} catch (Exception e) {}
//事件已被处理,所以需要返回true
return true;
};

处理回调事件

ViewDragHelper 在处理触摸事件时会通过传入的callback 给我们反馈,通过对回调方法的处理即可实现简单的拖拽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//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;
}
};

DragLayout 布局到 xml 中

给左面板和主面板设置不同的背景颜色便于拖拽时观察效果,运行工程,即可实现简单的拖拽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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>

限定拖拽范围

现在左面板和主面板可以任意拖动,本节要实现左面板不动,拖动时,主面板在一定范围内拖动

OnFinishInflate()介绍

onFinishInflate()在控件inflate 完成时会被调用,可以在这个方法中查找子控件

  • 可以通过findViewById()的方式查找子控件
  • 可以通过子view 索引的方式查找子控件

这里采用第二种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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);
};

获取控件宽高

在onMeasure()方法中可以获取到控件的宽高,也可以在onSizeChanged()方法中去获取宽高,onMeasure()方法调用后会检测宽高值有没有变化,有变化才调用onSizeChanged()方法,无变化则不调用,所以onSizeChanged()调用的次数比onMeasure()少,在这里我们在onSizeChanged()方法中去获取宽高,同时计算出拖拽范围为宽度的60%

1
2
3
4
5
6
7
8
9
@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);
}

限定主面板的拖动范围

对callback 中的其它几个方法进行重写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//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 处理,所以也能实现拖动

1
2
3
4
5
6
7
@Override
//返回拖拽的范围,返回一个大于零的值,计算动画执行的时长,水平方向是否可以被滑开
public int getViewVerticalDragRange(View child) {
//computeSettleDuration 计算动画执行的时长
//checkTouchSlop 检查是否可以被滑动(没有孩子处理触摸事件,最后返回给DragLayout 处理)
return mRange;
}

限定主面板的拖拽范围,当建议的值left 小于0 时,让left 等于0,大于mRange 时等于mRange,然后再将left 返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@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:" + oldLeft
+ " dx:" + dx);
if (child == mMainContent) {
left = fixLeft(left);
}
return left;
}
/
* 修正左边的位置,限定拖拽范围在0 到mRange 间变化
*
* @param left
* @return
*/
private int fixLeft(int left) {
if (left < 0) {
left = 0;
} else if (left > mRange) {
left = mRange;
}
return left;
}

当控件位置变化时会调用onViewPositionChanged()方法,可以在此方法中做伴随动画,状态更新,事件回调,left 表示最新的水平位置,dx 表示刚刚发生的水平变化量。

此时左面板还可以任意拖动,为了实现拖动左面板时界面表现为拖动主面板,可以对changedView 进行判断,如果changedView 是左面板,则通过layout()把左面板放回到原来的位置,然后把变化量dx 累加给主面板,再通过layout()方法来移动主面板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
// 当控件位置变化时调用,可以做伴随动画,状态更新,事件回调
public void onViewPositionChanged(View changedView, int left, int top,
int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
// left 最新的水平位置
// dx 刚刚发生的水平变化量
System.out.println("onViewPositionChanged: left:" + left + " dx:"
+ dx);
if (changedView == mLeftContent) {
// 如果滑动的是左面板
// 1.放回到原来的位置
mLeftContent.layout(0, 0, mWidth, mHeight);
// 2.把变化量传递给主面板,主面板旧的值+变化量
int newLeft = mMainContent.getLeft() + dx;
// 需要修正左边值
newLeft = fixLeft(newLeft);
mMainContent.layout(newLeft, 0, newLeft + mWidth, mHeight);
}
// offsetLeftAndRight 在低版本中没有重绘界面,手动调用重绘
invalidate();
}

注意:由于onViewPositionChanged()方法调用前调用了offsetLeftAndRight()方法,此方法在低版本中没有重绘界面,并且在高版本中也有一个bug,最后一帧没有被绘制,所以需要手动调用一次invalidate(),否则在低版本中无法实现拖拽效果

结束动画

拖拽过程中当手指抬起时,需要实现一个打开,关闭面板的动画,结束动画可以在 onViewReleased()方法实现

跳转的结束动画

onViewReleased()方法在松手之后会被调用,此时可以做结束动画,结束动画只需要考虑需要打开的
情况,其它则为需要关闭情况

  • 当水平方向的速度等于 0,并且主面板此时左边的位置在拖拽范围中轴线的右边则需要执行打开动

画,即 mMainContent.getLeft() > mRange*0.5f

  • 当水平方向的速度大于 0 时,则需要执行打开动画
  • 其它情况则需要执行关闭动画
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//5.决定松手后要做的事件,结束动画
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
//releasedChild 被释放的孩子
//xvel 水平方向的速度,向左为-,向右为+
System.out.println("onViewReleased: xvel:"+xvel);
//考虑开启的情况,其它情况则关闭的情况
if(xvel == 0 && mMainContent.getLeft()> mRange*0.5f){
//在允许滑动的范围的中轴线右边,则打开
open();
}else if(xvel > 0){
//速度向右时,则打开
open();
}else{
//关闭
close();
}
}

open(),close()创建为 DragLayout 的方法,这样方便外界调用

1
2
3
4
5
6
7
8
//直接打开
protected void open() {
mMainContent.layout(mRange, 0, mRange + mWidth, mHeight);
}
//直接关闭
protected void close() {
mMainContent.layout(0, 0, 0 + mWidth, mHeight);
}

平滑的结束动画

首先实现平滑的打开动画,在这里需要用到 ViewDragHelper 提供的一个方法smoothSlideViewTo(child,finalLeft,finalTop),三个参数的意思分别是:

  • child 需要平滑移动的 view
  • finalLeft 需要移动到的终点左边位置
  • finalTop 需要移动到的终点的上边位置

smoothSlideViewTo()方法的返回值为 true,表示位置不是最终位置,需要重绘界面

重载一个 open(boolean isSmooth)方法,用参数 isSmooth 标识是调用平滑动画还是跳转动画,open()方法则直接调用 open(true),默认为平滑动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void open() {
open(true);
}
protected void open(boolean isSmooth) {
int finalLeft = mRange;
if(isSmooth){
//触发一个平滑动画
if(mViewDragHelper.smoothSlideViewTo(mMainContent, finalLeft, 0)){
//invalidate();可能会漏帧
ViewCompat.postInvalidateOnAnimation(this);
};
}else{
//直接跳转
mMainContent.layout(finalLeft, 0, finalLeft + mWidth, mHeight);
}
}

注意:smoothSlideViewTo()方法返回 true,需要重绘界面,此时不建议使用 invalidate(),因为在动画的过程中可能会丢帧,推荐使用 ViewCompat.postInvalidateOnAnimation(this),参数一定要传子 view 所在的容器,因为只有容器才知道子 view 的具体位置

重绘命令调用后,还需要重写 computScroll()方法,重绘时,系统会在 draw()方法后调用 computScroll(),在该方法中调用 ViewDragHelper 的维持动画的方法
continueSettling(deferCallbacks)参数 deferCallbacks 表示是否延迟画下一帧,此处传入 true,返回值表示是否已经移动到最终位置,如果为 true,还没有移动到最终位置,需要重绘界面,这样 computeScroll()方法就会不断的调用,界面也就会不断的重绘,直到移动到最终位置

1
2
3
4
5
6
7
8
9
10
11
@Override
//维持动画的执行,高频率调用
public void computeScroll() {
super.computeScroll();
//调用完后会调用 draw()
if(mViewDragHelper.continueSettling(true)){
//参数传入 true,表示延迟画下一帧
//mViewDragHelper.continueSettling(true)
ViewCompat.postInvalidateOnAnimation(this);
}
}

同样的道理,关闭的平滑动画只需要修改 finalLeft = 0 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void close() {
close(true);
}
protected void close(boolean isSmooth) {
int finalLeft = 0;
if(isSmooth){
//触发一个平滑动画
if(mViewDragHelper.smoothSlideViewTo(mMainContent, finalLeft, 0)){
//invalidate();可能会漏帧
ViewCompat.postInvalidateOnAnimation(this);
};
}else{
mMainContent.layout(finalLeft, 0, finalLeft + mWidth, mHeight);
}
}

伴随动画

分解伴随动画

伴随动画是拖拽的过程中,左面板,主面板会跟随拖拽百分比所做的动画,该动画需要在onViewPositionChanged()回调方法中实现

  • 左面板:缩放动画,平移动画,透明度动画
  • 主面板:缩放动画
  • 背景: 亮度变化

实现伴随动画

创建一个方法 dispatchDragEvent(),在 onViewPositionChanged()方法中调用

1
2
3
4
5
6
7
public void onViewPositionChanged(View changedView, int left, int top,
int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
//...此处代码省略
dispatchDragEvent();
invalidate();
}

实现左面板的缩放动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected void dispatchDragEvent() {
//0.0f->1.0f 获取动画的百分比,主面板左边的位置引起的一系列变化
float percent = mMainContent.getLeft()*1.0f/mRange;
System.out.println("dispatchDragEvent: percent:"+percent);
//左面板:缩放动画,平移动画,透明度动画
//0.0f ->1.0f percent*0.5f => 0.0f -> 0.5f
//寻找规律->拷贝 FloatEvaluator.java 中的估值方法
//percent*0.5f + 0.5f => 0.5f -> 1.0f
//percent*(1.0f -0.6f)+0.6f => 0.6f -> 1.0f => start + percent(end - start)
//兼容低版本引入 nineoldandroid.jar
//用 ViewHelper 做属性动画
//1.缩放动画
ViewHelper.setScaleX(mLeftContent, evaluate(percent, 0.5f, 1.0f));
ViewHelper.setScaleY(mLeftContent, evaluate(percent, 0.5f, 1.0f));
}
//源码 FloatEvaluator.java 中拷贝的估值方法
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
  • 第 3 行通过主面板左边位置与拖拽范围的相除可以得到一个 0.0f ->1.0f 的比例值,因为在整个拖拽过
    程中,主面板左边位置的变化是引起一系列变化的原因
  • 第 7-10 行可以推出一个公式 start + percent(end - start),即通过 percent 的变化可以计算出 start 到 end 间
    的任意值。源码 FloatEvaluator.java 中已经提供了这么一个方法,将其拷贝到代码中,即第 20-23 行
  • 第 12-16 行为了兼容低版本引入 nineoldandroid.jar 中的 ViewHelper 做属性动画

同理可以实现其它伴随动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
protected void dispatchDragEvent() {
//0.0f->1.0f 获取动画的百分比,主面板左边的位置引起的一系列变化
float percent = mMainContent.getLeft()*1.0f/mRange;
System.out.println("dispatchDragEvent: percent:"+percent);
//左面板:缩放动画,平移动画,透明度动画
//0.0f ->1.0f percent*0.5f => 0.0f -> 0.5f
//寻找规律->拷贝 FloatEvaluator.java 中的估值方法
//percent*0.5f + 0.5f => 0.5f -> 1.0f
//percent*(1.0f -0.6f)+0.6f => 0.6f -> 1.0f => start + percent(end - start)
//兼容低版本引入 nineoldandroid.jar
//用 ViewHelper 做属性动画
//1.缩放动画,从 50%->100%
ViewHelper.setScaleX(mLeftContent, evaluate(percent, 0.5f, 1.0f));
ViewHelper.setScaleY(mLeftContent, evaluate(percent, 0.5f, 1.0f));
//2.平移动画,从宽度一半在屏幕外->全部移到屏幕内
ViewHelper.setTranslationX(mLeftContent, evaluate(percent, -mWidth*0.5f, 0f));
//3.透明度动画,从 20%->100%
ViewHelper.setAlpha(mLeftContent, evaluate(percent, 0.2f, 1.0f));
//主面板:缩放动画,从 100%->80%
ViewHelper.setScaleY(mMainContent, evaluate(percent, 1.0f, 0.8f));
//背景亮度变化,PorterDuff.Mode.SRC_OVER 叠加模式,直接叠加在上面
getBackground().setColorFilter((Integer)
evaluateColor(percent, Color.BLACK,Color.TRANSPARENT), PorterDuff.Mode.SRC_OVER);
}
//源码 ArgbEvaluator.java 中拷贝的估值方法
public Object evaluateColor(float fraction, Object startValue, Object endValue) {
//api18 以上的代码才有透明度的过滤
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))));
}
  • 第 27 行叠加模式 PorterDuff.Mode.SRC_OVER 表示直接叠加在上面
  • 第 30-48 行 ArgbEvaluator.java 源码中拷贝的估值方法,api18 以上的代码才有透明度的过滤

状态更新及事件回调

状态分析

拖拽的状态可以分为:

  • 打开状态
  • 关闭状态
  • 拖拽状态

通过枚举定义这三种状态,且定义默认状态为关闭

1
2
3
4
5
6
7
8
9
10
//默认状态为关闭
private Status status = Status.Close;
//提供 get()方法
public Status getStatus() {
return status;
}
//状态的枚举值,有三种状态,打开,关闭,拖拽中
public enum Status{
Open,Close,Draging;
}

事件回调分析

定义一个事件回调接口,事件回调和状态密切相关

  • 打开状态时回调 onOpen()方法
  • 关闭状态时回调 onClose()方法

拖拽中回调 onDraging(float percent)方法,并将拖拽百分比传出去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//接收外界注册的接口类,以便回调接口方法
private OnDragChangeListener onDragChangeListener;
//提供 set()方法,让外界注册监听接口类
public void setOnDragChangeListener(OnDragChangeListener onDragChangeListener) {
this.onDragChangeListener = onDragChangeListener;
}
//模仿 View 的 OnClickListener 的写法,定义一个内部的公开的接口
public interface OnDragChangeListener{
/
* 打开时调用
*/
public void onOpen();
/
* 关闭时调用
*/
public void onClose();
/
* 拖拽中调用
* @param percent 当前拖拽的百分比
*/
public void onDraging(float percent);
}

实现状态更新及事件回调

通过拖拽百分比可以判断当前的状态,在 dispatchDragEvent()方法中实现状态更新及事件回调

  • 百分比为 0,则为关闭状态
  • 百分比为 1,则为打开状态
  • 其它百分比,则为拖拽状态

事件回调需要先做空判断,拖拽状态调用频率高,直接调用即可,打开和关闭可以判断上次状态和当
前状态是否一致,不一致则调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
protected void dispatchDragEvent() {
//0.0f->1.0f 获取动画的百分比,主面板左边的位置引起的一系列变化
float percent = mMainContent.getLeft()*1.0f/mRange;
System.out.println("dispatchDragEvent: percent:"+percent);
Status lastStatus = status;
//更新状态,通过动画百分比判断
if(percent ==0 ){
status = Status.Close;
}else if(percent == 1){
status = Status.Open;
}else{
status = Status.Close;
}
if(onDragChangeListener != null){
//调用频率高,直接调用
onDragChangeListener.onDraging(percent);
}
if(lastStatus != status && onDragChangeListener != null){
if(status == Status.Open){
//最新状态是 open,说明刚才不是 open,则需要调用一下 onOpen 方法
onDragChangeListener.onOpen();
}else if(status == Status.Close){
//最新状态是 close,说明刚才不是 close,则需要调用一下 onClose 方法
onDragChangeListener.onClose();
}
}
//左面板:缩放动画,平移动画,透明度动画
//0.0f ->1.0f percent*0.5f => 0.0f -> 0.5f
//寻找规律->拷贝 FloatEvaluator.java 中的估值方法
//percent*0.5f + 0.5f => 0.5f -> 1.0f
//percent*(1.0f -0.6f)+0.6f => 0.6f -> 1.0f => start + percent(end - start)
//兼容低版本引入 nineoldandroid.jar
//用 ViewHelper 做属性动画
//1.缩放动画
ViewHelper.setScaleX(mLeftContent, evaluate(percent, 0.5f, 1.0f));
ViewHelper.setScaleY(mLeftContent, evaluate(percent, 0.5f, 1.0f));
//2.平移动画
ViewHelper.setTranslationX(mLeftContent, evaluate(percent, -mWidth*0.5f, 0f));
//3.透明度动画
ViewHelper.setAlpha(mLeftContent, evaluate(percent, 0.2f, 1.0f));
//主面板:缩放动画
ViewHelper.setScaleY(mMainContent, evaluate(percent, 1.0f, 0.8f));
//背景亮度变化,如果没有设置背景会出问题,PorterDuff.Mode.SRC_OVER 叠加模式,直接叠加在上
getBackground().setColorFilter((Integer)
evaluateColor(percent, Color.BLACK,Color.TRANSPARENT), PorterDuff.Mode.SRC_OVER);
}

触摸优化

填充界面数据

1.修改主界面 xml,左面板,主面板分别加入 ListView 及头像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<com.example.draglayout.widget.DragLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/dl"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg"
tools:context=".MainActivity" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="50dp"
android:paddingLeft="10dp"
android:paddingRight="50dp"
android:orientation="vertical"
android:paddingTop="50dp" >
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:contentDescription="@null"
android:src="@drawable/head" />
<ListView
android:id="@+id/lv_left"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</ListView>
</LinearLayout>
<com.example.draglayout.widget.MyLinearLayout
android:id="@+id/ll_my"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:orientation="vertical" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dip"
android:background="#18b6ef"
android:gravity="center_vertical" >
<ImageView
android:id="@+id/iv_header"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="10dp"
android:contentDescription="@null"
android:src="@drawable/head" />
</RelativeLayout>
<ListView
android:id="@+id/lv_main"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</ListView>
</com.example.draglayout.widget.MyLinearLayout>
</com.example.draglayout.widget.DragLayout>

2.ListView 数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Cheeses {
public static final String[] sCheeseStrings = {
"Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi",
"Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale",
"Xanadu", "Xynotyro", "Yarg Cornish", "Yarra Valley Pyramid", "Yorkshire Blue",
"Zamorano", "Zanetti Grana Padano", "Zanetti Parmigiano Reggiano"
};
public static final String[] NAMES = new String[]{"宋江", "卢俊义", "吴用",
"公孙胜", "关胜", "林冲", "秦明", "呼延灼", "花荣", "柴进", "李应", "朱仝", "鲁智 深",
"武松", "董平", "张清", "杨志", "徐宁", "索超", "戴宗", "刘唐", "李逵", "史进", " 穆弘",
"雷横", "李俊", "阮小二", "张横", "阮小五", " 张顺", "阮小七", "杨雄", "石秀", " 解珍",
" 解宝", "燕青", "朱武", "黄信", "孙立", "宣赞", "郝思文", "韩滔", "彭玘", "单廷珪 ",
"魏定国", "萧让", "裴宣", "欧鹏", "邓飞", " 燕顺", "杨林", "凌振", "蒋敬", "吕方 ",
"郭 盛", "安道全", "皇甫端", "王英", "扈三娘", "鲍旭", "樊瑞", "孔明", "孔亮", " 项充",
"李衮", "金大坚", "马麟", "童威", "童猛", "孟康", "侯健", "陈达", "杨春", "郑天寿 ",
"陶宗旺", "宋清", "乐和", "龚旺", "丁得孙", "穆春", "曹正", "宋万", "杜迁", "薛永 ", " 施恩",
"周通", "李忠", "杜兴", "汤隆", "邹渊", "邹润", "朱富", "朱贵", "蔡福", "蔡庆", " 李立",
"李云", "焦挺", "石勇", "孙新", "顾大嫂", "张青", "孙二娘", " 王定六", "郁保四", " 白胜",
"时迁", "段景柱"};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
public class DragLayout extends FrameLayout {
private static final String TAG = "TAG";
private View mLeftContent;
private View mMainContent;
private View mRightContent;
private int mWidth;
private int mHeight;
private int mRangeLeft;
private ViewDragHelper mDragHelper;
private Status mStatus = Status.Close;
private Direction mDirction = Direction.Left;
private OnDragListener mDragListener;
private boolean mScaleEnable = true;
private int mRightWidth;
private int mRangeRight;
public interface OnDragListener {
void onClose();
void onStartOpen(Direction direction);
void onOpen();
void onDrag(float percent);
}
public static enum Status {
Open, Close, Draging
}
public static enum Direction {
Left, Right, Default
}
public Direction getDirction() {
return mDirction;
}
public void setDirction(Direction dirction) {
mDirction = dirction;
}
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);
mDragHelper = ViewDragHelper.create(this, mCallBack);
mGestureDetector = new GestureDetectorCompat(context, mYGestureListener);
}
SimpleOnGestureListener mYGestureListener = new SimpleOnGestureListener() {
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
return Math.abs(distanceX) >= Math.abs(distanceY);
};
};
@Override
protected void onFinishInflate() {
Log.i(TAG, "--onFinishInflate");
mLeftContent = (View) getChildAt(0);
mRightContent = getChildAt(1);
mMainContent = (View) getChildAt(2);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Log.i(TAG, "--onSizeChanged");
mWidth = mMainContent.getMeasuredWidth();
mHeight = mMainContent.getMeasuredHeight();
mRightWidth = mRightContent.getMeasuredWidth();
mRangeLeft = (int) (mWidth * 0.6f);
mRangeRight = mRightWidth;
}
private int mMainLeft = 0;
ViewDragHelper.Callback mCallBack = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
// 1. 决定当前被拖拽的child是否拖的动。(抽象方法,必须重写)
Log.d(TAG, "tryCaptureView: " + (child == mMainContent) + " : "
+ (child == mLeftContent) + " : "
+ (child == mRightContent));
return true;
}
@Override
public int getViewHorizontalDragRange(View child) {
// 2. 决定拖拽的范围
return mWidth;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
// 3. 决定拖动时的位置,可在这里进行位置修正。(若想在此方向拖动,必须重写,因为默认返回0)
Log.d(TAG, "clampViewPositionHorizontal left: " + left + " dx: "
+ dx + " mRange: " + mRangeLeft);
return clampResult(mMainLeft + dx, left);
}
@Override
public void onViewPositionChanged(View changedView, int left, int top,
int dx, int dy) {
// 4. 决定了当View被拖动时,希望同时引发的其他变化
Log.d(TAG, "onViewPositionChanged left: " + left + " dx: " + dx);
if (changedView == mMainContent) {
mMainLeft = left;
} else {
mMainLeft += dx;
}
mMainLeft = clampResult(mMainLeft, mMainLeft);
if(changedView == mLeftContent || changedView == mRightContent){
layoutContent();
}
dispathDragEvent(mMainLeft);
invalidate();
};
/
* @param releasedChild
* 被释放的孩子
* @param xvel
* 释放时X方向的速度
* @param yvel
* 释放时Y方向的速度
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// 5. 决定当childView被释放时,希望做的事情——执行打开/关闭动画,更新状态
boolean scrollRight = xvel > 1.0f;
boolean scrollLeft = xvel < -1.0f;
if (scrollRight || scrollLeft) {
if (scrollRight && mDirction == Direction.Left) {
open(true, mDirction);
} else if (scrollLeft && mDirction == Direction.Right) {
open(true, mDirction);
} else {
close(true);
}
return;
}
if (releasedChild == mLeftContent && mMainLeft > mRangeLeft * 0.7f) {
open(true, mDirction);
} else if (releasedChild == mMainContent) {
if (mMainLeft > mRangeLeft * 0.3f)
open(true, mDirction);
else if (-mMainLeft > mRangeRight * 0.3f)
open(true, mDirction);
else
close(true);
} else if (releasedChild == mRightContent
&& -mMainLeft > mRangeRight * 0.7f) {
open(true, mDirction);
} else {
close(true);
}
}
@Override
public void onViewDragStateChanged(int state) {
if (mStatus == Status.Close && state == ViewDragHelper.STATE_IDLE
&& mDirction == Direction.Right) {
mDirction = Direction.Left;
}
}
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
};
};
private int clampResult(int tempValue, int defaultValue) {
Integer minLeft = null;
Integer maxLeft = null;
if (mDirction == Direction.Left) {
minLeft = 0;
maxLeft = 0 + mRangeLeft;
} else if (mDirction == Direction.Right) {
minLeft = 0 - mRangeRight;
maxLeft = 0;
}
if (minLeft != null && tempValue < minLeft)
return minLeft;
else if (maxLeft != null && tempValue > maxLeft)
return maxLeft;
else
return defaultValue;
}
private GestureDetectorCompat mGestureDetector;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.i(TAG, "--onMeasure");
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
Log.i(TAG, "--onLayout");
layoutContent();
}
private void layoutContent() {
mLeftContent.layout(0, 0, mWidth, mHeight);
mRightContent.layout(mWidth - mRightWidth, 0, mWidth, mHeight);
mMainContent.layout(mMainLeft, 0, mMainLeft + mWidth, mHeight);
}
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
public void setDragListener(OnDragListener mDragListener) {
this.mDragListener = mDragListener;
}
/
* 处理其他同步动画
*
* @param mainLeft
*/
protected void dispathDragEvent(int mainLeft) {
// 注意转换成float
float percent = 0;
if (mDirction == Direction.Left)
percent = mainLeft / (float) mRangeLeft;
else if (mDirction == Direction.Right)
percent = Math.abs(mainLeft) / (float) mRangeRight;
if (mDragListener != null) {
mDragListener.onDrag(percent);
}
// 更新动画
if (mScaleEnable) {
animViews(percent);
}
// 更新状态
Status lastStatus = mStatus;
if (updateStatus() != lastStatus) {
if(lastStatus == Status.Close && mStatus == Status.Draging){
mLeftContent.setVisibility(mDirction == Direction.Left ? View.VISIBLE : View.GONE);
mRightContent.setVisibility(mDirction == Direction.Right ? View.VISIBLE : View.GONE);
if(mDragListener != null){
mDragListener.onStartOpen(mDirction);
}
}
if (mStatus == Status.Close) {
if (mDragListener != null)
mDragListener.onClose();
} else if (mStatus == Status.Open) {
if (mDragListener != null)
mDragListener.onOpen();
}
}
}
private Status updateStatus() {
if (mDirction == Direction.Left) {
if (mMainLeft == 0) {
mStatus = Status.Close;
} else if (mMainLeft == mRangeLeft) {
mStatus = Status.Open;
} else {
mStatus = Status.Draging;
}
} else if (mDirction == Direction.Right) {
if (mMainLeft == 0) {
mStatus = Status.Close;
} else if (mMainLeft == 0 - mRangeRight) {
mStatus = Status.Open;
} else {
mStatus = Status.Draging;
}
}
return mStatus;
}
private void animViews(float percent) {
Log.d(TAG, "percent: " + percent);
animMainView(percent);
animBackView(percent);
}
private void animBackView(float percent) {
if (mDirction == Direction.Right) {
// 右边栏X, Y放大,向左移动, 逐渐显示
ViewHelper.setScaleX(mRightContent, 0.5f + 0.5f * percent);
ViewHelper.setScaleY(mRightContent, 0.5f + 0.5f * percent);
ViewHelper.setTranslationX(mRightContent,
evaluate(percent, mRightWidth + mRightWidth / 2.0f, 0.0f));
ViewHelper.setAlpha(mRightContent, percent);
} else {
// 左边栏X, Y放大,向右移动, 逐渐显示
ViewHelper.setScaleX(mLeftContent, 0.5f + 0.5f * percent);
ViewHelper.setScaleY(mLeftContent, 0.5f + 0.5f * percent);
ViewHelper.setTranslationX(mLeftContent,
evaluate(percent, -mWidth / 2f, 0.0f));
ViewHelper.setAlpha(mLeftContent, percent);
}
// 背景逐渐变亮
getBackground().setColorFilter(
caculateValue(percent, Color.BLACK, Color.TRANSPARENT),
PorterDuff.Mode.SRC_OVER);
}
private void animMainView(float percent) {
Float inverseP = null;
if (mDirction == Direction.Left) {
inverseP = 1 - percent * 0.25f;
} else if (mDirction == Direction.Right) {
inverseP = 1 - percent * 0.25f;
}
// 主界面X,Y缩小
if (inverseP != null) {
if (mDirction == Direction.Right) {
ViewHelper.setPivotX(mMainContent, mWidth);
ViewHelper.setPivotY(mMainContent, mHeight / 2.0f);
} else {
ViewHelper.setPivotX(mMainContent, mWidth / 2.0f);
ViewHelper.setPivotY(mMainContent, mHeight / 2.0f);
}
ViewHelper.setScaleX(mMainContent, inverseP);
ViewHelper.setScaleY(mMainContent, inverseP);
}
}
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
private int caculateValue(float fraction, Object start, Object end) {
int startInt = (Integer) start;
int startIntA = startInt >> 24 & 0xff;
int startIntR = startInt >> 16 & 0xff;
int startIntG = startInt >> 8 & 0xff;
int startIntB = startInt & 0xff;
int endInt = (Integer) end;
int endIntA = endInt >> 24 & 0xff;
int endIntR = endInt >> 16 & 0xff;
int endIntG = endInt >> 8 & 0xff;
int endIntB = endInt & 0xff;
return ((int) (startIntA + (endIntA - startIntA) * fraction)) << 24
| ((int) (startIntR + (endIntR - startIntR) * fraction)) << 16
| ((int) (startIntG + (endIntG - startIntG) * fraction)) << 8
| ((int) (startIntB + (endIntB - startIntB) * fraction));
}
float mDownX;
private SwipeListAdapter adapter;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(getStatus() == Status.Close){
int actionMasked = MotionEventCompat.getActionMasked(ev);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN:
mDownX = ev.getRawX();
break;
case MotionEvent.ACTION_MOVE:
if(adapter.getUnClosedCount() > 0){
return false;
}
float delta = ev.getRawX() - mDownX;
if(delta < 0){
return false;
}
break;
default:
mDownX = 0;
break;
}
}
return mDragHelper.shouldInterceptTouchEvent(ev)
& mGestureDetector.onTouchEvent(ev);
}
public void close(){
close(true);
}
public void close(boolean withAnim) {
mMainLeft = 0;
if (withAnim) {
if (mDragHelper.smoothSlideViewTo(mMainContent, mMainLeft, 0)) {
ViewCompat.postInvalidateOnAnimation(this);
}
} else {
layoutContent();
dispathDragEvent(mMainLeft);
}
}
public void open(){
open(true);
}
public void open(boolean withAnim) {
open(withAnim, Direction.Left);
}
public void open(boolean withAnim, Direction d) {
mDirction = d;
if (mDirction == Direction.Left)
mMainLeft = mRangeLeft;
else if (mDirction == Direction.Right)
mMainLeft = -mRangeRight;
if (withAnim) {
// 引发动画的开始
if (mDragHelper.smoothSlideViewTo(mMainContent, mMainLeft, 0)) {
// 需要在computeScroll中使用continueSettling方法才能将动画继续下去(因为ViewDragHelper使用了scroller)。
ViewCompat.postInvalidateOnAnimation(this);
}
} else {
layoutContent();
dispathDragEvent(mMainLeft);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
mDragHelper.processTouchEvent(event);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
public Status getStatus() {
return mStatus;
}
public void switchScaleEnable() {
this.mScaleEnable = !mScaleEnable;
if (!mScaleEnable) {
animBackView(1.0f);
}
}
public void setAdapterInterface(SwipeListAdapter adapter) {
this.adapter = adapter;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class DragRelativeLayout extends RelativeLayout {
private DragLayout dl;
public DragRelativeLayout(Context context) {
super(context);
}
public DragRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DragRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setDragLayout(DragLayout dl) {
this.dl = dl;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (dl.getStatus() != Status.Close) {
return true;
}
return super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (dl.getStatus() != Status.Close) {
if (event.getAction() == MotionEvent.ACTION_UP) {
dl.close(true);
}
return true;
}
return super.onTouchEvent(event);
}
}

代码:https://github.com/JackChen1999/DragLayout

坚持原创技术分享,您的支持将鼓励我继续创作!