前言
一個(gè)月前看了今日頭條新的屏幕適配方案漱病,這是傳送門(mén)鸵荠,對(duì)此不禁拍案叫絕,為此我想把這種方案融入到我工具類中直接一行代碼即可適配御铃,如今最新 1.19.0 版 AndroidUtilCode 已有其最新的適配方案饺窿,其相關(guān)函數(shù)在 ScreenUtils 中歧焦,相關(guān) API 如下所示:
adaptScreen4VerticalSlide : 適配垂直滑動(dòng)的屏幕
adaptScreen4HorizontalSlide: 適配水平滑動(dòng)的屏幕
cancelAdaptScreen : 取消適配屏幕
isAdaptScreen : 是否適配屏幕
效果
UtilApk 中的 ScreenAdaptActivity 以設(shè)計(jì)圖為 360dp 寬度 來(lái)做適配,我們?cè)O(shè)置兩個(gè) view 寬度為 180dp肚医,代碼如下所示:
public class ScreenAdaptActivity extends BaseActivity {
private TextView tvUp;
private TextView tvDown;
public static void start(Context context) {
Intent starter = new Intent(context, ScreenAdaptActivity.class);
context.startActivity(starter);
}
@Override
public void initData(@Nullable Bundle bundle) {
if (ScreenUtils.isPortrait()) {
ScreenUtils.adaptScreen4VerticalSlide(this, 360);
} else {
ScreenUtils.adaptScreen4HorizontalSlide(this, 360);
}
}
@Override
public int bindLayout() {
return R.layout.activity_screen_adapt;
}
@Override
public void initView(Bundle savedInstanceState, View contentView) {
}
@Override
public void doBusiness() {
}
@Override
public void onWidgetClick(View view) {
}
public void toggleFullScreen(View view) {
ScreenUtils.toggleFullScreen(this);
}
@Override
protected void onDestroy() {
ScreenUtils.cancelAdaptScreen(this);
super.onDestroy();
}
}
其在 1080x1920 420dpi(xxhdpi) 下的效果如下所示:
其在 768x1280 320dpi(xhdpi) 下的效果如下所示:
其在 480x800 240dpi(hdpi) 下的效果如下所示:
其在 320x480 160dpi(mdpi) 下的效果如下所示:
如上就是豎屏以 360dp 為寬度和橫屏以 360dp 為高度的適配效果绢馍。
原理
如果看了上面今日頭條的那篇適配文章,那么你可能已經(jīng)知道其原理了忍宋,不明白的話可以繼續(xù)看下我的解釋:
我們知道 px = dp * density
痕貌,我們要適配的話需要確保 dp 不變?nèi)バ薷?density风罩,而安卓默認(rèn) density = dpi / 160
糠排,其意思就是 1dp 有多少 px,也就是像素密度超升,我們開(kāi)發(fā)是按照一份設(shè)計(jì)稿來(lái)做的入宦,那么有沒(méi)有什么辦法來(lái)讓 density 和設(shè)計(jì)稿尺寸做聯(lián)系呢?假設(shè)我們?cè)O(shè)計(jì)稿是寬度是 1080px室琢,資源放在 xxhdpi乾闰,那么我們寬度轉(zhuǎn)換為 dp 就是 1080 / 3 = 360dp,要在不同設(shè)備上寬度都表現(xiàn)為 360dp盈滴,那么就需要修改其 density = screenWidthPx / 360
涯肩,這樣就滿足了上述條件轿钠,而和 density 相關(guān)的還有 densityDpi、scaledDensity病苗,我們根據(jù) density 等比修改 densityDpi疗垛、scaledDensity 即可。
由于 API 26 及以上的 Activity#getResources()#getDisplayMetrics()
和 Application#getResources()#getDisplayMetrics()
是不同的引用硫朦,所以在 API 26 及以上適配是沒(méi)有影響的贷腕,但在 API 26 以下 Activity#getResources()#getDisplayMetrics()
和 Application#getResources()#getDisplayMetrics()
是相同的引用,導(dǎo)致適配有問(wèn)題咬展,這里要感謝 @MirkoWu 提出的問(wèn)題泽裳,后面會(huì)有解決方案。
如果我們以 xxhdpi 的 360dp 來(lái)適配的話破婆,首先在 AS 中預(yù)覽是個(gè)問(wèn)題涮总,在接入第三方 SDK 帶有界面或者 View 的話會(huì)導(dǎo)致它的尺寸全然不對(duì),因?yàn)槲覀兡菢舆m配后界面寬度只有 360dp祷舀,而第三方 SDK 中很有可能寫(xiě)的布局會(huì)超出 360dp妹卿,這便會(huì)引發(fā)新的問(wèn)題,當(dāng)然這也是有響應(yīng)的解決之道蔑鹦,比如暫時(shí)取消適配夺克,但我們有更好的方式,著重看下面介紹嚎朽。
我著重推薦以 mdpi 為特例來(lái)適配铺纽,比如前面說(shuō)到的 xxhdpi 的 360dp,那么在 mdpi 下就是 360 * 3 = 1080dp哟忍,這樣我們新建一個(gè)寬為 1080px 的 mdpi 設(shè)備(可以通過(guò)修改設(shè)備尺寸來(lái)達(dá)到 mdpi)狡门,然后切換為該設(shè)備來(lái)預(yù)覽布局就完美解決了以上問(wèn)題,我們?cè)趯?xiě)布局的時(shí)候設(shè)計(jì)圖是 36px锅很,那么我們直接就寫(xiě) 36dp 即可其馏,設(shè)計(jì)圖字體是 24px, 我們直接就寫(xiě) 24sp 即可爆安,這樣便可達(dá)到和設(shè)計(jì)圖一致的效果叛复。另外,圖片資源放在需要適配的最高 dpi 下面即可扔仓,比如 drawable-xxhdpi
或者 drawable-xxxhdpi
褐奥,這樣在高清屏上也不會(huì)導(dǎo)致失真。
但是這樣會(huì)導(dǎo)致獲取狀態(tài)欄和導(dǎo)航欄高度有問(wèn)題翘簇,其獲取狀態(tài)欄高度代碼為如下所示:
public static int getStatusBarHeight() {
Resources resources = Utils.getApp().getResources();
int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
return resources.getDimensionPixelSize(resourceId);
}
由于使用的是 Application#getResources撬码,這會(huì)導(dǎo)致最后計(jì)算狀態(tài)欄高度使用的是修改過(guò)后的 density,在這里也要感謝 @magic0908 無(wú)意間提到的 Resources.getSystem()
來(lái)獲取系統(tǒng)的 Resources
,果不其然可以獲取到正確高度的狀態(tài)欄高度版保,代碼如下所示:
public static int getStatusBarHeight() {
Resources resources = Resources.getSystem();
int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
return resources.getDimensionPixelSize(resourceId);
}
同理獲取導(dǎo)航欄高度也可以這樣呜笑。
考慮到了 Resources.getSystem()
夫否,那么我們?cè)谶m配上豈不是可以更方便,不用區(qū)分版本什么的 Activity#getResources()#getDisplayMetrics()
和 Application#getResources()#getDisplayMetrics()
叫胁,也不需要什么中間變量來(lái)記錄適配前的值慷吊,那些值我們直接在 Resources#getSystem()#getDisplayMetrics()
中獲取 density
、densityDpi
曹抬、scaledDensity
即可溉瓶,而且在修改系統(tǒng)字體的時(shí)候,Resources#getSystem()#getDisplayMetrics()
也會(huì)相應(yīng)地改變谤民,這樣也就不需要注冊(cè) registerComponentCallbacks
來(lái)監(jiān)聽(tīng)系統(tǒng)字體的改變堰酿,所以最終的源碼很是簡(jiǎn)潔,但其中間遇到的問(wèn)題很是復(fù)雜张足,光工具類我這些天就更新了很多版本來(lái)解決其問(wèn)題触创,從1.18.0
到 1.18.7
,有六個(gè)版本都是和這個(gè)適配有關(guān)系为牍,但最終還是完美地找到了解決方案哼绑,也要感謝大家的幫助,其最終源碼如下所示:
/**
* Adapt the screen for vertical slide.
*
* @param activity The activity.
* @param designWidthInPx The size of design diagram's width, in pixel.
*/
public static void adaptScreen4VerticalSlide(final Activity activity,
final int designWidthInPx) {
adaptScreen(activity, designWidthInPx, true);
}
/**
* Adapt the screen for horizontal slide.
*
* @param activity The activity.
* @param designHeightInPx The size of design diagram's height, in pixel.
*/
public static void adaptScreen4HorizontalSlide(final Activity activity,
final int designHeightInPx) {
adaptScreen(activity, designHeightInPx, false);
}
/**
* Reference from: https://mp.weixin.qq.com/s/d9QCoBP6kV9VSWvVldVVwA
*/
private static void adaptScreen(final Activity activity,
final int sizeInPx,
final boolean isVerticalSlide) {
final DisplayMetrics systemDm = Resources.getSystem().getDisplayMetrics();
final DisplayMetrics appDm = Utils.getApp().getResources().getDisplayMetrics();
final DisplayMetrics activityDm = activity.getResources().getDisplayMetrics();
if (isVerticalSlide) {
activityDm.density = activityDm.widthPixels / (float) sizeInPx;
} else {
activityDm.density = activityDm.heightPixels / (float) sizeInPx;
}
activityDm.scaledDensity = activityDm.density * (systemDm.scaledDensity / systemDm.dens
activityDm.densityDpi = (int) (160 * activityDm.density);
appDm.density = activityDm.density;
appDm.scaledDensity = activityDm.scaledDensity;
appDm.densityDpi = activityDm.densityDpi;
}
/**
* Cancel adapt the screen.
*
* @param activity The activity.
*/
public static void cancelAdaptScreen(final Activity activity) {
final DisplayMetrics systemDm = Resources.getSystem().getDisplayMetrics();
final DisplayMetrics appDm = Utils.getApp().getResources().getDisplayMetrics();
final DisplayMetrics activityDm = activity.getResources().getDisplayMetrics();
activityDm.density = systemDm.density;
activityDm.scaledDensity = systemDm.scaledDensity;
activityDm.densityDpi = systemDm.densityDpi;
appDm.density = systemDm.density;
appDm.scaledDensity = systemDm.scaledDensity;
appDm.densityDpi = systemDm.densityDpi;
}
/**
* Return whether adapt screen.
*
* @return {@code true}: yes<br>{@code false}: no
*/
public static boolean isAdaptScreen() {
final DisplayMetrics systemDm = Resources.getSystem().getDisplayMetrics();
final DisplayMetrics appDm = Utils.getApp().getResources().getDisplayMetrics();
return systemDm.density != appDm.density;
}
坑點(diǎn)
在原理里都已經(jīng)說(shuō)完了哈碉咆。
建議
新老項(xiàng)目都可以用這套方案抖韩,老項(xiàng)目中如果有新的 Activity 加進(jìn)來(lái),那么可以對(duì)其使用該方案來(lái)適配疫铜,然后在啟動(dòng)其他老的 Activity 時(shí)候 cancelAdaptScreen
即可茂浮。新項(xiàng)目我建議采用我工具類中的使用,可以讓你爽到極致壳咕,在 BaseActivity
中 setContentView(xx)
之前調(diào)用適配代碼即可席揽,記得第二個(gè)參數(shù)一定要傳入設(shè)計(jì)圖的實(shí)際像素尺寸,不再是曾經(jīng)的 dp 尺寸了谓厘。
有了固定的尺寸幌羞,那么我們百分比是不是就很好實(shí)現(xiàn)了,計(jì)算后直接寫(xiě) xxdp 即可竟稳,這樣在所有設(shè)備上也都是一定的比例属桦,哪里還需要什么百分比布局什么的來(lái)做?是不是 so easy住练,更多風(fēng)騷的操作可待你解鎖地啰。
結(jié)語(yǔ)
如果我的工具類對(duì)你的適配造成了影響,歡迎到 AndroidUtilCode 提 issue讲逛,感謝今日頭條的方案,讓我可以站在巨人的肩膀上裝一次 13岭埠。
最后
記得屏幕適配一定要用 1.19.0 版本及以上
記得屏幕適配一定要用 1.19.0 版本及以上
記得屏幕適配一定要用 1.19.0 版本及以上
給大家?guī)?lái)了麻煩盏混,sorry蔚鸥。