IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android 自定义斑马波纹进度条 -> 正文阅读

[移动开发]Android 自定义斑马波纹进度条

参考地址:?GitHub - Ccapton/Android-ColorfulProgressBar: Android 自定义彩色ProgressBar,类似Bootstrap

android绘图canvas.clipRect()方法的作用_moble_xie的博客-CSDN博客_cliprect

实现效果

源码奉上

activity


import android.os.Bundle
import android.widget.SeekBar
import android.widget.TextView
import com.orhanobut.logger.Logger
import com.pb.test.R
import com.pb.test.base.BaseActivity
import com.pb.test.bilibili.progressbar.myprogressbar.LiveLikeColorfulViewV3

/**
 *
 * @author : YingYing Zhang
 * @e-mail : 540108843@qq.com
 * @time   : 2022-09-15
 * @desc   :
 *
 */
class MyCustomProgressBarActivity : BaseActivity() {
    private var colorfulView: LiveLikeColorfulViewV3? = null
    private var seekBarX: SeekBar? = null
    private var seekBarRadius: SeekBar? = null
    private var tvTranslationX: TextView? = null
    private var tvRadius: TextView? = null

    companion object {
        const val TEST_STR = "MyCustomProgressBarActivity"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my_custom_progress_bar)

        colorfulView = findViewById(R.id.colorful_view)
        seekBarX = findViewById(R.id.seek_bar_x)
        seekBarRadius = findViewById(R.id.seek_bar_radius)
        tvTranslationX = findViewById(R.id.tv_translation_x)
        tvRadius = findViewById(R.id.tv_radius)

        colorfulView?.post {
            colorfulView?.startAnimation()

        }

        seekBarX?.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                val width = colorfulView?.width?.toFloat() ?: 0F
                val translationX = -width + (colorfulView?.width?.times(progress)?.div(100F) ?: 0F)
                Logger.d("seekBarX onProgressChanged translationX = $translationX, progress = $progress")
                colorfulView?.translationX = translationX
                tvTranslationX?.text = "平移 x: $progress"
            }

            override fun onStartTrackingTouch(seekBar: SeekBar?) {}

            override fun onStopTrackingTouch(seekBar: SeekBar?) {}
        })

        seekBarRadius?.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                val radius = progress + 15.0
                Logger.d("seekBarRadius onProgressChanged radius = $radius, progress = $progress")
                LiveLikeColorfulViewV3.COLORFUL_VIEW_RADIUS = radius
                tvRadius?.text = "条纹角度: $radius"
            }

            override fun onStartTrackingTouch(seekBar: SeekBar?) {}

            override fun onStopTrackingTouch(seekBar: SeekBar?) {}
        })
    }
}

自定义 view 代码如下:


import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import android.view.animation.LinearInterpolator
import com.orhanobut.logger.Logger
import com.pb.test.BuildConfig
import com.pb.test.R
import kotlin.math.tan

/**
 *
 * @author : YingYing Zhang
 * @e-mail : 540108843@qq.com
 * @time   : 2022-09-23
 * @desc   : 斑马波纹条 view 第三版, 第二版也不知道去哪了, 反正感觉就该是 v3
 *
 */
class LiveLikeColorfulViewV3 @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private val paint: Paint = Paint().apply {
        color = resources.getColor(R.color.progress_color)
        isAntiAlias = true
        style = Paint.Style.FILL
    }
    private var itemWidth = 40

    private var isRadius = true

    private var xPoint = 0
    private var valueAnimator: ValueAnimator? = null

    // 圆角 path
    private var roundRectPath: Path? = null
    // 斑马条纹 path
    private var stripPath: Path? = null


    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        val curHeight = measuredHeight
        if (BuildConfig.DEBUG) {
            if (curHeight == 0) {
                throw Exception("LiveLikeColorfulViewV3 onDraw height is 0!!!")
            }
            if (COLORFUL_VIEW_RADIUS > 90.0) {
                throw Exception("LiveLikeColorfulViewV3 onDraw radius must be in (0, 90]")
            }
        }

        // 现在倾斜角度是 45度, 通过 tan45 = 1来计算坐标点值, delta 临边
        val delta = curHeight / tan(Math.toRadians(COLORFUL_VIEW_RADIUS))
        Logger.d("LiveLikeColorfulView onDraw delta = $delta, height = $curHeight")

        /**
         * 绘制斑马条纹
         * 以 xPoint 中心, 左右两边画斑波纹条, xPoint 根据属性动画动态变化
         */
        val stripPath = getStripPath()
        // 画 xPoint 左边线条
        var tempXPoint = xPoint
        while (tempXPoint >= -delta) {
            val x = tempXPoint.toFloat()
            val y = curHeight.toFloat()

            stripPath.moveTo(x, y)
            stripPath.lineTo(x, y)
            stripPath.lineTo((x + delta).toFloat(), 0F)
            stripPath.lineTo((x + delta - itemWidth).toFloat(), 0F)
            stripPath.lineTo(x - itemWidth, y)
            stripPath.lineTo(x, y)

            tempXPoint -= 2 * itemWidth
        }
        // 画 xPoint 右边线条
        tempXPoint = xPoint
        while (tempXPoint <= measuredWidth + delta) {
            val x = tempXPoint.toFloat()
            val y = curHeight.toFloat()

            stripPath.moveTo(x, y)
            stripPath.lineTo(x, y)
            stripPath.lineTo((x + delta).toFloat(), 0F)
            stripPath.lineTo((x + delta - itemWidth).toFloat(), 0F)
            stripPath.lineTo(x - itemWidth, y)
            stripPath.lineTo(x, y)

            tempXPoint += 2 * itemWidth
        }
        stripPath.close()

        // clipxx 方法只对设置以后的 drawxx 起作用,已经画出来的图形, 是不会有作用的
        // 也就是说 clipxx需要先调用!!
        if (isRadius) {
            canvas?.clipPath(getRoundRectPath())
        }

        canvas?.drawPath(stripPath, paint)
    }

    fun startAnimation() {
        if (isRunning()) return
        val distance = measuredWidth
        valueAnimator = ValueAnimator.ofInt(0, distance)
        val time = distance / COLORFUL_VIEW_X_DEFAULT_SPEED * 1_000
        valueAnimator?.duration = time
        Logger.d(
            "live colorful view v3 -> distance = $distance, " +
                    "time = $time, " +
                    "speed = $COLORFUL_VIEW_X_DEFAULT_SPEED"
        )
        valueAnimator?.repeatCount = ValueAnimator.INFINITE
        valueAnimator?.addUpdateListener {
            xPoint = (it?.animatedValue ?: 0) as Int
            invalidate()
        }
        valueAnimator?.interpolator = LinearInterpolator()
        valueAnimator?.start()
    }

    private fun getRoundRectPath(isForced: Boolean = false): Path {
        if (roundRectPath == null || isForced) {
            roundRectPath = Path()
            Logger.d("zyy only init once round rect path")
            val rect = RectF(0F, 0F, width.toFloat(), height.toFloat())
            roundRectPath?.addRoundRect(rect, height.toFloat() / 2, height.toFloat() / 2, Path.Direction.CW)
        }
        return roundRectPath ?: Path()
    }

    private fun getStripPath(): Path {
        if (stripPath == null) {
            Logger.d("zyy only init once strip path")
            stripPath = Path()
        }
        stripPath?.reset()
        return stripPath ?: Path()
    }

    fun configChanged() {
        getRoundRectPath(true)
    }

    fun stopAnimation() {
        if (isRunning()) {
            valueAnimator?.end()
        }
    }

    fun isRunning() = valueAnimator?.isRunning == true

    fun setRoundRadius() {
        isRadius = true
        invalidate()
    }

    fun resetRadius() {
        isRadius = false
        invalidate()
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        valueAnimator?.removeAllUpdateListeners()
    }

    companion object {
        // 斑马波纹内部移动速度, 标准速度: 每秒移动 400px
        private const val COLORFUL_VIEW_X_DEFAULT_SPEED = 250L
        // 斑马条纹倾斜角度
        var COLORFUL_VIEW_RADIUS = 45.0
    }
}
activity_my_custom_progress_bar.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".bilibili.progressbar.MyCustomProgressBarActivity">

    <com.pb.test.bilibili.progressbar.myprogressbar.LiveLikeColorfulViewV3
        android:id="@+id/colorful_view"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:background="@drawable/custom_progress_bar_bg"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/tv_translation_x"
        android:layout_width="100dp"
        android:layout_height="40dp"
        android:gravity="center"
        android:text="平移 x: 100"
        android:textSize="12sp"
        android:background="@color/amber_200"
        android:layout_marginTop="10dp"
        app:layout_constraintTop_toBottomOf="@id/colorful_view"
        app:layout_constraintStart_toStartOf="@id/colorful_view" />

    <SeekBar
        android:id="@+id/seek_bar_x"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="0dp"
        android:layout_height="24dp"
        android:max="100"
        android:progress="100"
        android:layout_marginStart="10dp"
        android:layout_marginEnd="10dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        app:layout_constraintTop_toTopOf="@id/tv_translation_x"
        app:layout_constraintBottom_toBottomOf="@id/tv_translation_x"
        app:layout_constraintStart_toEndOf="@id/tv_translation_x"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/tv_radius"
        android:layout_width="100dp"
        android:layout_height="40dp"
        android:gravity="center"
        android:text="条纹角度: 45"
        android:textSize="12sp"
        android:layout_marginTop="10dp"
        android:background="@color/amber_200"
        app:layout_constraintTop_toBottomOf="@id/tv_translation_x"
        app:layout_constraintStart_toStartOf="@id/colorful_view" />

    <SeekBar
        android:id="@+id/seek_bar_radius"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="0dp"
        android:layout_height="24dp"
        android:max="75"
        android:progress="45"
        android:layout_marginStart="10dp"
        android:layout_marginEnd="10dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        app:layout_constraintTop_toTopOf="@id/tv_radius"
        app:layout_constraintBottom_toBottomOf="@id/tv_radius"
        app:layout_constraintStart_toEndOf="@id/tv_radius"
        app:layout_constraintEnd_toEndOf="parent"
        tools:progress="50" />
</androidx.constraintlayout.widget.ConstraintLayout>

custom_progress_bar_bg.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <gradient
        android:angle="180"
        android:startColor="#F1B300"
        android:endColor="#FF9B04" />

    <corners android:radius="20dp" />
</shape>

实现思路

看到炫酷动画, 不要慌, 我们逐步分析动画的各个动作,?

1. 进度条从左向右移动

2. 绘制斑马条纹

3. 斑马条纹从左向右移动

4. 剪切成为圆角

对, 就这三步...

第一步

进度条整体的移动, 通过设置 view translationX 就可以移动啦

第二步

绘制斑马条纹, 这一步需要了解 canvas 的一些常用 API, 我就是吃了不了解的苦, 这一步浪费了好些时间.

首先背景色是一块渐变色, 我们通过 gradient 实现就好啦~

斑马条纹实际上就是渐变色上的半透明小方块,

绘制方块的方法就是, canvas.drawPath,

这个 path 我们可以来自由定义形状,

通过 x, y 轴我们可以定位到小方块的4个点的坐标,

通过 path.lineTo 把这4个点连接起来, 就可以把小方块绘制出来啦

然后通过 while 循环可以绘制进度条上的多个方块, 此时需要注意边界场景, 在坐标 x = 0 的位置需要再往左边绘制 delta =?height / tan(Math.toRadians(COLORFUL_VIEW_RADIUS)) 的距离, 因为左边 delta 距离内的小方块会倾斜到 x = 0 的位置右边, 大概大家画一个坐标轴就很清晰啦~

第三步

让所有斑马条纹(小透明方块)向右移动, 因为此时 view 整体已经在沿 x 轴方向位移了, 小方块在 view 内部向右移动的效果就只能通过变更绘制的起点, 起点的位置从 0 到 view 的 width 来回移动, 就可以实现 小方块在 view 内部做平移啦~

具体实现的时候需要注意, 选择的起点需要分两边开始画, 绘制起点左边的小方块, 和绘制起点右边的小方块~!

第四部

基本效果已经出来了, 可恶的 UI 小姐姐竟然还需要一个圆角, 还好 canvas 提供了 clipPath 函数,?

然后 path.addRoundRect 就可以把想要的圆角添加进来啦~

需要注意的是 clipPath 需要再 绘制小方块之前, 不然是无法生效的~

坎坷过程

看到效果的时候有点懵, 有点慌, 不知道该怎么整...

首先开始在 github 上搜一些类似的效果, 在此感谢大佬的无私分享, 参考大佬实现的效果, 修修补补, 最后发现无法实现需求的效果...吐血...

然后就开始自己写, 还研究了一番?xfermode, 整出来了一个半成品, 但还差一丢丢,?

最后的最后, 重新整理了思路, 达成效果...

思考

不了解 canvas 各种好使的 API 吃了大亏, 需要多学习啊...

想做两件事

1. 因为现在 clipPath 缺少了 绘制圆角的函数, 想写一个 View 的扩展函数来提高便用性,?

2. xfermode 用起来实在是不舒服, 想封装一套好用的 API 来快速实现各种规则形状内剪切效果...

待我再学习一波, 再来更新上面两项...

菜鸡一枚, 有问题大佬们及时指正, 以防误导他人 0-0

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-09-30 01:04:20  更:2022-09-30 01:05:25 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 22:40:58-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码