Android鬼点子 如此Q弹!

368 阅读9分钟

在uplabs上看到一个设计师设计录音备忘录的设计。

静态

动态图很惊艳:

动态.gif
自己尝试实现了一下,大约完成了50%的效果吧。
效果.gif

目前比较不满意的是背景气泡冒出时的粘连效果不是很理性。点击之后变成水滴向上移动的动画没有实现。另外,有的地方计算量比较大,没有优化,极少情况下出现卡顿。如果要真正运用到项目中的话,还需要继续优化。哦,对了没有实现padding啊什么的,还有颜色的变化都是直接写在了代码里面。

首先分析一下这个效果的各个部分。 1.有两坨重叠的,DuangDuang的东西,第一层是纯色,第二层的半透明。 2.这两坨东西有一种蠕动的效果。 3.这两坨东西的大小是变化的。 4.时不时有气泡从背景冒出。 5.气泡冒出时有粘连效果。 6.对了,开始点击的时候有一种果冻的弹性效果。

然后完整代码如下:

package com.greendami.View

import android.content.Context
import android.graphics.*
import android.support.animation.DynamicAnimation
import android.support.animation.SpringAnimation
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.View
import com.greendami.pptimer.R


/**
 * Created by GreendaMi on 2017/8/3.
 */
class PPBubble(context: Context?, attrs: AttributeSet?) : DynamicAnimation.OnAnimationUpdateListener, View(context, attrs) {
    override fun onAnimationUpdate(animation: DynamicAnimation<out DynamicAnimation<*>>?, value: Float, velocity: Float) {
        scaleY = value
        //点击动画完成
        if (value == 1f) {
            canUpdatePoints = true
        }
    }

    private var velocityTracker: VelocityTracker = VelocityTracker.obtain()
    private var downX: Float = 0.toFloat()
    private var downY: Float = 0.toFloat()
    val animX = SpringAnimation(this, SpringAnimation.SCALE_X, 1f)

    var mPaint = Paint()
    var scale = 0f

    var mPoints = ArrayList<PPPoint>()
    var mRs = ArrayList<Int>()//用于计算个点位置
    var mRads = ArrayList<Float>()//用于计算速度

    var bPoints = ArrayList<PPPoint>()
    var bRs = ArrayList<Int>()//用于计算个点位置
    var bRads = ArrayList<Float>()//用于计算速度

    var mBubbles = ArrayList<Bubble>()

    var mR = 0
    var mEffect: PathEffect = CornerPathEffect(0f)//平滑过渡的角度
    var startRads = 60f

    var mColors = ArrayList<Int>()

    var canUpdatePoints = false

    var colorIndex = 0


    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        initPaint()
        mR = measuredWidth / 8
        mEffect = CornerPathEffect(mR / 2f)
        creatPoints(mPoints, mRs, mRads)
        creatPoints(bPoints, bRs, bRads)

        animX.addUpdateListener(this)
        mColors.add(context.resources.getColor(R.color.c1))
        mColors.add(context.resources.getColor(R.color.c2))
        mColors.add(context.resources.getColor(R.color.c3))
        mColors.add(context.resources.getColor(R.color.c4))
        mColors.add(context.resources.getColor(R.color.c5))
        mColors.add(context.resources.getColor(R.color.c6))
        mColors.add(context.resources.getColor(R.color.c5))
        mColors.add(context.resources.getColor(R.color.c4))
        mColors.add(context.resources.getColor(R.color.c3))
        mColors.add(context.resources.getColor(R.color.c2))
        mColors.add(context.resources.getColor(R.color.c1))


    }


    private fun creatPoints(mPoints: ArrayList<PPPoint>, mRs: ArrayList<Int>, mRads: ArrayList<Float>) {

        var r0 = mR
        var r1 = mR
        var r2 = mR
        var r3 = mR
        var r4 = mR
        var r5 = mR

        mRs.add(r0)
        mRs.add(r1)
        mRs.add(r2)
        mRs.add(r3)
        mRs.add(r4)
        mRs.add(r5)

        mRs.forEach { mRads.add((Math.random() * 360).toFloat()) }


        var p0 = PPPoint(measuredWidth / 2 + r0.toFloat(), measuredHeight / 2f)
        var p1 = PPPoint(measuredWidth / 2 + r1 / 2f, (measuredHeight / 2f - r1 * Math.sqrt(3.0) / 2f).toFloat())
        var p2 = PPPoint(measuredWidth / 2 - r2 / 2f, (measuredHeight / 2f - r2 * Math.sqrt(3.0) / 2f).toFloat())
        var p3 = PPPoint(measuredWidth / 2 - r3.toFloat(), measuredHeight / 2f)
        var p4 = PPPoint(measuredWidth / 2 - r4 / 2f, (measuredHeight / 2 + r4 * Math.sqrt(3.0) / 2f).toFloat())
        var p5 = PPPoint(measuredWidth / 2 + r5 / 2f, (measuredHeight / 2 + r5 * Math.sqrt(3.0) / 2f).toFloat())
        mPoints.add(p0)
        mPoints.add(p1)
        mPoints.add(p2)
        mPoints.add(p3)
        mPoints.add(p4)
        mPoints.add(p5)
    }


    private fun initPaint() {
        mPaint.reset()
        mPaint.isAntiAlias = true
        mPaint.pathEffect = mEffect
        mPaint.color = context.resources.getColor(R.color.colorPrimary)
        mPaint.style = Paint.Style.FILL
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        var co = mPaint.color
        mPaint.color = co + 0x222222
        if (canUpdatePoints) {
            mPaint.pathEffect = null
            //画泡泡
            drawBubble(canvas)
        }
        mPaint.color = co + 0x222222
        mPaint.pathEffect = mEffect
        //画背景
        drawCircle(canvas, bPoints)
        mPaint.color = co
        drawCircle(canvas, mPoints)


        if (canUpdatePoints) {
            updatePoints(bPoints, bRs, bRads, (mR * 1.2f).toInt(), scale)
            updatePoints(mPoints, mRs, mRads, mR, scale)
            scale += 2
            if (scale % 360f == 0f) {
                mPaint.color = getNextColor()
                scale = 0f
                Log.e("TAg", "变色!")
            }
        }

        postInvalidate()
    }

    private fun drawBubble(canvas: Canvas?) {
        //此处控制泡泡的数量
        if (mBubbles.size < 5) {
            var bubble = Bubble()
            bubble.x = width / 2f
            bubble.y = height / 2f

            bubble.rad = (Math.random() * 360).toFloat()
            bubble.distance = 0f
            bubble.speed = (Math.random() * 3).toFloat()
            bubble.r = mR * 0.4f + (Math.random() * mR * 0.1f).toFloat() * bubble.speed
            mBubbles.add(bubble)
//            Log.e("TAg", "生产一个泡泡")
        }
        mBubbles.forEach {
            it.r -= 0.4f
            it.distance += it.speed
            it.x = width / 2f + it.distance * Math.cos(toRadians(it.rad)).toFloat()
            it.y = height / 2f - it.distance * Math.sin(toRadians(it.rad)).toFloat()
        }

        var tempBubbles = ArrayList<Bubble>()
        mBubbles.forEach {
            if (it.r > 0f) {
                tempBubbles.add(it)
            }
        }
        mBubbles = tempBubbles
        mBubbles.forEach {
            canvas?.drawCircle(it.x, it.y, it.r, mPaint)
            var mR = getRecentR()
            if (it.distance < this.mR + it.r * 1) {
                //小球的切点
                var th = toRadians(it.rad) - Math.acos(((mR - it.r) / it.distance).toDouble())
                var th2 = toRadians(it.rad) + Math.acos(((mR - it.r) / it.distance).toDouble())
                var x1 = (-Math.cos(th) * it.r + it.x).toFloat()
                var y1 = (Math.sin(th) * it.r + it.y).toFloat()
                var x2 = (-Math.cos(th2) * it.r + it.x).toFloat()
                var y2 = (Math.sin(th2) * it.r + it.y).toFloat()

                var x3 = (Math.cos(th2) * mR + width / 2).toFloat()
                var y3 = (-Math.sin(th2) * mR + height / 2).toFloat()
                var x4 = (Math.cos(th) * mR + width / 2).toFloat()
                var y4 = (-Math.sin(th) * mR + height / 2).toFloat()

                var cx = (it.x + width / 2) / 4
                var cy = (it.y + height / 2) / 4
                var cx1 = (it.x + x4) / 4 + cx
                var cy1 = (it.y + y4) / 4 + cy
                var cx2 = (it.x + x3) / 4 + cx
                var cy2 = (it.y + y3) / 4 + cy
//                var cx1 = ((it.x + x4) / 2).toFloat()
//                var cy1 = ((it.y + y4) / 2).toFloat()
//                var cx2 = ((it.x + x3) / 2).toFloat()
//                var cy2 = ((it.y + y3) / 2).toFloat()
                var p = Path()

                p.moveTo(it.x, it.y)
                p.lineTo(x2, y2)
                p.cubicTo(x2, y2, cx1, cy1, x4, y4)
                p.lineTo(x3, y3)
                p.cubicTo(x3, y3, cx2, cy2, x1, y1)
                p.lineTo(it.x, it.y)
                p.close()
                canvas?.drawPath(p, mPaint)
//                mPaint.alpha = 255
//                canvas?.drawCircle(cx1.toFloat(), cy1.toFloat(), 4f, mPaint)
//                canvas?.drawCircle(cx2.toFloat(), cy2.toFloat(), 4f, mPaint)
//                mPaint.color = context.resources.getColor(R.color.abc_btn_colored_borderless_text_material)
//                canvas?.drawCircle(x1.toFloat(), y1.toFloat(), 4f, mPaint)
//                mPaint.color = context.resources.getColor(R.color.background_floating_material_dark)
//                canvas?.drawCircle(x2.toFloat(), y2.toFloat(), 4f, mPaint)
//                mPaint.strokeWidth = 4f
//                mPaint.color = context.resources.getColor(R.color.colorPrimary)
//                canvas?.drawLine(width / 2f, height / 2f, it.x, it.y, mPaint)
//                mPaint.color = context.resources.getColor(R.color.abc_btn_colored_borderless_text_material)
//                canvas?.drawLine(x3.toFloat(), y3.toFloat(), x4.toFloat(), y4.toFloat(), mPaint)
//                mPaint.color = context.resources.getColor(R.color.colorPrimary)
//                canvas?.drawLine(x3.toFloat(), y3.toFloat(), width / 2f, height / 2f, mPaint)
//                canvas?.drawLine(width / 2f, height / 2f, x4.toFloat(), y4.toFloat(), mPaint)
            }

        }
    }

    private fun getRecentR(): Int {
        var r = mRs[0]
        mRs.forEach { if (it < r) r = it }
        return r
    }

    private fun getNextColor(): Int {
        if (colorIndex == mColors.size - 1) {
            colorIndex = 0
        } else {
            colorIndex++
        }
        return mColors[colorIndex]
    }

    private fun updatePoints(mPoints: ArrayList<PPPoint>, mRs: ArrayList<Int>, mRads: ArrayList<Float>, mR: Int, scale: Float) {

        var range = 0.1f
        var temp_mR = mR + (mR * 0.15f * Math.sin(toRadians(scale))).toInt()
        mRs[0] = temp_mR + (temp_mR * range * Math.cos(toRadians(mRads[0]))).toInt()
        mRs[1] = temp_mR + (temp_mR * range * Math.cos(toRadians(mRads[1]))).toInt()
        mRs[2] = temp_mR + (temp_mR * range * Math.cos(toRadians(mRads[2]))).toInt()
        mRs[3] = temp_mR + (temp_mR * range * Math.cos(toRadians(mRads[3]))).toInt()
        mRs[4] = temp_mR + (temp_mR * range * Math.cos(toRadians(mRads[4]))).toInt()
        mRs[5] = temp_mR + (temp_mR * range * Math.cos(toRadians(mRads[5]))).toInt()

        mPoints[0].x = measuredWidth / 2f + (Math.cos(toRadians(startRads + 0)) * mRs[0]).toFloat()
        mPoints[1].x = measuredWidth / 2f + (Math.cos(toRadians(startRads + 60)) * mRs[1]).toFloat()
        mPoints[2].x = measuredWidth / 2f + (Math.cos(toRadians(startRads + 120)) * mRs[2]).toFloat()
        mPoints[3].x = measuredWidth / 2f + (Math.cos(toRadians(startRads + 180)) * mRs[3]).toFloat()
        mPoints[4].x = measuredWidth / 2f + (Math.cos(toRadians(startRads + 240)) * mRs[4]).toFloat()
        mPoints[5].x = measuredWidth / 2f + (Math.cos(toRadians(startRads + 300)) * mRs[5]).toFloat()

        mPoints[0].y = measuredHeight / 2f - (Math.sin(toRadians(startRads + 0)) * mRs[0]).toFloat()
        mPoints[1].y = measuredHeight / 2f - (Math.sin(toRadians(startRads + 60)) * mRs[1]).toFloat()
        mPoints[2].y = measuredHeight / 2f - (Math.sin(toRadians(startRads + 120)) * mRs[2]).toFloat()
        mPoints[3].y = measuredHeight / 2f - (Math.sin(toRadians(startRads + 180)) * mRs[3]).toFloat()
        mPoints[4].y = measuredHeight / 2f - (Math.sin(toRadians(startRads + 240)) * mRs[4]).toFloat()
        mPoints[5].y = measuredHeight / 2f - (Math.sin(toRadians(startRads + 300)) * mRs[5]).toFloat()

        startRads += 0.05f

        mRads[0] = mRads[0] + (Math.random() * 3f).toFloat()
        mRads[1] = mRads[1] + (Math.random() * 3f).toFloat()
        mRads[2] = mRads[2] + (Math.random() * 3f).toFloat()
        mRads[3] = mRads[3] + (Math.random() * 3f).toFloat()
        mRads[4] = mRads[4] + (Math.random() * 3f).toFloat()
        mRads[5] = mRads[5] + (Math.random() * 3f).toFloat()


    }


    private fun drawCircle(canvas: Canvas?, mPoints: ArrayList<PPPoint>) {
        val path = Path()
        path.moveTo(mPoints[0].x, mPoints[0].y)
        path.lineTo(mPoints[1].x, mPoints[1].y)
        path.lineTo(mPoints[2].x, mPoints[2].y)
        path.lineTo(mPoints[3].x, mPoints[3].y)
        path.lineTo(mPoints[4].x, mPoints[4].y)
        path.lineTo(mPoints[5].x, mPoints[5].y)
        path.close()
        canvas?.drawPath(path, mPaint)

    }


    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                return true
            }
            MotionEvent.ACTION_MOVE -> {
                animX.cancel()
                return true
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {

                if (this.scale != 1f) {
                    animX.spring.stiffness = getStiffness()
                    animX.spring.dampingRatio = getDamping()
                    animX.setStartVelocity(5f)
                    animX.start()
                    canUpdatePoints = false
                }

                velocityTracker.clear()
                return true
            }
        }
        return false
    }

    private fun getDamping(): Float {
        return 0.2f
    }

    private fun getStiffness(): Float {
        return 250f
    }

    fun toRadians(angel: Float): Double {
        return (Math.PI * angel / 180)
    }


}

下面我逐一介绍一下,每一种效果我是如何实现的。 1.有两坨重叠的,DuangDuang的东西,第一层是纯色,第二层的半透明 这个效果我使用的是六边形,然后过渡圆角实现的。DuangDuang的效果是通过改变六边形每个角到圆心的距离实现蠕动的效果。为了这个效果更逼真,给这个六边形加上旋转的效果。第一层和第二层的效果是一样的,只是半径不同而已。

六边形

计算生成六边形顶点的方法是

 private fun creatPoints(mPoints: ArrayList<PPPoint>, mRs: ArrayList<Int>, mRads: ArrayList<Float>) {...}

mPoints存放计算后的顶点(6个),mRs是每个顶点到圆心的半径,mRads是每条半径的角度。

全局变量mPoints存放上层六边形的顶点,bPoints存放背景六边形顶点。 在onDrawa方法中调用了

updatePoints(bPoints, bRs, bRads, (mR * 1.2f).toInt(), scale)
updatePoints(mPoints, mRs, mRads, mR, scale)

来更新六边形的顶点的数据。

private fun updatePoints(mPoints: ArrayList<PPPoint>, mRs: ArrayList<Int>, mRads: ArrayList<Float>, mR: Int, scale: Float) {

        var range = 0.1f
        var temp_mR = mR + (mR * 0.15f * Math.sin(toRadians(scale))).toInt()
        mRs[0] = temp_mR + (temp_mR * range * Math.cos(toRadians(mRads[0]))).toInt()
        mRs[1] = temp_mR + (temp_mR * range * Math.cos(toRadians(mRads[1]))).toInt()
        mRs[2] = temp_mR + (temp_mR * range * Math.cos(toRadians(mRads[2]))).toInt()
        mRs[3] = temp_mR + (temp_mR * range * Math.cos(toRadians(mRads[3]))).toInt()
        mRs[4] = temp_mR + (temp_mR * range * Math.cos(toRadians(mRads[4]))).toInt()
        mRs[5] = temp_mR + (temp_mR * range * Math.cos(toRadians(mRads[5]))).toInt()

        mPoints[0].x = measuredWidth / 2f + (Math.cos(toRadians(startRads + 0)) * mRs[0]).toFloat()
        mPoints[1].x = measuredWidth / 2f + (Math.cos(toRadians(startRads + 60)) * mRs[1]).toFloat()
        mPoints[2].x = measuredWidth / 2f + (Math.cos(toRadians(startRads + 120)) * mRs[2]).toFloat()
        mPoints[3].x = measuredWidth / 2f + (Math.cos(toRadians(startRads + 180)) * mRs[3]).toFloat()
        mPoints[4].x = measuredWidth / 2f + (Math.cos(toRadians(startRads + 240)) * mRs[4]).toFloat()
        mPoints[5].x = measuredWidth / 2f + (Math.cos(toRadians(startRads + 300)) * mRs[5]).toFloat()

        mPoints[0].y = measuredHeight / 2f - (Math.sin(toRadians(startRads + 0)) * mRs[0]).toFloat()
        mPoints[1].y = measuredHeight / 2f - (Math.sin(toRadians(startRads + 60)) * mRs[1]).toFloat()
        mPoints[2].y = measuredHeight / 2f - (Math.sin(toRadians(startRads + 120)) * mRs[2]).toFloat()
        mPoints[3].y = measuredHeight / 2f - (Math.sin(toRadians(startRads + 180)) * mRs[3]).toFloat()
        mPoints[4].y = measuredHeight / 2f - (Math.sin(toRadians(startRads + 240)) * mRs[4]).toFloat()
        mPoints[5].y = measuredHeight / 2f - (Math.sin(toRadians(startRads + 300)) * mRs[5]).toFloat()

        startRads += 0.05f

        mRads[0] = mRads[0] + (Math.random() * 3f).toFloat()
        mRads[1] = mRads[1] + (Math.random() * 3f).toFloat()
        mRads[2] = mRads[2] + (Math.random() * 3f).toFloat()
        mRads[3] = mRads[3] + (Math.random() * 3f).toFloat()
        mRads[4] = mRads[4] + (Math.random() * 3f).toFloat()
        mRads[5] = mRads[5] + (Math.random() * 3f).toFloat()
    }

我使用sinX,通过改变X的值,使sinX的值在-1和+1之间呈函数变化。当X是匀速变化时,sinX是非匀速变化的。当角度是随机增加的时候,sinX的变化就更是随机的。

var temp_mR = mR + (mR * 0.15f * Math.sin(toRadians(scale))).toInt()

这里是计算基础半径,整个六边形的放大和缩小的动画是在这里实现的,变化的范围是-0.15和+0.15之间。 实际的6个顶点的半径是在基础半径上计算的,变化范围是-range~+range的范围。 startRads 是当前半径的角度。用半径+角度,计算出顶点的位置。 背景六边形的基础半径略大。

至此,1.2.3的效果就实现了。

2.然后是气泡冒出效果 在 private fun drawBubble(canvas: Canvas?) {}方法中,是实现气泡效果的。

//此处控制泡泡的数量
        if (mBubbles.size < 5) {
            var bubble = Bubble()
            bubble.x = width / 2f
            bubble.y = height / 2f

            bubble.rad = (Math.random() * 360).toFloat()
            bubble.distance = 0f
            bubble.speed = (Math.random() * 3).toFloat()
            bubble.r = mR * 0.4f + (Math.random() * mR * 0.1f).toFloat() * bubble.speed
            mBubbles.add(bubble)
//            Log.e("TAg", "生产一个泡泡")
        }

这里是产生一个气泡的过程。气泡的半径,发射的角度,发射的速度都是随机的。distance 是当前气泡的距离圆心的距离。x,y是气泡的圆心坐标。

mBubbles.forEach {
            it.r -= 0.4f
            it.distance += it.speed
            it.x = width / 2f + it.distance * Math.cos(toRadians(it.rad)).toFloat()
            it.y = height / 2f - it.distance * Math.sin(toRadians(it.rad)).toFloat()
        }

这里是在计算气泡的半径(随着气泡飞出,半径减小),计算气泡的距离,计算坐标。

var tempBubbles = ArrayList<Bubble>()
        mBubbles.forEach {
            if (it.r > 0f) {
                tempBubbles.add(it)
            }
        }
        mBubbles = tempBubbles

移除半径小于等于0的气泡。

mBubbles.forEach {
            canvas?.drawCircle(it.x, it.y, it.r, mPaint)
            var mR = getRecentR()
            if (it.distance < this.mR + it.r * 1) {
                //小球的切点
                var th = toRadians(it.rad) - Math.acos(((mR - it.r) / it.distance).toDouble())
                var th2 = toRadians(it.rad) + Math.acos(((mR - it.r) / it.distance).toDouble())
                var x1 = (-Math.cos(th) * it.r + it.x).toFloat()
                var y1 = (Math.sin(th) * it.r + it.y).toFloat()
                var x2 = (-Math.cos(th2) * it.r + it.x).toFloat()
                var y2 = (Math.sin(th2) * it.r + it.y).toFloat()

                var x3 = (Math.cos(th2) * mR + width / 2).toFloat()
                var y3 = (-Math.sin(th2) * mR + height / 2).toFloat()
                var x4 = (Math.cos(th) * mR + width / 2).toFloat()
                var y4 = (-Math.sin(th) * mR + height / 2).toFloat()

                var cx = (it.x + width / 2) / 4
                var cy = (it.y + height / 2) / 4
                var cx1 = (it.x + x4) / 4 + cx
                var cy1 = (it.y + y4) / 4 + cy
                var cx2 = (it.x + x3) / 4 + cx
                var cy2 = (it.y + y3) / 4 + cy
                var p = Path()
                p.moveTo(it.x, it.y)
                p.lineTo(x2, y2)
                p.cubicTo(x2, y2, cx1, cy1, x4, y4)
                p.lineTo(x3, y3)
                p.cubicTo(x3, y3, cx2, cy2, x1, y1)
                p.lineTo(it.x, it.y)
                p.close()
                canvas?.drawPath(p, mPaint)
            }

        }

这里根据计算后的结果画出气泡。后面就是粘连效果的实现。这里使用了贝塞尔曲线。 以一个小球为例:

粘连示意.png

it.x和it.y是气泡的圆心坐标,width / 2和height / 2是大圆的圆心坐标。大圆的半径var mR = getRecentR(),这里是找出6个半径中最短的。 AB的距离是气泡的distance,∠ABE是气泡的角度it.rad,代码中计算的th是∠EBC,th2是∠DBE。角度知道了,半径也知道了,就可以算出4个切点的坐标了。 cx1,cy2就是第三个辅助点。点4 ,c1和点2,三个点构成一条贝塞尔曲线。点3 ,c2和点1,是另一条。 至此,粘连效果完成。

3.点击时有个模仿物理效果的放大缩小动画,果冻的弹性效果。使用的是SpringAnimation,具体代码很简单,没使用过的老哥们可以看看我的上一篇