一篇文章带你掌握自定义TextView

前言

自定义View是分两种的,一种是继承自View,一种是继承自ViewGroup。之前我写过关于继承自ViewGroup的博客,同时还有流式布局实战(流式布局讲解链接)。那么这一篇主要是来继承自ViewTextView绘制。
这边为了演示方便,就继承自AppCompatTextView。因为这样可以不用重写onMeasure。把重心放在draw上面,即只需要重写onDraw
两个东西,画布和画笔,即CanvasPaint。这两个东西就不过多介绍了
OK,下面让我们走进自定义TextView

1.baseLine

我们在CanvasdrawText方法中,有一个参数是y。这个y,就是baseLine,也就是基准线
一篇文章带你掌握自定义TextView_第1张图片
那么啥玩意是基准线呢?我们看这么一张图
一篇文章带你掌握自定义TextView_第2张图片
上面有标出baseLine在哪。通俗来讲,就是所有文字都差不多根据这根线对齐。当然并不是说完全对齐,也不是说完全不超过这根线,但是可以确定的是,所有的文字,是以它为基准的。可以把它看作针对自定义TextView中坐标系的0

2.如何将TextView水平对齐

为了方便显示效果,我们先用一个方法,画出X轴的中心线
	//画x轴的中心线
    private void drawCenterLineX(final Canvas canvas){
     
        Paint paint = new Paint();
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.RED);
        paint.setStrokeWidth(3);

        canvas.drawLine(getWidth()/2,0,getWidth()/2,getHeight(),paint);
    }
方式①:更改文字对齐方式
    float x = getWidth()/2;
	paint.setTextAlign(Paint.Align.CENTER);
	canvas.drawText(mText,x,99,paint);

即设置为中间对齐。默认为左对齐,如果不设置,即没有使用setTextAlign方法的话,会是这种情况
一篇文章带你掌握自定义TextView_第3张图片

即采用的默认的TextView最左边(即x)为getWidth()/2。如果设置的话就是TextView的中间为getWidth()/2,即
一篇文章带你掌握自定义TextView_第4张图片

方式②:利用TextView宽度
		float x = getWidth()/2 - paint.measureText(mText)/2;
        canvas.drawText(mText,x,99,paint);

这个逻辑很简单我就不再解释了。measureText方法为测量文字宽度,源码为

	/**
    * Return the width of the text.
    *
    * @param text  The text to measure. Cannot be null.
    * @return      The width of the text
    */
    public float measureText(String text) {
     
        if (text == null) {
     
            throw new IllegalArgumentException("text cannot be null");
        }
        return measureText(text, 0, text.length());
    }

3.如何将TextView竖直对齐

首先要了解四个变量的含义:top、bottom、ascent、descent

这四个东西可以理解成四条线。先放一下源码的意思

	//此类是Paint.FontMetrics,它保存着TextView的一些尺寸信息;
	/**
     * Class that describes the various metrics for a font at a given text size.
     * Remember, Y values increase going down, so those values will be positive,
     * and values that measure distances going up will be negative. This class
     * is returned by getFontMetrics().
     */
    public static class FontMetrics {
     
        /**
         * The maximum distance above the baseline for the tallest glyph in
         * the font at a given text size.
         */
        public float   top;
        /**
         * The recommended distance above the baseline for singled spaced text.
         */
        public float   ascent;
        /**
         * The recommended distance below the baseline for singled spaced text.
         */
        public float   descent;
        /**
         * The maximum distance below the baseline for the lowest glyph in
         * the font at a given text size.
         */
        public float   bottom;
        /**
         * The recommended additional space to add between lines of text.
         */
        public float   leading;
    }

ok,不太懂没关系,看下面的图
一篇文章带你掌握自定义TextView_第5张图片
那条红色的长线就是baseLine,前面也介绍过。我们发现这两个字,英文字母上下没有超过ascentdescent,更没有超过topbottom。也就是说正常情况下,一些常见的字比如汉字,英文单词,它的上下是不会超过ascentdescent的。而topascent还要靠上,bottomdescent还要靠下,所以更不会超过topbottom

一篇文章带你掌握自定义TextView_第6张图片
再看这张图,这个文字就不是我们平时常见的了。你会发现它上面好像超过了ascent,下面超过了descent。但是始终没有超过topbottom。所以我们可以得到这样的结论

  • 常见语言(例如汉语和英语),它的范围是不会超过 ascentdescent的。而非常见语言(例如阿拉伯语等),它的范围是有可能会超过ascentdescent,但是不会超过topbottom的。

所以我们平时还是重点关注ascent和descent。
那么怎么利用ascentdescent,将文字垂直居中呢?我们来看下面一张图
一篇文章带你掌握自定义TextView_第7张图片
我们这里就只管ascentdescent。如果将baseLine设置为getHeight()/2,那么它就会在如图所示位置,这显然是不垂直居中的。所以我们要将baseLine在此基础上,减去descent,也就是上移从baseLinedescent的距离,让文字的下界和getHeight()/2对齐
一篇文章带你掌握自定义TextView_第8张图片
然后会发现它的descent到了getHeight()/2的位置,利用我们的数学思维,很容易就能想出,再将此时的textView下移文字高度的一半,它就可以居中了,即+(descent-ascent)/2,
一篇文章带你掌握自定义TextView_第9张图片
所以说baseLine这条线,经历了getHeight()/2,然后上移动descent,然后下降(descent-ascent)/2。即此时的baseLine = getHeight()/2 - descent + (descent-ascent)/2。化简后就是baseLine = getHeight()/2 -(descent+ascent)/2。具体到代码中,就是

		//让他垂直居中
        Paint.FontMetrics fontMetrics = paint.getFontMetrics();
        float baseLine = getHeight()/2 -(fontMetrics.descent + fontMetrics.ascent)/2;
        //让他水平居中
        float x = getWidth()/2 - paint.measureText(mText)/2;
        canvas.drawText(mText,x,baseLine,paint);

效果
一篇文章带你掌握自定义TextView_第10张图片

4.Canvas

Canvas相当于PS当中的图层,它不一定只有一个。由于它在这里不是重点,所以我就暂时不深究。但是它有两个比较重要的方法,需要掌握。
canvas.save();保存已有的画布状态。

	/**
     * Saves the current matrix and clip onto a private stack.
     * 

* Subsequent calls to translate,scale,rotate,skew,concat or clipRect, * clipPath will all operate as usual, but when the balancing call to * restore() is made, those calls will be forgotten, and the settings that * existed before the save() will be reinstated. * * @return The value to pass to restoreToCount() to balance this save() */ public int save() { return nSave(mNativeCanvasWrapper, MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG); }

看他源码注释的第一行,能够大概知道,save方法是把当前的状态参数保存到一个栈里面。
canvas.restore()恢复画布状态(如果save()3次,只调一次restore()方法只能恢复最后一次save()的状态,调2次才会恢复到第二次save()的状态)

	/**
     * This call balances a previous call to save(), and is used to remove all
     * modifications to the matrix/clip state since the last save call. It is
     * an error to call restore() more times than save() was called.
     */
    public void restore() {
     
        if (!nRestore(mNativeCanvasWrapper)
                && (!sCompatibilityRestore || !isHardwareAccelerated())) {
     
            throw new IllegalStateException("Underflow in restore - more restores than saves");
        }
    }

5.clipRect实现文字渐变

clipRect函数是android.graphics.Canvas类下一个用于对画布进行矩形裁剪的方法。 它裁剪了我们想要的绘制区域 。说白了就是让这个画布的尺寸,小于或等于原尺寸。然后只显示裁剪之后的部分(这个东西我们平时裁剪图片的时候也很常用)
一篇文章带你掌握自定义TextView_第11张图片

  • 因为画布是分层的,我们渐变的思路是,弄一个黑色的底层,一个彩色的上层。然后利用clipRect方法,逐渐从彩色一层的最左边开始剪裁,缓缓向右剪裁,这样红色一层就慢慢显现,就实现了渐变的效果了。

自定义TextView完整代码如下

public class SimpleColorChangeTextView extends AppCompatTextView {
     
    String mText = "我最帅了";
	
	private float mPercent = 0.0f;

	//get&set
    public float getPercent() {
     
        return mPercent;
    }

    public void setPercent(float mPercent) {
     
        this.mPercent = mPercent;
        invalidate();
    }

   
	//构造方法
    public SimpleColorChangeTextView(@NonNull Context context) {
     
        super(context);
    }

    public SimpleColorChangeTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
     
        super(context, attrs);
    }

    public SimpleColorChangeTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
     
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
     
        super.onDraw(canvas);
        //画水平中心线和垂直中心线
        drawCenterLineX(canvas);
        drawCenterLineY(canvas);

        //画底层黑色
        drawTextCenter(canvas);

        //画上面的一层彩色文字
        drawColorTextCenter(canvas);

    }
    
    //将文字写到中央
    private void drawTextCenter(Canvas canvas){
     
        //创建画笔
        Paint paint = new Paint();
        paint.setTextSize(80);
        //让他垂直居中
        Paint.FontMetrics fontMetrics = paint.getFontMetrics();
        float baseLine = getHeight()/2 -(fontMetrics.descent + fontMetrics.ascent)/2;
        //让他水平居中
        float x = getWidth()/2 - paint.measureText(mText)/2;

        canvas.drawText(mText,x,baseLine,paint);
    }

    //将彩色文字写到中央
    private void drawColorTextCenter(Canvas canvas){
     
        //canvas.save();
        //创建画笔
        Paint paint = new Paint();
        paint.setTextSize(80);
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.RED);
        paint.setAntiAlias(true);//设置抗锯齿
        //让他垂直居中
        Paint.FontMetrics fontMetrics = paint.getFontMetrics();
        float baseLine = getHeight()/2 -(fontMetrics.descent + fontMetrics.ascent)/2;
        //让他水平居中
        float x = getWidth()/2 - paint.measureText(mText)/2;

        //将裁剪的右边界动态化
        float right = x + mPercent*(paint.measureText(mText));
        Rect rect = new Rect((int)x,0,(int)right,getHeight());
        canvas.clipRect(rect);

        canvas.drawText(mText,x,baseLine,paint);
        //canvas.restore();
    }

    //画x轴的中心线
    private void drawCenterLineX(final Canvas canvas){
     
        Paint paint = new Paint();
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.RED);
        paint.setStrokeWidth(3);

        canvas.drawLine(getWidth()/2,0,getWidth()/2,getHeight(),paint);
    }

    //画y轴的中心线
    private void drawCenterLineY(final Canvas canvas){
     
        Paint paint = new Paint();
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.RED);
        paint.setStrokeWidth(3);

        canvas.drawLine(0,getHeight()/2,getWidth(),getHeight()/2,paint);
    }
}

然后我们在MainActivity中使用属性动画,动态改变mPercent 的值(别忘了invalidate)。

public class MainActivity extends AppCompatActivity {
     

    @Override
    protected void onCreate(Bundle savedInstanceState) {
     
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        View mView = findViewById(R.id.myView);

        Handler handler = new Handler();
        handler.postDelayed(new Runnable() {
     
            @Override
            public void run() {
     
                //属性动画
                ObjectAnimator.ofFloat(mView,"percent",0,1).setDuration(5000).start();

            }
        },2000);//延迟2s开始属性动画
    }
}

效果如下
一篇文章带你掌握自定义TextView_第12张图片
但是这样会可能导致内存抖动,因为onDraw会调用多次,而且很多new。内存抖动可能会导致耗电,卡顿,甚至崩溃等负面影响,所以我们得想办法解决。

6.优化

我们要避免过度绘制(即同一个像素点,绘制了多次)。如果把这个打开
一篇文章带你掌握自定义TextView_第13张图片
然后显示粉色或红色,一般就是过度绘制了
在这里插入图片描述
如果是红色的,那么一定是要修改的。我们这里是显示绿色,因为我们绘制了两次,即底层黑色一直都有,然后上层红色慢慢覆盖,一共是两层。而实际上是没这个必要的,我们绘制红色部分的时候,被覆盖掉的黑色部分是可以去掉的
更改后的代码(注意这里别忘了使用saverestore,来保存图层)

	//将文字写到中央
	//主要是这一个方法里面改动,下面那个方法就加了save和restore方法
    private void drawTextCenter(Canvas canvas){
     
        canvas.save();
        //创建画笔
        Paint paint = new Paint();
        paint.setTextSize(80);
        //让他垂直居中
        Paint.FontMetrics fontMetrics = paint.getFontMetrics();
        float baseLine = getHeight()/2 -(fontMetrics.descent + fontMetrics.ascent)/2;
        //让他水平居中
        float x = getWidth()/2 - paint.measureText(mText)/2;

        //避免过度绘制
        float left = x + (paint.measureText(mText))*mPercent;
        Rect rect = new Rect((int)left,0,getWidth(),getHeight());
        canvas.clipRect(rect);
        canvas.drawText(mText,x,baseLine,paint);
        canvas.restore();
    }

    //将彩色文字写到中央
    private void drawColorTextCenter(Canvas canvas){
     
        canvas.save();
        //创建画笔
        Paint paint = new Paint();
        paint.setTextSize(80);
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.RED);
        paint.setAntiAlias(true);//设置抗锯齿
        //让他垂直居中
        Paint.FontMetrics fontMetrics = paint.getFontMetrics();
        float baseLine = getHeight()/2 -(fontMetrics.descent + fontMetrics.ascent)/2;
        //让他水平居中
        float x = getWidth()/2 - paint.measureText(mText)/2;

        //将裁剪的右边界动态化
        float right = x + mPercent*(paint.measureText(mText));
        Rect rect = new Rect((int)x,0,(int)right,getHeight());
        canvas.clipRect(rect);

        canvas.drawText(mText,x,baseLine,paint);
        canvas.restore();
    }

7.与自定义TextView相关的API总结

一篇文章带你掌握自定义TextView_第14张图片

你可能感兴趣的:(Android高级UI,canvas,移动开发,安卓,面试)