安卓开发笔记——自定义控件学习小结

在很多安卓岗位的职位描述上,都会提到一个“自定义控件”。这个东西上手其实并不难,但真想做好自定义控件,要会的东西还挺多。下面我就分享个简单的例子,给自己,也给需要的人。

需求:服务端传过来的数据长这个样子:
这是一个<important>甲方爸爸</important>特别<important>强调</important>的需求
在TextView中要显示成这个样子:
这是一个\color{#ff8200}{甲方爸爸}特别\color{#ff8200}{强调}的需求

就这个需求

下面就是我的进化之路:

零、不使用自定义控件

其实就是简单的文字替换,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>样子的
我这里显示:这段\color{#ff8200}{文字}\color{#ff8200}{这个}样子的
但按照需求应该:这段\color{#ff8200}{文字}是<important>这个<important>样子的

这个是不太符合需求的,不过这个是线上项目,服务端的大哥指着房顶中央空调跟我保证过不会有这种情况,所以暂且忽略掉。
而且我也没想好如果真是这样该怎么处理,希望看到的能回贴帮我想一下,谢谢~


下面贴出第二套方案的完整代码:

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);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352