自定义 Drawable 实现一只灵动的鱼

围巾🧣 2022年04月26日 779次浏览

画出鱼身体

自定义 Drawable

重写方法

继承 Drawable,实现父类方法,设置占用宽高

class FishDrawable : Drawable() {
  
    override fun setAlpha(p0: Int) {
        mPaint.alpha = p0
    }

    override fun setColorFilter(p0: ColorFilter?) {
        mPaint.colorFilter = p0
    }

    override fun getOpacity(): Int {
        return PixelFormat.TRANSLUCENT
    }

    // 宽高
    override fun getIntrinsicWidth() = (8.38f * HEAD_RADIUS).toInt()
    override fun getIntrinsicHeight() = (8.38f * HEAD_RADIUS).toInt()
}

鱼身体组成

计算点坐标

由一个点,角度+长度可以算出另一个点的坐标:

分别用三角函数计算新坐标到原坐标的 deltaX 和 deltaY,原坐标加上差值即为新坐标

deltaX: 先把角度转为弧度,再算 cos 值,乘以长度即为 deltaX, deltaY 同理

鱼头

先定义一个鱼身体中心,即重心,身体长度,鱼的整体朝向角度

定义鱼头半径,根据重心,身体长度一半,朝向角度可以算出鱼头圆心坐标

根据半径和圆心即可画圆(鱼头)

身体

计算身体底部重心坐标

计算身体四个角的坐标

计算鱼两侧二阶贝塞尔曲线的控制点坐标

计算 path

mPath.reset()
mPath.moveTo(topLeftPoint.x, topLeftPoint.y)
mPath.quadTo(controlLeft.x, controlLeft.y, bottomLeftPoint.x, bottomLeftPoint.y)
mPath.lineTo(bottomRightPoint.x, bottomRightPoint.y)
mPath.quadTo(controlRight.x, controlRight.y, topRightPoint.x, topRightPoint.y)

绘制 path

canvas.drawPath(mPath, mPaint)

鱼鳍

计算鱼鳍起始点

计算鳍终点、控制点

计算path

mPath.reset()
// 将画笔移动到起点
mPath.moveTo(startPoint.x, startPoint.y)
// 二阶贝塞尔曲线
mPath.quadTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y)

两个节肢

身体后面剩余部分由两个梯形组成,为了摇摆不出现断层,上下底看情况画上圆。

尾巴

画等边三角形: 根据 顶点、高、朝向角度, 可算出底部中心点, 进而算出底部另外两个点

画三角形

让鱼动起来

点击提示

onDraw 不断画圆,onTouchEvent 时通过属性动画改变圆的半径和透明度

鱼身摆动

定义一个循环的 animator,不断改变一个值,让鱼的尾部,鳍的位置角度跟这个值绑定,从而动起来,点击屏幕时调整频率,更加生动

运动轨迹

运动规律

位置变化:

轨迹为 三阶贝塞尔曲线就可, 取到起始点、两个控制点、终点

角度变化:

把鱼身角度调整为 鱼到

轨迹生成

使用三阶贝塞尔曲线,计算 imageView 的移动

取到鱼重心相对 imageView 的坐标,绝对坐标(起始点)、鱼头圆心的绝对坐标(控制点1)、

点击坐标(终点)

计算控制点2:

画路径 Path:

path.moveTo(fishMiddle.x - fishRelativeMiddle.x, fishMiddle.y - fishRelativeMiddle.y)
path.cubicTo(
  fishHead.x - fishRelativeMiddle.x, fishHead.y - fishRelativeMiddle.y,
  controlPoint.x - fishRelativeMiddle.x, controlPoint.y - fishRelativeMiddle.y,
  mTouchX - fishRelativeMiddle.x, mTouchY - fishRelativeMiddle.y
)

动画触发移动路径

val objectAnimator = ObjectAnimator.ofFloat(mIvFish, "x", "y", path)
objectAnimator.duration = 2000
objectAnimator.start()

移动时(动画执行)改变鱼的角度和摇摆幅度

atan2 - Wikipedia
        // 改变摇摆频率
        val objectAnimator = ObjectAnimator.ofFloat(mIvFish, "x", "y", path)
        objectAnimator.duration = 2000
        objectAnimator.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator?) {
                super.onAnimationEnd(animation)
                mFishDrawable.frequency = 1f
            }

            override fun onAnimationStart(animation: Animator?) {
                super.onAnimationStart(animation)
                mFishDrawable.frequency = 3f
            }
        })

        // 改变鱼的角度
        val pathMeasure = PathMeasure(path, false)
        val tan = FloatArray(2)
        objectAnimator.addUpdateListener {
            // 执行了整个周期的百分比
            val fraction = it.animatedFraction
            pathMeasure.getPosTan(pathMeasure.length * fraction, null, tan)
            val fishAngle = Math.toDegrees(atan2(-tan[1], tan[0]).toDouble()).toFloat()
            mFishDrawable.fishMainAngle = fishAngle
        }

完整代码参考

https://gitlab.xlxs.top/xlxsui/jinfish