自定义TextView的实现

说说需求

  • 自定义View是在Android开发中经常遇到的一种最基本功能的实现,那么,我们要如何实现一个自定义的TextView呢?这个TextView 包含两个功能,第一个是可以自定义字体颜色,大小等基本属性,第二个是根据不同的输入内容,折行居中显示。那么下面,我么就来一一实现以上两个基本功能。

说说实现过程

  1. 第一步呢,新建一个命名为AutoLinefeedTextView的类继承自View,实现他的基本构造方法。
  2. 第二步呢,在我们项目工程的/res/values/文件夹下查看是否有一个attrs 文件,若没有,则新建,否则直接打开编辑。在里面添加我们需要的自定义属性,我们在新建TextView的时候,由于我们要达到可以简单方便的在我们的布局文件中修改我们的文字内容,字体大小和字体颜色,所以,我们在自定义的时候也就需要自定义这些属性,首先对我们自定义的这个属性命名,这里我们为了方便使用,就和自定义View的名称保持一致,也命名为AutoLinefeedTextView,然后再对属性进行子属性的format定义,在自定义的format属性里,可以有很多种类,比如string,color,dimension,boolean等等,而我们自定义TextView的时候用到的很少,代码如下:

    <declare-styleable name="AutoLinefeedTextView">
        <attr name="mText" format="string"/>
        <attr name="mTextColor" format="color"/>
        <attr name="mTextSize" format="dimension"/>
    </declare-styleable>
    
  3. 完成上两步的准备工作之后,我们就要干正事啦,我们都知道,自定义View的很多时候呢,我们都要重写View里面的onDraw()方法,当然,这次也不例外。但是呢,在onDraw方法内部,我们会用到很多内容,比如画笔,比如文字,那么,onDraw函数是个经常被调用的函数,为了防止发生一直新建对象没有及时销毁导致OOM,我们在onDraw之前呢,就要首先定义好我们所需要用到的一些基本属性,那么,我们在哪里定义呢,当然是构造函数里面啦。。。接下来,我们就来初始化我们所需要用到的一些工具,我们可以创建一个叫init()的函数,方便我们在构造函数里面调用,当然,也可以构造函数之间相互调用,把初始化函数放在最终调用的构造函数里。由于在Android自定义属性的构造函数有多个,那么我们到底需要实现几个呢,一般情况下只需要实现三个,就是包含1,2,3个参数的三个,因为含有4个参数的构造函数是在Android5.1之后加进去的,我们很多时候可以不需要,那么这一次呢,我们也不实现,采用第二种方式对我们的所需对象进行初始化。

    3.1 首先呢,我们对我们所可能需要用到的东西进行声明,我们在脑海里面思考一下,自定义view我们需要用到些什么呢,和attrs里面一样,肯定有文字内容,文字大小,文字颜色,然后呢,我们要写字,,肯定需要笔啊,那么我们就还有一直画笔,然后呢,我们要写字,我们要控制文字写在哪里,呈现在什么位置,那我们肯定有个容器来承托这些内容啊,对,我们还需要一个Rect,那我们还需要什么呢,思考良久,要求是还要自动换行,怎么自动换行呢,哦,我们需要把文字内容根据屏幕大小分成一个list,然后依次显示在每一行,那么接下来,我们就开始show代码啦。

    private Rect mBound;
    private Paint mPaint;
    private int mTextSize;
    private int mTextColor;
    private String mTextStr;
    private List<String> mStrList;
    

    3.2 其次呢,我们就需要在我们的构造函数里对这些东西进行初始化啦,记得一点,我们的文字,文字大小和文字颜色我们是在自定义属性里面添加了自定义属性的,那么,我们要如何使用呢?接下来,就是我们的show time

    public AutoLinefeedTextView(Context context) {
        this(context, null);
    }
    
    public AutoLinefeedTextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }
    
    public AutoLinefeedTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    
        //  初始化我们刚才思考良久才想出来的List
        mStrList = new ArrayList<>();
        //别走神,这儿是关键,如何获取我们的自定义属性
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AutoLinefeedTextView, defStyleAttr, 0);
        //初始化我们的文字
        mTextStr = typedArray.getString(R.styleable.AutoLinefeedTextView_mText);
        //初始化我们的文字颜色,前一个参数为自定义的,如果我们在XML文件里面没有添加怎么办,没事,我们还有第二个参数,默认显示的颜色
        mTextColor = typedArray.getColor(R.styleable.AutoLinefeedTextView_mTextColor, Color.BLACK);
        //初始化我们的文字大小,和文字原理类似
        mTextSize = typedArray.getDimensionPixelSize(R.styleable.AutoLinefeedTextView_mTextSize, 100);
        //用完之后呢,方便下次使用,我们得回收一下下啦
        typedArray.recycle();
        // 对画笔进行初始化
        mPaint = new Paint();
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(mTextColor);
        //依托承载对象的工具
        mBound = new Rect();
        mPaint.getTextBounds(mTextStr, 0, mTextStr.length(), mBound);
    }
    

    3.3 再其次,正如前面所说,我们需要重写View的OnDraw函数,又由于我们需要居中显示,我们还得重写onMeasure函数,如果对onMeasure里面的MeasureSpec.EXACTLY不太懂的话呢,记得百度哦,很多博客哒,哈哈,这里假设我们都懂。再次show time。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    
        canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
        int width;
        int height;
        if (widthMode == MeasureSpec.EXACTLY){
            width = widthSize;
        } else {
            float textWidth = mBound.width();
            width = (int) (getPaddingLeft() + textWidth + getPaddingRight());
        }
    
        if (heightMode == MeasureSpec.EXACTLY){
            height = heightSize;
        } else {
            float textHeight = mBound.height();
            height = (int) (getPaddingTop() + textHeight + getPaddingBottom());
        }
    
        setMeasuredDimension(width, height);
    }
    

    3.4 完成以后,我们发现,诶,并没有用到我们的list啊,对,为什么呢?因为我们这个方式的显示时不正常的啦,我们会看到东西只有一行展示且显示不全,那么接下来,我们将要分析一下,如何折行显示并显示完全。。。想想看,我们应该按照怎样的逻辑把我们的一堆文字拆分成一个列表呢,思考一下我们会发现,我们的文字内容是一定的,我们的承托工具宽度是一定的,那么刚好,分成几行,那不就是显示完所有文字所需要的宽度/承托工具的宽度的行数么?可是,除不尽怎么办?没事,向上取整就好啦,这里呢,我们采取另一个办法,我们看我们的除法发生后,获取的值是否包含小数点,如果不包含,那么代表刚好整除,结果就是行数,如果包含,那么我们选择小数点前面的数字+1的方式作为行数。。。毕竟我们需要把所有东西都显示出来啊。。。知道了多少行,那么每一行显示些什么内容呢?难道文字那么乖乖听话自己就换行啦?当然不可能。那么我们如何拆分文字呢,那就要用到我们超级强大的subString函数啦,我们在知道函数以后,采用一个循环,把我们的文字拆分成一个列表装进我们刚才已经定义好的list里面,但是还有一点需要谨记,我们拆分一次之后,原字符串也得subString哦,为什么呢,因为前面的文字已经被显示出来啦,不然一直重复显示,就达不到完全显示的效果啦。。思路总结完毕,那么接下开又是我们的show time。睁大双眼,别眨眼哦

    boolean isOpenLines = true;//这是用来判断是否需要折行显示的变量
    float lineNum;
    float splineNum;
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
        float textWidth = mBound.width();
        if (mStrList.size() == 0){
            int padding = getPaddingLeft() + getPaddingRight();
            int specWidth = widthSize - padding;
            if (textWidth < specWidth){
                lineNum = 1;
                mStrList.add(mTextStr);
            } else {
                isOpenLines = false;
                splineNum = textWidth / specWidth;
                String splineNumStr = splineNum + "";
                if (splineNumStr.contains(".")){
                    lineNum = Integer.parseInt(splineNumStr.substring(0, splineNumStr.indexOf("."))) + 1;
                } else {
                    lineNum = splineNum;
                }
                int lineLength = (int)(mTextStr.length() / splineNum);
                for (int i = 0; i < lineNum; i ++){
                    String lineStr;
                    if (mTextStr.length() < lineLength){
                        lineStr = mTextStr.substring(0, mTextStr.length());
                    } else {
                        lineStr = mTextStr.substring(0, lineLength);
                    }
    
                    mStrList.add(lineStr);
    
                    if (!TextUtils.isEmpty(lineStr)){
                        if (mTextStr.length() < lineLength){
                            mTextStr = mTextStr.substring(0, mTextStr.length());
                        } else {
                            mTextStr = mTextStr.substring(0, lineLength);
                        }
                    } else {
                        break;
                    }
                }
            }
        }
    
        int width;
        int height;
    
        if (widthMode == MeasureSpec.EXACTLY){
            width = widthSize;
        } else {
            if (isOpenLines){
                width = (int) (getPaddingLeft() + textWidth + getPaddingRight());
            } else {
                width = widthSize;
            }
        }
    
        if (heightMode == MeasureSpec.EXACTLY){
            height = heightSize;
        } else {
            float textHeight = mBound.height();
            if (isOpenLines){
                height = (int) (getPaddingTop() + textHeight + getPaddingBottom());
            } else {
                height = (int) (getPaddingTop() + textHeight * lineNum + getPaddingBottom());
            }
        }
    
        setMeasuredDimension(width, height);
    }
    

    3.5 当然,我们的onMeasure完成改造之后,我们的onDraw也不能落下啊。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    
        for (int i = 0; i < mStrList.size(); i ++){
            mPaint.getTextBounds(mStrList.get(i), 0, mStrList.get(i).length(), mBound);
            canvas.drawText(mStrList.get(i), (getWidth() / 2 - mBound.width() / 2), (getPaddingTop() + (mBound.height() * i)), mPaint);
        }
    }
    

    3.6 以上几个步骤,就完成了我们的自定义布局的显示啦,那么剩下的最后一步,大家肯定都会的哟,就是在我们需要用到我们自定义样式的布局的位置,把它加进去哟。。。完成以后,就大功告成啦。。。。开不开心,激不激动,简不简单。。。O(∩_∩)O哈哈哈~

说说感想

  • 以前一直觉得自定义布局好难啊,根本不知道从何下手,亲自实践过后发现,也就那么回事嘛,多动手,多实践,实践出真知这句话,在哪里都适用。。。回过头来想一想,自定义TextView不就是三个那么几个小步骤么?首先新建一个类,然后在attrs下建立一个自己需要的自定义内容,再然后在我们的构造函数里对我们所需要用到的东西初始化,然后如果内容需要绘制,就重写我们的onDraw函数,如果我们的布局大小需要计算整合,就再重写我们的onMeasure函数,如果还有,我们的布局位置需要改变,那么我们就再重写我们的onLayout函数。。。完成以上几步,那么我们的自定义textview也就基本实现啦。。。。万事开头难,开始了一切就简单啦。。。