美文网首页功能专区
根据控件的宽度自动改变TextSize的TextView

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

作者: 快让开我要开大了 | 来源:发表于2021-01-18 15:03 被阅读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,感兴趣的朋友可以去下载下来研究或使用

    相关文章

      网友评论

        本文标题:根据控件的宽度自动改变TextSize的TextView

        本文链接:https://www.haomeiwen.com/subject/jvwiaktx.html