今天UI来找到我说:“这个优惠券的金额能不能做成根据数字的长度来改变大小的?”我依稀记得TextView有一个autoSize的属性是可以实现自动缩放字体大小的,就一口答应了下来,说可以试着做一下。但做的过程并不顺利,这里写一篇文章把自己的探索过程记录下来,方便自己日后查阅,如果这个过程有什么错误或者更好的实现方法,也希望大佬们不吝赐教!
TextView的autoSize属性我之前只是看了一下,没真正用过,接到需求之后马上去搜索一下用法。根据网上介绍的方法把autoSize属性设置好了,结果发现并没有效果,TextView该怎么显示还是怎么显示。经过一顿折腾,发现只有给TextView一个固定的宽度或者match_parent才能发挥autoSize的能力,在wrap_content状态下是没有效果的,有大神去深究了一下源码,感兴趣的可以去看一下,这里不再赘述
关于autofittextview的width不能为wrap_content这件事
既然autoSize的属性实现不了,那就只好自己来写个自定义View了。本着不重复造轮子的原则,到网上找了一下别人写的自定义View,但没找到符合需求的,只好着手自己写一个了。
开始构思实现思路:先确定TextView的宽度,然后看看设定的textSize能不能把文字显示完全,不能的话就缩小textSize,直到刚好能显示完。
说干就干!
一、TextView的宽高
先贴代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
this.measuredHeight = getTextHeight();
this.measuredWidth = getTextWidth(widthMeasureSpec);
setMeasuredDimension(measuredWidth, measuredHeight);
}
/**
* 获取文字实际占用宽度
* @return
*/
private int getTextWidth(int widthMeasureSpec) {
if (getText() == null || TextUtils.isEmpty(getText().toString())) {
return 0;
}
//控件可以使用的最大宽度
int w1 = MeasureSpec.getSize(widthMeasureSpec);
//文字实际占用的宽度
Rect rect = new Rect();
getPaint().getTextBounds(getText().toString(), 0, getText().toString().length(), rect);
textActualWidth = rect.width();
int width = 0;
if (textActualWidth > w1) {
width = w1;
} else {
width = textActualWidth;
}
return width;
}
/**
* 获取文字实际占用高度
* @return
*/
private int getTextHeight() {
if (getText() == null || TextUtils.isEmpty(getText().toString())) {
return 0;
}
Rect rect = new Rect();
getPaint().getTextBounds(getText().toString(), 0, getText().toString().length(), rect);
int height = rect.height();
return height;
}
MeasureSpec.getSize()这个方法能获取到控件可使用的最大宽度,按照预设的textSize,如果文字占用的宽度大于控件的最大宽度,那就以最大宽度为界限,缩小字体;如果文字占用宽度小于最大宽度,那就不用处理。
对此方法不明白的朋友可以看一下这篇文章。
Android之自定义View的死亡三部曲之(Measure)
二、textSize缩放
获取到了TextView宽度,我们开始来处理textSize。还是先上代码:
@Override
protected void onDraw(Canvas canvas) {
if (!isCalculationComplete) {
reMeasure();
}
super.onDraw(canvas);
}
/**
* 根据控件的宽度 重新设置文字的大小 让其能一行完全显示
*/
private void reMeasure() {
float size = getTextSize();
float width = textActualWidth;
while (width > measuredWidth) {
size--;
Rect rect = new Rect();
Paint paint = new Paint();
paint.setTextSize(size);
paint.getTextBounds(getText().toString(), 0, getText().toString().length(), rect);
width = rect.width();
}
//设置文字大小
setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
}
简单的使用一下
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#f5f5f4">
<LinearLayout
android:id="@+id/ll_top_content"
android:layout_width="170dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="bottom"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:background="@color/white"
android:layout_marginTop="10dp"
android:paddingLeft="10dp"
android:paddingRight="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="减"/>
<com.xgh.mytextsizedemo.CustomDynamicSizeTextView
android:id="@+id/tv_top_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="40sp"
android:text="12345"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:layout_marginLeft="5dp"
android:text="元"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
public class MainActivity extends AppCompatActivity {
private TextView mTvTopContent;
private Context mContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContext = this;
mTvTopContent = findViewById(R.id.tv_top_content);
mTvTopContent.setText("123456789012345678");
}
}
运行一下看看效果
第一次运行效果.png
可以看到,文字确实是有缩小,但是却换行了,也就是说宽度没算对。打印了一下rect.width()和rect.height(),发现拿到的尺寸比预想中的要小很多。
在这里,用paint.measureText()方法获取到的才是我们想要的宽度,高度可以借助paint.getFontMetricsInt()来获得。用一张图来分析这几个方法的意义:
图解.png
灰色背景代表的是整个TextView,其中Rect拿到的其实只是TextView中文字占用的最小尺寸,而不是整个TextView的宽高。我们可以利用FontMetricsInt.top和FontMetricsInt.bottom来算出高,paint.measureText()算出宽。
paint的坐标轴和我们平常了解的不太一样,它是以控件中间偏下一点的位置作为原坐标的,也就是Baseline线和左边的交点
坐标轴.png
因此,控件的宽高有了一个新的计算方式:
/**
* 获取文字实际占用宽度
* @return
*/
private int getTextWidth(int widthMeasureSpec) {
if (getText() == null || TextUtils.isEmpty(getText().toString())) {
return 0;
}
int w1 = MeasureSpec.getSize(widthMeasureSpec);
Paint paint = new Paint();
paint.setTextSize(getTextSize());
textActualWidth = (int) paint.measureText(getText().toString());
int width = 0;
if (textActualWidth > w1) {
width = w1;
} else {
width = textActualWidth;
}
return width;
}
/**
* 获取文字实际占用高度
* @return
*/
private int getTextHeight() {
if (getText() == null || TextUtils.isEmpty(getText().toString())) {
return 0;
}
Rect rect = new Rect();
Paint paint = new Paint();
paint.setTextSize(getTextSize());
paint.getTextBounds(getText().toString(), 0, getText().toString().length(), rect);
Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt();
int height = fontMetricsInt.bottom - fontMetricsInt.top;
return height;
}
运行效果:
修改宽高获取方式.png
宽度是没问题了,但高度还是太高。这其实是因为我们虽然调整了文字尺寸,但控件的高度还是一开始的高度,所以在调整完TextSize之后,还要再去设置一下控件高度。
private void setTextHeight() {
if (getText() == null || TextUtils.isEmpty(getText().toString())) {
return;
}
Rect rect = new Rect();
getPaint().getTextBounds(getText().toString(), 0, getText().toString().length(), rect);
Paint.FontMetricsInt fontMetricsInt = getPaint().getFontMetricsInt();
int height = fontMetricsInt.bottom - fontMetricsInt.top;
setHeight(height);
}
private void reMeasure() {
......
//设置文字大小
setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
//重新设置控件高度
setTextHeight();
}
但这样一来,其实你会发现,代码陷入了一个死循环,计算控件宽度——>调整TextSize和setHeight()——>触发onMeasure方法重新计算宽度——>调整TextSize和setHeight()......所以要在调整完TextSize之后禁止再触发调整的方法。
@Override
protected void onDraw(Canvas canvas) {
if (!isCalculationComplete) {
reMeasure();
}
super.onDraw(canvas);
}
@Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
isCalculationComplete = false;
super.onTextChanged(text, start, lengthBefore, lengthAfter);
}
private void reMeasure() {
......
//设置文字大小
setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
isCalculationComplete = true;
//重新设置控件高度
setTextHeight();
}
运行效果:
重新设置高度.png
完美!但我的需求是用在优惠券列表上,需要再试试在RecyclerView上的实战效果。
代码不上了,直接看效果。
咋一看没什么问题.png
咋一看没什么问题,滑动一下看看
1明显小了.png
它变了!.png
为什么会这样?其实是因为RecyclerView会复用控件,我们在第一次计算好控件的尺寸之后执行了一次setHeight(),这样就改变了控件的最大宽度的值(measuredWidth),下次计算的时候会重新拿到measuredWidth,导致控件越算越小。
而且不同的item也可能复用同一个TextView,由于上一个item改变了TextView的TextSize,导致下一个item的初始TextSize变小了,出现了同样是1位数,但尺寸不一样的情况。
知道原因之后,我们再改进一下方法。
private float oldTextSize = 0;//记录初始TextSize
public CustomDynamicSizeTextView(Context context, AttributeSet attrs) {
super(context, attrs);
//记录一下TextSize
setOldTextSize(getTextSize());
}
private int getTextWidth(int widthMeasureSpec) {
......
paint.setTextSize(getOldTextSize());
......
}
private int getTextHeight() {
......
if (isCalculationComplete){
paint.setTextSize(getTextSize());
}
else {
paint.setTextSize(getOldTextSize());
}
......
}
private void reMeasure() {
float size = getOldTextSize();
......
}
这一次就没有问题了。
最后再做一点优化和补充:
假如数字特别长,而你的初始尺寸又设置得非常大,那while()方法调整尺寸的时候要循环几百次那么多,不是太友好。这里可以做一下优化:
/**
* 根据控件的宽度 重新设置文字的大小 让其能一行完全显示
*/
private void reMeasure() {
float size = getOldTextSize();
if (textActualWidth+3>measuredWidth){
double magnification = Arith.div(textActualWidth, measuredWidth);
size = (float) Math.ceil(Arith.div(getOldTextSize()*1.0, magnification));
//先大致设置一个尺寸
Paint roughlyPaint = new Paint();
roughlyPaint.setTypeface(getPaint().getTypeface());
roughlyPaint.setTextScaleX(getPaint().getTextScaleX());
roughlyPaint.setTextSize(size);
int width = (int) roughlyPaint.measureText(getText().toString());
//如果还是太大,再慢慢细调
while (width+3 > measuredWidth) {
size--;
//设置文字大小
Paint tempPaint = new Paint();
tempPaint.setTypeface(getPaint().getTypeface());
tempPaint.setTextScaleX(getPaint().getTextScaleX());
tempPaint.setTextSize(size);
width = (int) tempPaint.measureText(getText().toString());
}
}
//设置文字大小
setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
isCalculationComplete = true;
//重新设置控件高度
setTextHeight();
}
width+3是因为我发现如果width == measuredWidth的时候,也会出现换行的情况,具体是为什么我就没去深究了....(懒)
到这里为止,这个自定义控件就写完了!一开始的时候以为写一个这样的控件会很简单,谁知道真正动手的时候才发现有这样那样的问题。写一个自定义View简单,但写出来的View要适应这样那样的需求就不简单了,而且安卓的机型众多,写的时候也要考虑到这方面,实现起来没有想象中的简单。
网友评论