本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
在 Dribble 上偶然看到了一组交互如下:
当时在心里问自己能不能做,答案肯定是能做的,不过我比较懒,觉得中间那个伸缩变化要编写很多代码,所以懒得理。后来,为了不让自己那么浮躁,也为了锻炼自己的耐心程度,还是坚持实现它了。这个过程,觉得自己还是有所收获,把握了一些想当然的细节,输理了对于自定义 View 的流程。
我将这个自定义 View,起了一个名字叫做 LoadButton。
这篇文章涉及到的知识点有如下:
1. 自定义 View 时的基本流程,包含 attrs.xml 中属性的编写,构造方法中属性的获取,onMeasure() 中尺寸的测量。onDraw() 中界面的实现。
2. 可以让 Android 初学者再次感受一次回调机制的美妙。
3. 属性动画的基本使用。
第一步,先确定尺寸
先观察 LoadView 的形态。
上面的显示的是两种形状,一个是圆角矩形,另外一个就是圆。两个形态尺寸区别是,高相同,宽度不一致。
我们再进一步分析形态 1。
形态 1 可以看成是左右两个半圆和中间一个矩形。再回顾下示例图片中的动画表现。
圆角矩形最终变成了一个圆。我们可以用线框图来渐进表现它。
当进行动画时,中间的矩形部分不停地缩小,当它缩小为 0 时,形态 1 就转变成了形态 2。
上面的能够说明什么呢?说明 LoadButton 由 3 个部分组成,左右的半圆和中间的矩形,即使是形态 2 也可以看做是左右半圆和中间宽度为 0 的矩形组成。
细化尺寸
我们进一步讨论尺寸相关的情况。
我们知道对于普通开发者而言,自定义一个 View 测量尺寸的时候我们通常要关注的测量模式是 MeasureSpec.EXACTLY 和 MeasureSpec.AT_MOST 两种。要了解更多详细的信息可以阅读我写的这篇博文《长谈:关于 View Measure 测量机制,让我一次把话说完》。接下来,我们详细讨论一下这两种情况。
MeasureSpec.EXACTLY
当一个 View 的 layout_width 或者 layout_height 的取值为 match_parent 或 30dp 这样具体的数值时,这就表明它的测量模式是 MeasureSpec.EXACTLY。它已经获得了精确的数值了,按照常理我们是不应该再去干涉它,parent 给出的建议尺寸是什么,我们就把尺寸设置成什么,但是结合开发的实际情况来看,我们有一个底线,为了保证 LoadView 的完整性,也就是再差的情况下,parent 给出来的建议尺寸也不能小于形态 2。否则如下图情况就不是我们想要的了
MeasureSpec.AT_MOST
当一个 View 的 layout_width 或者 layout_height 的取值为 wrap_content 时,它的测量模式就是 MeasureSpec.AT_MOST,这个时候我们需要自己根据内容计算尺寸。而 LoadButton 的内容是什么呢?它的内容有 text 还有 加载成功或者加载失败的图片。因为图片大小在形态 2 中的圆形内可以确认。所以问题的关键就在于 LoadButton 文字内容宽高的尺寸测量。
text 内容自然是居中显示,然后它距离中间的 rect 上下左右间距也要考虑。这个时候的 rect 尺寸就是相对应的文字尺寸加上相对应方向上的 padding 值,这些 padding 值通过在 attrs.xml 中自定义属性然后在布局文件中赋予。
最后整体 LoadButton 尺寸自然是中间 rect 加上左右两个半圆的半径,但是这还不是最终的尺寸,最终的尺寸还是要和 parent 给的建议尺寸比较,不能大于它。
上面分析了尺寸测量相关,所以顺着思路进行的话,编码也只是水到渠成的事情了。
public class LoadButton extends View {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//用于保存最终尺寸
int resultW = widthSize;
int resultH = heightSize;
// contentW contentH 用于确定中间矩形的尺寸
int contentW = 0;
int contentH = 0;
if ( widthMode == MeasureSpec.AT_MOST ) {
mTextWidth = (int) mTextPaint.measureText(mText);
contentW += mTextWidth + mLeftRightPadding * 2 + mRadiu * 2;
resultW = contentW < widthSize ? contentW : widthSize;
}
if ( heightMode == MeasureSpec.AT_MOST ) {
contentH += mTopBottomPadding * 2 + mTextSize;
resultH = contentH < heightSize ? contentH : heightSize;
}
resultW = resultW < 2 * mRadiu ? 2 * mRadiu : resultW;
resultH = resultH < 2 * mRadiu ? 2 * mRadiu : resultH;
// 修整圆形的半径
mRadiu = resultH / 2;
// 记录中间矩形的宽度值
rectWidth = resultW - 2 * mRadiu;
setMeasuredDimension(resultW,resultH);
Log.d(TAG,"onMeasure: w:"+resultW+" h:"+resultH);
}
}
第二步,绘制
测量是在 onMeasure() 方法中进行,而绘制就是在 onDraw() 方法中进行的,这是 Android 开发者都知道的事情。所以这一节的重点在于 onDraw() 这个方法。
为了不给读者造成困扰,我先张贴自定的属性,及在构造方法中获取属性值的代码。其它的细节应该看名字就大概知道了。
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="LoadButton">
<attr name="android:text" />
<attr name="android:textSize" />
<attr name="stroke_color" format="color|reference" />
<attr name