在uplabs上看到一个设计师设计录音备忘录的设计。
动态图很惊艳:
自己尝试实现了一下,大约完成了50%的效果吧。目前比较不满意的是背景气泡冒出时的粘连效果不是很理性。点击之后变成水滴向上移动的动画没有实现。另外,有的地方计算量比较大,没有优化,极少情况下出现卡顿。如果要真正运用到项目中的话,还需要继续优化。哦,对了没有实现padding啊什么的,还有颜色的变化都是直接写在了代码里面。
首先分析一下这个效果的各个部分。 1.有两坨重叠的,DuangDuang的东西,第一层是纯色,第二层的半透明。 2.这两坨东西有一种蠕动的效果。 3.这两坨东西的大小是变化的。 4.时不时有气泡从背景冒出。 5.气泡冒出时有粘连效果。 6.对了,开始点击的时候有一种果冻的弹性效果。
然后完整代码如下:
package com.greendami.Viewimport android.content.Contextimport android.graphics.*import android.support.animation.DynamicAnimationimport android.support.animation.SpringAnimationimport android.util.AttributeSetimport android.util.Logimport android.view.MotionEventimport android.view.VelocityTrackerimport android.view.Viewimport 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>?, 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 () var mRs = ArrayList ()//用于计算个点位置 var mRads = ArrayList ()//用于计算速度 var bPoints = ArrayList () var bRs = ArrayList ()//用于计算个点位置 var bRads = ArrayList ()//用于计算速度 var mBubbles = ArrayList () var mR = 0 var mEffect: PathEffect = CornerPathEffect(0f)//平滑过渡的角度 var startRads = 60f var mColors = ArrayList () 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 , mRs: ArrayList , mRads: ArrayList ) { 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 () 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 , mRs: ArrayList , mRads: ArrayList , 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 ) { 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, mRs: ArrayList , mRads: ArrayList ) {...}复制代码
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, mRs: ArrayList , mRads: ArrayList , 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() 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) } }复制代码
这里根据计算后的结果画出气泡。后面就是粘连效果的实现。这里使用了贝塞尔曲线。 以一个小球为例:
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,具体代码很简单,没使用过的老哥们可以看看我的。