由于Android碎片化嚴(yán)重署辉,屏幕分辨率千奇百怪,而想要在各種分辨率的設(shè)備上顯示基本一致的效果,適配成本越來(lái)越高剑辫。雖然Android官方提供了dp單位來(lái)適配,但其在各種奇怪分辨率下表現(xiàn)卻不盡如人意影兽,因此下面探索一種簡(jiǎn)單且低侵入的適配方式揭斧。
談?wù)刣pi 和 dp
- dpi全名為dot per inch,它表示每英寸上的像素點(diǎn)個(gè)數(shù)峻堰,所以它也常為屏幕密度讹开。 在Android中使用DisplayMetrics中的densityDpi字段表示該值,并且不少文檔中常用dpi來(lái)簡(jiǎn)化或者指代densityDpi捐名。在手機(jī)屏幕一定的情況下旦万,如果分辨率越高那么該值則越大,這就意味著畫面越清晰镶蹋、細(xì)膩和逼真成艘。
- The density-independent pixel(dp)is equivalent to one physical pixel on a 160 dpi screen, which is the baseline density assumed by the system for a “medium” density screen.
已知Android的多個(gè)顯示級(jí)別中有一個(gè)mdpi,它被稱為基準(zhǔn)密度贺归。
當(dāng)dpi=160時(shí)1px=1dp淆两,也就是說(shuō)所有dp和px的轉(zhuǎn)換都是基于mdpi而言的。
已知公式
- px = density * dp;
- density = dpi / 160;
屏幕尺寸拂酣、分辨率秋冰、像素密度三者關(guān)系
通常情況下,一部手機(jī)的分辨率是寬x高婶熬,屏幕大小是以寸為單位剑勾,那么三者的關(guān)系是:
以華為P7為例,計(jì)算其dpi值赵颅。先利用勾股定理得其對(duì)角線的像素值為2202.91虽另,再除以對(duì)角線的大小5,即2202.91/5=440.582饺谬;此處計(jì)算出的440.58便是該設(shè)備的真實(shí)屏幕密度dpi捂刺。
現(xiàn)在我們?cè)偻ㄟ^(guò)代碼來(lái)獲取設(shè)備的dpi值
private void getDisplayInfo(){
Resources resources=getResources();
DisplayMetrics displayMetrics = resources.getDisplayMetrics();
float density = displayMetrics.density;
int densityDpi = displayMetrics.densityDpi;
Log.i(TAG, "density = " + density);
Log.i(TAG, "densityDpi = " + densityDpi);
}
輸出:
density = 3.0
densityDpi = 480
發(fā)現(xiàn)代碼中獲取到的densityDpi=480和我們計(jì)算出來(lái)的屏幕實(shí)際密度值440.582不一樣。因?yàn)樵诿坎渴謾C(jī)出廠時(shí)都會(huì)為該手機(jī)設(shè)置屏幕密度商蕴,若其屏幕的實(shí)際密度是440dpi那么就會(huì)將其屏幕密度設(shè)置為與之接近的480dpi叠萍;如果實(shí)際密度為325dpi那么就會(huì)將其屏幕密度設(shè)置為與之接近的320dpi。
這也就是說(shuō)常見的屏幕密度是與每個(gè)顯示級(jí)別的最大值相對(duì)應(yīng)的绪商,比如:120苛谷、160、240格郁、320腹殿、480独悴、640等。順便說(shuō)一下锣尉,看到代碼中的density么刻炒?其實(shí)它就是一個(gè)倍數(shù)關(guān)系,它表示當(dāng)前設(shè)備的densityDpi和160的比值自沧,480/160=3倍關(guān)系屬于xxhdpi坟奥。從而邏輯分辨率為640dp * 360dp
其實(shí),關(guān)于這一點(diǎn)拇厢,我們從Android源碼對(duì)于densityDpi的注釋也可以看到一些端倪:
The screen density expressed as dots-per-inch.
May be either DENSITY_LOW爱谁,DENSITY_MEDIUM or DENSITY_HIGH
請(qǐng)注意這里的措辭”May be”,它也沒有說(shuō)一定非要是DENSITY_LOW孝偎、DENSITY_MEDIUM访敌、 DENSITY_HIGH這些系統(tǒng)常量。 這就是Android”碎片化”的一個(gè)佐證衣盾。
dp并不能做到適配
假設(shè)我們UI設(shè)計(jì)圖是按屏幕寬度為360dp來(lái)設(shè)計(jì)的寺旺,如果屏幕寬度為1080/(440/160)=392.7dp,也就是屏幕是比設(shè)計(jì)圖要寬的势决。這種情況下阻塑, 即使使用dp也是無(wú)法在不同設(shè)備上顯示為同樣效果的。 同時(shí)還存在部分設(shè)備屏幕寬度不足360dp果复,這時(shí)就會(huì)導(dǎo)致按360dp寬度來(lái)開發(fā)實(shí)際顯示不全的情況叮姑。
對(duì)比其他方案
資源目錄名 。要講的的很多. 例如可以針對(duì)不同的屏幕提供不同的布局据悔,甚至針對(duì)pad與手機(jī)提供兩套完全不同的布局樣式。
但是通常情況下耘沼,設(shè)計(jì)師并不會(huì)對(duì)不同屏幕提供不同的設(shè)計(jì)圖极颓,他們的需求僅僅是不同屏幕下控件對(duì)屏幕的相對(duì)大小一致,直接使用dp并不能滿足這一點(diǎn)群嗤,而對(duì)各種屏幕適配一遍又顯得略為繁瑣菠隆,并且修改也較為麻煩。ConstraintLayout狂秘。百分比支持庫(kù)deprecated之后推薦使用的布局骇径,看起來(lái)似乎略復(fù)雜。
建立多套分辨率者春。編寫腳本將長(zhǎng)度轉(zhuǎn)換成各分辨率下的長(zhǎng)度破衔,缺點(diǎn)是難以覆蓋市面上的所有分辨率。此處有優(yōu)化, 可以參考我的另外一篇文章
AutoLayout支持庫(kù)钱烟。該庫(kù)的想法非常好:對(duì)照設(shè)計(jì)圖晰筛,使用px編寫布局嫡丙,不影響預(yù)覽;繪制階段將對(duì)應(yīng)設(shè)計(jì)圖的px數(shù)值計(jì)算轉(zhuǎn)換為當(dāng)前屏幕下適配的大卸恋凇曙博;為簡(jiǎn)化接入,inflate時(shí)自動(dòng)將各Layout轉(zhuǎn)換為對(duì)應(yīng)的AutoLayout怜瞒,從而不需要在所有的xml中更改父泳。但是同時(shí)該庫(kù)也存在擴(kuò)展性較差。對(duì)于每一種ViewGroup都要對(duì)應(yīng)編寫對(duì)應(yīng)的AutoLayout進(jìn)行擴(kuò)展吴汪,對(duì)于各View的每個(gè)需要適配的屬性都要編寫代碼進(jìn)行適配擴(kuò)展惠窄;在onMeasure階段進(jìn)行數(shù)值計(jì)算。消耗性能浇坐,并且這對(duì)于非LayoutParams中的屬性存在較多不合理之處睬捶。
探索新的適配方式
梳理需求
首先來(lái)梳理下我們的需求,一般我們?cè)O(shè)計(jì)圖都是以固定的尺寸來(lái)設(shè)計(jì)的近刘。比如以分辨率1920px * 1080px來(lái)設(shè)計(jì)擒贸,以density為3來(lái)標(biāo)注,也就是屏幕其實(shí)是640dp * 360dp觉渴。如果我們想在所有設(shè)備上顯示完全一致介劫,其實(shí)是不現(xiàn)實(shí)的,因?yàn)槠聊桓邔挶炔皇枪潭ǖ模?6:9案淋、4:3甚至其他寬高比層出不窮座韵,寬高比不同,顯示完全一致就不可能了踢京。但是通常下誉碴,我們只需要以寬或高一個(gè)維度去適配,比如我們Feed是上下滑動(dòng)的瓣距,只需要保證在所有設(shè)備中寬的維度上顯示一致即可黔帕,再比如一個(gè)不支持上下滑動(dòng)的頁(yè)面,那么需要保證在高這個(gè)維度上都顯示一致蹈丸,尤其不能存在某些設(shè)備上顯示不全的情況成黄。
因此,總結(jié)下大致需求如下:
- 支持以寬或者高一個(gè)維度去適配逻杖,保持該維度上和設(shè)計(jì)圖一致奋岁;
- 不能影響現(xiàn)有dp和sp單位的使用
尋找突破口
布局文件中unit轉(zhuǎn)換,最終都是調(diào)用 TypedValue#applyDimension(int unit, float value, DisplayMetrics metrics) 來(lái)進(jìn)行轉(zhuǎn)換:
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;
}
各種單位說(shuō)明:
PT(point)點(diǎn): 一個(gè)專用的印刷單位“點(diǎn)”, 也是一種像素單位
IN: 英寸
MM: 毫米.
根據(jù)公式很容易得出 1 IN = 25.4MM = 72PT.
對(duì)DIP和SP下手對(duì)于老項(xiàng)目不夠友好, 只能選擇這三個(gè)單位. 又會(huì)發(fā)現(xiàn)這三個(gè)單位轉(zhuǎn)換得到像素值的時(shí)候都會(huì)與metrics.xdpi
有關(guān)
xdpi: The exact physical pixels per inch of the screen in the X dimension.
其實(shí)說(shuō)白了就是X橫軸方向的dpi.
一般給的圖都是以像素為單位的. 例如1920*1080 5寸屏的我們?nèi)绻?pt = 1px. 則如果需要120px的寬度, 我們不用想寫成120pt就OK了.
要求得的1pt實(shí)際對(duì)應(yīng)的px / 屏幕寬度px = 1px / 設(shè)計(jì)圖寬度px
要求得的1pt實(shí)際對(duì)應(yīng)的px = 屏幕寬度px / 設(shè)計(jì)圖寬度px
然后
metrics.xdpi * (1.0f/72) = 對(duì)于1pt表示的像素
metrics.xdpi = 1*72=72
當(dāng)前情況下容易得出 xdpi = 72
, 我們還是算出原來(lái)的xdpi為440, 也就是大概差了6倍.如果假設(shè)1pt = 1px, 在使用過(guò)程中發(fā)現(xiàn)1pt變現(xiàn)為6px, 也就是突然變大了, 你就知道pt失效導(dǎo)致的.自己去找原因并解決.
最終方案
- 已知設(shè)計(jì)圖寬度為1080px, 以寬這個(gè)維度來(lái)適配荸百。
- 適配后的 xdpi= 72f * 設(shè)備真實(shí)寬(單位px) / 設(shè)計(jì)圖寬度闻伶,接下來(lái)只需要把我們計(jì)算好的 density 在系統(tǒng)中修改下即可
final Point size = new Point();
((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getSize(size);
final Resources resources = context.getResources();
resources.getDisplayMetrics().xdpi = size.x / designWidth * 72f;
總結(jié)
- 使用冷門的pt作為長(zhǎng)度單位,按照上述想法將其重定義為與屏幕大小相關(guān)的相對(duì)單位管搪,不會(huì)對(duì)dp等常用單位的使用造成影響虾攻。
- 繪制铡买。編寫xml時(shí)完全對(duì)照設(shè)計(jì)稿上的尺寸來(lái)編寫,只不過(guò)單位換為pt霎箍。假如設(shè)計(jì)圖寬度為200奇钞,一個(gè)控件在設(shè)計(jì)圖上標(biāo)注的長(zhǎng)度為3,只需要在初始化時(shí)定義寬度為200漂坏,繪制該控件時(shí)長(zhǎng)度寫為3pt景埃,那么在任何大小的屏幕上該控件所表現(xiàn)的長(zhǎng)度都為屏幕寬度的3/200。如果需要在代碼中動(dòng)態(tài)轉(zhuǎn)換成px的話顶别,使用
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PT, value, metrics)
-
預(yù)覽谷徙。實(shí)時(shí)預(yù)覽時(shí)繪制頁(yè)面是很重要的一個(gè)環(huán)節(jié)。如果美工偷懶拿了個(gè)iPhone6的標(biāo)準(zhǔn)尺寸1334x750的設(shè)計(jì)圖驯绎,為了實(shí)現(xiàn)于正常繪制時(shí)一樣的預(yù)覽功能完慧,創(chuàng)建一個(gè)長(zhǎng)為1334磅,寬為750磅的設(shè)備作為預(yù)覽剩失,經(jīng)換算約為21.25英寸((sqrt(1334^2 + 750^2))/72)屈尼。預(yù)覽時(shí)選擇這個(gè)設(shè)備即可。
這是因?yàn)橛幸粋€(gè)已知條件
1pt = 1px
則等價(jià)于xdpi = 72
因?yàn)?code>1334px * 750px , 則對(duì)角線px = 1530.3px = 1530.3pt = 21.25 inch
如果采用1920px*1080px
的屏幕同理啦.
該方案由于不是自己原創(chuàng), 我偷偷貼個(gè)代碼, 沒人發(fā)現(xiàn)吧
package xxx.yyy.zzz;
import java.lang.reflect.Field;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.WindowManager;
/**
* Created by Caodongyao
* 轉(zhuǎn)載請(qǐng)聯(lián)系作者并注明出處 http://www.reibang.com/p/b6b9bd1fba4d
* 使用方法: Application#onCreate中調(diào)用一次即可
*/
public class ScreenHelper {
/**
* 重新計(jì)算displayMetrics.xhdpi, 使單位pt重定義為設(shè)計(jì)稿的相對(duì)長(zhǎng)度
*
* @see #activate()
*
* @param context
* @param designWidth
* 設(shè)計(jì)稿的寬度
*/
public static void resetDensity(Context context, float designWidth) {
if (context == null) return;
final Point size = new Point();
((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getSize(size);
final Resources resources = context.getResources();
resources.getDisplayMetrics().xdpi = size.x / designWidth * 72f;
DisplayMetrics metrics = getMetricsOnMiui(resources);
if (metrics != null) {
metrics.xdpi = size.x / designWidth * 72f;
}
}
/**
* 恢復(fù)displayMetrics為系統(tǒng)原生狀態(tài)拴孤,單位pt恢復(fù)為長(zhǎng)度單位磅
*
* @see #inactivate()
*
* @param context
*/
public static void restoreDensity(Context context) {
context.getResources().getDisplayMetrics().setToDefaults();
DisplayMetrics metrics = getMetricsOnMiui(context.getResources());
if (metrics != null)
metrics.setToDefaults();
}
// 解決MIUI更改框架導(dǎo)致的MIUI7+Android5.1.1上出現(xiàn)的失效問(wèn)題(以及極少數(shù)基于這部分miui去掉art然后置入xposed的手機(jī))
private static DisplayMetrics getMetricsOnMiui(Resources resources) {
if ("MiuiResources".equals(resources.getClass().getSimpleName())
|| "XResources".equals(resources.getClass().getSimpleName())) {
try {
Field field = Resources.class.getDeclaredField("mTmpMetrics");
field.setAccessible(true);
return (DisplayMetrics) field.get(resources);
} catch (Exception e) {
return null;
}
}
return null;
}
private Application.ActivityLifecycleCallbacks mActivityLifecycleCallbacks;
private Application mApplication;
private float mDesignWidth;
/**
*
* @param application
* application
* @param width
* 設(shè)計(jì)稿寬度
*/
public ScreenHelper(Application application, float width) {
mApplication = application;
mDesignWidth = width;
mActivityLifecycleCallbacks = new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
// 通常情況下application與activity得到的resource雖然不是一個(gè)實(shí)例脾歧,但是displayMetrics是同一個(gè)實(shí)例,只需調(diào)用一次即可
// 為了面對(duì)一些不可預(yù)計(jì)的情況以及向上兼容演熟,分別調(diào)用一次較為保險(xiǎn)
resetDensity(mApplication, mDesignWidth);
resetDensity(activity, mDesignWidth);
}
@Override
public void onActivityStarted(Activity activity) {
resetDensity(mApplication, mDesignWidth);
resetDensity(activity, mDesignWidth);
}
@Override
public void onActivityResumed(Activity activity) {
resetDensity(mApplication, mDesignWidth);
resetDensity(activity, mDesignWidth);
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
};
}
/**
* 激活本方案
*/
public void activate() {
resetDensity(mApplication, mDesignWidth);
mApplication.registerActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
}
/**
* 恢復(fù)系統(tǒng)原生方案
*/
public void inactivate() {
restoreDensity(mApplication);
mApplication.unregisterActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
}
/**
* 轉(zhuǎn)換pt為px
* @param context context
* @param value 需要轉(zhuǎn)換的pt值鞭执,若context.resources.displayMetrics經(jīng)過(guò)resetDensity()的修改則得到修正的相對(duì)長(zhǎng)度,否則得到原生的磅
* @return px值
*/
public static float pt2px(Context context, float value){
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PT, value, context.getResources().getDisplayMetrics());
}
}
FAQ
若存在webview導(dǎo)致適配失效的問(wèn)題
可以先繼承WebView并重寫setOverScrollMode(int mode)
方法芒粹,在方法中調(diào)用super之后調(diào)用一遍ScreenHelper.resetDensity(getContext(), designWidth)
規(guī)避
若存在dialog中適配失效的問(wèn)題
可以在dialog的oncreate中調(diào)用一遍ScreenHelper.resetDensity(getContext(), designWidth)
規(guī)避
旋轉(zhuǎn)屏幕之后適配失效
可以在onConfigurationChanged中調(diào)用ScreenHelper .resetDensity(getContext(), designWidth)
規(guī)避
特定國(guó)產(chǎn)機(jī)型ROM中偶先f(wàn)ragment失效
可以在fragment的onCreateView中調(diào)用ScreenHelper .resetDensity(getContext(), designWidth)
規(guī)避
總結(jié)
- 總而言之這是一套按比例適配的方式
- 以上說(shuō)的某些情況下xdpi會(huì)被還原導(dǎo)致失效, 表現(xiàn)形式為字體大小, View的寬和高突然擴(kuò)大好幾倍的情況發(fā)生, 需要使ScreenHelper#resetDensity方法還原.
- 該方案只考慮x軸方向, 毋需或者暫不考慮y軸方向
- 如何選擇基準(zhǔn)設(shè)備呢, 這當(dāng)然根據(jù)UI給的切圖而定, 但現(xiàn)在的UI一般都是以蘋果設(shè)備為原型而定. 我給的參考是手機(jī)參考
1920px*1080px
16:9的屏幕,一般而言可以做到手機(jī)和Pad通吃,如果你們公司遵循"更大的屏幕顯示更多的內(nèi)容", 可以和美工協(xié)商規(guī)劃.