字體大小自適應TextView-騰訊QMUI開源QMUIFontFitTextView控件解讀

要想成爲一名優秀的Android開發,一份 知識體系 是必不可少的~

前言

需要做一個寬度給定的情況下,字體大小自適應的TextView,突然想起之前看過騰訊QMUI團隊開源的Android UI框架——QMUI Android,裏面就有這個控件,也看過它的源碼,但是當時只是感興趣,並沒有刻意記下來,現在遇到需求了,就再去參考參考大神們的操作,這次就記錄下來。

源碼解讀

這個控件叫做QMUIFontFitTextView,首先是它的構造函數:

public QMUIFontFitTextView(Context context) {
        this(context, null);
    }

    public QMUIFontFitTextView(Context context, AttributeSet attrs) {
        super(context, attrs);

        mTestPaint = new Paint();
        mTestPaint.set(this.getPaint());

        TypedArray array = context.obtainStyledAttributes(attrs,
                R.styleable.QMUIFontFitTextView);
        minSize = array.getDimensionPixelSize(
                R.styleable.QMUIFontFitTextView_qmui_minTextSize, Math.round(14 * QMUIDisplayHelper.DENSITY));
        maxSize = array.getDimensionPixelSize(
                R.styleable.QMUIFontFitTextView_qmui_maxTextSize, Math.round(18 * QMUIDisplayHelper.DENSITY));
        array.recycle();
        //max size defaults to the initially specified text size unless it is too small
    }

有兩個,第二個在設置了AttributeSet attrs的情況下,所做的操作主要有:

初始化Paint對象、根據屏幕密度設置字體大小的最大值和最小值。

接下來就是重點,refitText方法,這個方法就是實現字體大小自適應的關鍵邏輯。

/* Re size the font so the specified text fits in the text box
     * assuming the text box is the specified width.
     */
    private void refitText(String text, int textWidth) {
        if (textWidth <= 0)
            return;
        int targetWidth = textWidth - this.getPaddingLeft() - this.getPaddingRight();
        float hi = maxSize;
        float lo = minSize;
        float size;
        final float threshold = 0.5f; // How close we have to be

        mTestPaint.set(this.getPaint());

        mTestPaint.setTextSize(maxSize);
        if(mTestPaint.measureText(text) <= targetWidth) {
            lo = maxSize;
        } else {
            mTestPaint.setTextSize(minSize);
            if(mTestPaint.measureText(text) < targetWidth) {
                while((hi - lo) > threshold) {
                    size = (hi+lo)/2;
                    mTestPaint.setTextSize(size);
                    if(mTestPaint.measureText(text) >= targetWidth)
                        hi = size; // too big
                    else
                        lo = size; // too small
                }
            }
        }

        // Use lo so that we undershoot rather than overshoot
        this.setTextSize(TypedValue.COMPLEX_UNIT_PX, lo);
    }

首先看上面的註釋,意思是說寬度一定要設置成具體值,這個很好理解,要是寬度可以變化,這個控件就沒有意義了。

代碼中,首先算出可以顯示文字的寬度:

int targetWidth = textWidth - this.getPaddingLeft() - this.getPaddingRight();

然後設置最高值和最低值,相當於一個區間的兩個端點:

float hi = maxSize;
float lo = minSize;

接下來:

final float threshold = 0.5f; // How close we have to be

這個是幹什麼的呢?看註釋,個人理解就是設置字與字之間間距的一個臨界值,後面會用到。

然後,下面的邏輯就是,先把文字大小設置成最大值,如果這個寬度比可顯示寬度小,那就lo = maxSize;,並最終以這個大小顯示,那爲什麼lo = maxSize;呢,我們要看到最後,設置大小的時候,

// Use lo so that we undershoot rather than overshoot
this.setTextSize(TypedValue.COMPLEX_UNIT_PX, lo);

也就是說,是以lo值去設置的,原因就是註釋說的:Use lo so that we undershoot rather than overshoot

如果把大小設置成最大值之後,計算出來的寬度大於可顯示寬度,那麼就需要重新適配。先把大小設置成最小值,然後如果這時候小於可顯示寬度,那麼就可以在這個基礎上進行放大,但是要保證在一定的範圍內,這個範圍就是hi - lo) > threshold,然後

size = (hi+lo)/2;
mTestPaint.setTextSize(size);

也就是取最大最小值的中間值,如果這時候又大於可顯示寬度了,就是放太大了,就又需要縮小一點,把這個中間值作爲最大值,再去跟最小值算中間值。如果還是小於可顯示寬度,那就是放太小了,繼續放大,把中間值作爲最小值,再去跟最大值算中間值。循環進行,直到條件不滿足。整個過程都是通過Paint對象去操作,算出合適的大小值之後再把TextView的字體大小設置成這個值。

接下來就是重寫onMeasure、onTextChanged、onSizeChanged方法,並在裏面調用refitText方法:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
        int height = getMeasuredHeight();
        refitText(this.getText().toString(), parentWidth);
        this.setMeasuredDimension(parentWidth, height);
    }

    @Override
    protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
        refitText(text.toString(), this.getWidth());
    }

    @Override
    protected void onSizeChanged (int w, int h, int oldw, int oldh) {
        if (w != oldw) {
            refitText(this.getText().toString(), w);
        }
    }

完成。

最後

我自己從事 Android 開發,從業這麼久,我也積累了一些珍藏的資料,分享出來,希望可以幫助到大家提升進階

分享一份由幾位大佬一起收錄整理的Android學習PDF+架構視頻+面試文檔+源碼筆記高級架構技術進階腦圖、Android開發面試專題資料,高級進階架構資料

如果你有需要的話,可以在這Android學習PDF+架構視頻+面試文檔+源碼筆記免費領取

喜歡本文的話,不妨順手給我點個小贊、評論區留言或者轉發支持一下唄~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章