Android 螢幕適配從未如斯簡單(已廢棄該使用方式)

blankj發表於2018-08-02

最新適配檢視這篇文章:Android 螢幕適配終結者

issue 地址:「Android 螢幕適配終結者」問題彙總

以下內容為老的適配方式,已不推薦使用。

前言

一個月前看了今日頭條新的螢幕適配方案,這是傳送門,對此不禁拍案叫絕,為此我想把這種方案融入到我工具類中直接一行程式碼即可適配,如今最新 1.19.0 版 AndroidUtilCode 已有其最新的適配方案,其相關函式在 ScreenUtils 中,相關 API 如下所示:

adaptScreen4VerticalSlide  : 適配垂直滑動的螢幕
adaptScreen4HorizontalSlide: 適配水平滑動的螢幕
cancelAdaptScreen          : 取消適配螢幕
isAdaptScreen              : 是否適配螢幕
複製程式碼

效果

UtilApk 中的 ScreenAdaptActivity 以設計圖為 360dp 寬度 來做適配,我們設定兩個 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 為高度的適配效果。

原理

如果看了上面今日頭條的那篇適配文章,那麼你可能已經知道其原理了,不明白的話可以繼續看下我的解釋: 我們知道 px = dp * density,我們要適配的話需要確保 dp 不變去修改 density,而安卓預設 density = dpi / 160,其意思就是 1dp 有多少 px,也就是畫素密度,我們開發是按照一份設計稿來做的,那麼有沒有什麼辦法來讓 density 和設計稿尺寸做聯絡呢?假設我們設計稿是寬度是 1080px,資源放在 xxhdpi,那麼我們寬度轉換為 dp 就是 1080 / 3 = 360dp,要在不同裝置上寬度都表現為 360dp,那麼就需要修改其 density = screenWidthPx / 360,這樣就滿足了上述條件,而和 density 相關的還有 densityDpi、scaledDensity,我們根據 density 等比修改 densityDpi、scaledDensity 即可。

由於 API 26 及以上的 Activity#getResources()#getDisplayMetrics()Application#getResources()#getDisplayMetrics() 是不同的引用,所以在 API 26 及以上適配是沒有影響的,但在 API 26 以下 Activity#getResources()#getDisplayMetrics()Application#getResources()#getDisplayMetrics() 是相同的引用,導致適配有問題,這裡要感謝 @MirkoWu 提出的問題,後面會有解決方案。

如果我們以 xxhdpi 的 360dp 來適配的話,首先在 AS 中預覽是個問題,在接入第三方 SDK 帶有介面或者 View 的話會導致它的尺寸全然不對,因為我們那樣適配後介面寬度只有 360dp,而第三方 SDK 中很有可能寫的佈局會超出 360dp,這便會引發新的問題,當然這也是有響應的解決之道,比如暫時取消適配,但我們有更好的方式,著重看下面介紹。

我著重推薦以 mdpi 為特例來適配,比如前面說到的 xxhdpi 的 360dp,那麼在 mdpi 下就是 360 * 3 = 1080dp,這樣我們新建一個寬為 1080px 的 mdpi 裝置(可以通過修改裝置尺寸來達到 mdpi),然後切換為該裝置來預覽佈局就完美解決了以上問題,我們在寫佈局的時候設計圖是 36px,那麼我們直接就寫 36dp 即可,設計圖字型是 24px, 我們直接就寫 24sp 即可,這樣便可達到和設計圖一致的效果。另外,圖片資源放在需要適配的最高 dpi 下面即可,比如 drawable-xxhdpi 或者 drawable-xxxhdpi,這樣在高清屏上也不會導致失真。

但是這樣會導致獲取狀態列和導航欄高度有問題,其獲取狀態列高度程式碼為如下所示:

public static int getStatusBarHeight() {
    Resources resources = Utils.getApp().getResources();
    int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
    return resources.getDimensionPixelSize(resourceId);
}
複製程式碼

由於使用的是 Application#getResources,這會導致最後計算狀態列高度使用的是修改過後的 density,在這裡也要感謝 @magic0908 無意間提到的 Resources.getSystem() 來獲取系統的 Resources,果不其然可以獲取到正確高度的狀態列高度,程式碼如下所示:

public static int getStatusBarHeight() {
    Resources resources = Resources.getSystem();
    int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
    return resources.getDimensionPixelSize(resourceId);
}
複製程式碼

同理獲取導航欄高度也可以這樣。

考慮到了 Resources.getSystem(),那麼我們在適配上豈不是可以更方便,不用區分版本什麼的 Activity#getResources()#getDisplayMetrics()Application#getResources()#getDisplayMetrics(),也不需要什麼中間變數來記錄適配前的值,那些值我們直接在 Resources#getSystem()#getDisplayMetrics() 中獲取 densitydensityDpiscaledDensity 即可,而且在修改系統字型的時候,Resources#getSystem()#getDisplayMetrics() 也會相應地改變,這樣也就不需要註冊 registerComponentCallbacks 來監聽系統字型的改變,所以最終的原始碼很是簡潔,但其中間遇到的問題很是複雜,光工具類我這些天就更新了很多版本來解決其問題,從1.18.01.18.7,有六個版本都是和這個適配有關係,但最終還是完美地找到了解決方案,也要感謝大家的幫助,其最終原始碼如下所示:

/**
 * 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;
}
複製程式碼

坑點

在原理裡都已經說完了哈。

建議

新老專案都可以用這套方案,老專案中如果有新的 Activity 加進來,那麼可以對其使用該方案來適配,然後在啟動其他老的 Activity 時候 cancelAdaptScreen 即可。新專案我建議採用我工具類中的使用,可以讓你爽到極致,在 BaseActivitysetContentView(xx) 之前呼叫適配程式碼即可,記得第二個引數一定要傳入設計圖的實際畫素尺寸,不再是曾經的 dp 尺寸了。

有了固定的尺寸,那麼我們百分比是不是就很好實現了,計算後直接寫 xxdp 即可,這樣在所有裝置上也都是一定的比例,哪裡還需要什麼百分比佈局什麼的來做?是不是 so easy,更多風騷的操作可待你解鎖。

結語

如果我的工具類對你的適配造成了影響,歡迎到 AndroidUtilCode 提 issue,感謝今日頭條的方案,讓我可以站在巨人的肩膀上裝一次 13。

最後

記得螢幕適配一定要用 1.19.0 版本及以上

記得螢幕適配一定要用 1.19.0 版本及以上

記得螢幕適配一定要用 1.19.0 版本及以上

給大家帶來了麻煩,sorry。

GitHub issue

螢幕適配問題彙總

相關文章