自定义View
是分两种的,一种是继承自View
,一种是继承自ViewGroup
。之前我写过关于继承自ViewGroup
的博客,同时还有流式布局实战(流式布局讲解链接)。那么这一篇主要是来继承自View
的TextView
绘制。
这边为了演示方便,就继承自AppCompatTextView
。因为这样可以不用重写onMeasure
。把重心放在draw
上面,即只需要重写onDraw
。
两个东西,画布和画笔,即Canvas
和Paint
。这两个东西就不过多介绍了
OK,下面让我们走进自定义TextView
我们在Canvas
的drawText
方法中,有一个参数是y
。这个y
,就是baseLine
,也就是基准线。
那么啥玩意是基准线呢?我们看这么一张图
上面有标出baseLine
在哪。通俗来讲,就是所有文字都差不多根据这根线对齐。当然并不是说完全对齐,也不是说完全不超过这根线,但是可以确定的是,所有的文字,是以它为基准的。可以把它看作针对自定义TextView
中坐标系的0
。
//画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
最左边(即x
)为getWidth()/2
。如果设置的话就是TextView
的中间为getWidth()/2
,即
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());
}
这四个东西可以理解成四条线。先放一下源码的意思
//此类是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,不太懂没关系,看下面的图
那条红色的长线就是baseLine
,前面也介绍过。我们发现这两个字,英文字母上下没有超过ascent
和descent
,更没有超过top
和bottom
。也就是说正常情况下,一些常见的字比如汉字,英文单词,它的上下是不会超过ascent
和descent
的。而top
比ascent
还要靠上,bottom
比descent
还要靠下,所以更不会超过top
和bottom
。
再看这张图,这个文字就不是我们平时常见的了。你会发现它上面好像超过了ascent
,下面超过了descent
。但是始终没有超过top
和bottom
。所以我们可以得到这样的结论
ascent
和descent
的。而非常见语言(例如阿拉伯语等),它的范围是有可能会超过ascent
和descent
,但是不会超过top
和bottom
的。所以我们平时还是重点关注ascent和descent。
那么怎么利用ascent
和descent
,将文字垂直居中呢?我们来看下面一张图
我们这里就只管ascent
和descent
。如果将baseLine
设置为getHeight()/2
,那么它就会在如图所示位置,这显然是不垂直居中的。所以我们要将baseLine
在此基础上,减去descent
,也就是上移从baseLine
到descent
的距离,让文字的下界和getHeight()/2
对齐
然后会发现它的descent
到了getHeight()/2
的位置,利用我们的数学思维,很容易就能想出,再将此时的textView
下移文字高度的一半,它就可以居中了,即+(descent-ascent)/2
,
所以说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);
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");
}
}
clipRect
函数是android.graphics.Canvas
类下一个用于对画布进行矩形裁剪的方法。 它裁剪了我们想要的绘制区域 。说白了就是让这个画布的尺寸,小于或等于原尺寸。然后只显示裁剪之后的部分(这个东西我们平时裁剪图片的时候也很常用)
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开始属性动画
}
}
效果如下
但是这样会可能导致内存抖动,因为onDraw
会调用多次,而且很多new
。内存抖动可能会导致耗电,卡顿,甚至崩溃等负面影响,所以我们得想办法解决。
我们要避免过度绘制(即同一个像素点,绘制了多次)。如果把这个打开
然后显示粉色或红色,一般就是过度绘制了
如果是红色的,那么一定是要修改的。我们这里是显示绿色,因为我们绘制了两次,即底层黑色一直都有,然后上层红色慢慢覆盖,一共是两层。而实际上是没这个必要的,我们绘制红色部分的时候,被覆盖掉的黑色部分是可以去掉的。
更改后的代码(注意这里别忘了使用save
和restore
,来保存图层)
//将文字写到中央
//主要是这一个方法里面改动,下面那个方法就加了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();
}