本篇文章已授權(quán)微信公眾號(hào) guolin_blog(郭霖)獨(dú)家發(fā)布
前言
雖然去年寫的一篇文章【一種非常好用的Android屏幕適配】就包含字體大小適配,但那篇文章講的是根據(jù)不同屏幕尺寸來適配字體大小的哭廉,接下來我要聊的是字體大小適配中的其他幾種場(chǎng)景炕桨。
場(chǎng)景一
有這樣一個(gè)需求葛作,界面上需要顯示一個(gè)標(biāo)題文本邮屁,但是該標(biāo)題的文案長(zhǎng)度是不固定的,要求標(biāo)題的文案全部顯示出來,不能用省略號(hào)顯示枪蘑,并且標(biāo)題所占的寬高是固定的。例如標(biāo)題的文案為 “這是標(biāo)題岖免,該標(biāo)題的名字比較長(zhǎng)岳颇,產(chǎn)品要求不換行全部顯示出來”,如下圖所示颅湘,第一個(gè)為不符合需求的標(biāo)題话侧,第二個(gè)為符合需求的標(biāo)題。
也就是說TextView控件的寬高需要固定闯参,然后根據(jù)標(biāo)題的文案長(zhǎng)度動(dòng)態(tài)改變字體大小瞻鹏,也就是上圖第二個(gè)標(biāo)題的效果。那是怎么實(shí)現(xiàn)的呢鹿寨?
以前的做法一般是測(cè)量TextView文本所占的寬度與TextView控件的寬度對(duì)比新博,動(dòng)態(tài)改變TextView的字體大小,寫起來即麻煩又耗性能脚草。但是現(xiàn)在不用這么麻煩了赫悄,Android 8.0 新增了用來動(dòng)態(tài)改變TextView字體大小的新特性 Autosizing TextViews,只需要簡(jiǎn)單設(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)題的名字比較長(zhǎng)姑隅,產(chǎn)品要求不換行全部顯示出來"
android:textSize="18sp"
android:autoSizeTextType="uniform"
android:autoSizeMaxTextSize="18sp"
android:autoSizeMinTextSize="10sp"
android:autoSizeStepGranularity="1sp"/>
</LinearLayout>
可以看到TextView控件多了如下屬性:
- autoSizeTextType:設(shè)置TextView是否支持自動(dòng)改變字體大小,none表示不支持倔撞,uniform表示支持讲仰。
- autoSizeMinTextSize:最小字體大小,例如設(shè)置為10sp误窖,表示文字最多只能縮小到10sp叮盘。
- autoSizeMaxTextSize:最大字體大小秩贰,例如設(shè)置為18sp霹俺,表示文字最多只能放大到18sp。
- autoSizeStepGranularity:縮放粒度毒费,即每次字體大小變化的數(shù)值丙唧,例如設(shè)置為1sp,表示每次縮小或放大的值為1sp觅玻。
上面的只是針對(duì)于8.0的設(shè)備有效想际,如果想要兼容8.0以下設(shè)備,則需要用AppCompatTextView代替TextView溪厘,并且上面幾個(gè)屬性的命名空間需要用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)題的名字比較長(zhǎng)畸悬,產(chǎn)品要求不換行全部顯示出來"
android:textSize="18sp"
app:autoSizeTextType="uniform"
app:autoSizeMaxTextSize="18sp"
app:autoSizeMinTextSize="10sp"
app:autoSizeStepGranularity="1sp"/>
</LinearLayout>
肯定很多人說 “為什么自己寫的時(shí)候不用AppCompatTextView也能兼容8.0以下設(shè)備呢侧甫?”,那是因?yàn)槟惝?dāng)前的xml文件對(duì)應(yīng)的Activity繼承的是AppCompatActivity蹋宦,如果繼承的是Activity或FragmentActivity是不能達(dá)到兼容的披粟。這一點(diǎn)其實(shí)官方文檔 Autosizing TextViews 也沒有說清楚,導(dǎo)致很多人誤解了冷冗,各位可以自己驗(yàn)證下守屉。
動(dòng)態(tài)編碼方式
使用 TextViewCompat 的setAutoSizeTextTypeWithDefaults()方法設(shè)置TextView是否支持自動(dòng)改變字體大小,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為需要?jiǎng)討B(tài)改變字體大小的TextView,參數(shù)2為是否支持自動(dòng)改變字體大小的類型思灌,AUTO_SIZE_TEXT_TYPE_UNIFORM表示支持俺叭,AUTO_SIZE_TEXT_TYPE_NONE表示不支持。 - setAutoSizeTextTypeUniformWithConfiguration()
參數(shù)1為需要?jiǎng)討B(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 新增的特性,可以用來動(dòng)態(tài)改變TextView字體大小玩祟。如果要兼容8.0以下設(shè)備腹缩,則需要滿足以下2個(gè)條件中的其中一個(gè)。
- 在xml中用AppCompatTextView代替TextView空扎,并且上面幾個(gè)屬性的命名空間用app命名空間藏鹊。
- 當(dāng)前Activity繼承AppCompatActivity,而不是Activity或FragmentActivity转锈。
Autosizing TextViews更多屬性請(qǐng)參考 Autosizing TextViews
場(chǎng)景二
很多人肯定遇到過這種情況盘寡,測(cè)試扔個(gè)圖片過來,然后說怎么運(yùn)行在這個(gè)測(cè)試機(jī)后下面的內(nèi)容都擋住了(如下右圖撮慨,左圖為正常情況)竿痰,你不是說做了屏幕適配的嗎?然后你拿測(cè)試的手機(jī)一看甫煞,設(shè)置里面竟然選了 特大 字體菇曲。
嗯... 經(jīng)過這么一看基本就知道什么問題了。原因是你在xml文件寫死了控件的高度抚吠,并且TextView的字體單位用的是sp常潮,這種情況下到手機(jī)設(shè)置中改變字體大小,那么界面中的字體大小就會(huì)隨系統(tǒng)改變楷力。
那么我們應(yīng)該怎么解決這個(gè)問題呢喊式?這時(shí)候我們可以觀察下微信的做法,經(jīng)過研究發(fā)現(xiàn)微信的字體是不會(huì)隨著系統(tǒng)字體大小的改變而改變的萧朝,并且微信本身是有改變字體大小功能的岔留。微信中改變字體大小后不僅字體大小改變了,控件的寬高也會(huì)跟著改變检柬。所以可以猜到微信的字體適配是如下方式實(shí)現(xiàn)的:
字體大小不隨系統(tǒng)改變
想要實(shí)現(xiàn)字體大小不隨系統(tǒng)改變有兩種方式:
1. xml方式
TextView的字體單位不使用sp献联,而是用dp。因?yàn)閟p單位的字體大小會(huì)隨系統(tǒng)字體大小的改變而改變,而dp單位則不會(huì)里逆。
2. 動(dòng)態(tài)編碼方式
字體大小是否隨系統(tǒng)改變可以通過Configuration類的fontScale變量來控制进胯,fontScale變量默認(rèn)為1,表示字體大小不隨系統(tǒng)字體大小的改變而改變原押,那么我們只需要保證fontScale始終為1即可胁镐。具體代碼如下,一般放在Activity的基類BaseActivity即可诸衔。
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.fontScale != 1) { //fontScale不為1盯漂,需要強(qiáng)制設(shè)置為1
getResources();
}
}
@Override
public Resources getResources() {
Resources resources = super.getResources();
if (resources.getConfiguration().fontScale != 1) { //fontScale不為1,需要強(qiáng)制設(shè)置為1
Configuration newConfig = new Configuration();
newConfig.setToDefaults();//設(shè)置成默認(rèn)值笨农,即fontScale為1
resources.updateConfiguration(newConfig, resources.getDisplayMetrics());
}
return resources;
}
雖然兩種方式都可以解決場(chǎng)景二的問題就缆,但是一般都是使用動(dòng)態(tài)編碼方式,原因如下:
- 若應(yīng)用需要增加類似微信可以改變字體大小的功能磁餐,如果在xml中用的是dp單位违崇,那么該功能將無法實(shí)現(xiàn)阿弃!
- 若需求改成字體大小需要隨系統(tǒng)字體大小的改變而改變诊霹,只需要?jiǎng)h掉該段代碼即可。
- 官方推薦使用sp作為字體單位渣淳。
控件寬高盡量不要固定
原因是如果應(yīng)用需要增加類似微信可以改變字體大小的功能脾还,如果控件寬高固定的話,調(diào)大字體會(huì)導(dǎo)致控件顯示不下入愧,這不是我們需要的效果鄙漏。
場(chǎng)景三
有這樣一種情況,當(dāng)你按照設(shè)計(jì)圖的標(biāo)注去寫一個(gè)TextView控件的時(shí)候棺蛛,寬高用的是wrap_content怔蚌,也沒有設(shè)置任何padding,但是運(yùn)行在手機(jī)上該TextView所占的寬高卻比設(shè)計(jì)圖的要大旁赊。如下圖所示桦踊,字體周圍多了很多空白部分。
這是因?yàn)門extView本身就含有內(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"/>
運(yùn)行效果如下圖中的第二個(gè)“Hello”(第一個(gè)“Hello”為普通TextView),看起來好像是可以的妖爷,但是仔細(xì)看發(fā)現(xiàn)還是留有一點(diǎn)內(nèi)邊距的蝶涩。
一般的應(yīng)用可能不在乎那點(diǎn)內(nèi)邊距,但如果做的是TV上的應(yīng)用就要求比較嚴(yán)格了,因?yàn)門V界面一般是不支持上下左右滾動(dòng)的绿聘,如果設(shè)計(jì)圖上的內(nèi)容剛好占滿屏幕暗挑,那么這些內(nèi)邊距就會(huì)導(dǎo)致個(gè)別控件顯示不全。所以在這種情況下是必須要解決的斜友,既然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();
}
/**
* 計(jì)算文本參數(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>
運(yùn)行效果如下圖中的第三個(gè)“Hello”(第一個(gè)為普通TextView洛史,第二個(gè)為加了includeFontPadding屬性的TextView)惯殊,完美解決!
OK也殖!字體大小適配中最常用的三種場(chǎng)景都講了土思,如果還有其他場(chǎng)景歡迎補(bǔ)充~
項(xiàng)目地址:FontAdaptation