前言

上一篇文章用扇形图练习了一下安卓的多点触控,实现了单指旋转、二指放大、三指移动,四指以上同时按下进行复位的功能。今天这篇文章用很多应用常见的小红点,来练习一下贝塞尔曲线的使用。

需求

这里想法来自QQ的拖动小红点取消显示聊天条数功能,不过好像是记忆里的了,现在看了下好像效果变了。总而言之,就是一个小圆点,拖动的时候变成水滴状,超过一定范围后触发消失回调,核心思想如下:

  • 1、一个正方形view,中间是小红点,小红点距离边框有一定距离
  • 2、拖动小红点,小红点会变形,并产生尾焰效果
  • 3、释放时,如果在设定范围外小红点消失,范围内则恢复

效果图

这里效果在距离小的时候,还是不错的,当移动范围过大时,虽然水滴状的曲线还是连续的,但是变形严重了,不过这个功能并不需要拖动太长距离把,只要限定好消失范围,还是能满足要求的。

代码

import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.core.animation.addListener
import kotlin.math.asin
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin/*** 拖拽消失的小红点** @author silence* @date 2022-11-07**/
class RedDomView @JvmOverloads constructor(context: Context,attributeSet: AttributeSet? = null,defStyleAttr: Int = 0
): View(context, attributeSet, defStyleAttr) {companion object{const val STATE_NORMAL = 0const val STATE_DRAGGING = 1const val STATE_SETTING = 2const val STATE_FINISHED = 3}// 状态private var mState = STATE_NORMAL/*** 红点半径占控件宽高的比例*/var domPercent = 0.25f/*** 红点消失的长度占最短宽高的比例*/var disappearPercent = 0.25f/*** 消失回调*/var listener: OnDisappearListener? = null// 半径private var mDomRadius: Float = 0f// 消失长度private var mDisappearLength = 0f// 滑动距离和移动距离的缩放比例private val mDraggingScale = 0.5f// 圆心所在位置private var mRadiusX = 0fprivate var mRadiusY = 0f// 上一次touch的点private var mLastX = 0fprivate var mLastY = 0f// 绘制拖拽时的路径private val path = Path()// 恢复的属性动画private val animator = ValueAnimator.ofFloat(0f, 1f)// 画笔private val mPaint = Paint().apply {strokeWidth = 5fcolor = Color.REDstyle = Paint.Style.FILLflags = Paint.ANTI_ALIAS_FLAG}/*** 重置*/fun reset() {mState = STATE_NORMALmRadiusX = width / 2fmRadiusY = height / 2finvalidate()}override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)val width = getDefaultSize(100, widthMeasureSpec)val height = getDefaultSize(100, heightMeasureSpec)// 计算得到半径mDomRadius = (if (width < height) width else height) * domPercentmRadiusX = width / 2fmRadiusY = height / 2f// 消失长度mDisappearLength = (if (width < height) width else height) * disappearPercentsetMeasuredDimension(width, height)}override fun onTouchEvent(event: MotionEvent): Boolean {// 结束了不应该接受事件,通过设置OnClickListener使用reset去重置if (mState == STATE_FINISHED) {if (event.action == MotionEvent.ACTION_DOWN) performClick()else return true}when(event.action) {MotionEvent.ACTION_DOWN -> {mLastX = event.xmLastY = event.y// 设置中或者拖拽时,快速重新按下,应该再次接手动画if(mState != STATE_NORMAL) {animator.removeAllListeners()animator.cancel()}mState = STATE_DRAGGING}MotionEvent.ACTION_MOVE -> {// 注意canvas移动和手指移动是一致的,view的scroll移动的是窗口val dx = event.x - mLastXval dy = event.y - mLastY// 移动圆心mRadiusX += dx * mDraggingScalemRadiusY += dy * mDraggingScalemLastX = event.xmLastY = event.y// 请求重绘invalidate()}MotionEvent.ACTION_UP -> {mState = STATE_SETTING// 这里用属性动画模拟拖拽,回到初始圆心val upRadiusX = mRadiusXval upRadiusY = mRadiusYanimator.addUpdateListener {// 根据比例,按直线移动圆心到中点val progress = it.animatedValue as FloatmRadiusX = upRadiusX + (width / 2f - upRadiusX) * progressmRadiusY = upRadiusY + (height / 2f - upRadiusY) * progressinvalidate()}animator.addListener(onEnd = {mState = STATE_NORMAL})animator.duration = 100animator.start()}}return true}@Suppress("RedundantOverride")override fun performClick(): Boolean {return super.performClick()}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)when(mState) {STATE_NORMAL -> {// 正常状态是一个圆canvas.drawCircle(width / 2f, height / 2f, mDomRadius, mPaint)}STATE_DRAGGING, STATE_SETTING -> {// 圆心和中点连线相对于X轴的夹角,注意atan2是四象限敏感[-PI, PI],atan范围为[-PI/2, PI/2]val radiansLine = atan2((mRadiusY - height / 2f).toDouble(),(mRadiusX - width /2f).toDouble()).toFloat()// 圆心和中点连线的长度,通过角度算,分母为零为什么没问题?val lineLength = (mRadiusX - width /2f) / cos(radiansLine)// 判断是否达到消失要求,如果消失不应该再绘制if (lineLength > mDisappearLength) {mState = STATE_FINISHEDlistener?.onDisappear()return}// 以圆心为顶点,切点、圆心、中心的夹角值,是一个正值val radiansCenter = asin(mDomRadius / lineLength)// 切点和中心连线长度val length = lineLength * cos(radiansCenter)// 由角度获取两个切点的坐标值val x1 = width /2f + length * cos(radiansLine + radiansCenter)val y1 = height / 2f + length * sin(radiansLine + radiansCenter)val x2 = width /2f + length * cos(radiansLine - radiansCenter)val y2 = height / 2f + length * sin(radiansLine - radiansCenter)// 绘制// 普通代码,一个圆加三角形
//                canvas.drawCircle(mRadiusX, mRadiusY, mDomRadius, mPaint)
//                path.reset()
//                path.moveTo(x1, y1)
//                path.lineTo(width / 2f, height / 2f)
//                path.lineTo(x2, y2)
//                path.close()// 强行贝塞尔曲线// 先用完整的圆覆盖lineLength < 2 * mDomRadius的情况,大于时圆会被覆盖canvas.drawCircle(mRadiusX, mRadiusY, mDomRadius, mPaint)path.reset()path.moveTo(x1, y1)// 拟合圆弧,三阶贝塞尔曲线,控制点在圆心和中点连线的圆外var tempX1 = x1 + (length * cos(radiansLine + radiansCenter))var tempY1 = y1 + ( length * sin(radiansLine + radiansCenter))var tempX2 = x2 + (length * cos(radiansLine - radiansCenter))var tempY2 = y2 + ( length * sin(radiansLine - radiansCenter))// 接近圆不是圆path.cubicTo(tempX1, tempY1, tempX2, tempY2, x2, y2)// 尾焰,第一个控制点在切线延长线上,第二个控制点在圆心连线上(越短尾越尖)tempX1 = x2 - length * cos(radiansLine - radiansCenter)tempY1 = y2 - length * sin(radiansLine - radiansCenter)tempX2 = width / 2f + (lineLength * 0.25f * cos(radiansLine))tempY2 = height / 2f + (lineLength * 0.25f * sin(radiansLine))// 第一条path.cubicTo(tempX1, tempY1, tempX2, tempY2, width / 2f, height / 2f)// 另一段tempX1 = tempX2tempY1 = tempY2tempX2 = x1 - (length * cos(radiansLine + radiansCenter))tempY2 = y1 - ( length * sin(radiansLine + radiansCenter))path.cubicTo(tempX1, tempY1, tempX2, tempY2, x1, y1)path.close()canvas.drawPath(path, mPaint)}STATE_FINISHED -> {}}// 这里便于调试,把消失范围画一下,多加一只画笔,省的麻烦canvas.drawCircle(width / 2f, height / 2f, mDisappearLength, tempPaint)}private val tempPaint = Paint().apply {strokeWidth = 3fstyle = Paint.Style.STROKEcolor = Color.LTGRAYpathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f)flags = Paint.ANTI_ALIAS_FLAG}interface OnDisappearListener{fun onDisappear()}
}

主要问题

关于onMeasure、onTouchEvent以及onDraw的内容就不讲了,这里已经是第十篇自定义view的文章了,下面主要介绍下贝塞尔曲线绘制水滴状的功能。

简单画法

这里最简单的画法就是用一个圆和一个三角形解决了。每次移动对小圆点移动,然后计算得到view中心在圆上的两个切点,将两个切点和view中心围起来画一个实心的三角形,组合起来的效果就是一个近似的小水滴了。

使用贝塞尔曲线

要实现更逼真的效果,使用直线是肯定不行的了,这里就要用到曲线了。首先想到的就是弧线了,可是用弧线和上面的圆是没去别的,后面我就直接全用贝塞尔曲线做了。

我这把这个水滴形状的小红点分了三段,都是用三阶的贝塞尔曲线画的,绘制的时候最重要的就是找控制点了。首先要知道贝塞尔曲线的临近控制点和端点的连线,就是曲线在该端点的切线,要保证三段线的连续,保证三段线在同一端点的切线一致就行。这里最上面的那段类似圆弧的曲线,就取了切线延长线上的点作为控制点,尾焰那段取切线内上的点,这样在(x1, y1)(x2, y2)上就连续了,至于控制点距离端点距离取值的大小就试着取看效果了。剩下在view中点那侧的控制点,就取在中点和圆心上,这样水滴的尾巴看起来就顺眼。

几个控制点的选取和展现的效果相关性很大,我觉得我选的点看起来还行。

自定义view实战(10):贝塞尔曲线绘制小红点相关推荐

  1. 有趣的自定义View — 玫瑰·三阶贝塞尔曲线

    "玫瑰贝塞尔曲线"效果如下: 一.效果要求 1)在布局中某个位置处玫瑰开始由小而大,淡入出现: 2)出现的玫瑰,颜色随机而定,玫瑰可在布局内做动画亦可在整个界面中做动画,如上图: ...

  2. android 图片处理过程中添加进度条,『Android自定义View实战』给我一个图标,还你一个水波纹进度球...

    前言 我们都知道,平时表现进度的方式有千千万万种(没有UI想不到的,只有你做不到的= =.),其中有一种就是水波纹进度球的形式,网上很多种实现都是直接采用纯色填充的方式,即水波纹都是纯颜色填充,效果看 ...

  3. android运动轨迹怎么画,Android 利用三阶贝塞尔曲线绘制运动轨迹的示例

    本篇文章主要介绍了Android 利用三阶贝塞尔曲线绘制运动轨迹的示例,分享给大家,具体如下: 实现点赞效果,自定义起始点以及运动轨迹 效果图: xml布局: xmlns:tools="ht ...

  4. Android 系统(201)---Android 自定义View实战系列 :时间轴

    Android 自定义View实战系列 :时间轴 Android开发中,时间轴的 UI需求非常常见,如下图: 本文将结合 自定义View & RecyclerView的知识,手把手教你实现该常 ...

  5. 自定义View实战(一) 汽车速度仪表盘

    自定义View实战(一) 汽车速度仪表盘 转载请以链接形式标明出处: http://blog.csdn.net/lxk_1993/article/details/51373269 本文出自:[lxk_ ...

  6. android 贝塞尔曲线_OpenGL 实践之贝塞尔曲线绘制

    说到贝塞尔曲线,大家肯定都不陌生,网上有很多关于介绍和理解贝塞尔曲线的优秀文章和动态图. 以下两个是比较经典的动图了. 二阶贝塞尔曲线: 三阶贝塞尔曲线: 由于在工作中经常要和贝塞尔曲线打交道,所以简 ...

  7. Android进阶之自定义View实战(二)九宫格手势解锁实现

    一.引言 在上篇博客Android进阶之自定义View实战(一)仿iOS UISwitch控件实现中我们主要介绍了自定义View的最基本的实现方法.作为自定义View的入门篇,仅仅介绍了Canvas的 ...

  8. 【Android自定义View实战】之自定义评价打分控件RatingBar,可以自定义星星大小和间距...

    [Android自定义View实战]之自定义评价打分控件RatingBar,可以自定义星星大小和间距

  9. android 行布局选择器,『自定义View实战』—— 银行种类选择器

    在工作中难免遇到自定义 View 的相关需求,本身这方面比较薄弱,因此做个记录,也是自己学习和成长的积累.自定义View实战 前言 年前的最后一个开发需求,将之前H5开卡界面转变成native.意思就 ...

最新文章

  1. 利用STL离散化处理数据(unique)
  2. barrier相關知識點整理(还没搞完)
  3. 【机器学习】贝叶斯线性回归(最大后验估计+高斯先验)
  4. 现代软件工程 作业 团队项目计划
  5. 动态填充html select tag的options
  6. PHP面向对象学习五 类中接口的应用
  7. ESP32 之 ESP-IDF 教学(十一)WiFi篇—— WiFi两种模式
  8. 计算机操作系统|汤小丹|第四版|习题答案(三)
  9. 仓库管理软件 v1.0 绿色破解版
  10. 软件系统演示脚本实践(草稿)
  11. Uber AI 研究院深度解构 ICLR 2019 最佳论文「彩票假设」!
  12. 使用MediaRecorder录制音频
  13. 重置计算机网络设置路由器,重新设置路由器的步骤
  14. win10c盘清理(win10磁盘清理和磁盘整理)
  15. 天蝎项目整机柜服务器技术规格,天蝎整机柜服务器技术规范2.5.doc
  16. 爬虫技术:携程爬虫阳光问政数据
  17. android自动切换暗色,根据环境光亮度自动切换,让 Android 10 的暗色主题更智能:Auto Dark Theme...
  18. 详解数据库设计的四个阶段
  19. 南华大学的计算机专业学校排名,2019南华大学专业排名
  20. idea中java项目显示不对_Intellj Idea中的maven工程Java文件颜色不对,未被识别的解决...

热门文章

  1. iOS 获取相册中视频大小
  2. G:\Windows Kits\10\include\10.0.18362.0\ucrt\inttypes.h(96): error C2143: 语法错误: 缺少“{”(在“__cdecl”的前面)
  3. 死磕VR,爱奇艺的元宇宙大局观
  4. Love 6 从零开始的计算机学习之路(一)---- 我的大学历程(不定期更新到大学毕业)
  5. 河北省2019网络安全竞赛线上赛部分题解
  6. 【解决问题】mybatis plus 读取数据库没有返回值问题 返回值都为null
  7. Design Play 设计稿预览,实时预览设计效果,最好用的设计稿预览工具
  8. 怎样读取另一台电脑的mysql数据库_如何访问另一台电脑的数据库
  9. 计算机二级学校会设考点吗,2020年9月北京计算机二级考试考点设置
  10. 固定ipv6地址申请