聊聊 Android 中的字體大小適配

本篇文章已授權(quán)微信公眾號 guolin_blog(郭霖)獨家發(fā)布

前言

雖然去年寫的一篇文章【一種非常好用的Android屏幕適配】就包含字體大小適配,但那篇文章講的是根據(jù)不同屏幕尺寸來適配字體大小的,接下來我要聊的是字體大小適配中的其他幾種場景。

場景一

有這樣一個需求,界面上需要顯示一個標(biāo)題文本,但是該標(biāo)題的文案長度是不固定的,要求標(biāo)題的文案全部顯示出來,不能用省略號顯示,并且標(biāo)題所占的寬高是固定的。例如標(biāo)題的文案為 “這是標(biāo)題,該標(biāo)題的名字比較長,產(chǎn)品要求不換行全部顯示出來”,如下圖所示,第一個為不符合需求的標(biāo)題,第二個為符合需求的標(biāo)題。

也就是說TextView控件的寬高需要固定,然后根據(jù)標(biāo)題的文案長度動態(tài)改變字體大小,也就是上圖第二個標(biāo)題的效果。那是怎么實現(xiàn)的呢?

以前的做法一般是測量TextView文本所占的寬度與TextView控件的寬度對比,動態(tài)改變TextView的字體大小,寫起來即麻煩又耗性能。但是現(xiàn)在不用這么麻煩了,Android 8.0 新增了用來動態(tài)改變TextView字體大小的新特性 Autosizing TextViews,只需要簡單設(shè)置一下屬性即可。

例如上圖中符合需求的效果可以這樣寫:

xml 方式
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center">

    <TextView
        android:layout_width="340dp"
        android:layout_height="50dp"
        android:background="@drawable/shape_bg_008577"
        android:gravity="center_vertical"
        android:maxLines="1"
        android:text="這是標(biāo)題,該標(biāo)題的名字比較長,產(chǎn)品要求不換行全部顯示出來"
        android:textSize="18sp"
        android:autoSizeTextType="uniform"
        android:autoSizeMaxTextSize="18sp"
        android:autoSizeMinTextSize="10sp"
        android:autoSizeStepGranularity="1sp"/>
</LinearLayout>

可以看到TextView控件多了如下屬性:

  • autoSizeTextType:設(shè)置TextView是否支持自動改變字體大小,none表示不支持,uniform表示支持。
  • autoSizeMinTextSize:最小字體大小,例如設(shè)置為10sp,表示文字最多只能縮小到10sp。
  • autoSizeMaxTextSize:最大字體大小,例如設(shè)置為18sp,表示文字最多只能放大到18sp。
  • autoSizeStepGranularity:縮放粒度,即每次字體大小變化的數(shù)值,例如設(shè)置為1sp,表示每次縮小或放大的值為1sp。

上面的只是針對于8.0的設(shè)備有效,如果想要兼容8.0以下設(shè)備,則需要用AppCompatTextView代替TextView,并且上面幾個屬性的命名空間需要用app命名空間。如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:gravity="center">

    <android.support.v7.widget.AppCompatTextView
        android:layout_width="340dp"
        android:layout_height="50dp"
        android:background="@drawable/shape_bg_008577"
        android:gravity="center_vertical"
        android:maxLines="1"
        android:text="這是標(biāo)題,該標(biāo)題的名字比較長,產(chǎn)品要求不換行全部顯示出來"
        android:textSize="18sp"
        app:autoSizeTextType="uniform"
        app:autoSizeMaxTextSize="18sp"
        app:autoSizeMinTextSize="10sp"
        app:autoSizeStepGranularity="1sp"/>
</LinearLayout>

肯定很多人說 “為什么自己寫的時候不用AppCompatTextView也能兼容8.0以下設(shè)備呢?”,那是因為你當(dāng)前的xml文件對應(yīng)的Activity繼承的是AppCompatActivity,如果繼承的是Activity或FragmentActivity是不能達到兼容的。這一點其實官方文檔 Autosizing TextViews 也沒有說清楚,導(dǎo)致很多人誤解了,各位可以自己驗證下。

動態(tài)編碼方式

使用 TextViewCompat 的setAutoSizeTextTypeWithDefaults()方法設(shè)置TextView是否支持自動改變字體大小,setAutoSizeTextTypeUniformWithConfiguration()方法設(shè)置最小字體大小、最大字體大小與縮放粒度。如下所示:

        TextView tvText = findViewById(R.id.tv_text);
        TextViewCompat.setAutoSizeTextTypeWithDefaults(tvText,TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM);
        TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(tvText,10,18,1, TypedValue.COMPLEX_UNIT_SP);
  • setAutoSizeTextTypeWithDefaults()
    參數(shù)1為需要動態(tài)改變字體大小的TextView,參數(shù)2為是否支持自動改變字體大小的類型,AUTO_SIZE_TEXT_TYPE_UNIFORM表示支持,AUTO_SIZE_TEXT_TYPE_NONE表示不支持。
  • setAutoSizeTextTypeUniformWithConfiguration()
    參數(shù)1為需要動態(tài)改變字體大小的TextView,參數(shù)2、3、4分別為最小字體大小、最大字體大小與縮放粒度,參數(shù)5為參數(shù)2、3、4的單位,例如sp 、dp、px等。

同樣,如果要兼容8.0以下設(shè)備,要么在xml中用AppCompatTextView代替TextView,要么當(dāng)前Activity繼承AppCompatActivity。

小結(jié)

Autosizing TextViews是Android 8.0 新增的特性,可以用來動態(tài)改變TextView字體大小。如果要兼容8.0以下設(shè)備,則需要滿足以下2個條件中的其中一個。

  • 在xml中用AppCompatTextView代替TextView,并且上面幾個屬性的命名空間用app命名空間。
  • 當(dāng)前Activity繼承AppCompatActivity,而不是Activity或FragmentActivity。

Autosizing TextViews更多屬性請參考 Autosizing TextViews

場景二

很多人肯定遇到過這種情況,測試扔個圖片過來,然后說怎么運行在這個測試機后下面的內(nèi)容都擋住了(如下右圖,左圖為正常情況),你不是說做了屏幕適配的嗎?然后你拿測試的手機一看,設(shè)置里面竟然選了 特大 字體。

嗯... 經(jīng)過這么一看基本就知道什么問題了。原因是你在xml文件寫死了控件的高度,并且TextView的字體單位用的是sp,這種情況下到手機設(shè)置中改變字體大小,那么界面中的字體大小就會隨系統(tǒng)改變。

那么我們應(yīng)該怎么解決這個問題呢?這時候我們可以觀察下微信的做法,經(jīng)過研究發(fā)現(xiàn)微信的字體是不會隨著系統(tǒng)字體大小的改變而改變的,并且微信本身是有改變字體大小功能的。微信中改變字體大小后不僅字體大小改變了,控件的寬高也會跟著改變。所以可以猜到微信的字體適配是如下方式實現(xiàn)的:

字體大小不隨系統(tǒng)改變

想要實現(xiàn)字體大小不隨系統(tǒng)改變有兩種方式:

1. xml方式

TextView的字體單位不使用sp,而是用dp。因為sp單位的字體大小會隨系統(tǒng)字體大小的改變而改變,而dp單位則不會。

2. 動態(tài)編碼方式

字體大小是否隨系統(tǒng)改變可以通過Configuration類的fontScale變量來控制,fontScale變量默認為1,表示字體大小不隨系統(tǒng)字體大小的改變而改變,那么我們只需要保證fontScale始終為1即可。具體代碼如下,一般放在Activity的基類BaseActivity即可。

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        if (newConfig.fontScale != 1) { //fontScale不為1,需要強制設(shè)置為1
            getResources();
        }
    }

    @Override
    public Resources getResources() {
        Resources resources = super.getResources();
        if (resources.getConfiguration().fontScale != 1) { //fontScale不為1,需要強制設(shè)置為1
            Configuration newConfig = new Configuration();
            newConfig.setToDefaults();//設(shè)置成默認值,即fontScale為1
            resources.updateConfiguration(newConfig, resources.getDisplayMetrics());
        }
        return resources;
    }

雖然兩種方式都可以解決場景二的問題,但是一般都是使用動態(tài)編碼方式,原因如下:

  • 若應(yīng)用需要增加類似微信可以改變字體大小的功能,如果在xml中用的是dp單位,那么該功能將無法實現(xiàn)!
  • 若需求改成字體大小需要隨系統(tǒng)字體大小的改變而改變,只需要刪掉該段代碼即可。
  • 官方推薦使用sp作為字體單位。
控件寬高盡量不要固定

原因是如果應(yīng)用需要增加類似微信可以改變字體大小的功能,如果控件寬高固定的話,調(diào)大字體會導(dǎo)致控件顯示不下,這不是我們需要的效果。

場景三

有這樣一種情況,當(dāng)你按照設(shè)計圖的標(biāo)注去寫一個TextView控件的時候,寬高用的是wrap_content,也沒有設(shè)置任何padding,但是運行在手機上該TextView所占的寬高卻比設(shè)計圖的要大。如下圖所示,字體周圍多了很多空白部分。


這是因為TextView本身就含有內(nèi)邊距造成的,那么TextView有沒有屬性可以去除內(nèi)邊距呢?答案是有的,該屬性為 includeFontPadding,設(shè)置為false表示不包含字體內(nèi)邊距,具體代碼如下:

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimary"
        android:text="Hello"
        android:textSize="50sp"
        android:includeFontPadding="false"/>

運行效果如下圖中的第二個“Hello”(第一個“Hello”為普通TextView),看起來好像是可以的,但是仔細看發(fā)現(xiàn)還是留有一點內(nèi)邊距的。


一般的應(yīng)用可能不在乎那點內(nèi)邊距,但如果做的是TV上的應(yīng)用就要求比較嚴格了,因為TV界面一般是不支持上下左右滾動的,如果設(shè)計圖上的內(nèi)容剛好占滿屏幕,那么這些內(nèi)邊距就會導(dǎo)致個別控件顯示不全。所以在這種情況下是必須要解決的,既然TextView自帶屬性不能解決,那就只能自定義了。具體代碼如下:

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;

public class NoPaddingTextView extends AppCompatTextView {
    private Paint   mPaint             = getPaint();
    private Rect    mBounds            = new Rect();
    private Boolean mRemoveFontPadding = false;//是否去除字體內(nèi)邊距,true:去除 false:不去除

    public NoPaddingTextView(Context context) {
        super(context);
    }

    public NoPaddingTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initAttributes(context, attrs);
    }

    public NoPaddingTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttributes(context, attrs);
    }

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mRemoveFontPadding) {
            calculateTextParams();
            setMeasuredDimension(mBounds.right - mBounds.left, -mBounds.top + mBounds.bottom);
        }
    }

    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
    }

    protected void onDraw(Canvas canvas) {
        drawText(canvas);
    }

    /**
     * 初始化屬性
     */
    private void initAttributes(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.NoPaddingTextView);
        mRemoveFontPadding = typedArray.getBoolean(R.styleable.NoPaddingTextView_removeDefaultPadding, false);
        typedArray.recycle();
    }

    /**
     * 計算文本參數(shù)
     */
    private String calculateTextParams() {
        String text = getText().toString();
        int textLength = text.length();
        mPaint.getTextBounds(text, 0, textLength, mBounds);
        if (textLength == 0) {
            mBounds.right = mBounds.left;
        }
        return text;
    }

    /**
     * 繪制文本
     */
    private void drawText(Canvas canvas) {
        String text = calculateTextParams();
        int left = mBounds.left;
        int bottom = mBounds.bottom;
        mBounds.offset(-mBounds.left, -mBounds.top);
        mPaint.setAntiAlias(true);
        mPaint.setColor(getCurrentTextColor());
        canvas.drawText(text, (float) (-left), (float) (mBounds.bottom - bottom), mPaint);
    }
}

將NoPaddingTextView需要的屬性定義在attr.xml文件中,如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="NoPaddingTextView">
        <attr name="removeDefaultPadding" format="boolean"/>
    </declare-styleable>

</resources>

布局文件中使用,如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="horizontal">

    <com.wildma.fontadaptation.NoPaddingTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimary"
        android:text="Hello"
        android:textSize="50sp"
        app:removeDefaultPadding="true"/>

</LinearLayout>

運行效果如下圖中的第三個“Hello”(第一個為普通TextView,第二個為加了includeFontPadding屬性的TextView),完美解決!


OK!字體大小適配中最常用的三種場景都講了,如果還有其他場景歡迎補充~

項目地址:FontAdaptation

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容