QQ红点拖拽效果

今天来做一下QQ列表上的红点拖拽效果

思路:首先我们得给小圆点定义一些状态,默认状态,手指点上去的状态,手指一动时的状态,手指松开时的状态。在onTouchEvent方法中更新状态值,最后在onDraw中根据不同的状态值绘制圆和path。思路很简单,就是绘制的时候我们需要把中学时候学的几何数学拿来用一下啦。

先定义一些成员变量,把状态啊,画笔啊,半径啊,原点等都初始化好然后在开始,具体可以到最下面点击源码查看

先看onDraw方法

首先,只要不是爆炸状态,我们都需要绘制移动的圆点和上面的数字。

1
2
3
4
5
6
//只要不是爆炸的情况都要绘制圆和字
if (mState != BUBBLE_STATE_BLAST) {
canvas.drawCircle(mMovePoint.x, mMovePoint.y, mMoveRadius, mBubblePaint);
mTextPaint.getTextBounds(mText, 0, mText.length(), mTextRect);
canvas.drawText(mText, mMovePoint.x - mTextRect.width() / 2, mMovePoint.y + mTextRect.height() / 2, mTextPaint);
}

然后就是当我们手指点到圆上开始拖拽的的状态,这时候我们需要绘制一个静止的圆和一个移动的圆,当两个圆小于一定的距离的时候,我们需要在他们之间绘制一个黏性的效果,其实就是绘制两条二街贝塞尔曲线。

绘制贝塞尔曲线的时候,需要求曲线的起始点,结束点和控制点的坐标,这时候会用到中学几何数学的小知识

计算角度 在直角三角形中,非直角的sin值等于对边长比斜边长.使用勾股定理计算即可。
sinA=对边/斜边 cosB=邻边/斜边 tanA=对边/邻边

看着下面的图绘制会更清晰

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
//链接状态绘制静止的圆和赛贝尔曲线
if (mState == BUBBLE_STATE_CLICK) {
//绘制静止的圆
canvas.drawCircle(mQuitPoint.x, mQuitPoint.y, mQuitRadius, mBubblePaint);
//绘制贝塞尔曲线
//找到控制点
float controlX = (mMovePoint.x + mQuitPoint.x) / 2;
float controlY = (mMovePoint.y + mQuitPoint.y) / 2;
//计算角度 在直角三角形中,非直角的sin值等于对边长比斜边长.使用勾股定理计算即可
//sinA=对边/斜边 cosB=邻边/斜边 tanA=对边/邻边
float sinThet = (mMovePoint.y - mQuitPoint.y) / mDist;
float cosThet = (mMovePoint.x - mQuitPoint.x) / mDist;

//A点
float ax = mQuitPoint.x - mQuitRadius * sinThet;
float ay = mQuitPoint.y + mQuitRadius * cosThet;
//B点
float bx = mMovePoint.x - mMoveRadius * sinThet;
float by = mMovePoint.y + mMoveRadius * cosThet;
//C点
float cx = mMovePoint.x + mMoveRadius * sinThet;
float cy = mMovePoint.y - mMoveRadius * cosThet;
//D点
float dx = mQuitPoint.x + mQuitRadius * sinThet;
float dy = mQuitPoint.y - mQuitRadius * cosThet;

//设置path的路径
mBezierPath.reset();
mBezierPath.moveTo(ax, ay);
mBezierPath.quadTo(controlX, controlY, bx, by);

mBezierPath.lineTo(cx, cy);
mBezierPath.quadTo(controlX, controlY, dx, dy);
mBezierPath.close();
canvas.drawPath(mBezierPath, mBubblePaint);
}

找到各个点之后就简单了,使用path的api将各个点连接起来,最后绘制就ok。

最后是爆炸状态,我们在初始化的时候定义一个有5张爆炸小图片的bitmap数组,使用属性动画控制数组的下标,然后循环绘制这几张图片。

1
2
3
4
5
6
7
8
//爆炸状态绘制爆炸图片
if (mState == BUBBLE_STATE_BLAST && mBlastIndex < mBlastDrawablesArray.length) {
mBlastRect.left = mMovePoint.x - mMoveRadius;
mBlastRect.top = mMovePoint.y - mMoveRadius;
mBlastRect.right = mMovePoint.x + mMoveRadius;
mBlastRect.bottom = mMovePoint.y + mMoveRadius;
canvas.drawBitmap(mBlastBitmapsArray[mBlastIndex], null, mBlastRect, mBlastPaint);
}

OK 下面是onTouchEvent中,我们需要根据手指的各种事件来切换当前的状态

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
public boolean onTouchEvent(MotionEvent event) {
float x = event.getRawX();
float y = event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//勾股定理算出点击位置和静止圆的圆心距离
mDist = (float) Math.hypot(x - mQuitPoint.x, y - mQuitPoint.y);
if (mState == BUBBLE_STATE_DEFAULT) {
//如果手指点击到了圆上或者圆的附近
if (mDist < mMoveRadius + MOVE_OFFSET) {
mState = BUBBLE_STATE_CLICK;
}
}
break;
case MotionEvent.ACTION_MOVE:
if (mState != BUBBLE_STATE_DEFAULT) {
//勾股定理算出点击位置和静止圆的圆心距离,也就是手指一动的距离
mDist = (float) Math.hypot(x - mQuitPoint.x, y - mQuitPoint.y);
mMovePoint.x = event.getRawX();
mMovePoint.y = event.getRawY();
//如果手指点击到了圆上或者圆的附近
if (mState == BUBBLE_STATE_CLICK) {
//手指一动的距离小于我们定义的一个最大的距离,就绘制贝塞尔曲线,反之就是分离状态
if (mDist < mMaxDist - MOVE_OFFSET) {
mQuitRadius = (mMoveRadius - mDist / 8);
} else {
mState = BUBBLE_STATE_BREAK;
}
}
}
invalidate();
break;
case MotionEvent.ACTION_UP:
//如果还没断开直接返回原状
if (mState == BUBBLE_STATE_CLICK) {
//执行回弹动画
startBackAnim();
}
//断开了
else if (mState == BUBBLE_STATE_BREAK) {
//如果断开了,小球的位置移动到距离2倍移动小球的距离以内也返回原状
if (mDist < mMoveRadius * 2) {
//执行回弹动画
startBackAnim();
} else {
mState = BUBBLE_STATE_BLAST;
//执行爆炸动画
startBlastAnim();
}
}
break;
default:
}
return true;
}

DOWN事件,如果我们的手指点击到圆上或者圆的附近(附近使用一个偏移量MOVE_OFFSET来定义),就把状态改成点击连接的状态

MOVE事件,判断手指移动动的距离小于我们定义的一个最大的距离,就绘制贝塞尔曲线和静止的圆,反之就定义为分离状态。

UP事件,如果静止圆和移动圆还没断开直接返回原状,执行回弹动画,如果已经断开了,在判断如果移动的圆这时候移动到了原来的点的一定范围内,就还需要回到原点,执行回弹动画,反之就执行爆炸动画。

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
//爆炸动画
private void startBlastAnim() {
ValueAnimator animator = ValueAnimator.ofInt(0, 5);
animator.setDuration(500);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mBlastIndex = (int) animation.getAnimatedValue();
invalidate();
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if(mOnExecuteFinishListener!=null){
mOnExecuteFinishListener.onFinish(EXECUTE_STATE_BLAST);
}
}
});
animator.start();
}

//回弹动画
private void startBackAnim() {
PointF start = new PointF(mMovePoint.x, mMovePoint.y);
PointF end = new PointF(mQuitPoint.x, mQuitPoint.y);
//系统的PointFEvaluator只能支持21以上的,编译不通过。所以自己弄了一个把它代码抄过来就行啦
ValueAnimator animator = ValueAnimator.ofObject(new MyPointFEvaluator(), start, end);
animator.setDuration(200);
animator.setInterpolator(new OvershootInterpolator(5f));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mMovePoint = (PointF) animation.getAnimatedValue();
invalidate();
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mState = BUBBLE_STATE_DEFAULT;
if(mOnExecuteFinishListener!=null){
mOnExecuteFinishListener.onFinish(EXECUTE_STATE_BACK);
}
}
});
animator.start();
}

最后就是两个简单的属性动画,爆炸动画用来控制我们的bitmap数组的index值。回弹动画来控制两个点的移动,使用系统默认的插值器OvershootInterpolator(运动到终点后,冲过终点后再回弹)。

这里的PointFEvaluator这个估值器系统有提供,但是只支持5.0以上,所以自定了一个PointFEvaluator,把系统的源码抄一下即可。

OK到这里效果就出来了可以看下图

效果出来了,那怎么用呢,现在是在我们自定义的view中可以全屏拖动绘制,但是如果把这个veiw放到列表中怎么办呢,只能在列表的那一条区域中拖拽吗,当然不符合我们的预期

思路就是,当我们点击列表中的红点的时候,通过当前的Window对象拿到我们的跟布局DecorView,然后把我们自定义的view放到跟布局中,把当前的textview设置隐藏,然后在我们的自定义view的手指点击的地方开始绘制圆就可以了

这里需要注意onTouchEvent获取坐标使用event.getRawX()和event.getRawY(),不能使用event.getX()和event.getY()了,因为我们现在的自定义veiw和点击的textveiw不在一个布局中。getRawX()是相对于屏幕来说的而getX()是相对于父布局来说的。

所以我们在textvew的onTouchListener中,找到DecorView,然后把我们的自定义view放进去,最后把事件传递到我们的自定义view中就好了。

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
public class QQViewListenter implements View.OnTouchListener , QQBubbleView.OnExecuteFinishListener {

private Context mContext;

private ViewGroup mViewGroup;
private QQBubbleView mQQBubbleView;
private View currentClickView;
public QQViewListenter(Context context) {
mContext = context;
Window window = ((Activity) context).getWindow();
mViewGroup = (ViewGroup) window.getDecorView();
mQQBubbleView = new QQBubbleView(context);
}

@Override
public boolean onTouch(View v, MotionEvent event) {
currentClickView = v;
Log.e("onTouch","x--"+event.getRawX()+"y--"+event.getRawY()+"--event"+event.getAction());
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if(mViewGroup!=null){
mViewGroup.addView(mQQBubbleView);
}
ViewParent parent = v.getParent();
if (parent == null) {
return false;
}
if(v instanceof TextView){
String text = ((TextView) v).getText().toString();
mQQBubbleView.setText(text);
}
//防止父容器消费事件
parent.requestDisallowInterceptTouchEvent(true);
int width = v.getWidth();
mQQBubbleView.setCenter(event.getRawX(),event.getRawY(),width/2);
mQQBubbleView.setOnDismissListener(this);
currentClickView.setVisibility(View.INVISIBLE);
}
//事件传递
mQQBubbleView.onTouchEvent(event);
return true;
}

@Override
public void onFinish(int type) {
if(mViewGroup!=null&&mQQBubbleView!=null){
mViewGroup.removeView(mQQBubbleView);
}
if(type == EXECUTE_STATE_BACK){
currentClickView.setVisibility(View.VISIBLE);
}else {
currentClickView.setVisibility(View.GONE);
}
}
}

OnExecuteFinishListener是我们自顶一个view中定义的一个接口,用来监听手指抬起之后的的状态结果,因为完事后我们需要移除我们添加到DecorView中的我们自己的veiw。

使用的时候,我们在Adapter中的textveiw设置setOnTouchListener监听传入我们上面写的QQViewListenter即可。

最终效果:

源码位置

# UI

コメント

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×