前言
雖然去年寫的一篇文章【一種非常好用的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