|
“进度按钮”是谷歌在其材质指南中介绍的一个选项,用于显示应用程序中的非阻塞进度。不幸的是,Google 没有提供开箱即用的实现,但是我们可以看到如何使用现有的组件来实现它,以及如何在不改变布局的情况下将它添加到现有的应用程序中。
首先我们来看一下要实现的效果:
progressButton
要做到这样的效果我们可以有两种方法。
- 方法一:将 ProgressBar 添加到我们的布局中,但是这种方法并不简单方便。虽然默认按钮有一些预定义的填充,但是并不容易在按钮中心对齐 ProgressBar。如果我们想在后面添加这样的文本怎么办?我们是否应该再次更改每个屏幕的布局?
- 方法二:是使用第三方组件。这种方法的一个主要缺点是缺少灵活性,很多时候并不是十分符合我们的预期要求。
- 方法三: 可以使用 Android ー AnimationDrawable 提供的 drawable 来显示进度。幸运的是,我们在一个 support-v4包中有一个现成可用的 progress drawable。我们还有一个解决方案来显示与文本一起的 drawable ー SpannableString + DynamicDrawableSpan。所以只要几段代码:
- // create progress drawable
- val progressDrawable = CircularProgressDrawable(this).apply {
- // let's use large style just to better see one issue
- setStyle(CircularProgressDrawable.LARGE)
- setColorSchemeColors(Color.WHITE)
- //bounds definition is required to show drawable correctly
- val size = (centerRadius + strokeWidth).toInt() * 2
- setBounds(0, 0, size, size)
- }
- // create a drawable span using our progress drawable
- val drawableSpan = object : DynamicDrawableSpan() {
- override fun getDrawable() = progressDrawable
- }
- // create a SpannableString like "Loading [our_progress_bar]"
- val spannableString = SpannableString("Loading ").apply {
- setSpan(drawableSpan, length - 1, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
- }
- //start progress drawable animation
- progressDrawable.start()
- button.text = spannableString
复制代码
以上代码所实现的效果如图所示:
progressButton_step
可以看到上面的代码并不能达到我们预期中的那样美好,它存在三个问题:
1. 动画只有两帧,毫无动画感可言;
2. 文字与进度条(那个圆圈)没有对齐;
3. 文字与进度条之间没有间距;
首先我们来解决第一个问题。动画只有两帧,因为我们的按钮不知道什么时候重绘它的状态,所以在缺少条件的情况下,DynamicDrawableSpan 不会触发按钮重绘。这意味着我们必须手动操作,我们可以订阅 AnimationDrawable 动画更新,并调用 button.invalidate ()在动画激活时强制按钮调用绘制。所以我们用下面的代码来优化第一个问题:
- ...
- //start progress drawable animation
- progressDrawable.start()
- val callback = object : Drawable.Callback {
- override fun unscheduleDrawable(who: Drawable, what: Runnable) {
- }
- override fun invalidateDrawable(who: Drawable) {
- button.invalidate()
- }
- override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {
- }
- }
- progressDrawable.callback = callback
- button.text = spannableString
复制代码
优化后的效果如图:
progressButton_step_02
可以明显的感觉到好了很多,但是第二个和第三个问题依然存在,那么这两个问题怎么解决呢?遇到这种问题,我们会首先想到自定义绘制文本,所以我们可以扩展 imagepan 并重写 getSize 和 draw 方法。字体度量的解释可以参考 orhanobut。
重写getSize:
- override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fontMetricsInt: Paint.FontMetricsInt?): Int {
- // get drawable dimensions
- val rect = drawable.bounds
- fontMetricsInt?.let {
- val fontMetrics = paint.fontMetricsInt
- // get a font height
- val lineHeight = fontMetrics.bottom - fontMetrics.top
- //make sure our drawable has height >= font height
- val drHeight = Math.max(lineHeight, rect.bottom - rect.top)
- val centerY = fontMetrics.top + lineHeight / 2
- //adjust font metrics to fit our drawable size
- fontMetricsInt.apply {
- ascent = centerY - drHeight / 2
- descent = centerY + drHeight / 2
- top = ascent
- bottom = descent
- }
- }
- //return drawable width which is in our case = drawable width + margin from text
- return rect.width() + marginStart
- }
复制代码
重写draw:
- override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
- canvas.save()
- val fontMetrics = paint.fontMetricsInt
- // get font height. in our case now it's drawable height
- val lineHeight = fontMetrics.bottom - fontMetrics.top
- // adjust canvas vertically to draw drawable on text vertical center
- val centerY = y + fontMetrics.bottom - lineHeight / 2
- val transY = centerY - drawable.bounds.height() / 2
- // adjust canvas horizontally to draw drawable with defined margin from text
- canvas.translate(x + marginStart, transY.toFloat())
- drawable.draw(canvas)
- canvas.restore()
- }
复制代码
并使用新的 Spannable 类:
- val drawableSpan = CustomDrawableSpan(progressDrawable, marginStart = 20)
复制代码 效果如图:
progressButton_step_03
可以看到与我们预期的已经很像了,但是点击后到加载中之间还是缺少一共动画效果,可以使用 ObjectAnimator 来修复。
github: https://github.com/razir/ProgressButton
|
|