公司正在开发的项目中有很多这样需求的样式,例图如下:
目前的做法是使用 textview 然后设置它的 background , 在 drawable 里面设置 xml 资源来指定 颜色 、圆角 、是否填充、边框线宽等。这样的做法没有问题,常规都是这样做,有点小不爽的是只要我的颜色、圆角角度或者其他参数有任意变化我都需要去新建一个 xml 资源文件,最后资源文件特别多写起来还有一定程度的费时力。
那么开始想办法优化,需求推动技术。文章的主角自定义 View 登场了。
思考
首先看到上面这么对例图,开始思考,我大致将上述图分为两类
- 空心、半圆边框线
- 实心、矩形带圆角、左侧支持添加图片引用 (最好支持选中和非选中状态颜色文字图片变化)
选型
此自定义 View 是继承 View 还是继承 TextView ? 分析例图,居中的文字颜色等都是 TextView 具备的属性,如果继承 view 自己需要处理的部分测量绘制的工作量会更大,这里我们尝试继承于 TextView。
先来看个半成品
最初的设计我是准备将上文思考部分的的两类功能聚合在一起
于是我的自定义属性 和 代码中:
public enum ShapeMode {
/**
* 普通模式
*/
NORMAL(0),
/**
* 圆角模式
*/
FILLET(1),
/**
* 圆形模式
*/
CIRCULAR(2);
private int mode;
ShapeMode(int mode) {
this.mode = mode;
}
public int getMode() {
return mode;
}
public static ShapeMode setValue(int mode) {
for (ShapeMode s : ShapeMode.values()) {
if (mode == s.getMode()) {
return s;
}
}
return NORMAL;
}
}
<declare-styleable name="JuLiveTextView">
<attr name="Mode">
<enum name="Fillet" value="1" /> <!--圆角模式-->
<enum name="Circular" value="2" /><!--圆形模式-->
</attr>
</declare-styleable>
定义了两个模式 Fillet 和 Circular 圆角模式 和 圆形模式的枚举,并且要求必须指定其一。如果没指定两者其一就使用强制抛出异常
throw new IllegalArgumentException("ShapeMode must be specified as FILLET or CIRCULAR, otherwise use TextView");
嘿,小老弟。既不用圆角模式也不用圆形模式你还用我干嘛,回去用 TextView 去吧。
以为自己这样的设计优良,暗中自得了一把,后来在编码的过程中感觉自定义属性过于复杂,不仅要求使用者理解两个 Mode 的意思,还要求使用者针对不同的 Mode 去设置类似于
<declare-styleable name="JuLiveTextView">
......
<attr name="filletColor" format="color" /> <!--圆角模式下背景颜色-->
<attr name="filletRadius" format="dimension" /> <!--圆角模式下圆角半径-->
<attr name="circularColor" format="color" /> <!--圆形模式下边框线颜色-->
<attr name="circularLineSize" format="dimension" /> <!--圆形模式下边框线粗细-->
<attr name="circularRadius" format="dimension" /> <!--圆形模式下圆角半径-->
</declare-styleable>
不同的 Mode 模式下还需要指定不同 circularXX 或 filletXX 对开发者理解和使用成本过高,自己在写代码中逻辑也不够清晰,遂放弃这种聚合设计。
一分为二我将两个 view 分别命名为
- CircularTextView 空心、半圆边框线
- RectTextView 实心、矩形带圆角、左侧支持添加图片引用
CircularTextView
先易后难的原则先来 CircularTextView
来确定一下 CircularTextView 的自定义属性 ,我们来看几个设计稿的例图
从上面两个例子我们得出:
1 文字的颜色大小字体,我们不需要理会依赖父类 TextView 的实现
2 线框的宽高 width dp 和 height dp 也不理会依赖父类,这里没有 wrap_content 的场景
3 边框线的宽度,目前所看到的设计稿全是 1dp 在 View 中直接指定死
4 边框线的颜色,很简单同父类的 TextView
5 文字默认在 View 中设置居中
好了,综上所述得出。我们唯一需要自定义属性的就是圆角角度
<declare-styleable name="CircularTextView">
<attr name="circularRadius" format="dimension" />
</declare-styleable>
public class CircularTextView extends AppCompatTextView {
private float mCircularRadius;
private RectF circularRect;
private Paint mPaint;
public CircularTextView(Context context) {
this(context, null);
}
public CircularTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CircularTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircularTextView);
mCircularRadius = typedArray.getDimension(R.styleable.CircularTextView_circularRadius, 0);
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(getCurrentTextColor());
mPaint.setStrokeWidth(2);
mPaint.setAntiAlias(true);
circularRect = new RectF();
setGravity(Gravity.CENTER);
typedArray.recycle();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
circularRect.left = 0;
circularRect.right = getWidth();
circularRect.top = 0;
circularRect.bottom = getHeight();
canvas.drawRoundRect(circularRect, mCircularRadius, mCircularRadius, mPaint);
}
}
上真机效果图
完全符合我们的预期 ,而且使用超简单。
<com.custom.view.julivetextview.CircularTextView
android:layout_width="230dp"
android:layout_height="44dp"
android:layout_margin="10dp"
android:text="我是文案"
android:textColor="#00C0EB"
android:textSize="16sp"
app:circularRadius="22dp" />
唯一根据设计稿指定圆角 dp 即可, 利用 TextView 自身存在的属性,短短不到 40 行代码就实现了我的需求,也达到了易用性 !
注意: 点击事件符合 TextView 的点击事件,无需代码中自定义
RectTextView
来分析下 RectTextView 需要什么自定义属性
1 圆角角度
2 背景颜色
<declare-styleable name="RectTextView">
<attr name="rectRadius" format="dimension" />
<attr name="rectColor" format="color" />
</declare-styleable>
public class RectTextView extends AppCompatTextView {
private Paint mPaint;
private RectF mRect;
private float mRectRadius;
public RectTextView(Context context) {
this(context, null);
}
public RectTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RectTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RectTextView);
mRectRadius = typedArray.getDimension(R.styleable.RectTextView_rectRadius, 0);
int rectColor = typedArray.getColor(R.styleable.RectTextView_rectColor, 0);
mPaint = new Paint();
mPaint.setColor(rectColor);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
mRect = new RectF();
setGravity(Gravity.CENTER);
typedArray.recycle();
}
@Override
protected void onDraw(Canvas canvas) {
mRect.left = 0;
mRect.right = getWidth();
mRect.top = 0;
mRect.bottom = getHeight();
canvas.drawRoundRect(mRect, mRectRadius, mRectRadius, mPaint);
super.onDraw(canvas);
}
}
使用很简单
<com.custom.view.julivetextview.RectTextView
android:layout_width="118dp"
android:layout_height="42dp"
android:layout_margin="10dp"
android:text="我是文案"
android:textColor="#FFF"
android:textSize="16sp"
app:rectColor="#00C0EB"
app:rectRadius="4dp" />
效果图
这里提个小插曲,也算一个坑,在 onDraw 方法中一直将自己的绘制逻辑放在 super.onDraw 后导致 TextViev 的字样一直被 RcetF 覆盖。
以为这样就完事了,我们注意到上面例图是存在文字左侧小 icon 的图片,记得 TextView 有个 drawableLeft 的属性来试一把
惨案,由于 RcetF 是固定的 width height , 这个图片肯定会被撑起来,解决方案是让设计小姐姐切对应大小的 icon ? 不 ,这不是我们的风格
public RectTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
......
Drawable drawable = getResources().getDrawable(R.mipmap.ic_launcher);
drawable.setBounds(0, 0, (int) getTextSize(), (int) getTextSize());
setCompoundDrawablePadding(10);
setCompoundDrawables(drawable, null, null, null);
typedArray.recycle();
}
将设置的 drawable 进行边界设置,保持和字体一样宽高 ,来看效果
图案大小符合我们的预期,只是这个间距是什么鬼?
xml 和 代码中
android:drawablePadding="1dp"
setCompoundDrawablePadding(10);
都没有效果,搞不动了留待有缘人帮忙解决