画BaseLine平齐的文本
最近在项目中遇到好几处类似下方这样的带单位、左大右小的布局
1.png 2.png
看起来很平常,两个不同size的TextView左右布局,同时它们的baseline是平齐的,有人马上就想到了在RelativeLayout中的子View可以使用layout_alignBaseline属性完成这个布局,这里贴下xml文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
>
<TextView
android:id="@+id/tv_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:includeFontPadding="false"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_unit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@id/tv_value"
android:layout_toRightOf="@id/tv_value"
android:includeFontPadding="false"
android:textColor="@color/white"
android:textSize="14sp" />
</RelativeLayout>
本来以为到这里就结束了,但是设计告诉我说,这个底边并没有平齐,那个kg的g明明下沉了,我要的效果不是这样的……然后我就解释了一下因为这个g比较特殊,你要是m就齐了,设计大哥就问了句那我要的能实现不……
这里就不得不说一下TextView的绘制了
Text的绘制
熟悉自定义view的同学应该知道,自定义view其实就是绘制文字和图像,这就涉及到canvas的drawText()方法了,这里有好几个重载的方法,看看我们经常常用的这个方法的源码:
/**
* Draw the text, with origin at (x,y), using the specified paint. The
* origin is interpreted based on the Align setting in the paint.
*
* @param text The text to be drawn
* @param x The x-coordinate of the origin of the text being drawn
* @param y The y-coordinate of the baseline of the text being drawn
* @param paint The paint used for the text (e.g. color, size, style)
*/
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
native_drawText(mNativeCanvasWrapper, text, 0, text.length(), x, y, paint.mBidiFlags,
paint.getNativeInstance(), paint.mNativeTypeface);
}
这四个参数除了y其他的都很好理解,text是要绘制的文本内容,x是x轴方向的开始绘制的值,y参数说明里的baseline又是什么东东……
先上图:
3.png
这张图里有四根线,其中3就是baseline,可以看到汉字和英文字母都会超出这个线,阿拉伯数字就不会超出这个baseline。也就是说drawtext在垂直方向是以3为基准的,所以当我们想把文本垂直居中绘制在某一个view里,y的值是不能直接设为getHeight()/2的,也就是图中的2号线,y值应该向下偏移到3的位置。
这里就说下3号线距离1号线和4号线分别对应两个值,他们是FontMetrics这个类中的两个值,ascent(负数)和descent的绝对值。在FontMetrics有五个float类型值:
-
leading 留给文字音标符号的距离
-
ascent 从baseline线到最高的字母顶点到距离,负值
-
top 从baseline线到字母最高点的距离加上ascent, |top|=|ascent|+|leading|
-
descent 从baseline线到字母最低点到距离
-
bottom 和top类似,系统为一些极少数符号留下的空间,top和bottom总会比ascent和descent大一点的就是这些少到忽略的特殊符号。
想要了解详情的可以看看FontMetrics这个类的源码。
所以到这里,实现平齐效果的思路就很明了,自定义一个view,继承自View,画两个text即可实现,关键在于第二个text的baseline设置成多少。
先贴上完整的类
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
/**
* Description:
* Created by hdz on 2017/6/1.
*/
public class AlignmentView extends View {
//数值的画笔
private Paint valuePaint;
//单位的画笔
private Paint unitPaint;
private int valueColor = Color.BLACK;
private int unitColor = Color.BLACK;
private float valueTextSize = 30;
private float unitTextSize = 24;
//数值和单位之间的padding
private float space = 0;
/**
* 类型
* 0:下方未超出baseline
* 1:下方超出baseline
*/
private int type;
private String value;
private String unit;
//数值对应baseline的y值
private float valueDrawY;
private Paint.FontMetrics valueMetrics;
private Paint.FontMetrics unitMetrics;
public AlignmentView(Context context) {
this(context, null, 0);
}
public AlignmentView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AlignmentView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
if (attrs == null) {
return;
}
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.AlignmentView, defStyleAttr, 0);
value = array.getString(R.styleable.AlignmentView_value);
unit = array.getString(R.styleable.AlignmentView_unit);
space = array.getDimension(R.styleable.AlignmentView_space, 0);
unitColor = array.getColor(R.styleable.AlignmentView_unitColor, Color.BLACK);
valueColor = array.getColor(R.styleable.AlignmentView_valueColor, Color.BLACK);
valueTextSize = array.getDimension(R.styleable.AlignmentView_valueSize, 30);
unitTextSize = array.getDimension(R.styleable.AlignmentView_unitSize, 24);
type = array.getInt(R.styleable.AlignmentView_type, 0);
if (TextUtils.isEmpty(value)) {
value = "数值";
}
if (TextUtils.isEmpty(unit)) {
unit = "单位";
}
valuePaint = new Paint();
valuePaint.setAntiAlias(true);
valuePaint.setTextSize(valueTextSize);
valuePaint.setColor(valueColor);
valuePaint.setTextAlign(Paint.Align.LEFT);
unitPaint = new Paint();
unitPaint.setAntiAlias(true);
unitPaint.setTextSize(unitTextSize);
unitPaint.setColor(unitColor);
unitPaint.setTextAlign(Paint.Align.LEFT);
array.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
valueMetrics = valuePaint.getFontMetrics();
unitMetrics = unitPaint.getFontMetrics();
//获取文字的宽高
float valueH = valueMetrics.descent - valueMetrics.ascent;
float valueW = valuePaint.measureText(value);
float unitH = unitMetrics.descent - unitMetrics.ascent;
float unitW = unitPaint.measureText(unit);
int realH = (int) (Math.max(valueH, unitH) + getPaddingBottom() + getPaddingTop());
int realW = (int) (valueW + unitW + getPaddingLeft() + getPaddingRight() + space);
int width = measureSize(realW, widthMeasureSpec);
int height = measureSize(realH, heightMeasureSpec);
setMeasuredDimension(width, height);
}
private int measureSize(int defaultSize, int measureSpec) {
int resultSize = defaultSize;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
break;
case MeasureSpec.EXACTLY:
resultSize = size;
break;
}
return resultSize;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawValue(canvas);
drawUnit(canvas);
canvas.drawLine(getPaddingLeft(), valueDrawY, getWidth(), valueDrawY, unitPaint);
}
// 画左侧的数值
private void drawValue(Canvas canvas) {
//y值的计算,向下偏移
valueDrawY = getHeight() / 2 + (Math.abs(valueMetrics.ascent) - valueMetrics.descent) / 2;
canvas.drawText(value, getPaddingLeft(), valueDrawY, valuePaint);
}
// 画右侧的单位
private void drawUnit(Canvas canvas) {
float valueWidth = valuePaint.measureText(value);
float x = getPaddingLeft() + valueWidth + space;
float y = valueDrawY;
if (type == 1) {
// 当底部超出baseline的时候,应该向上偏移单位对应的descent值
y = valueDrawY - unitMetrics.descent;
}
canvas.drawText(unit, x, y, unitPaint);
}
// 向外部提供设置值的方法
public void setValue(String value) {
this.value = value;
invalidate();
}
public void setUnit(String unit) {
this.unit = unit;
invalidate();
}
}
attrs:
<declare-styleable name="AlignmentView">
<attr name="value" format="string"/>
<attr name="unit" format="string"/>
<attr name="valueColor" format="color" />
<attr name="unitColor" format="color" />
<attr name="valueSize" format="dimension" />
<attr name="unitSize" format="dimension" />
<attr name="space" format="dimension"/>
<attr name="type">
<enum name="NORMAL" value="0"/>
<enum name="DESCENT_BEYOND" value="1"/>
</attr>
</declare-styleable>
这里需要说下就是drawUnit方法中drawText中的y值,当y = valueDrawY时也就达到了上面layout_alignBaseline的效果,如果还要调整,就需要
y = valueDrawY - unitMetrics.descent了,这里descent应该是unit所对应的。
布局文件
<com.example.handezhao.align.AlignmentView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:background="#57df87"
android:padding="10dp"
app:space="5dp"
app:unitColor="#00f"
app:unitSize="20sp"
app:valueColor="#454545"
app:valueSize="40sp"
app:value="1234567890"
app:unit="abcdefghijk"
android:layout_marginTop="20dp"
app:type="DESCENT_BEYOND"
/>
最后看看两种type的效果:
type0.png type1.png😆😆😆,这里的type可以根据不同的需求增加,比如右上角之类的……