Android自定义View绘制贝塞尔曲线中小红点的方法

目录
  • 前言
  • 需求
  • 效果图
  • 代码
  • 主要问题
    • 简单画法
    • 使用贝塞尔曲线

前言

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

需求

这里想法来自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 = 0
        const val STATE_DRAGGING = 1
        const val STATE_SETTING = 2
        const 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 = 0f
    private var mRadiusY = 0f
    // 上一次touch的点
    private var mLastX = 0f
    private var mLastY = 0f
    // 绘制拖拽时的路径
    private val path = Path()
    // 恢复的属性动画
    private val animator = ValueAnimator.ofFloat(0f, 1f)
    // 画笔
    private val mPaint = Paint().apply {
        strokeWidth = 5f
        color = Color.RED
        style = Paint.Style.FILL
        flags = Paint.ANTI_ALIAS_FLAG
    }
    /**
     * 重置
     */
    fun reset() {
        mState = STATE_NORMAL
        mRadiusX = width / 2f
        mRadiusY = height / 2f
        invalidate()
    }
    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) * domPercent
        mRadiusX = width / 2f
        mRadiusY = height / 2f
        // 消失长度
        mDisappearLength = (if (width < height) width else height) * disappearPercent
        setMeasuredDimension(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.x
                mLastY = event.y
                // 设置中或者拖拽时,快速重新按下,应该再次接手动画
                if(mState != STATE_NORMAL) {
                    animator.removeAllListeners()
                    animator.cancel()
                }
                mState = STATE_DRAGGING
            }
            MotionEvent.ACTION_MOVE -> {
                // 注意canvas移动和手指移动是一致的,view的scroll移动的是窗口
                val dx = event.x - mLastX
                val dy = event.y - mLastY
                        // 移动圆心
                mRadiusX += dx * mDraggingScale
                mRadiusY += dy * mDraggingScale
                mLastX = event.x
                mLastY = event.y
                // 请求重绘
                invalidate()
            }
            MotionEvent.ACTION_UP -> {
                mState = STATE_SETTING
                // 这里用属性动画模拟拖拽,回到初始圆心
                val upRadiusX = mRadiusX
                val upRadiusY = mRadiusY
                animator.addUpdateListener {
                    // 根据比例,按直线移动圆心到中点
                    val progress = it.animatedValue as Float
                    mRadiusX = upRadiusX + (width / 2f - upRadiusX) * progress
                    mRadiusY = upRadiusY + (height / 2f - upRadiusY) * progress
                    invalidate()
                }
                animator.addListener(onEnd = {
                    mState = STATE_NORMAL
                })
                animator.duration = 100
                animator.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_FINISHED
                    listener?.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 = tempX2
                tempY1 = tempY2
                tempX2 = 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 = 3f
        style = Paint.Style.STROKE
        color = Color.LTGRAY
        pathEffect = 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中点那侧的控制点,就取在中点和圆心上,这样水滴的尾巴看起来就顺眼。

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

到此这篇关于Android自定义View绘制贝塞尔曲线中小红点的方法的文章就介绍到这了,更多相关Android贝塞尔曲线内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android使用贝塞尔曲线画心形

    本文实例为大家分享了Android使用贝塞尔曲线画心形的具体代码,供大家参考,具体内容如下 一开始我只是想画个圆,可画着画着就成了心形,那就将错就错 1. 创建一个Activity RelativeLayout container = findViewById(R.id.download_container);     DisplayMetrics metrics = new DisplayMetrics();     getWindowManager().getDefaultDisplay()

  • Android通过交互实现贝塞尔曲线的绘制

    目录 前言 获取触控位置 交互绘制实现 绘制代码 运行效果 总结 前言 之前几篇我们介绍了贝塞尔曲线的原理.绘制曲线和动效实现,这些都是代码预设好的,如果我们要根据需要自行绘制曲线,就需要使用交互来实现了.本篇我们先来介绍简单的交互式绘图,通过获取触控位置来设定贝塞尔曲线的控制点,从而实现交互式绘制曲线. 获取触控位置 第一个要解决的问题是如何获取手指在屏幕的触控位置.在 Flutter 中,提供了一个 Listener 组件,可以监听各类触控事件.Listener 的组件构造方法定义如下: c

  • Android利用贝塞尔曲线绘制动画的示例代码

    目录 彩虹系列 弹簧动画 复杂立体感动画 总结 前面我们花了几篇介绍了贝塞尔曲线的原理和绘制贝塞尔曲线,着实让我们见识到了贝塞尔曲线的美.好奇心驱使我想看看贝塞尔曲线动起来会是什么样?本篇就借由动画驱动贝塞尔曲线绘制看看动起来的贝塞尔曲线什么效果. 彩虹系列 通过动画控制绘制的结束点,就可以让贝塞尔曲线动起来.例如下面的动图展示的效果,看起来像搭了一个滑滑梯一样.实际上就是用7条贝塞尔曲线实现的,我们使用了 Animation 对象的值来控制绘制的结束点,从而实现了对应的动画效果. 具体源码如下

  • Android贝塞尔曲线实现加入购物车抛物线动画

    本文实例为大家分享了Android贝塞尔曲线实现加入购物车抛物线动画的具体代码,供大家参考,具体内容如下 先上图看效果 步骤: a.确定动画的起终点b.在起终点之间使用二次贝塞尔曲线填充起终点之间的点的轨迹c.设置属性动画,ValueAnimator插值器,获取中间点的坐标d.将执行动画的控件的x.y坐标设为上面得到的中间点坐标e.开启属性动画f.当动画结束时的操作 获取控件在屏幕中的绝对坐标: int[] parentLocation = new int[2]; mRLayout.getLoc

  • Android自定义View绘制贝塞尔曲线实现流程

    目录 前言 二阶贝塞尔曲线 三阶贝塞尔曲线 前言 对于Android开发,实现贝塞尔曲线还是比较方便的,有对应的API供你调用.由于一阶贝塞尔曲线就是一条直线,实际没啥多大用处,因此,下面主要讲解二阶和三阶. 二阶贝塞尔曲线 在Android中,使用quadTo来实现二阶贝塞尔 path.reset() path.moveTo(startX, startY) path.quadTo(currentX, currentY, endX, endY) canvas.drawPath(path, cur

  • Android自定义View绘制贝塞尔曲线的方法

    本文实例为大家分享了Android自定义View绘制贝塞尔曲线的具体代码,供大家参考,具体内容如下 在平面内任选 3 个不共线的点,依次用线段连接. 在第一条线段上任选一个点 D.计算该点到线段起点的距离 AD,与该线段总长 AB 的比例. 根据上一步得到的比例,从第二条线段上找出对应的点 E,使得 AD:AB = BE:BC. 连接这两点 DE. 从新的线段 DE 上再次找出相同比例的点 F,使得 DF:DE = AD:AB = BE:BC. 到这里,我们就确定了贝塞尔曲线上的一个点 F.接下

  • Android自定义view贝塞尔曲线

    本文实例为大家分享了Android自定义view贝塞尔曲线,供大家参考,具体内容如下 贝塞尔曲线 以一个简单的贝塞尔曲线为例,二阶曲线原理 贝塞尔曲线很多功能都会用到,比如小火箭发射,再比如淘宝的购物车功能 所幸的是Android有封装好的贝塞尔曲线,我们直接拿过来用就可以了: //二阶贝赛尔  public void quadTo(float x1, float y1, float x2, float y2)  public void rQuadTo(float dx1, float dy1,

  • Android 贝塞尔曲线绘制一个波浪球

    目录 前言 一.绘制 backgroundColor 文本 二.构建 circlePath 三.绘制波浪线 四.取交集 五.绘制 foregroundColor 文本 六.添加动画 七.使用 前言 当 flutter 的现有组件无法满足产品要求的 UI 效果时,我们就需要通过自绘组件的方式来进行实现了.本篇文章就来介绍如何用 flutter 自定义实现一个带文本的波浪球,效果如下所示: 先来总结下 WaveLoadingWidget 的特点,这样才能归纳出实现该效果所需要的步骤: widget

  • Android自定义View绘制贝塞尔曲线中小红点的方法

    目录 前言 需求 效果图 代码 主要问题 简单画法 使用贝塞尔曲线 前言 上一篇文章用扇形图练习了一下安卓的多点触控,实现了单指旋转.二指放大.三指移动,四指以上同时按下进行复位的功能.今天这篇文章用很多应用常见的小红点,来练习一下贝塞尔曲线的使用. 需求 这里想法来自QQ的拖动小红点取消显示聊天条数功能,不过好像是记忆里的了,现在看了下好像效果变了.总而言之,就是一个小圆点,拖动的时候变成水滴状,超过一定范围后触发消失回调,核心思想如下: 1.一个正方形view,中间是小红点,小红点距离边框有

  • Android自定义View绘制彩色圆弧

    本文实例为大家分享了Android自定义View绘制彩色圆弧的具体代码,供大家参考,具体内容如下 效果如下: 自定义View代码如下: package com.example.yan; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; i

  • Android自定义View绘制居中文本

    本文实例为大家分享了Android自定义View绘制居中文本的具体代码,供大家参考,具体内容如下 自定义view的步骤: 1.自定义View的属性2.在View的构造方法中获得我们自定义的属性3.重写onMesure(非必须)4.重写onDraw 1.自定义View的属性,首先在res/values/ 下建立一个attrs.xml , 在里面定义我们的属性,只定义三个,有文本.颜色和字体大小: <!--CustomTextView-->     <declare-styleable na

  • Android自定义view绘制表格的方法

    本文实例为大家分享了Android自定义view绘制表格的具体代码,供大家参考,具体内容如下 先上效果图 平时很少有这样的表格需求,不过第一想法就是自定义view绘制表格,事实上我确实是用的canvas来绘制的,整个过程看似复杂,实为简单,计算好各个点的坐标后事情就完成一半了.不废话show code import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; imp

  • Android自定义View绘制随机生成图片验证码

    本篇文章讲的是Android自定义View之随机生成图片验证码,开发中我们会经常需要随机生成图片验证码,但是这个是其次,主要还是想总结一些自定义View的开发过程以及一些需要注意的地方. 按照惯例先看看效果图: 一.先总结下自定义View的步骤: 1.自定义View的属性 2.在View的构造方法中获得我们自定义的属性 3.重写onMesure 4.重写onDraw 其中onMesure方法不一定要重写,但大部分情况下还是需要重写的 二.View 的几个构造函数 1.public CustomV

  • Android自定义View绘制的方法及过程(二)

    上一篇<Android 自定义View(一) Paint.Rect.Canvas介绍>讲了最基础的如何自定义一个View,以及View用到的一些工具类.下面讲下View绘制的方法及过程 public class MyView extends View { private String TAG = "--------MyView"; private int width, height; public MyView(Context context, AttributeSet a

  • Android自定义View绘制四位数随机码

    现在有这样一个需求,实现显示随机随机数可能在代码中直接很简单的就实现了,但是现在我们直接自定义View来实现这个效果,那么我们来分析一波吧,我们允许开发者自己设置这个textview的大小,颜色,和初始四位随机数的文字,那么我们需要提供自定义属性,好吧,首先把自定义属性的简单使用介绍一下吧: 首先在res/values文件夹下建利attrs.xml文件,由于这次我们功能决定我们要提供三个自定义属性,分别是textTitle String类型的,textColor是color类型的,textSiz

  • 自定义滑动按钮为例图文剖析Android自定义View绘制

    自定义View一直是横在Android开发者面前的一道坎. 一.View和ViewGroup的关系 从View和ViewGroup的关系来看,ViewGroup继承View. View的子类,多是功能型的控件,提供绘制的样式,比如imageView,TextView等,而ViewGroup的子类,多用于管理控件的大小,位置,如LinearLayout,RelativeLayout等,从下图可以看出 从实际应用中看,他们又是组合关系,我们在布局中,常常是一个ViewGroup嵌套多个ViewGro

随机推荐