衆所周知,Android受權的廠商不可勝數,生產出的機型也數不勝數,致使尺寸碎片化很嚴重。固然,都9102年了,你們逐漸獲得了最優解,國內主流機型基本上都在720、1080、1440徘徊,最多高度上各有所長,可是仍是保留着很多其餘分辨率的手機,先來看一組數據(來源:友盟)——html
如圖所證上述結論的正確性,可是能夠看到,每一年都有比例不小的其餘尺寸的手機佔據着市場份額,更況且那些還在服役的古董機器。我相信,這部分用戶羣是不可能被產品經理所割捨的。git
爲了解決這個問題,咱們固然能夠——github
可是我想說,以上種種,只是一個Android開發應該具有的基本素質。也許有人會問,這些還不夠嗎?並且dp、sp不已是官方適配過了的單位嗎?下面咱們就來逐步剖析。web
設備獨立像素(dp、sp),又叫邏輯像素,是一種用縮放因子(scale)計算出來的、和像素有必定的換算比例的、不受設備分辨率和密度(ppi)制約的尺寸單位。bash
那麼什麼是分辨率,什麼是ppi,什麼是dpi。微信
分辨率是指在手機屏幕中橫豎都有多少個像素點,所謂的1080x1920便是指,屏幕的高有1920個像素點,寬有1080個像素點。當咱們繼續查看手機參數的時候,會看到下一個指標,叫作ppi(Pixels Per Inch),表示每英寸所包含的像素點個數,ppi越大,屏幕越細膩,可是超過了肉眼的分辨率是沒有多大意義的,以榮耀10爲例,它的分辨率是1080x2280,那麼對角線所具備的像素點個數爲2522.86,而主屏尺寸是5.84英寸,那麼咱們能夠得出每英寸所包含的像素點個數爲431.997≈432,即ppi=432。那麼dpi又是什麼?dpi(Dots per Inch),字面意思是每英寸包含的點數,可是實際上它如今更多的用於表示顯示策略中的一個參數,在Android中,它是能夠在系統中設置的、是可變的、是用於計算縮放因子的,也許在不少文章中咱們均可以看到ppi就是dpi這樣的言論,可是其實它們已經和最初的釋義有所差別,具體參照WHAT IS DPI,我的認爲這篇文章是講述比較全面、靠譜、符合事實的。app
而獨立像素,爲何不受分辨率和密度制約?框架
咱們首先明白,當咱們假定的認爲像素點都是趨於正方形時,密度只能影響視覺呈現的物理大小和精細程度,屏幕上高寬都爲x個像素所組成的正方形,在相同分辨率不一樣密度的手機中,他們只是視覺大小不同,可是佔據屏幕的比例是一致的。ide
那麼咱們只須要分析爲何獨立像素不受分辨率制約。佈局
咱們知道,每一個手機出廠時都寫好了固定的dpi在手機的系統文件中,而dpi是造成獨立像素的一個重要參數,咱們能夠根據dpi計算出dp和px的換算比例,也就是縮放因子(scale),從官方的文檔,咱們能夠得出一個公式
1dp = 1px * scale = 1px * dpi / 160
複製代碼
也就是說,只要按照相同的規則控制dpi的數值,咱們在寬度的緯度上,能夠作到將全部分辨率都換算成一個數值的設備獨立像素。
打個比方,如今大部分720x1280的手機的dpi都等於320,根據公式可得,寬度爲360dp,而1080x1920的手機大部分的dpi都等於480,一樣根據公式所得寬度爲360dp。
那麼相同的設備獨立像素能夠作什麼呢?只要咱們設置成寬度爲180dp,那麼它永遠是佔據屏幕寬度一半的比例。
而如果咱們直接使用像素做爲控件的單位,那麼是沒法保證它在不一樣分辨率的手機是佔據相同的比例。
一樣舉個例子,在720x1280的手機上咱們設置寬度爲360px,它將佔據一半的屏幕,而在1080x1920的手機上只能佔據三分之一的屏幕。
這就是爲何官方推薦咱們使用設備獨立像素做爲尺寸的單位。那麼問題來了,既然設備獨立像素這麼優秀,那麼咱們——
上節舉例中說到,大部分720x1280、1080x1920的手機寬度都是360dp,而大部分480x800的手機(dpi=240)寬度是320dp,那麼當設計稿是360dp的時候,會發生什麼。
舉個例子,以下圖所示,兩臺設備分辨率一致,可是dpi不一致,前者是480dpi,後者是540dpi(ps:不要問有沒有這種機器,nove4e就是這樣的),而設計稿是以360dp爲基準,熱度排名和貢獻排名的寬度比例是3:4,則咱們能夠看到其在320dp下表現比較差,即便咱們再如何佈局,如何使用屬性,它永遠是不完美的,由於它的邏輯寬度永遠都比設計稿少40dp。
而除了320dp、360dp,單國內手機的邏輯寬度就還有345.6dp、375.6dp、392.7dp、411.4dp、423.5dp等等。固然,理論上來講更多的邏輯寬度應該顯示更多的內容,然而現實的狀況每每不容許,這意味着——
總之,就是人力成本過高。可是經過二次適配,咱們能夠作到一套設計圖適配「全部」設備,一套佈局「全家」適用。也許這不是最好的方案,可是綜合來看這是最合適的方案,是最具性價比的方案。那麼咱們要——
作二次適配的方法有多種,大致能夠分爲窮舉和Hook。
注:由於當下大部分app的應用場景只在於豎屏,即便有橫屏的界面也只須要保持高度不變,寬度自適應。退一步講,真的有個別頁面的高度也須要自適應時,能夠具體場景具體分析,即便不作二次適配,也是ok的。所以如下的適配方法只從寬度的緯度來說訴。
咱們都知道,寬高限定符的匹配規則是,雙邊都小於屏幕分辨率的最接近的值。根據這個規則,咱們儘量的枚舉出全部的分辨率(雖然分辨率有不少,可是咱們只須要按照寬度來枚舉便可,高度設置成略大於寬度)。而根據testin、wetest雲真機的分辨率,咱們能夠得出文件結構以下:
+-- res
| +-- values
| +-- values-330x320
| +-- values-490x480
| +-- values-550x540
| +-- values-650x640
| +-- values-730x720
| +-- values-780x768
| +-- values-810x800
| +-- values-1100x1080
| +-- values-1160x1152
| +-- values-1210x1200
| +-- values-1450x1440
| +-- values-2170x2160
複製代碼
而後以1080px爲基準,計算出1px在其餘分辨率下的等比值 (注:默認values=values-1100x1080),假設目標分辨率的寬爲W,則公式爲:
px' = W/1080
複製代碼
舉個例子,720px的分辨率的dimens值爲:
<resources>
<dimen name="x1">0.66px</dimen>
<dimen name="x2">1.33px</dimen>
<dimen name="x3">2.0px</dimen>
<dimen name="x4">2.66px</dimen>
<dimen name="x5">3.33px</dimen>
<dimen name="x6">4.0px</dimen>
<dimen name="x7">4.66px</dimen>
<dimen name="x8">5.33px</dimen>
<dimen name="x9">6.0px</dimen>
<dimen name="x10">6.66px</dimen>
.
.
.
<dimen name="x1080">720px</dimen>
</resources>
複製代碼
配置好後,咱們從以上分辨率中選擇9種採樣,看看實際運行效果如何:
由上圖可見,運行結果是很是符合咱們預期值的,佔一半屏幕的仍是佔一半,熱度排名和貢獻排名的間距也基本差很少,惟一比較明顯的是每行的文字字數±1,這是因爲換算以後的像素有小數點形成的,可是這是能夠接受的。
而後咱們再來分析一下極端狀況,首先由於咱們是窮舉分辨率,因此此處不須要考慮dpi,又由於咱們窮舉的分辨率的寬是320-2160,因此咱們能夠從這個角度考慮邊界值:
咱們再看看三、四、5的運行效果如何:
從結果能夠看出,咱們出現的極端狀況都是比預期值要寬,這是由於咱們分辨率限定符是向下匹配的。
綜上所述,咱們得出結論:
最小寬度限定符,是指在邏輯寬度上限定使用小於而且最接近於屏幕寬度的資源。而邏輯寬度(W')能夠從分辨率(W)和dpi得知:
W' = W / ( dpi / 160 )
複製代碼
咱們能夠和窮舉分辨率限定符同樣,窮舉出全部可能的邏輯寬度。爲了分析,咱們暫定文件結構以下:
+-- res
| +-- values
| +-- values-sw320dp
| +-- values-sw360dp
| +-- values-sw411dp
複製代碼
而後以360dp爲基準,計算出1dp在其餘邏輯寬度下的等比值 (注:默認values=values-sw360dp),假設目標邏輯寬度爲W,則公式爲:
dp' = W/360
複製代碼
一樣的舉個例子,320dp的邏輯寬度的dimens值爲:
<resources>
<dimen name="dp_1">0.89dp</dimen>
<dimen name="dp_2">1.78dp</dimen>
<dimen name="dp_3">2.67dp</dimen>
<dimen name="dp_4">3.56dp</dimen>
<dimen name="dp_5">4.44dp</dimen>
<dimen name="dp_6">5.33dp</dimen>
<dimen name="dp_7">6.22dp</dimen>
<dimen name="dp_8">7.11dp</dimen>
<dimen name="dp_9">8.00dp</dimen>
<dimen name="dp_10">8.89dp</dimen>
.
.
.
<dimen name="dp_360">320dp</dimen>
</resources>
複製代碼
咱們來看看實際的運行效果:
顯而易見,又是符合咱們預期的,可是不可避免的是邏輯寬度依舊存在刺頭(緣由見文章開頭——爲何要二次適配),好比384dp(Nexus 4)、392dp(XiaoMi MIX2),因此咱們再次來看看極端狀況:
好了,由於最小寬度限定符依舊是向下匹配的,從而又回到了和上一節如出一轍的狀況——極端狀況比預期值要寬,因此此處咱們再也不重複貼圖。
那麼咱們總結一下此節:
上面說了如何窮舉來進行適配,可是如何窮舉的既完整又簡潔是一個難點,那麼可不可能有一個測量的終點,全部的間距、大小、尺寸都會經過這裏,咱們在這個終點進行自動化適配就行了?固然是有的。
咱們知道,view是須要先measure而後layout而後才draw的,那麼切入點就來了——onMeasure。
典型的例子是AndroidAutoLayout,它的使用方法詳見ReadMe,這裏再也不贅述。其核心思想是即是經過重寫其onMeasure,在調用super.onMeasure(widthMeasureSpec, heightMeasureSpec)以前從新根據屏幕寬度及高度設置了相關屬性的值,如padding、margin、height、width、textSize。
固然,AndroidAutoLayout的上一次提交代碼已是在4 yeas ago,在它的設計之初,是假定的認爲全部的手機的高寬比都是在一個恰當的範圍,好比720x1280,因此它的高寬都分別進行了不一樣比例的縮放適配。然而,9102年,手機的高寬比顯然已經多種多樣。因此,AndroidAutoLayout已經進入了它的侷限性。可是,它依舊是咱們能夠借鑑的目標,咱們只須要將其高按照寬的縮放比例來縮放,或高或矮的手機自適應高度便可。(有興趣的同窗能夠嘗試一下,這裏只講如何hook~)
那麼,onMeasure中怎麼hook呢?在AndroidAutoLayout中,它寫了不少的自定義ViewGroup,好比AutoLinearLayout、AutoRelativeLayout、AutoFrameLayout,其實裏面的代碼都大同小異,咱們以AutoLinearLayout爲例——
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!isInEditMode()) { //這句代碼不用管,用來判斷是不是IDE預覽模式的
mHelper.adjustChildren();
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
複製代碼
能夠看到,AndroidAutoLayout在super.onMeasure以前只作了一件事,就是adjustChildren,修改控件屬性值。
public void adjustChildren() {
AutoLayoutConifg.getInstance().checkParams(); //這句話不用管,用來檢查庫配置的
for (int i = 0, n = mHost.getChildCount(); i < n; i++) {
View view = mHost.getChildAt(i);
ViewGroup.LayoutParams params = view.getLayoutParams();
if (params instanceof AutoLayoutParams) {
AutoLayoutInfo info = ((AutoLayoutParams) params).getAutoLayoutInfo();
if (info != null) {
info.fillAttrs(view);
}
}
}
}
複製代碼
而adjustChildren中循環取到了全部表層childView的AutoLayoutParams,AutoLayoutParams繼承於父類LayoutParams,它也沒幹啥事,主要是將須要適配的屬性(如textSize)存儲起來。
public static AutoLayoutInfo getAutoLayoutInfo(Context context,AttributeSet attrs) {
...
//原來設計的時候是和寬度的相關的屬性按寬度縮放,和高度相關的屬性按高度縮放。可是總有特例,因此baseWidth、baseHeight就是用來強制約束縮放方向的。
int baseWidth = a.getInt(R.styleable.AutoLayout_Layout_layout_auto_basewidth, 0);
int baseHeight = a.getInt(R.styleable.AutoLayout_Layout_layout_auto_baseheight, 0);
...
for (int i = 0; i < n; i++) {
...
switch (index) {
case INDEX_TEXT_SIZE:
info.addAttr(new TextSizeAttr(pxVal, baseWidth, baseHeight));
break;
case INDEX_PADDING:
info.addAttr(new PaddingAttr(pxVal, baseWidth, baseHeight));
break;
...
}
}
return info;
}
複製代碼
固然,不一樣的AutoAttr會實現各自的縮放方法,其實就是很簡單的計算出設計稿寬高和屏幕寬高的比值,而後和attribute的原始值相乘,獲得最終的屬性值。
public void apply(View view) {
int val;
if (useDefault()) {
val = defaultBaseWidth() ? getPercentWidthSize() : getPercentHeightSize();
} else if (baseWidth()) {
val = getPercentWidthSize();
} else {
val = getPercentHeightSize();
}
if (val > 0) {
val = Math.max(val, 1);//for very thin divider
}
execute(view, val);//執行縮放,並將其設置到view或者layoutParams中
}
複製代碼
而apply在哪裏被調用,其實就是在adjustChildren中的fillAttrs。
這樣,hook就完成了。固然,AutoAttr、Helper、AutoLayoutParams以及內部封裝的AutoLayoutActivity、AutoUtils,這些都是設計思想,重要的是實現思路,其實你也能夠簡單粗暴的糅合在一塊兒,啊哈哈~
既然是完美適配,那就隨便貼幾個亂七八糟的分辨率吧~
按照慣例,如下是此節總結:
話說回來,其實onMeasure是一個淺層次的hook點,它雖然優勢很明顯,可是一樣的缺點也很明顯,那有沒有一個切入點,既能夠自動適配,又不用寫這麼多代碼,侵入性也不高呢?字節跳動團隊給了咱們答案。
前面有講到dp和px之間的關係,咱們能夠知道:
1dp = 1px * dpi / 160
複製代碼
而系統必定有一個地方是用來轉換這些單位的,好比TypedValue中:
public static float applyDimension(int unit, float value,DisplayMetrics metrics)
{
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}
複製代碼
好比BitmapFactory中:
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,@Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
validate(opts);
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
//inDensity是指資源所在的drawable文件夾的密度,inTargetDensity是指屏幕密度
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
複製代碼
咱們觀察能夠看到,他們都用到了DisplayMetrics.densityDpi這個屬性,那麼咱們只須要根據屏幕密度來修改這個屬性值,就能夠僞裝屏幕永遠爲360dp的邏輯寬度。
而DisplayMetrics能夠從三個地方獲得:
// 系統的屏幕尺寸
val systemMetrics = Resources.getSystem().displayMetrics
// application的屏幕尺寸
val applicationMetrics = application.resources.displayMetrics
// activity的屏幕尺寸
val activityMetrics = activity.resources.displayMetrics
複製代碼
咱們只須要修改application和activity的就行,system則建議不修改,用於保留一份原始數據,並且即便改了也沒什麼用,它是用於獲取系統資源的。
從上文咱們能夠很容易的得知當前屏幕的邏輯寬度是:
/**
* 注意這裏widthPixels不要用real width,緣由有二:
* 1.available display width可能會小於real width,雖然大部分實際場景中是同樣的;
* 2.在1的場景中(好比說屏幕左右有個裝飾欄?),咱們可能會出現,application中應用的是available display width,activity中應用的是real width,
* 可是若是activity中使用了application.resource,那麼此時間距大小會略小,這並無什麼關係。
* 反過來若是咱們修改的時候用的是real width,那麼此時就會顯示不下。
*/
val widthInDp = resources.displayMetrics.run {
widthPixels / (densityDpi / 160)
}
複製代碼
那麼咱們能夠設置densityDpi爲:
val targetDpi = resources.displayMetrics.widthPixels * 160 / 360 //360是咱們的設計稿的邏輯寬度
val sysMetrics = Resources.getSystem().displayMetrics
resources.displayMetrics.run {
densityDpi = targetDpi
density = targetDpi / 160f
scaledDensity = density * sysMetrics.scaledDensity / sysMetrics.density //由於用戶會修改字體大小,所以須要根據原比例來獲得新的scaledDensity
}
application.resources.displayMetrics.run {
densityDpi = targetDpi
density = targetDpi / 160f
scaledDensity = density * sysMetrics.scaledDensity / sysMetrics.density
}
複製代碼
恢復的時候使用:
/**
* 這裏直接使用sysMetrics進行恢復,緣由有二
* 1.不用記錄中間值
* 2.在使用應用時若是修改了系統字體大小,sysMetrics會同步修改,不用再監聽registerComponentCallbacks
*/
val sysMetrics = Resources.getSystem().displayMetrics
resources.displayMetrics.run {
densityDpi = sysMetrics.densityDpi
density = sysMetrics.density
scaledDensity = sysMetrics.scaledDensity
}
application.resources.displayMetrics.run {
densityDpi = sysMetrics.densityDpi
density = sysMetrics.density
scaledDensity = sysMetrics.scaledDensity
}
複製代碼
看看適配效果~
看起來perfect是否是?代碼也很簡單是否是?好像也沒什麼侵入性是否是?然而萬物有利也有弊:
/**
* 繼承webView,複寫此方法
**/
override fun setOverScrollMode(mode: Int) {
super.setOverScrollMode(mode)
adaptDensityDpi()
}
/**
* 或者複寫activity的此方法
*/
override fun getResources(): Resources {
adaptDensityDpi() //注意避免死循環及重複修改
return super.getResources()
}
複製代碼
因此以上的代碼其實只能應用於demo當中,以期證實咱們的方向是對的。剩下的咱們還須要進一步解決上述列出的這些問題,還須要適配,須要封裝,須要提升易用性、健壯性~好比AndroidAutoSize。
好了,本文到此結束。說了這麼多,我的仍是比較傾向於最後一種方案。固然,一個方案的成熟是須要成長的,項目也是,人也是。其餘的,仁者見仁,智者見智咯。
本文做者: timedance
本文連接: www.tktimedance.com/posts/a5563…
Demo地址: github.com/timedance/S… 版權聲明: 本博客全部文章除特別聲明外,均採用 BY-NC-ND 許可協議。轉載請註明出處!