自定义view——叶子loading

本文介绍了如何使用Kotlin实现一个自定义的Android加载动画,包括绘制圆弧进度条、叶子旋转飘动效果。文章中详细讲解了绘制各个组件的步骤,并提到了如何将风扇旋转与进度关联,尽管此功能尚未实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本篇文章参考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,或者全部手动画(风扇除外)




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值