public class MyTextView1 extends View { private final static String TAG = "MyTextView"; //文字 private String mText; //文字的颜色 private int mTextColor; //文字的大小 private int mTextSize; //绘制的范围 private Rect mBound; private Paint mPaint; private int start = 0; int width; int height; private int textWidth; private int textHeight; public MyTextView1(Context context) { this(context, null); } public MyTextView1(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public MyTextView1(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyTextView); try { mText = a.getString( R.styleable.MyTextView_text); mTextColor = a.getColor( R.styleable.MyTextView_textColor, 0xffc6c6c6); mTextSize = (int) a.getDimensionPixelSize(R.styleable.MyTextView_textSize, (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics())); Log.d("MyTextView", "--textSize--" + mTextSize); } finally { a.recycle(); } init(); } /** * 初始化数据 */ private void init() { Log.i(TAG, "init :" ); //初始化Paint数据 mPaint = new Paint(); mPaint.setColor(mTextColor); mPaint.setTextSize(mTextSize); //获取绘制的宽高 } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); Log.i(TAG, "onMeasure :" ); mBound = new Rect(); mPaint.getTextBounds(mText, 0, mText.length(), mBound); width = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); height = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(widthMeasureSpec); // Log.d("MyTextView", "------mBound.height-----" + mBound.height() + "---paint width-----" + mPaint.measureText(mText)); if (widthMode == MeasureSpec.AT_MOST) { textWidth = (int) (getPaddingLeft() + getPaddingRight() +mPaint.measureText(mText)); } else { textWidth = width; } if (heightMode == MeasureSpec.AT_MOST) { Paint.FontMetrics fm = mPaint.getFontMetrics(); textHeight = (int) (getPaddingBottom() + getPaddingTop() + fm.bottom-fm.top); // Log.d("MyTextView", "------fm.ascent+" + fm.ascent + "---paint height+++" + fm.descent); } else { textHeight = height; } setMeasuredDimension(textWidth, textHeight); // Log.d("MyTextView", "------textWidth-----" + textWidth + "---textHeight-----" + textHeight); } @Override protected void onDraw(Canvas canvas) { // Log.i(TAG, "onDraw :" + getPaddingTop()); String subText = mText.substring(0, start); String subTextColor = mText.substring(start, mText.length()); Paint.FontMetrics fm = mPaint.getFontMetrics(); //绘制文字 canvas.drawText(subText,0,getHeight() / 2 -fm.descent + (fm.bottom - fm.top)/2 , mPaint); canvas.save(); mPaint.setColor(mTextColor); canvas.translate( mPaint.measureText(subText), 0); canvas.drawText(subTextColor, 0, getHeight() / 2 -fm.descent + (fm.bottom - fm.top)/2 , mPaint); canvas.restore(); //注意一下我们这里的getWidth()和getHeight()是获取的px Log.i(TAG, "fm():" +(fm.bottom - fm.top)); } public void setmText(String text) { this.mText = text; // invalidate(); } public void setStart(int start) { this.start = start; // invalidate(); } public void setTextColor(int color) { this.mTextColor = color; Log.i(TAG, "onDraw :setTextColor" ); // requestLayout(); } @Override protected void onFinishInflate() { super.onFinishInflate(); Log.i(TAG, "onFinishInflate :" ); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); Log.i(TAG, "onSizeChanged :" ); } }
好了,根据那些老鸟的方法写出来了,那么运行一下看看结果。 为了更好的查看效果,加上原生TextView做对比
很明显可以看出自定义的宽度小了,高度也不够,宽高文字都不能完整的绘制。
获取很多人看到这个会觉得奇怪,以前没有发现这种效果,因为这里宽高设置为wrap_content,并且没有padding,如果设置了padding或许很难看出这些细微的效果,因此很多开发者以为这就是满意的效果了。
2.绘制水平,垂直居中文本
之前我也以为绘制文本嘛,再简单不过的啦,深入研究一下才发现,哎哟,有文章哦。
OK,说一下解决思路吧。上图所示,宽高都出现了问题,都偏小了。这里宽度问题比较容易解决,高度才比较麻烦。
2.1宽度偏小
宽度偏小是因为文字测量出现了误差,
原始方式,这是一种粗略的文字宽度计算
value = mBound.width();
- 1
改进,这是比较精确的测量文字宽度的方式
value = mPaint.measureText(mText);
- 1
开发者可以自行打印对比一下 mBound.width(); 和 mPaint.measureText(mText); 的值。
上图中,第1个是原生TextView,第2个是修改的过的,第三个是没有修改的,明显看到宽度已经和原生一样,
而且最后一个文字也完整绘制出来了。第三个可以
2.2高度偏小
高度偏小就比较麻烦了。不是一行代码可以解决的了
先了解一下Android是怎么样绘制文字的,这里涉及到几个概念,分别是文本的top,bottom,ascent,descent,baseline。
看下面的图(摘自网络):
解释一下这张图片。(摘自网络)
Baseline是基线,在Android中,文字的绘制都是从Baseline处开始的,Baseline往上至字符“最高处”的距离我们称之为ascent(上坡度),Baseline往下至字符“最低处”的距离我们称之为descent(下坡度);
leading(行间距)则表示上一行字符的descent到该行字符的ascent之间的距离;
top和bottom文档描述地很模糊,其实这里我们可以借鉴一下TextView对文本的绘制,TextView在绘制文本的时候总会在文本的最外层留出一些内边距,为什么要这样做?因为TextView在绘制文本的时候考虑到了类似读音符号,下图中的A上面的符号就是一个拉丁文的类似读音符号的东西:
Baseline是基线,Baseline以上是负值,以下是正值,因此 ascent,top是负值, descent和bottom是正值。
OK,知道了这几个概念之后就开始想想要怎么修改了。
我们先修改高度偏小的问题
原始代码,
value = mBound.height();
- 1
修改后代码
FontMetrics fontMetrics = mPaint.getFontMetrics();
value = Math.abs((fontMetrics.bottom - fontMetrics.top));
- 1
- 2
结合图一,bottom和top相减的绝对值就是view的高度height。注意:Baseline以上是负值,以下是正值
OK,高度和宽度大小和原生的大小一样了,那么现在怎么使得文字垂直居中呢?
查阅了网上资料和测试了多次的结果得出一个计算 Y 值的计算公式:
FontMetricsInt fm = mPaint.getFontMetricsInt();
int startY = getHeight() / 2 - fm.descent + (fm.bottom - fm.top) / 2;
int startY = getHeight() / 2 - fm.descent + (fm.descent - fm.ascent)/ 2;
- 1
- 2
- 3
- 4
- 5
- 6
getHeight():控件的高度
getHeight()/2-fm.descent:意思是将整个文字区域抬高至控件的1/2
+ (fm.bottom - fm.top) / 2:(fm.bottom - fm.top)其实就是文本的高度,意思就是将文本下沉文本高度的一半
- 执行:getHeight()/2-fm.descent , 将整个文字区域抬高至控件的1/2
- 执行: + (fm.bottom - fm.top) / 2 , 将文本下沉文本高度的一半