Android drawPath实现QQ拖拽泡泡

2023-10-28 21:10

本文主要是介绍Android drawPath实现QQ拖拽泡泡,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

这两天学习了使用Path绘制贝塞尔曲线相关,然后自己动手做了一个类似QQ未读消息可拖拽的小气泡,效果图如下:

这里写图片描述

接下来一步一步的实现整个过程。

基本原理

其实就是使用Path绘制三点的二次方贝塞尔曲线来完成那个妖娆的曲线的。然后根据触摸点不断绘制对应的圆形,根据距离的改变改变原始固定圆形的半径大小。最后就是松手后返回或者爆裂的实现。

Path介绍:

顾名思义,就是一个路径的意思,Path里面有很多的方法,本次设计主要用到的相关方法有

moveTo() 移动Path到一个指定的点 
quadTo() 绘制二次贝塞尔曲线,接收两个点,第一个是控制弧度的点,第二个是终点。 
lineTo() 就是连线 
close() 闭合Path路径, 
reset() 重置Path的相关设置

  • Path入门热身:

    path.reset();
    path.moveTo(200, 200);
    //第一个坐标是对应的控制的坐标,第二个坐标是终点坐标
    path.quadTo(400, 250, 600, 200);canvas.drawPath(path, paint);
    canvas.translate(0, 200);
    //调用close,就会首尾闭合连接
    path.close();
    canvas.drawPath(path, paint);
    

    记得不要在onDraw方法中new Path或者 Paint哟!

Path

具体实现拆分:

其实整个过程就是绘制了两个贝塞尔二次曲线的的闭合Path路径,然后在上面添加两个圆形。

原理图1 
原理图2

  • 闭合的Path 路径实现从左上点画二次贝塞尔曲线到左下点,左下点连线到右下点,右下点二次贝塞尔曲线到右上点,最后闭合一下!!

  • 相关坐标的确定 
    这是这次里面的难点之一,因为涉及到了数学里面的一个sin,cos,tan等等,我其实也忘完了,然后又脑补了一下,废话不多说,直接上图!! 
    旋转过程的角标

为什么自己要亲自去画一下呢,因为画了你才知道,在360旋转的过程中,角标体系是有两套的,如果就使用一套来画的话,就画出现在旋转的过程中曲线重叠在一起的情况!

问题已经抛出来了,接下来直接看看代码实现!

角度确定

根据贴出来的原理图可以知道,我们可以使用起始圆心坐标和拖拽的圆心坐标,根据反正切函数来得到具体的弧度。

int dy = Math.abs(CIRCLEY - startY);
int dx = Math.abs(CIRCLEX - startX);angle = Math.atan(dy * 1.0 / dx);

ok,这里的startX,Y就是移动过程中的坐标。angle就是得到的对应的弧度(角度)。

相关Path绘制

前面已经提到在旋转的过程中有两套坐标体系,一开始我也很纠结这个坐标体系要怎么确定,后面又恍然大悟,其实相当于就是一三象限正比例增长,二四象限,反比例增长。

 flag = (startY - CIRCLEY  ) * (startX- CIRCLEX ) <= 0;//增加一个flag,用于判断使用哪种坐标体系。

最最重要的来了,绘制相关的Path路径!

 path.reset();if (flag) {//第一个点path.moveTo((float) (CIRCLEX - Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY - Math.cos(angle) * ORIGIN_RADIO));path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (startX - Math.sin(angle) * DRAG_RADIO), (float) (startY - Math.cos(angle) * DRAG_RADIO));
path.lineTo((float) (startX + Math.sin(angle) * DRAG_RADIO), (float) (startY + Math.cos(angle) * DRAG_RADIO));path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (CIRCLEX + Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY + Math.cos(angle) * ORIGIN_RADIO));
path.close();
canvas.drawPath(path, paint);} else {//第一个点path.moveTo((float) (CIRCLEX - Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY + Math.cos(angle) * ORIGIN_RADIO));path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (startX - Math.sin(angle) * DRAG_RADIO), (float) (startY + Math.cos(angle) * DRAG_RADIO));path.lineTo((float) (startX + Math.sin(angle) * DRAG_RADIO), (float) (startY - Math.cos(angle) * DRAG_RADIO));path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (CIRCLEX + Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY - Math.cos(angle) * ORIGIN_RADIO));path.close();canvas.drawPath(path, paint);}

这里的代码就是把图片上相关的数学公式Java化而已!

到这里,其实主要的工作就完成的差不多了! 
接下来,设置paint 为填充的效果,最后再画两个圆

 paint.setStyle(Paint.Style.FILL)canvas.drawCircle(CIRCLEX, CIRCLEY, ORIGIN_RADIO, paint);//默认的canvas.drawCircle(startX == 0 ? CIRCLEX : startX, startY == 0 ? CIRCLEY : startY, DRAG_RADIO, paint);//拖拽的

就可以绘制出想要的效果了!

这里不得不再说说onTouch的处理!

  case MotionEvent.ACTION_DOWN://有事件先拦截再说!!getParent().requestDisallowInterceptTouchEvent(true);CurrentState = STATE_IDLE;animSetXY.cancel();startX = (int) ev.getX();startY = (int) ev.getRawY();break;

处理一下事件分发的坑!

测量和布局

这样基本过得去了,但是我们的布局什么的还没有处理,math_parent是万万没法使用到具体项目当中去的! 
测量的时候,如果发现不是精准模式,那么都手动去计算出需要的宽度和高度。

 @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int modeWidth = MeasureSpec.getMode(widthMeasureSpec);int modeHeight = MeasureSpec.getMode(heightMeasureSpec);if (modeWidth == MeasureSpec.UNSPECIFIED || modeWidth == MeasureSpec.AT_MOST) {widthMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_RADIO * 2, MeasureSpec.EXACTLY);}if (modeHeight == MeasureSpec.UNSPECIFIED || modeHeight == MeasureSpec.AT_MOST) {heightMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_RADIO * 2, MeasureSpec.EXACTLY);}super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

然后在布局变化时,获取相关坐标,确定初始圆心坐标:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);CIRCLEX = (int) ((w) * 0.5 + 0.5);CIRCLEY = (int) ((h) * 0.5 + 0.5);
}

然后清单文件里面就可以这样配置了:

 <com.lovejjfg.circle.DragBubbleViewandroid:id="@+id/dbv"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center"/>

这样之后,又会出现一个问题,那就是wrap_content 之后,这个View能绘制的区域只有自身那么大了,拖拽了都看不见了!这个坑怎么办呢,其实很简单,父布局加上Android:clipChildren="false" 的属性! 
这个坑也算是解决了!!

相关状态的确定

我们是不希望它可以无限的拖拽的,就是有一个拖拽的最远距离,还有就是放手后的返回,爆裂。那么对应的,这里需要确定几种状态:

  private final static int STATE_IDLE = 1;//静止的状态private final static int STATE_DRAG_NORMAL = 2;//正在拖拽的状态private final static int STATE_DRAG_BREAK = 3;//断裂后的拖拽状态private final static int STATE_UP_BREAK = 4;//放手后的爆裂的状态private final static int STATE_UP_BACK = 5;//放手后的没有断裂的返回的状态private final static int STATE_UP_DRAG_BREAK_BACK = 6;//拖拽断裂又返回的状态private int CurrentState = STATE_IDLE;private int MIN_RADIO = (int) (ORIGIN_RADIO * 0.4);//最小半径private int MAXDISTANCE = (int) (MIN_RADIO * 13);//最远的拖拽距离

确定好这些之后,在move的时候,就要去做相关判断了:

  case MotionEvent.ACTION_MOVE://移动的时候startX = (int) ev.getX();startY = (int) ev.getY();updatePath();invalidate();break;private void updatePath() {int dy = Math.abs(CIRCLEY - startY);int dx = Math.abs(CIRCLEX - startX);double dis = Math.sqrt(dy * dy + dx * dx);if (dis <= MAXDISTANCE) {//增加的情况,原始半径减小if (CurrentState == STATE_DRAG_BREAK || CurrentState == STATE_UP_DRAG_BREAK_BACK) {CurrentState = STATE_UP_DRAG_BREAK_BACK;} else {CurrentState = STATE_DRAG_NORMAL;}ORIGIN_RADIO = (int) (DEFAULT_RADIO - (dis / MAXDISTANCE) * (DEFAULT_RADIO - MIN_RADIO));Log.e(TAG, "distance: " + (int) ((1 - dis / MAXDISTANCE) * MIN_RADIO));Log.i(TAG, "distance: " + ORIGIN_RADIO);} else {CurrentState = STATE_DRAG_BREAK;}
//        distance = dis;flag = (startY - CIRCLEY) * (startX - CIRCLEX) <= 0;Log.i("TAG", "updatePath: " + flag);angle = Math.atan(dy * 1.0 / dx);
}

updatePath() 的方法之前已经看过部分了,这次的就是完整的。 
这里做的事就是根据拖拽的距离更改相关的状态,并根据百分比来修改原始圆形的半径大小。还有就是之前介绍的确定相关的弧度!

最后放手的时候:

   case MotionEvent.ACTION_UP:if (CurrentState == STATE_DRAG_NORMAL) {CurrentState = STATE_UP_BACK;valueX.setIntValues(startX, CIRCLEX);valueY.setIntValues(startY, CIRCLEY);animSetXY.start();} else if (CurrentState == STATE_DRAG_BREAK) {CurrentState = STATE_UP_BREAK;invalidate();} else {CurrentState = STATE_UP_DRAG_BREAK_BACK;valueX.setIntValues(startX, CIRCLEX);valueY.setIntValues(startY, CIRCLEY);animSetXY.start();}break;

自动返回这里使用到的 ValueAnimator

 animSetXY = new AnimatorSet();valueX = ValueAnimator.ofInt(startX, CIRCLEX);valueY = ValueAnimator.ofInt(startY, CIRCLEY);animSetXY.playTogether(valueX, valueY);valueX.setDuration(500);valueY.setDuration(500);valueX.setInterpolator(new OvershootInterpolator());valueY.setInterpolator(new OvershootInterpolator());valueX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {startX = (int) animation.getAnimatedValue();Log.e(TAG, "onAnimationUpdate-startX: " + startX);invalidate();}});valueY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {startY = (int) animation.getAnimatedValue();Log.e(TAG, "onAnimationUpdate-startY: " + startY);invalidate();}});

最后在看看完整的onDraw方法吧!

 @Override
protected void onDraw(Canvas canvas) {switch (CurrentState) {case STATE_IDLE://空闲状态,就画默认的圆if (showCircle) {canvas.drawCircle(CIRCLEX, CIRCLEY, ORIGIN_RADIO, paint);//默认的}break;case STATE_UP_BACK://执行返回的动画case STATE_DRAG_NORMAL://拖拽状态 画贝塞尔曲线和两个圆path.reset();if (flag) {//第一个点path.moveTo((float) (CIRCLEX - Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY - Math.cos(angle) * ORIGIN_RADIO));path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (startX - Math.sin(angle) * DRAG_RADIO), (float) (startY - Math.cos(angle) * DRAG_RADIO));path.lineTo((float) (startX + Math.sin(angle) * DRAG_RADIO), (float) (startY + Math.cos(angle) * DRAG_RADIO));path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (CIRCLEX + Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY + Math.cos(angle) * ORIGIN_RADIO));path.close();canvas.drawPath(path, paint);} else {//第一个点path.moveTo((float) (CIRCLEX - Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY + Math.cos(angle) * ORIGIN_RADIO));path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (startX - Math.sin(angle) * DRAG_RADIO), (float) (startY + Math.cos(angle) * DRAG_RADIO));path.lineTo((float) (startX + Math.sin(angle) * DRAG_RADIO), (float) (startY - Math.cos(angle) * DRAG_RADIO));path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (CIRCLEX + Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY - Math.cos(angle) * ORIGIN_RADIO));path.close();canvas.drawPath(path, paint);}if (showCircle) {canvas.drawCircle(CIRCLEX, CIRCLEY, ORIGIN_RADIO, paint);//默认的canvas.drawCircle(startX == 0 ? CIRCLEX : startX, startY == 0 ? CIRCLEY : startY, DRAG_RADIO, paint);//拖拽的}break;case STATE_DRAG_BREAK://拖拽到了上限,画拖拽的圆:case STATE_UP_DRAG_BREAK_BACK:if (showCircle) {canvas.drawCircle(startX == 0 ? CIRCLEX : startX, startY == 0 ? CIRCLEY : startY, DRAG_RADIO, paint);//拖拽的}break;case STATE_UP_BREAK://画出爆裂的效果canvas.drawCircle(startX - 25, startY - 25, 10, circlePaint);canvas.drawCircle(startX + 25, startY + 25, 10, circlePaint);canvas.drawCircle(startX, startY - 25, 10, circlePaint);canvas.drawCircle(startX, startY, 18, circlePaint);canvas.drawCircle(startX - 25, startY, 10, circlePaint);break;}}

到这里,成品就出来了!!

总结:

1、确定默认圆形的坐标; 
2、根据move的情况,实时获取最新的坐标,根据移动的距离(确定出角度),更新相关的状态,画出相关的Path路径。超出上限,不再画Path路径。 
3、松手时,根据相关的状态,要么带Path路径执行动画返回,要么不带Path路径直接返回,要么直接爆裂!

相关源码请移步Github,喜欢就请粉一个吧,有问题欢迎留言或者issue。。

这篇关于Android drawPath实现QQ拖拽泡泡的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/295964

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

C++——stack、queue的实现及deque的介绍

目录 1.stack与queue的实现 1.1stack的实现  1.2 queue的实现 2.重温vector、list、stack、queue的介绍 2.1 STL标准库中stack和queue的底层结构  3.deque的简单介绍 3.1为什么选择deque作为stack和queue的底层默认容器  3.2 STL中对stack与queue的模拟实现 ①stack模拟实现