Android仿QQ消息拖拽效果(一)(二阶贝塞尔曲线使用)

前言

本文参考辉哥的贝塞尔曲线 - QQ消息汽包拖拽,很适合初学贝塞尔知识,大家可以去看看原文。

最终效果

请添加图片描述

实现思路

效果分析

  • 整体分为两个圆点,一个是固定圆点(当手指按下时就已经确定了位置),一个是移动圆点(跟随手指移动变化位置),这里通过重写onTouchEvent事件,监听move事件时改变圆点坐标即可实现。
  • 当移动圆点移动时,固定圆点半径会慢慢变小,当移动到一定距离后,半径不再变化,当更远时,不再绘制固定点以及旁边贝塞尔连线。

贝塞尔曲线坐标计算
贝塞尔曲线坐标计算
通过上面的公式,我们可以分别计算出P0P1P2P3对应点坐标,同时我们选择c0c1的中心点(centerX,centerY)作为贝塞尔曲线的控制点,使用path.quadTo方法进行二阶贝塞尔曲线绘制。

相关代码

package com.crystal.view.animation

import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import kotlin.math.sqrt

/**
 * 仿qq消息拖拽效果【二阶贝塞尔曲线学习】
 * on 2022/11/10
 */
class BesselView : View {
    //画笔工具
    private val paint = Paint()

    //固定点
    private var fixPoint: PointF? = null

    //跟随手指移动点
    private var movePoint: PointF? = null

    //固定点半径【当移动点距离远时,会逐渐变小】
    private var fixPointRadius = 0f

    //固定点半径最小值
    private var fixPointMinRadius = 0f

    //固定圆半径最大值
    private var fixPointMaxRadius = 0f

    //移动点半径
    private var movePointRadius = 0f


    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context, attrs, defStyleAttr
    ) {
        paint.color = Color.RED
        paint.isDither = true
        paint.isAntiAlias = true
        fixPointMinRadius = dp2px(3f)
        fixPointMaxRadius = dp2px(7f)
        movePointRadius = dp2px(8f)
    }


    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                if (fixPoint == null) {
                    fixPoint = PointF()
                }
                fixPoint?.x = event.x
                fixPoint?.y = event.y
            }
            MotionEvent.ACTION_MOVE -> {
                if (movePoint == null) {
                    movePoint = PointF()
                }
                movePoint?.x = event.x
                movePoint?.y = event.y
                fixPointRadius = (fixPointMaxRadius - getPointCenterDistance() / 14f).toFloat()
            }
            MotionEvent.ACTION_UP -> {
                fixPoint = null
                movePoint = null
                invalidate()
                return super.onTouchEvent(event)
            }
        }
        //坐标变化时,不断重绘
        invalidate()
        return true
    }


    override fun onDraw(canvas: Canvas) {
        if (fixPoint == null || movePoint == null) {
            return
        }
        //绘制移动点
        canvas.drawCircle(movePoint!!.x, movePoint!!.y, movePointRadius, paint)

        //绘制固定点和贝塞尔曲线【当距离过大时,不绘制贝塞尔曲线和固定点】
        if (fixPointRadius > fixPointMinRadius) {
            canvas.drawCircle(fixPoint!!.x, fixPoint!!.y, fixPointRadius, paint)
            drawBesselLine(canvas)
        }
    }

    /**
     * 绘制二阶贝塞尔曲线
     */
    private fun drawBesselLine(canvas: Canvas) {
        //分别计算角a的sin值和cos值
        val sina = (movePoint!!.y - fixPoint!!.y) / getPointCenterDistance()
        val cosa = (movePoint!!.x - fixPoint!!.x) / getPointCenterDistance()
        //求出p0点坐标
        val p0 = PointF(
            (fixPoint!!.x + fixPointRadius * sina).toFloat(),
            (fixPoint!!.y - fixPointRadius * cosa).toFloat()
        )
        //求出p2点坐标
        val p2 = PointF(
            (fixPoint!!.x - fixPointRadius * sina).toFloat(),
            (fixPoint!!.y + fixPointRadius * cosa).toFloat()
        )
        //求出p1点坐标
        val p1 = PointF(
            (movePoint!!.x + movePointRadius * sina).toFloat(),
            (movePoint!!.y - movePointRadius * cosa).toFloat()
        )
        //求出p3点坐标
        val p3 = PointF(
            (movePoint!!.x - movePointRadius * sina).toFloat(),
            (movePoint!!.y + movePointRadius * cosa).toFloat()
        )

        //绘制贝塞尔曲线
        val path = Path()
        path.moveTo(p0.x, p0.y)
        path.quadTo(getCircleCenterPoint().x, getCircleCenterPoint().y, p1.x, p1.y)
        path.lineTo(p3.x, p3.y)
        path.quadTo(getCircleCenterPoint().x, getCircleCenterPoint().y, p2.x, p2.y)
        path.close()
        canvas.drawPath(path, paint)
    }


    /**
     * dp 转 px
     */
    private fun dp2px(dp: Float): Float {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics)
    }


    /**
     * 计算两点距离
     */
    private fun getPointCenterDistance(): Double {
        val dx = movePoint!!.x - fixPoint!!.x
        val dy = movePoint!!.y - fixPoint!!.y
        return sqrt((dx * dx + dy * dy).toDouble())
    }

    /**
     * 计算两个圆心连接中心点坐标 作为二阶贝塞尔曲线的控制点
     */
    private fun getCircleCenterPoint(): PointF {
        val centerX = (movePoint!!.x + fixPoint!!.x) / 2
        val centerY = (movePoint!!.y + fixPoint!!.y) / 2
        return PointF(centerX, centerY)
    }
}

总结

通过本文中的效果实现,学习了二阶贝塞尔曲线的绘制,对于自定义View而言,最重要的还是将效果进行拆分细化,拆分后每一步实现其实很简单!

结语

如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值