自定义View-webview进度条(指示器)

效果展示

Screen_recording_20240614_082748.gif

代码细节

主要逻辑在startAnim里,执行了几次动画效果,感兴趣的话需要研究下动画细节

class WebIndicator @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) {

    /**
     * 进度条颜色
     */
    @ColorInt
    private var mColor = ResourcesCompat.getColor(resources, R.color.color_1aad19, null)

    /**
     * 进度条的画笔
     */
    private val mPaint = Paint().apply {
        isAntiAlias = true
        color = mColor
        isDither = true
        strokeCap = Paint.Cap.SQUARE
    }

    /**
     * 进度条动画
     */
    private var mAnimator: Animator? = null

    /**
     * 控件的宽度
     */
    private var mTargetWidth = 0

    /**
     * 当前匀速动画最大的时长
     */
    private var mCurrentMaxUniformSpeedDuration = MAX_UNIFORM_SPEED_DURATION

    /**
     * 当前加速后减速动画最大时长
     */
    private var mCurrentMaxDecelerateSpeedDuration = MAX_DECELERATE_SPEED_DURATION

    /**
     * 结束动画时长
     */
    private var mCurrentDoEndAnimationDuration = DO_END_ANIMATION_DURATION

    /**
     * 当前进度条的状态
     */
    private var indicatorStatus = 0
    private var mCurrentProgress = 0f

    /**
     * 默认的高度
     */
    var mWebIndicatorDefaultHeight: Int = 3

    init {
        val ta = context.obtainStyledAttributes(attrs, R.styleable.WebIndicator)
        mColor = ta.getColor(R.styleable.WebIndicator_web_indicator_color, mColor)
        mPaint.color = mColor
        ta.recycle()
        mTargetWidth = context.resources.displayMetrics.widthPixels
        mWebIndicatorDefaultHeight = dp2px(3)
    }

    fun setColor(color: Int) {
        this.mColor = color
        mPaint.color = color
    }

    fun setColor(color: String?) {
        this.setColor(Color.parseColor(color))
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val wMode = MeasureSpec.getMode(widthMeasureSpec)
        var w = MeasureSpec.getSize(widthMeasureSpec)
        val hMode = MeasureSpec.getMode(heightMeasureSpec)
        var h = MeasureSpec.getSize(heightMeasureSpec)

        if (wMode == MeasureSpec.AT_MOST) {
            w = min(w.toDouble(), context.resources.displayMetrics.widthPixels.toDouble()).toInt()
        }
        if (hMode == MeasureSpec.AT_MOST) {
            h = mWebIndicatorDefaultHeight
        }
        this.setMeasuredDimension(w, h)
    }

    override fun dispatchDraw(canvas: Canvas) {
        canvas.drawRect(0f, 0f, mCurrentProgress / 100 * this.width, this.height.toFloat(), mPaint)
    }

    fun show() {
        if (visibility == GONE) {
            this.visibility = VISIBLE
            mCurrentProgress = 0f
            this.alpha = 1.0f
            startAnim(false)
        }
    }

    fun showForTest() {
        this.visibility = VISIBLE
        mCurrentProgress = 0f
        this.alpha = 1.0f
        startAnim(false)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        this.mTargetWidth = measuredWidth
        val screenWidth = context.resources.displayMetrics.widthPixels
        if (mTargetWidth >= screenWidth) {
            mCurrentMaxDecelerateSpeedDuration = MAX_DECELERATE_SPEED_DURATION
            mCurrentMaxUniformSpeedDuration = MAX_UNIFORM_SPEED_DURATION
            mCurrentDoEndAnimationDuration = MAX_DECELERATE_SPEED_DURATION
        } else {
            //取比值
            val rate = (mTargetWidth / screenWidth).toFloat()
            mCurrentMaxUniformSpeedDuration = (MAX_UNIFORM_SPEED_DURATION * rate).toInt()
            mCurrentMaxDecelerateSpeedDuration = (MAX_DECELERATE_SPEED_DURATION * rate).toInt()
            mCurrentDoEndAnimationDuration = (DO_END_ANIMATION_DURATION * rate).toInt()
        }
    }

    fun setProgress(progress: Float) {
        if (visibility == GONE) {
            visibility = VISIBLE
        }
        if (progress < 95f) {
            return
        }
        if (indicatorStatus != FINISH) {
            startAnim(true)
        }
    }

    fun hide() {
        indicatorStatus = FINISH
    }

    private fun startAnim(isFinished: Boolean) {
        val v = (if (isFinished) 100 else 95).toFloat()
        if (mAnimator?.isStarted == true) {
            mAnimator?.cancel()
        }
        mCurrentProgress = if (mCurrentProgress == 0f) 0.00000001f else mCurrentProgress
        if (!isFinished) {
            AnimatorSet().apply {
                val p1 = v * 0.60f
                val p2 = v
                val animator0 = ValueAnimator.ofFloat(mCurrentProgress, p1)
                val animator1 = ValueAnimator.ofFloat(p1, p2)
                val residue = 1f - mCurrentProgress / 100 - 0.05f
                val duration = (residue * mCurrentMaxUniformSpeedDuration).toLong()
                //前半段40%+后半段60%
                val duration4 = (duration * 0.4f).toLong()
                val duration6 = (duration * 0.6f).toLong()
                animator0.interpolator = LinearInterpolator()
                animator0.setDuration(duration4)
                animator0.addUpdateListener(mAnimatorUpdateListener)

                animator1.interpolator = LinearInterpolator()
                animator1.setDuration(duration6)
                animator1.addUpdateListener(mAnimatorUpdateListener)
                play(animator1).after(animator0)
                start()
                this@WebIndicator.mAnimator = this
            }
        } else {
            var segment95Animator: ValueAnimator? = null
            if (mCurrentProgress < 95f) {
                segment95Animator = ValueAnimator.ofFloat(mCurrentProgress, 95f)
                val residue = 1f - mCurrentProgress / 100f - 0.05f
                segment95Animator.setDuration((residue * mCurrentMaxDecelerateSpeedDuration).toLong())
                segment95Animator.interpolator = DecelerateInterpolator()
                segment95Animator.addUpdateListener(mAnimatorUpdateListener)
            }
            val alphaAnimator = ObjectAnimator.ofFloat(this, "alpha", 1f, 0f)
            alphaAnimator.setDuration(mCurrentDoEndAnimationDuration.toLong())
            val valueAnimatorEnd = ValueAnimator.ofFloat(95f, 100f)
            valueAnimatorEnd.setDuration(mCurrentDoEndAnimationDuration.toLong())
            valueAnimatorEnd.addUpdateListener(mAnimatorUpdateListener)
            var animatorSet = AnimatorSet()
            animatorSet.playTogether(alphaAnimator, valueAnimatorEnd)
            if (segment95Animator != null) {
                val animatorSet0 = AnimatorSet()
                animatorSet0.play(animatorSet).after(segment95Animator)
                animatorSet = animatorSet0
            }
            animatorSet.addListener(mAnimatorListenerAdapter)
            animatorSet.start()
            mAnimator = animatorSet
        }
        indicatorStatus = STARTED
    }

    private val mAnimatorUpdateListener = AnimatorUpdateListener { animation ->
        val t = animation.animatedValue as Float
        this@WebIndicator.mCurrentProgress = t
        this@WebIndicator.invalidate()
    }

    private val mAnimatorListenerAdapter: AnimatorListenerAdapter = object : AnimatorListenerAdapter() {
        override fun onAnimationEnd(animation: Animator) {
            doEnd()
        }
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        /**
         * animator cause leak , if not cancel;
         */
        if (mAnimator?.isStarted == true) {
            mAnimator?.cancel()
            mAnimator = null
        }
    }

    private fun doEnd() {
        if (indicatorStatus == FINISH && mCurrentProgress == 100f) {
            visibility = GONE
            mCurrentProgress = 0f
            //动画执行结束一定要将透明度还原,否则后面的draw操作看不到东西
            this.alpha = 1f
        }
        indicatorStatus = UN_START
    }

    fun reset() {
        mCurrentProgress = 0f
        if (mAnimator?.isStarted == true) {
            mAnimator?.cancel()
        }
    }

    fun setProgress(newProgress: Int) {
        setProgress(newProgress.toFloat())
    }

    fun dp2px(dip: Int): Int {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip.toFloat(), context.resources.displayMetrics).toInt()
    }

    companion object {
        /**
         * 默认匀速动画最大的时长
         */
        const val MAX_UNIFORM_SPEED_DURATION: Int = 8 * 1000

        /**
         * 默认加速后减速动画最大时长
         */
        const val MAX_DECELERATE_SPEED_DURATION: Int = 450

        /**
         * 结束动画时长 , Fade out 。
         */
        const val DO_END_ANIMATION_DURATION: Int = 600
        const val UN_START: Int = 0
        const val STARTED: Int = 1
        const val FINISH: Int = 2
    }
}

源码地址

https://github.com/treech/MyView

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,527评论 6 544
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,687评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,640评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,957评论 1 318
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,682评论 6 413
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 56,011评论 1 329
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 44,009评论 3 449
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,183评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,714评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,435评论 3 359
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,665评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,148评论 5 365
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,838评论 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,251评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,588评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,379评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,627评论 2 380

推荐阅读更多精彩内容