聊聊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 PaintmPaint= getPaint(); private RectmBounds= 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.myapplication.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