本篇文章参考http://blog.youkuaiyun.com/tianjian4592
kotlin实现,做了一些修改
最近刚学完kotlin,顺便练练自定义view,于是想到以前学习自定义view时看到的一个不错的demo就拿来练练。效果图如下(时间问题部分功能未实现):
首先我们分析下这个gif需要做什么
1.进度条
2.叶子随机旋转和飘动
3.风扇旋转与进度关联(未关联,有兴趣可以实现下)
4.结束时风扇旋转并缩小,同时100%字体放大
这里的背景,风扇和叶子都是使用的图片(风扇最好和右边的圆一样大,或者全部自己画)
进度条
进度条分两部分:最左边圆弧和矩形
如上盗图:
我们可以根据弧半径及进度条的高度一半和半径与进度差计算出红色角的反函数,从而根据反函数结果用Math.toDegrees()将弧度转换成角度,从而可以画出进度的橙色弧,具体看代码:
创建圆弧和进度以及空白绘制区域
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) mOuterDestHeight = h mOuterDestWidth = w mProgressWidth = mOuterDestWidth - mLeftMagin - mRightMagin mArcRadius = (mOuterDestHeight - 2 * mLeftMagin)/2 mOuterSrcRect = Rect(0,0,mOutwidth,mOutHeight) mOuterDestRect = Rect(0,0,mOuterDestWidth,mOuterDestHeight) mArcRectF = RectF( mLeftMagin.toFloat(), mLeftMagin.toFloat(), (mLeftMagin + 2 * mArcRadius).toFloat(), (mOuterDestHeight - mLeftMagin).toFloat() ) mWhiteRectF = RectF( (mLeftMagin + mCurrentProgressPosition).toFloat(), mLeftMagin.toFloat(), (mOuterDestWidth - mRightMagin).toFloat(), (mOuterDestHeight - mLeftMagin).toFloat() ) mOrangeRectF = RectF( (mLeftMagin + mArcRadius).toFloat(), mLeftMagin.toFloat(), (mOuterDestWidth - mRightMagin).toFloat(), (mOuterDestHeight - mLeftMagin).toFloat() ) }
绘制进度条
//绘制进度条 private fun drawProgress(canvas: Canvas?) { if (progress >= 100) progress = 100 mCurrentProgressPosition = mProgressWidth * progress / 100 if (mCurrentProgressPosition < mArcRadius){ canvas!!.drawArc(mArcRectF, 90F, 180F,false,mSpacePaint) mWhiteRectF!!.left = (mLeftMagin + mArcRadius).toFloat() canvas.drawRect(mWhiteRectF,mSpacePaint) val angle = Math.toDegrees(Math.acos(((mArcRadius.toDouble() - mCurrentProgressPosition.toDouble()) / mArcRadius.toDouble()))) val sweep = 2 * angle drawLeafs(canvas) canvas.drawArc(mArcRectF, (180 - angle).toFloat(), sweep.toFloat(),false,mProgressPaint) }else{ mWhiteRectF!!.left = (mCurrentProgressPosition + mLeftMagin).toFloat() canvas!!.drawRect(mWhiteRectF,mSpacePaint) drawLeafs(canvas) canvas.drawArc(mArcRectF, 90F, 180F,false,mProgressPaint) mOrangeRectF!!.right = (mCurrentProgressPosition + mLeftMagin).toFloat() canvas.drawRect(mOrangeRectF,mProgressPaint) } }
代码逻辑无非就是根据进度值计算当前进度位置从而计算其他部分的区域,就不多说了,看代码绘制叶子
//绘制叶子 private fun drawLeafs(canvas: Canvas?) { val nowTime = System.currentTimeMillis() for (item in this!!.mLeafs!!){ if (nowTime > item.startTime && item.startTime != 0L){ getLeafLocaltion(item,nowTime) canvas!!.save() val m = Matrix() val c = Camera() val m3d = Matrix() val tranx = mLeftMagin + item.x val trany = mLeftMagin + item.y m.postTranslate(tranx.toFloat(), trany.toFloat()) //绑定mLeafFloatTime 可以控制旋转速度 val rotateChangeSpeed = ((nowTime - item.startTime) % mLeafRotateTime) / mLeafRotateTime.toFloat() val angle = rotateChangeSpeed * 360 val rotate = if (item.rotateDirection == 0) angle + item.varrotateAngle else -angle - item.varrotateAngle Log.e("TAG","rotate:${rotate},rotateChangeSpeed:${rotateChangeSpeed},angle:${angle}") m.postRotate(rotate, (tranx + mLeafWidth / 2).toFloat(), (trany + mLeafHeight / 2).toFloat()) c.save() c.rotate(angle,0F,0F) c.getMatrix(m3d) c.restore() m3d.preTranslate((-tranx - mLeafWidth / 2).toFloat(), (-trany - mLeafHeight / 2).toFloat()) m3d.postTranslate(tranx + mLeafWidth.toFloat(), trany + mLeafHeight.toFloat()) canvas.concat(m3d) canvas.drawBitmap(mLeafBitmap,m,mBitmapPaint) canvas.restore() }else{ continue } } }
此处我添加了3D旋转效果,具体逻辑看代码
//获取/设置叶子位置 private fun getLeafLocaltion(leaf: Leaf,nowTime: Long) { val intervalTime = nowTime - leaf.startTime if (intervalTime < 0) return else if (intervalTime > mLeafFloatTime){ leaf.startTime = System.currentTimeMillis() + Random().nextInt(mLeafFloatTime) } leaf.x = mProgressWidth - (mProgressWidth * (intervalTime.toDouble() / mLeafFloatTime.toDouble()) + mLeftMagin).toInt() leaf.y = getLeafLocaltionY(leaf) } // 通过叶子信息获取当前叶子的Y值 private fun getLeafLocaltionY(leaf: Leaf): Int { // y = A(wx+Q)+h val w = 2 * Math.PI / mProgressWidth var a = mMiddleAmplitude when(leaf.startType){ "TITLE" -> a = mMiddleAmplitude - mAmplitudeDisparity "MIDDLE" -> a = mMiddleAmplitude "BIG" -> a = mMiddleAmplitude + mAmplitudeDisparity } Log.i("TAG", "---a = " + a + "---w = " + w + "--leaf.x = " + leaf.x) return ((a * Math.sin(w * leaf.x)) + mArcRadius * 2 / 3).toInt() }
根据当前时间与启动时间的时间差算出叶子当前的x。
注:关键部分
y = A(wx+Q)+h (这个叫正弦型函数,有多少像我一样忘了初中数学的举手。。。)A决定峰值(即纵向拉伸压缩的倍数),w决定周期 最正小周期为 T=2π/|w| 这里周期T为进度条长度 所以w = 2 * Math.PI / mProgressWidth,Q(初相位)决定波形与x轴位置关系或者横向移动距离(左加右减),h决定波形与y轴位置关系或者纵向移动距离(上加下减)
其实给a设置下随机数效果估计会好些,有兴趣的同学可以试试
绘制风扇
//绘制风扇 //此处适配可能有问题 private fun drawFan(canvas: Canvas?) { val nowTime = System.currentTimeMillis() val progressDifference = if ((progress - oldProgress) > 0) progress - oldProgress else 1 canvas!!.save() val mFan = matrix mFan.postTranslate((UiUtils.dipToPx(context,5) + mProgressWidth - mFanWidth / 2).toFloat(), (mOutHeight / 2 - mFanHeight / 2).toFloat()) // if(progress > lastProgress){ // lastProgress = progress val mFanControlSpeed = ((nowTime - mFanStartTime) % mFanRotateTime) / mFanRotateTime.toDouble() val mFanRotate = mFanControlSpeed * 360 * progressDifference mFan.postRotate(mFanRotate.toFloat(),((UiUtils.dipToPx(context,5) + mProgressWidth)).toFloat(), (mOutHeight / 2 - mFanHeight / 2 + mFanHeight / 2).toFloat()) // Log.e("TAG","out:${mOutHeight},mOuterDestHeight:${mOuterDestHeight}") if (progress >= 100 && mFanScaleTime > 0){ mFanScaleTime-- mFanTextSize++ val mFanAngle = mFanScaleTime.toFloat() / mFanWidth.toFloat() mFan.postScale(mFanAngle, mFanAngle,((UiUtils.dipToPx(context,5) + mProgressWidth)).toFloat(), (mOutHeight / 2 - mFanHeight / 2 + mFanHeight / 2).toFloat()) // mFanTextSize = mFanScaleTime.toFloat() / mFanWidth.toFloat() mFanTextPaint!!.textSize = UiUtils.dipToPx(context,TEXT_SIZE) * mFanTextSize / mFanWidth canvas.drawText("100%", (UiUtils.dipToPx(context,5) + mProgressWidth).toFloat(), mFanTextHeight .toFloat(),mFanTextPaint) }else if (progress >= 100 && mFanScaleTime <= 0){ mFan.postScale(mFanWidth.toFloat(), mFanHeight.toFloat()) mFanTextPaint!!.textSize = UiUtils.dipToPx(context,TEXT_SIZE).toFloat() canvas.drawText("100%", (UiUtils.dipToPx(context,5) + mProgressWidth).toFloat(), mFanTextHeight.toFloat(),mFanTextPaint) } canvas.drawBitmap(mFanBitmap,mFan,mBitmapPaint) canvas.restore() }
根据背景高度确定圆心y,这里前面说过最好让UI切和背景圆一样大小的风扇,这样就可以根据风扇宽度确定圆心x,或者全部手动画(风扇除外)
源码:点击打开链接