回答

收藏

安卓开发自定义带进度条的进度按钮

Android Android 16588 人阅读 | 0 人回复 | 2020-09-25

“进度按钮”是谷歌在其材质指南中介绍的一个选项,用于显示应用程序中的非阻塞进度。不幸的是,Google 没有提供开箱即用的实现,但是我们可以看到如何使用现有的组件来实现它,以及如何在不改变布局的情况下将它添加到现有的应用程序中。

首先我们来看一下要实现的效果:

progressButton

progressButton



要做到这样的效果我们可以有两种方法。
  • 方法一:将 ProgressBar 添加到我们的布局中,但是这种方法并不简单方便。虽然默认按钮有一些预定义的填充,但是并不容易在按钮中心对齐 ProgressBar。如果我们想在后面添加这样的文本怎么办?我们是否应该再次更改每个屏幕的布局?
  • 方法二:是使用第三方组件。这种方法的一个主要缺点是缺少灵活性,很多时候并不是十分符合我们的预期要求。
  • 方法三: 可以使用 Android ー AnimationDrawable 提供的 drawable 来显示进度。幸运的是,我们在一个 support-v4包中有一个现成可用的 progress drawable。我们还有一个解决方案来显示与文本一起的 drawable ー SpannableString + DynamicDrawableSpan。所以只要几段代码:


  1. // create progress drawable
  2. val progressDrawable = CircularProgressDrawable(this).apply {
  3.     // let's use large style just to better see one issue
  4.     setStyle(CircularProgressDrawable.LARGE)
  5.     setColorSchemeColors(Color.WHITE)

  6.     //bounds definition is required to show drawable correctly
  7.     val size = (centerRadius + strokeWidth).toInt() * 2
  8.     setBounds(0, 0, size, size)
  9. }

  10. // create a drawable span using our progress drawable
  11. val drawableSpan = object : DynamicDrawableSpan() {
  12.     override fun getDrawable() = progressDrawable
  13. }

  14. // create a SpannableString like "Loading [our_progress_bar]"
  15. val spannableString = SpannableString("Loading ").apply {
  16.     setSpan(drawableSpan, length - 1, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
  17. }

  18. //start progress drawable animation
  19. progressDrawable.start()

  20. button.text = spannableString
复制代码



以上代码所实现的效果如图所示:

progressButton_step

progressButton_step

可以看到上面的代码并不能达到我们预期中的那样美好,它存在三个问题:
1. 动画只有两帧,毫无动画感可言;
2. 文字与进度条(那个圆圈)没有对齐;
3. 文字与进度条之间没有间距;

首先我们来解决第一个问题。动画只有两帧,因为我们的按钮不知道什么时候重绘它的状态,所以在缺少条件的情况下,DynamicDrawableSpan 不会触发按钮重绘。这意味着我们必须手动操作,我们可以订阅 AnimationDrawable 动画更新,并调用 button.invalidate ()在动画激活时强制按钮调用绘制。所以我们用下面的代码来优化第一个问题:

  1. ...

  2. //start progress drawable animation
  3. progressDrawable.start()

  4. val callback = object : Drawable.Callback {
  5.     override fun unscheduleDrawable(who: Drawable, what: Runnable) {
  6.     }

  7.     override fun invalidateDrawable(who: Drawable) {
  8.         button.invalidate()
  9.     }

  10.     override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {
  11.     }
  12. }
  13. progressDrawable.callback = callback

  14. button.text = spannableString
复制代码

优化后的效果如图:

progressButton_step_02

progressButton_step_02

可以明显的感觉到好了很多,但是第二个和第三个问题依然存在,那么这两个问题怎么解决呢?遇到这种问题,我们会首先想到自定义绘制文本,所以我们可以扩展 imagepan 并重写 getSize 和 draw 方法。字体度量的解释可以参考 orhanobut。

重写getSize:

  1. override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fontMetricsInt: Paint.FontMetricsInt?): Int {
  2.         // get drawable dimensions
  3.         val rect = drawable.bounds
  4.         fontMetricsInt?.let {
  5.             val fontMetrics = paint.fontMetricsInt

  6.             // get a font height
  7.             val lineHeight = fontMetrics.bottom - fontMetrics.top

  8.             //make sure our drawable has height >= font height
  9.             val drHeight = Math.max(lineHeight, rect.bottom - rect.top)

  10.             val centerY = fontMetrics.top + lineHeight / 2

  11.             //adjust font metrics to fit our drawable size
  12.             fontMetricsInt.apply {
  13.                 ascent = centerY - drHeight / 2
  14.                 descent = centerY + drHeight / 2
  15.                 top = ascent
  16.                 bottom = descent
  17.             }
  18.         }

  19.         //return drawable width which is in our case = drawable width + margin from text
  20.         return rect.width() + marginStart
  21. }
复制代码



重写draw:

  1. override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {

  2.         canvas.save()
  3.         val fontMetrics = paint.fontMetricsInt
  4.         // get font height. in our case now it's drawable height
  5.         val lineHeight = fontMetrics.bottom - fontMetrics.top

  6.         // adjust canvas vertically to draw drawable on text vertical center
  7.         val centerY = y + fontMetrics.bottom - lineHeight / 2
  8.         val transY = centerY - drawable.bounds.height() / 2

  9.         // adjust canvas horizontally to draw drawable with defined margin from text
  10.         canvas.translate(x + marginStart, transY.toFloat())

  11.         drawable.draw(canvas)

  12.         canvas.restore()
  13. }
复制代码



并使用新的 Spannable 类:
  1. val drawableSpan = CustomDrawableSpan(progressDrawable, marginStart = 20)
复制代码
效果如图:

progressButton_step_03

progressButton_step_03


可以看到与我们预期的已经很像了,但是点击后到加载中之间还是缺少一共动画效果,可以使用 ObjectAnimator 来修复。

github: https://github.com/razir/ProgressButton


分享到:
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则