图片 17

Android点赞控件–仿掘金点赞七成效果

前些日子偶然看到掘金推荐里的点赞效果,感觉有些酷炫,然后在一个无所事事的早上,我决定实现一个类似的功能,但是只有七成的效果,效果图如下。

前些天,在IXUS上看到一个很赞的动画,一下子就看对眼了,于是便决定用Android来实现一下,效果如下:

图片 1image

图片 2原图

这个效果我把它们分成了几个阶段:

设计在这里:Jana,点赞超棒的设计师

默认阶段:就是一个竖起来的大拇指

Android实现的效果如下:

收缩阶段:大拇指逐渐缩小直到消失

图片 3android实现

放大阶段:圆圈由小到大

现在开始分析如何实现这个动画:

圆环阶段:圆圈有中心破裂,露出大拇指

图片 4image.png

卫星阶段:出现卫星,向远处逐渐偏离同时逐渐消失

当我们看到一个动画要实现的时候,很多朋友可能就直接照着开始实现了,其实这样是很难实现。我们在接触到一个新动画的时候,首先要对动画进行分解。例如本动画,我们进行分析后,可以把动画分为以下几个部分:

话不多说,上代码吧

1. 圆环放大缩小消失效果:

图片 5圆环放大缩小效果

进行剖析后,我们可以发现这是一个圆环放大缩小的动画,有3个关键帧

图片 6关键帧1图片 7关键帧2图片 8关键帧3

从关键帧1->2->3->圆环消失,那对我们来说就是使用属性动画进行绘制圆环,我是通过绘制两个圆形成圆环(黄色大圆在下面,白色小圆在上面)的效果。代码如下

private void drawZoomRing(Canvas canvas) { mPaint.setShader; mPaint.setStrokeWidth; mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(ringColor); canvas.drawCircle(getMeasuredWidth()/2,getMeasuredHeight()/2,ringWidth/2,mPaint);//外圆大圆 mPaint.setColor(Color.WHITE); canvas.drawCircle(getMeasuredWidth()/2,getMeasuredHeight()/2,minRingCenterWidth/2,mPaint);//肉圆小圆 }

这里是通过控制大圆和小圆的半径大小,从而来控制圆环的位置和大小。然后只需要通过ValueAnimator动态改变ringWidthminRingCenterWidth的值然后invalidate(),即可以实现第一部分的圆环放大缩小效果。

package com.skateboard.favouriteviewimport android.animation.Animatorimport android.animation.ValueAnimatorimport android.content.Contextimport android.graphics.*import android.support.v4.content.ContextCompatimport android.util.AttributeSetimport android.view.Viewclass FavouriteView(context: Context, attrs: AttributeSet?, defStyle: Int) : View(context, attrs, defStyle){ private lateinit var paint: Paint private lateinit var statellitePaint: Paint private lateinit var path: Path private var spaceBetweenHandAndShoulder = 5 private var state = STATE_NORMAL private lateinit var valueAnimator: ValueAnimator private var size = 0 private var centerX = 0f private var centerY = 0f private var strokeWithScaleFraction = 1f private var statelliteOffsetFraction = 0f private var selectedColor = Color.BLACK private var normalColor = Color.BLACK companion object { val STATE_SELECTED = 1 val STATE_CIRCLE = 2 val STATE_RING = 3 val STATE_STATELLITE = 4 val STATE_NORMAL = 0 } constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context) : this(context, null, 0) init { if (attrs != null) { initParse } initPaint() initStatellitePaint() initClickEvent() } private fun initParse(attrs: AttributeSet) { val typedArray = context.obtainStyledAttributes(attrs, R.styleable.FavouriteView) selectedColor = typedArray.getColor(R.styleable.FavouriteView_selected_color, Color.BLACK) normalColor = typedArray.getColor(R.styleable.FavouriteView_normal_color, Color.BLACK) typedArray.recycle() } private fun initPaint() { paint = Paint(Paint.ANTI_ALIAS_FLAG) paint.color = normalColor paint.style = Paint.Style.STROKE paint.strokeWidth = 4f path = Path() val cornerPathEffect = CornerPathEffect paint.pathEffect = cornerPathEffect } private fun initStatellitePaint() { statellitePaint = Paint(Paint.ANTI_ALIAS_FLAG) statellitePaint.color = selectedColor statellitePaint.style=Paint.Style.FILL } private fun initAnimator() { valueAnimator = ValueAnimator.ofFloat valueAnimator.duration = 500 valueAnimator.addUpdateListener(updateListener) valueAnimator.addListener(object : Animator.AnimatorListener { override fun onAnimationStart(animation: Animator?) { } override fun onAnimationRepeat(animation: Animator?) { } override fun onAnimationEnd(animation: Animator?) { scaleX = Math.max(1f, scaleX) scaleY = Math.max(1f, scaleY) setState(STATE_SELECTED) } override fun onAnimationCancel(animation: Animator?) { } }) } private val updateListener = ValueAnimator.AnimatorUpdateListener { val time = Math.round(it.animatedValue as Float) when { time <= 100.0f -> { scaleX = Math.max(0f, 1f - time / 100f) scaleY = Math.max(0f, 1f - time / 100f) } time in 101..200 -> { scaleX = Math.min(1f, (time - 100) / 100f) scaleY = Math.min(1f, (time - 100) / 100f) setState(STATE_CIRCLE) } time in 201..300 -> { scaleX = 1f scaleY = 1f strokeWithScaleFraction = ((time - 200) / 100f) setState(STATE_RING) postInvalidate() } else -> { statelliteOffsetFraction = ((time - 300) / 100f) setState(STATE_STATELLITE) postInvalidate() } } } private fun setState(newState: Int) { if (state != newState) { state = newState postInvalidate() } } override fun onAttachedToWindow() { super.onAttachedToWindow() initAnimator() } private fun initClickEvent() { setOnClickListener { if (state == STATE_NORMAL) { startAnimate() } else { setState(STATE_NORMAL) } } } private fun startAnimate() { if (valueAnimator.isRunning) { return } else { valueAnimator.start() } } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val widthSize = MeasureSpec.getSize(widthMeasureSpec) val heightSize = MeasureSpec.getSize(heightMeasureSpec) size = Math.min(widthSize, heightSize) super.onMeasure(widthMeasureSpec, heightMeasureSpec) } override fun onDraw(canvas: Canvas?) { super.onDraw prepareToDraw() if (canvas != null) { when  { STATE_NORMAL -> { resetPaintColor() drawFinger } STATE_SELECTED -> { resetPaintColor() drawFinger } STATE_CIRCLE -> { resetPaintColor() drawCircle } STATE_RING -> { resetPaintColor() drawRing drawFinger } STATE_STATELLITE -> { resetPaintColor() drawStatellite } } } } private fun resetPaintColor() { when { state == STATE_NORMAL -> { paint.colorFilter = null paint.color = normalColor } state != STATE_STATELLITE -> { paint.colorFilter = null paint.color = selectedColor } else -> { paint.color = selectedColor if (statelliteOffsetFraction <= 0.5f) { val parameter = 1f statellitePaint.colorFilter = ColorMatrixColorFilter(floatArrayOf(1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, parameter, 0f)) } else { val parameter = 1 - statelliteOffsetFraction statellitePaint.colorFilter = ColorMatrixColorFilter(floatArrayOf(1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, parameter, 0f)) } } } } private fun prepareToDraw() { path.reset() centerX = (width / 2).toFloat() centerY = (height / 2).toFloat() } private fun drawFinger(canvas: Canvas) { paint.style = Paint.Style.STROKE paint.strokeWidth = 4f val fingerWidth = .toFloat() val fingerHeight = .toFloat() val centerX = (width / 2).toFloat() + fingerWidth / 8 val centerY = (height / 2).toFloat() + fingerHeight / 8 path.addRect(centerX - fingerWidth / 2, centerY - fingerHeight / 4, centerX - fingerWidth / 3, centerY + fingerHeight / 4, Path.Direction.CW) path.moveTo(centerX - fingerWidth / 3 + spaceBetweenHandAndShoulder, centerY - fingerHeight / 4) path.rLineTo(fingerWidth / 8, 0f) path.rLineTo(fingerWidth / 8, -fingerHeight / 2) path.rLineTo(fingerWidth / 6, fingerHeight / 4) path.rLineTo(-fingerWidth / 8, fingerHeight / 4) path.rLineTo(fingerWidth / 2 - fingerWidth / 6 - spaceBetweenHandAndShoulder, 0f) path.rLineTo(-fingerWidth / 8, fingerHeight / 2) path.lineTo(centerX - fingerWidth / 3 + spaceBetweenHandAndShoulder, centerY + fingerHeight / 4) path.close() canvas.drawPath(path, paint) } private fun drawCircle(canvas: Canvas) { val radius = (size.toFloat / 3 paint.style = Paint.Style.FILL canvas.drawCircle(centerX, centerY, radius, paint) } private fun drawStatellite(canvas: Canvas) { drawFinger drawSmallStatellites } private fun drawRing(canvas: Canvas) { val radius = (size.toFloat / 3 paint.style = Paint.Style.STROKE paint.strokeWidth = ((1 - strokeWithScaleFraction) * radius) canvas.drawCircle(centerX, centerY, radius - paint.strokeWidth / 2, paint) } private fun drawSmallStatellites(canvas: Canvas) { val bigRadius = (size.toFloat / 3 val smallRadius = (centerY - bigRadius) / 3 val offset = size / 2 - bigRadius - 2 * smallRadius canvas.drawCircle(centerX, centerY - bigRadius - offset * statelliteOffsetFraction, smallRadius, statellitePaint) canvas.drawCircle(centerX + bigRadius + offset * statelliteOffsetFraction, centerY, smallRadius, statellitePaint) canvas.drawCircle(centerX, centerY + bigRadius + offset * statelliteOffsetFraction, smallRadius, statellitePaint) canvas.drawCircle(centerX - bigRadius - offset * statelliteOffsetFraction, centerY, smallRadius, statellitePaint) }}
2. 圆弧转动缩小动画

图片 9圆弧转动缩小动画

这里其实就是处于不同圆下的两条圆弧,在做旋转和长度变化动画。我们先来看一下圆弧的方法:

canvas.drawArc(圆弧所在的圆的正方形框,开始角度,跨越角度,是否要连接圆心,mPaint);

那么我们就可以通过动态改变开始角度和跨越角度来实现圆弧的移动和长度变化,这里的圆弧也是有3个关键帧,圆弧长度初始状态(外弧90度,内弧2度)->圆弧长度达到180度->0度消失

实现代码如下:

private void drawArcLine(Canvas canvas) { mPaint.setColor(ringColor); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeCap(Paint.Cap.ROUND);//线的两边圆头模式 mPaint.setStrokeWidth(getMeasuredWidth;//通过画笔的宽度来控制圆弧的宽度 canvas.drawArc(mRectCenterArc, centerArcEndAngle-centerArcAngle,centerArcAngle,false,mPaint);//画内弧 mPaint.setStrokeWidth(getMeasuredWidth; canvas.drawArc(mRectOutSideArc,outSideArcStartAngle,outSideArcAngle,false,mPaint);//画外弧 }

细心的小伙伴可能已经发现了,画外弧和内弧的参数还像有点不太一样。因为内弧是顺时针旋转,外弧是逆时针旋转的,为了保证弧转动到某一点后不再移动,我们做了处理,内弧以逆向的思维来做。(可以编写代码试下这么做有什么好处)

这个控件的所有代码就在这里了,重点分一下几个关键点

3. 太阳出现以及旋转

图片 10太阳出现以及旋转

我们先分析一下太阳出现以及旋转动画下太阳的组成成分,可以发现太阳由以下二部分组成:两个正方形和一个圆形

图片 11image.png

代码实现如下:

private void drawSun(Canvas canvas) { mPaint.setStrokeWidth; mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(ringColor); mPaint.setShader(mFlowerLinearGradient);//设置颜色渐变 canvas.save(); canvas.rotate(sunRotateAngle,getMeasuredWidth()/2,getMeasuredHeight; canvas.drawRect(mRectFSunFlower,mPaint); canvas.rotate(45,getMeasuredWidth()/2,getMeasuredHeight;//第二个正方形比第一个正方形多旋转45度 mPaint.setShader(mFlowerRotateLinearGradient); canvas.drawRect(mRectFSunFlower,mPaint); canvas.restore(); mPaint.setShader; canvas.drawCircle(getMeasuredWidth()/2,getMeasuredHeight()/2,sunWidth/2,mPaint);//画圆 }

这里我们可以先画第一个绿色的正方形,然后画第二个红色的正文形,这里通过一个变量sunRotateAngle来控制两个正方形的旋转,从而实现太阳的旋转,第二个红色的正方形旋转总是比第一个多45度,从而错开,形成8角太阳花边的效果。最后绘制圆形。

可以看到我们在上面绘制正方形的时候为Paint画笔设置了shaderLinearGradient。为什么要设置这个呢?我们可以看到两个正方形并不是纯色的,而是一个渐变色。绿色正方形是从左上角渐变至右下角,红色正方形是从上到下渐变。

通过分析,动画的实现就很明了。一开始的放大缩小动画,需要控制圆的半径和正方形的长宽,即可以实现。旋转动画控制sunRotateAngle来旋转两个正方形。

绘制大拇指

4. 太阳阴影

图片 12太阳阴影

太阳的阴影就很简单了,聪明的你一下子就可以猜到用椭圆就可以很轻松的实现。

private void drawSunShadow(Canvas canvas) { mPaint.setColor(sunShadowColor); mPaint.setStyle(Paint.Style.FILL); mRectFSunShadow.set(getMeasuredWidth()/2-sunShadowWidth/2,getMeasuredHeight()- sunShadowHeight, getMeasuredWidth()/2+sunShadowWidth/2,getMeasuredHeight;//设置椭圆范围 canvas.drawOval(mRectFSunShadow,mPaint);//绘制椭圆 }

给定一个矩形可以确定一个唯一的椭圆,因此,我们可以通过固定矩形的高度,通过变量改变矩形的宽度来控制椭圆的宽度,从而实现拉长收缩动画。

5. 白云动画

图片 13白云动画

刚看到白云效果的时候,很多小伙伴都要晕了吧。这要怎么实现?这白云效果好恶心啊。对,如果你直接使用路径来画的话,是有点恶心,而且也很难实现移动放大动画,动的时候圆颜色还会变呢!但是如果我们换个思路呢?

图片 14a.gif

我们通过观察动画可以发现,白云是由5个圆实现的,然后对5个圆的位置进行适当的摆放。然后从底部向上缓缓出现,并且半径逐渐变大。同时在绘制的时候进行截取黑色选框部分。就可以实现我们的白云效果了。

图片 15image.png

private void drawCloud(Canvas canvas) { //CircleInfo用于记录每个圆的信息,圆心、半径、是否可见 mPath.reset(); mPaint.setShader(mCloudLinearGradient); if (mCircleInfoBottomOne.isCanDraw mPath.addCircle(mCircleInfoBottomOne.getX(),mCircleInfoBottomOne.getY(),mCircleInfoBottomOne.getRadius(), Path.Direction.CW);//左下1 if (mCircleInfoBottomTwo.isCanDraw mPath.addCircle(mCircleInfoBottomTwo.getX(),mCircleInfoBottomTwo.getY(),mCircleInfoBottomTwo.getRadius(), Path.Direction.CW);//底部2 if (mCircleInfoBottomThree.isCanDraw mPath.addCircle(mCircleInfoBottomThree.getX(),mCircleInfoBottomThree.getY(),mCircleInfoBottomThree.getRadius(), Path.Direction.CW);//底3 if (mCircleInfoTopOne.isCanDraw mPath.addCircle(mCircleInfoTopOne.getX(),mCircleInfoTopOne.getY(),mCircleInfoTopOne.getRadius(), Path.Direction.CW);//顶1 if (mCircleInfoTopTwo.isCanDraw mPath.addCircle(mCircleInfoTopTwo.getX(),mCircleInfoTopTwo.getY(),mCircleInfoTopTwo.getRadius(), Path.Direction.CW);//顶2 canvas.save(); canvas.clipRect(0,0,getMeasuredWidth(),getMeasuredHeight()/2+getMeasuredWidth;//截取黑色框部分 canvas.drawPath(mPath,mPaint); canvas.restore(); mPaint.setShader; }

白云颜色变幻效果也是通过Shader实现的。在这里通过Path添加各个圆进路径,然后通过Shader绘制整个路径。

private fun drawFinger(canvas: Canvas){ paint.style = Paint.Style.STROKE paint.strokeWidth = 4f val fingerWidth = .toFloat() val fingerHeight = .toFloat() val centerX = (width / 2).toFloat() + fingerWidth / 8 val centerY = (height / 2).toFloat() + fingerHeight / 8 path.addRect(centerX - fingerWidth / 2, centerY - fingerHeight / 4, centerX - fingerWidth / 3, centerY + fingerHeight / 4, Path.Direction.CW) path.moveTo(centerX - fingerWidth / 3 + spaceBetweenHandAndShoulder, centerY - fingerHeight / 4) path.rLineTo(fingerWidth / 8, 0f) path.rLineTo(fingerWidth / 8, -fingerHeight / 2) path.rLineTo(fingerWidth / 6, fingerHeight / 4) path.rLineTo(-fingerWidth / 8, fingerHeight / 4) path.rLineTo(fingerWidth / 2 - fingerWidth / 6 - spaceBetweenHandAndShoulder, 0f) path.rLineTo(-fingerWidth / 8, fingerHeight / 2) path.lineTo(centerX - fingerWidth / 3 + spaceBetweenHandAndShoulder, centerY + fingerHeight / 4) path.close() canvas.drawPath(path, paint)}
6. 白云阴影

图片 16白云阴影

代码实现如下:

private void drawCloudShadow(Canvas canvas) { mPaint.setStyle(Paint.Style.FILL_AND_STROKE); mPaint.setColor(cloudShadowColor); mPaint.setAlpha(cloudShadowAlpha); canvas.save(); canvas.clipRect(0,getMeasuredHeight()/2+getMeasuredWidth()/7f,getMeasuredWidth(),getMeasuredHeight;//截取多余部分 mRectFCloudShadow.set(getMeasuredWidth()/2-finalSunWidth/2,getMeasuredHeight()/2-finalSunWidth/2,getMeasuredWidth()/2+finalSunWidth/2,getMeasuredHeight()/2+finalSunWidth/2); mCloudShadowPath.reset(); mCloudShadowPath.moveTo(mCircleInfoBottomOne.getX(),getMeasuredHeight()/2+getMeasuredWidth;//白云底部一位置 mCloudShadowPath.arcTo(mRectFCloudShadow,15,45,false); canvas.drawPath(mCloudShadowPath,mPaint); canvas.restore(); mPaint.setAlpha; }

白色阴影的实现其实不算复杂,我们需要通过太阳的圆勾画出一条圆弧(arcTo,然后与白云底部1/5处的点连线形成一个伪扇形,然后截取掉超过白云的部分,渐变效果通过变量cloudShadowAlpha改变画笔的透明度即可。

图片 17image.png

通过以上几个步骤的了解,我们已经掌握了如何对动画进行分解,动画的每个部分如何进行绘制(在绘制动画时,应先找出关键帧或者通过动画最终画面反推动画过程)。剩下的就是,加上属性动画,控制每个动画的播放时机。这就不作具体的讲解,大家可自行查看源码。

源码戳我:github

大拇指的绘制是通过path来实现的,size=Math.min(width,height)大拇指占整个size的1/2,另外还要注意的是,当然还要注意为path设置patheffect

发表评论

电子邮件地址不会被公开。 必填项已用*标注