功能专区

根据控件的宽度自动改变TextSize的TextView

2021-01-18  本文已影响0人  快让开我要开大了

今天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要适应这样那样的需求就不简单了,而且安卓的机型众多,写的时候也要考虑到这方面,实现起来没有想象中的简单。

代码已上传github,感兴趣的朋友可以去下载下来研究或使用

上一篇下一篇

猜你喜欢

热点阅读