写点什么

这里有一份史上最详细仿 QQ 未读消息拖拽粘性效果的实现,快来收藏!

发布于: 2021 年 11 月 07 日


今天为大家带来一篇关于动画学习的自定义 View:类似 QQ 消息拖拽的效果。


其实 QQ 开始更新的那个时候我还没注意到这个小红点是可以拖拽的,后来无意间发现之后就把玩了好久,当时就感觉这个效果还挺好玩的,曾经有过一个念头去实现一个这样的效果,中间由于种种原因一直没去做,今天就算是对过去承诺的兑现吧。


其实网上已经有很多这样的资料了,也有现成的 demo,但大部分讲解的不够详细,很多计算都只是列个公式画个草图一笔带过,对于我们这些数学不好的人来说有点懵逼,好了,话不多说本篇文章将向你对中间的计算过程讲的明明白白的。


![](//upload-images.jianshu.io/upload_images/2057501-2dacb0afe5bb0df1.jpg?imageMogr2/auto-orient/strip%7Cima


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


geView2/2/w/250/format/webp)


开始之前我建议大家打开 QQ 先去熟悉一下这个拖拽效果,然后根据自己掌握的知识梳理一下自己去实现的思路,包括中间粘性效果的实现。按照惯例,先看看本篇文章能实现的最终效果



我来分析一下我对这个实现过程的理解:首先是在指定某个位置画一个圆出来,手指按到这个圆的时候再绘制一个可以根据手指位置移动的圆,随着手指的移动两个圆逐渐分离,分离的过程中两圆中间出现连接带,随着两圆圆心距的增大,半径也是根据某一比例系数扩大或缩小,当超过临界点的时候起始圆消失,只剩手指所在位置的圆,然后手指松开圆消失。


根据上面的分析我们得出绘制步骤:1、在指定位置绘制起始圆(圆中间可以带数字)2、使用贝塞尔曲线绘制两圆之间的连接带 3、处理 onTouchEvent 事件(down、move、up)4、添加一些动画特效



下面我们就按照上述步骤开始撸代码

1、绘制起始圆

当然我们要实现定义一些常量,画笔等的初始化代码我就不再展示了


//是否可拖拽 private boolean mIsCanDrag = false;//是否超过最大距离 private boolean isOutOfRang = false;//最终圆是否消失 private boolean disappear = false;


//两圆相离最大距离 private float maxDistance;


//贝塞尔曲线需要的点 private PointF pointA;private PointF pointB;private PointF pointC;private PointF pointD;//控制点坐标 private PointF pointO;


//起始位置点 private PointF pointStart;//拖拽位置点 private PointF pointEnd;


//根据滑动位置动态改变圆的半径 private float currentRadiusStart;private float currentRadiusEnd;


private Rect textRect = new Rect();//消息数 private int msgCount = 0;


画圆大家应该都不陌生,一行代码搞定,传入圆心坐标,半径,画笔即可


/**


  • 画起始小球

  • @param canvas 画布

  • @param pointF 点坐标

  • @param radius 半径*/private void drawStartBall(Canvas canvas, PointF pointF, float radius) {canvas.drawCircle(pointF.x, pointF.y, radius, circlePaint);}


/**


  • 画拖拽结束的小球

  • @param canvas 画布

  • @param pointF 点坐标

  • @param radius 半径*/private void drawEndBall(Canvas canvas, PointF pointF, float radius) {canvas.drawCircle(pointF.x, pointF.y, radius, circlePaint);}


初始化一些常量,我们 demo 演示以屏幕中心为圆心


@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);startX = w / 2;startY = h / 2;maxDistance = dp2px(100);radiusStart = dp2px(15);radiusEnd = dp2px(15);


currentRadiusEnd = radiusEnd;currentRadiusStart = radiusStart;}


这样我们就在屏幕中心处绘制了一个圆

2、根据贝塞尔曲线绘制连接带

这是本文的重点,计算过程会讲解的非常详细,通俗易懂我们先看下画出了是什么样的再去分析



两个圆我们知道怎么画的了,现在就来分析一下连接带的实现,可以看到是两段平滑的过渡,这样的弧度使用贝塞尔再好不过了,我们在简单回顾一下贝塞尔曲线的样子



看到这个效果是不是会心一笑,这 TM 就是我们要的效果下边看下我画的一个分析图,可以说是目前网上最详细的图文解释了(配上骄傲的表情)



注意:图中有一个角度描述错了 tanEAS1 应该是 tanESS1 由于带撇的点无法在 MD 语法中标示出来 故用 1 代替撇,例如 A`=A1


为了加深理解我在描述一下图中的意思:


起点圆我们定义为圆 S(start 的缩写),对应的圆心坐标为 S(Sx,Sy),可拖拽圆也就是终点圆定义为圆 E(end 的缩写),圆心坐标为 E(Ex,Ey)。连接带的路径可以从图上看出来是:A-->O-->B-->C-->O-->D-->A,其中 O 为 AOB 和 COD 这两段二阶贝塞尔曲线的控制点,图中绿线标注了五个角度,这五个角度是相等的,可以根据三角形的相关定理得出,为了充分说明我们是史上最详细的解释,我就举个例子说明一下为什么角度相等,数学不错的伙伴可以跳过这段啦,角 ASA1+ A1SE=90 度=A1SE+ESD1 可以推出角 ASA1=ESD1,同理可以的出其余标示角度相等,我们定义为角 A,后边我们就是根据角度计算各个点的坐标的


已知起点圆心 S(Sx,Sy),终点圆心 E(Ex,Ey),E 就是手指滑动所在的位置,可以根据 event.getX()和 event.getY()取到


我们以角 ESS1 为例进行计算:


tanESS1=tanA=S1E/SS1=(Ex-Sx)/(Ey-Sy)=rate,rate 就是这个角的斜率,然后根据反正切得出角 A,A=arctan(rate),这是反正切公式,忘记的可以去百度百科温故一下哦。


知道了角度 A 就可以根据角度加上正余弦函数算出各个点的坐标了,这个计算推倒过程我已写在图上了,下边就把上述计算过程用代码实现一下


/**


  • 设置贝塞尔曲线的相关点坐标 计算方式参照结算图即可看明白

  • (ps 为了画个清楚这个图花了不少功夫哦)*/private void setABCDOPoint() {//控制点坐标 pointO.set((pointStart.x + pointEnd.x) / 2.0f, (pointStart.y + pointEnd.y) / 2.0f);


float x = pointEnd.x - pointStart.x;float y = pointEnd.y - pointStart.y;


//斜率 tanA=ratedouble rate;rate = x / y;//角度 根据反正切函数算角度 float angle = (float) Math.atan(rate);


pointA.x = (float) (pointStart.x + Math.cos(angle) * currentRadiusStart);pointA.y = (float) (pointStart.y - Math.sin(angle) * currentRadiusStart);


pointB.x = (float) (pointEnd.x + Math.cos(angle) * currentRadiusEnd);pointB.y = (float) (pointEnd.y - Math.sin(angle) * currentRadiusEnd);


pointC.x = (float) (pointEnd.x - Math.cos(angle) * currentRadiusEnd);pointC.y = (float) (pointEnd.y + Math.sin(angle) * currentRadiusEnd);


pointD.x = (float) (pointStart.x - Math.cos(angle) * currentRadiusStart);pointD.y = (float) (pointStart.y + Math.sin(angle) * currentRadiusStart);}


至此关于贝塞尔曲线这部分就介绍完了,下边把圆个弧度代码串联起来就 ok 了,还费什么话先看看效果咋样,先把终点圆坐标定死在一个位置看下效果,为了方便看到绘制的路径我们把画笔样式设为 STROKE



这和我们的预期是一样的,计算了大半天总算没有白算,赶紧去抽根烟释放一下刚才计算时候紧张的心情(生怕算错),回来稳定一下情绪继续往下走。


3、处理 onTouchEvent 事件

3.1、处理 ACTION_DOWN 事件


手指按下的时候我们要判断手指所在位置是不是在起点圆上,只有按到起点圆上之后拖拽才有效,还记得我们文章开始的时候定义的变量 mIsCanDrag 吧


case MotionEvent.ACTION_DOWN:setIsCanDrag(event);break;


/**


  • 判断是否可以拖拽

  • @param event event*/private void setIsCanDrag(MotionEvent event) {Rect rect = new Rect();rect.left = (int) (startX - radiusStart);rect.top = (int) (startY - radiusStart);rect.right = (int) (startX + radiusStart);rect.bottom = (int) (startY + radiusStart);


//触摸点是否在圆的坐标域内 mIsCanDrag = rect.contains((int) event.getX(), (int) event.getY());}


3.2、处理 ACTION_MOVE 事件


手指按在起点圆是可 move 的前提,然后根据手指滑动取出移动点位置的坐标,这就是可拖拽的终点圆的坐标,


if (mIsCanDrag) {currentX = event.getX();currentY = event.getY();//设置拖拽圆的坐标 pointEnd.set(currentX, currentY);}


然后知道了起点圆的坐标和终点圆的坐标就可以得出所需要的各个点的坐标了,其中两圆圆心距也可以计算出来,然后根据圆心距与可拖拽最大距离的比例系数去设置两个圆的半径,当拖拽距离超过了最大距离我们通过改变状态去控制只绘制拖拽圆,否则绘制出两圆和中间的连接带,下面代码注释的很清楚了


/**


  • 设置当前计算的到的半径*/private void setCurrentRadius() {//两个圆心之间的距离 float distance = (float) Math.sqrt(Math.pow(pointStart.x - pointEnd.x, 2) + Math.pow(pointStart.y - pointEnd.y, 2));


//拖拽距离在设置的最大值范围内才绘制贝塞尔图形 if (distance <= maxDistance) {//比例系数 控制两圆半径缩放 float percent = distance / maxDistance;


//之所以*0.6 和 0.2 只为了设置拖拽过程圆变化的过大和过小这个系数是多次尝试的出的//你也可以适当调整系数达到自己想要的效果 currentRadiusStart = (1 - percent * 0.6f) * radiusStart;currentRadiusEnd = (1 + percent * 0.2f) * radiusEnd;


isOutOfRang = false;} else {isOutOfRang = true;currentRadiusStart = radiusStart;currentRadiusEnd = radiusEnd;}}


看下写到这一步的时候的效果



我们发现手指松开的时候圆并没有消失或者重置,因为我们还没出来 up 事件。


3.3、处理 ACTION_UP 事件


手指抬起的时候我们要判断抬起的时候终点圆所在位置和起点圆的圆心距是否超过设置最大距离,如果没有超过就还原拖拽状态,只保留一个起点圆,如果超过了最大距离就让圆消失


if (mIsCanDrag) {if (isOutOfRang) {//消失动画 disappear = true;invalidate();} else {disappear = false;//归位,重置各个点的坐标为开始状态 pointEnd.set(pointStart.x,pointStart.y);setCurrentRadius();setABCDOPoint();invalidate();}}


我们再来看下效果



看到这里核心的代码基本已经完成了,但是总感觉哪里不是很完美,哦,动画,少了一些动画效果看上去好生硬,我们就在手指离开的时候出来归位的动画

评论

发布
暂无评论
这里有一份史上最详细仿QQ未读消息拖拽粘性效果的实现,快来收藏!