聊聊Android中的字體適配

前言

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

場景一

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

也就是說TextView控件的寬高需要固定,然後根據標題的文案長度動態改變文字大小,也就是上圖第二個標題的效果。那是怎麼實現的呢?

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

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

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="這是標題,該標題的名字比較長,產品要求不換行全部顯示出來"
        android:textSize="18sp"
        android:autoSizeTextType="uniform"
        android:autoSizeMaxTextSize="18sp"
        android:autoSizeMinTextSize="10sp"
        android:autoSizeStepGranularity="1sp"/>
</LinearLayout>

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

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

上面的只是針對於8.0的設備有效,如果想要兼容8.0以下設備,則需要用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="這是標題,該標題的名字比較長,產品要求不換行全部顯示出來"
        android:textSize="18sp"
        app:autoSizeTextType="uniform"
        app:autoSizeMaxTextSize="18sp"
        app:autoSizeMinTextSize="10sp"
        app:autoSizeStepGranularity="1sp"/>
</LinearLayout>

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

動態編碼方式

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

        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()
    參數1爲需要動態改變文字大小的TextView,參數2爲是否支持自動改變文字大小的類型,AUTO_SIZE_TEXT_TYPE_UNIFORM表示支持,AUTO_SIZE_TEXT_TYPE_NONE表示不支持。
  • setAutoSizeTextTypeUniformWithConfiguration()
    參數1爲需要動態改變文字大小的TextView,參數2、3、4分別爲最小文字大小、最大文字大小與縮放粒度,參數5爲參數2、3、4的單位,例如sp 、dp、px等。

同樣,如果要兼容8.0以下設備,要麼在xml中用AppCompatTextView代替TextView,要麼當前Activity繼承AppCompatActivity。

小結

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

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

Autosizing TextViews更多屬性請參考 Autosizing TextViews

場景二

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

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

那麼我們應該怎麼解決這個問題呢?這時候我們可以觀察下微信的做法,經過研究發現微信的字體是不會隨着系統字體大小的改變而改變的,並且微信本身是有改變字體大小功能的。微信中改變字體大小後不僅字體大小改變了,控件的寬高也會跟着改變。所以可以猜到微信的字體適配是如下方式實現的:

字體大小不隨系統改變

想要實現字體大小不隨系統改變有兩種方式:

1. xml方式

TextView的字體單位不使用sp,而是用dp。因爲sp單位的字體大小會隨系統字體大小的改變而改變,而dp單位則不會。

2. 動態編碼方式

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

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

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

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

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

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

場景三

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


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

    <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),看起來好像是可以的,但是仔細看發現還是留有一點內邊距的。


一般的應用可能不在乎那點內邊距,但如果做的是TV上的應用就要求比較嚴格了,因爲TV界面一般是不支持上下左右滾動的,如果設計圖上的內容剛好佔滿屏幕,那麼這些內邊距就會導致個別控件顯示不全。所以在這種情況下是必須要解決的,既然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;//是否去除字體內邊距,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();
    }

    /**
     * 計算文本參數
     */
    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

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