在很多安卓岗位的职位描述上,都会提到一个“自定义控件”。这个东西上手其实并不难,但真想做好自定义控件,要会的东西还挺多。下面我就分享个简单的例子,给自己,也给需要的人。
需求:服务端传过来的数据长这个样子:
这是一个<important>甲方爸爸</important>特别<important>强调</important>的需求
在TextView中要显示成这个样子:
![4da27b1b80c1f818f304deea626db6a6.png](https://i-blog.csdnimg.cn/blog_migrate/30d8ecfbe16906be45ecc5e54492135b.png)
下面就是我的进化之路:
零、不使用自定义控件
其实就是简单的文字替换,Android中有一个Spannable
public static Spanned important(String text, String color) {
if ( text.split("<important>").length < 2) return new SpannableString(important);
text = text.replace("/ important", "/font></html") .replace(" important", "html><font color="" + color + """);
return Html.fromHtml(text);
}
纯属一个方法,于是在那个页面里,满眼望去全是important:
tvName.setText(important(object.getString("name")));
tvJob.setText(important(object.getString("job")));
tvDevice.setText(important(object.getString("device")));
然后就引出了第一种自定义控件
一、扩展控件
最简单的,是基于现有控件进行控件扩展。既然方法已经写好了,继承原来的TextView写一个新的控件,把setText()方法重写一下就好啦:
public class ImportantTextView extends AppCompatTextView {
public ImportantTextView(Context context) {
super(context);
}
public ImportantTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public ImportantTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setText(String text) {
setImportantText(text, "#ff8200");
}
public void setImportantText(String text, String color) {
if (text.split("<mportant>").length < 2) {
setText(text);
} else {
text = text.replace("/mportant", "/font></html").replace("em", "html><font color="" + color + """);
setText(Html.fromHtml(text));
}
}
}
补充:为什么继承AppCompatTextView而不是TextView?
AppCompatTextView是在API level 23引入的,继承自TextView,它是Android标准TextView的增强,特点就是可以自适应字体宽度大小变化。有一点要提的是,在android官方文档中,xml里写的传统TextView已经被编译器替换成AppCompatTextView了,不需要开发者再去手动替换。
另外,如果自定义控件上面继承TextView,会报错的。。。
但是,就这么写一个控件实在是拿不出手,其实就相当于把外面的方法放到了控件里,太没技术含量了
二、重新绘制控件
先重写onLayout():
略,因为没写~
再重写onMeasure():
略,因为也没写~
上面这两个方法,前者用来确定控件在父控件的位置,后者用来测量控件的宽高大小。
在这里我只用到了onDraw()方法:
@Override
protected void onDraw(Canvas canvas) {
if (text == null || text.isEmpty())
return; // 如果没有输入文字,也就没有意义画出控件了
if (text.split("<important>").length < 2)
canvas.drawText(text, 0, getHeight(), mPaint); // 如果没有重点标签或者标签只出了一个,也没有必要进行加工了
else {
String[] t = text.replace("/important", "")
.replace("important", "")
.split("<>");
float x = 0.0f;
for (int i = 0; i < t.length; i++) {
int textWidth = 0;
canvas.save();
if (i % 2 == 0) {
canvas.drawText(t[i], x, getY(), mPaint);
textWidth = getTextWidth(mPaint, t[i]);
} else {
canvas.drawText(t[i], x, getY(), mEMPaint);
textWidth = getTextWidth(mEMPaint, t[i]);
}
x = x + textWidth;
canvas.restore();
}
}
requestLayout();
}
在绘制的时候就直接把颜色涂到上面,这样比之前的更显技术含量~
没完,其实还有第三种
三、基于控件容器的自定义控件
最简单,效率一般,可读性一般,功能性简单,非通用方法
在这个需求要怎么写呢?
先要有一个布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:id="@+id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/text3"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/text4"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/text5"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/text6"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/text7"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/text8"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/text9"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
而控件代码如下:
public ImportantTextView3(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LayoutInflater.from(context).inflate(R.layout.layout_important, this, true);
tvTexts = new ArrayList<>();
tvTexts.add((TextView) findViewById(R.id.text1));
tvTexts.add((TextView) findViewById(R.id.text2));
tvTexts.add((TextView) findViewById(R.id.text3));
tvTexts.add((TextView) findViewById(R.id.text4));
tvTexts.add((TextView) findViewById(R.id.text5));
tvTexts.add((TextView) findViewById(R.id.text6));
tvTexts.add((TextView) findViewById(R.id.text7));
}
public void setText(String text) {
if (text == null || text.isEmpty())
return;
if (text.split("<important>").length < 2) {
tvTexts.get(0).setText(text);
} else {
String[] t = text.replace("/important", "").replace("important", "").split("<>");
int i = 0;
while (i < t.length && i < tvTexts.size() - 1) {
if (i % 2 != 0)
tvTexts.get(i).setTextColor(0xffff8200);
tvTexts.get(i).setText(t[i]);
i++;
}
if (i >= tvTexts.size() - 1) {
StringBuilder last = new StringBuilder();
for (; i < t.length; i++) {
last.append(t[i]);
}
tvTexts.get(tvTexts.size() - 1).setText(last);
}
}
}
简单粗暴,就是获得控件,给控件赋值。这种自定义控件可读性极强,但复用性极差,其实就是把很多原生空间整合到了一个控件容器,然后在项目中以整体形式出现。
这个控件不适用于这个需求,但并不代表这个方法没有用,通常这种做法用在列表控件或者重复性的布局上。
补充说明:
如果:
这段<important>文字</important>是<important>这个<important>样子的
我这里显示:
![5292a1d2727eb9b41ed0f58d01a2dfbf.png](https://i-blog.csdnimg.cn/blog_migrate/3ab0193a27cc79127f0654318da6cf15.png)
但按照需求应该:
![9a231cfc03e00bdd826860a19ec7bed9.png](https://i-blog.csdnimg.cn/blog_migrate/a968b730df9e8c8ecd630672ece0ad8b.png)
这个是不太符合需求的,不过这个是线上项目,服务端的大哥指着房顶中央空调跟我保证过不会有这种情况,所以暂且忽略掉。
而且我也没想好如果真是这样该怎么处理,希望看到的能回贴帮我想一下,谢谢~
下面贴出第二套方案的完整代码:
package com.myprj.important.ImportantTextView;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.Html;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
import com.myprj.important.R;
public class ImportantTextView2 extends AppCompatTextView {
private Paint mPaint, mImportantPaint;
private String text;
public ImportantTextView2(Context context) {
this(context, null);
}
public ImportantTextView2(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ImportantTextView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint(context, attrs);
}
private void initPaint(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.importantTextView2);
int color = array.getColor(R.styleable.ImportantTextView2_color, getTextColors().getDefaultColor());
int importantColor = array.getColor(R.styleable.ImportantTextView2_importantColor, 0xffff8200);
mPaint = getPaintByColor(color);
mImportantPaint = getPaintByColor(importantColor);
array.recycle();
}
private Paint getPaintByColor(int color) {
Paint paint = new Paint();
paint.setColor(color);
paint.setAntiAlias(true);
paint.setDither(true);
paint.setTextSize(getTextSize());
return paint;
}
@Override
protected void onDraw(Canvas canvas) {
if (text == null || text.isEmpty())
return;
if (text.split("<important>").length < 2)
canvas.drawText(text, 0, getHeight(), mPaint);
else {
String[] t = text.replace("/important", "").replace("important", "").split("<>");
float x = 0.0f;
for (int i = 0; i < t.length; i++) {
int textWidth = 0;
canvas.save();
if (i % 2 == 0) {
canvas.drawText(t[i], x, getY(), mPaint);
textWidth = getTextWidth(mPaint, t[i]);
} else {
canvas.drawText(t[i], x, getY(), mImportantPaint);
textWidth = getTextWidth(mImportantPaint, t[i]);
}
x = x + textWidth;
canvas.restore();
}
}
requestLayout();
}
public static int getTextWidth(Paint paint, String str) {
int iRet = 0;
if (str != null && str.length() > 0) {
int len = str.length();
float[] widths = new float[len];
paint.getTextWidths(str, widths);
for (int j = 0; j < len; j++) {
iRet += (int) Math.ceil(widths[j]);
}
}
return iRet;
}
public void setText(String text) {
this.text = text;
super.setText(text);
}
}