Android 屏幕適配從未如斯簡(jiǎn)單(8月10日最終更新版)

前言

一個(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) 下的效果如下所示:

xxhdpi

其在 768x1280 320dpi(xhdpi) 下的效果如下所示:

xhdpi

其在 480x800 240dpi(hdpi) 下的效果如下所示:

hdpi

其在 320x480 160dpi(mdpi) 下的效果如下所示:

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() 中獲取 densitydensityDpi曹抬、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.01.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)目我建議采用我工具類中的使用,可以讓你爽到極致壳咕,在 BaseActivitysetContentView(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蔚鸥。

GitHub issue

屏幕適配問(wèn)題匯總

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市许赃,隨后出現(xiàn)的幾起案子止喷,更是在濱河造成了極大的恐慌,老刑警劉巖混聊,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件弹谁,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡句喜,警方通過(guò)查閱死者的電腦和手機(jī)预愤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)咳胃,“玉大人植康,你說(shuō)我怎么就攤上這事≌剐福” “怎么了销睁?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)存崖。 經(jīng)常有香客問(wèn)我冻记,道長(zhǎng),這世上最難降的妖魔是什么来惧? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任檩赢,我火速辦了婚禮,結(jié)果婚禮上违寞,老公的妹妹穿的比我還像新娘贞瞒。我一直安慰自己,他們只是感情好趁曼,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布军浆。 她就那樣靜靜地躺著,像睡著了一般挡闰。 火紅的嫁衣襯著肌膚如雪乒融。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天摄悯,我揣著相機(jī)與錄音赞季,去河邊找鬼。 笑死奢驯,一個(gè)胖子當(dāng)著我的面吹牛申钩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播瘪阁,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼撒遣,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼邮偎!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起义黎,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤禾进,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后廉涕,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體泻云,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年狐蜕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宠纯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡馏鹤,死狀恐怖征椒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情湃累,我是刑警寧澤勃救,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站治力,受9級(jí)特大地震影響蒙秒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜宵统,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一晕讲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧马澈,春花似錦瓢省、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至涤伐,卻和暖如春馒胆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背凝果。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工祝迂, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人器净。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓型雳,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子四啰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,520評(píng)論 25 707
  • 此文轉(zhuǎn)載今日頭條技術(shù)團(tuán)隊(duì)D怠4只帧8躺埂! 在Android開(kāi)發(fā)中眷射,由于Android碎片化嚴(yán)重匙赞,屏幕分辨率千奇百怪,而想要...
    水大云霄閱讀 953評(píng)論 1 1
  • 在Android開(kāi)發(fā)中妖碉,由于Android碎片化嚴(yán)重涌庭,屏幕分辨率千奇百怪,而想要在各種分辨率的設(shè)備上顯示基本一致的...
    一只筆閱讀 690評(píng)論 0 15
  • 在我們學(xué)習(xí)如何進(jìn)行屏幕適配之前欧宜,我們需要先了解下為什么Android需要進(jìn)行屏幕適配坐榆。 由于Android系統(tǒng)的開(kāi)...
    知青的葉閱讀 1,486評(píng)論 0 2
  • 辦公室里的一天 2018年7月16日,今天是我們實(shí)踐的第五天冗茸。相對(duì)于前幾天在酷日下的工作席镀,今天在辦公室的工作環(huán)境還...
    c4b50d69f5f8閱讀 78評(píng)論 0 0