Android APP全面屏適配技術要點

shijiacheng發表於2018-10-16

全面屏的概念

為什麼先要解釋一下全面屏,因為這個詞在現在來講就是一個偽命題。全面屏字面意思就是手機的正面全部都是螢幕,100%的屏佔比。但是現在推出所謂“全面屏”手機的廠商沒有一個能達到全面的。

那麼下面來說一下Android開發領域對全面屏的理解和定義吧。

一般手機的螢幕縱橫比為16:9,如1080x1920、1440x2560等,其比值為1.77,在全面屏手機出現之前,Android中預設的最大螢幕縱橫比(maximum aspect ratio)為1.86,即能夠相容16:9的螢幕。

一些手機廠商為了追求更大的螢幕空間以及更極致的使用者體驗,於是提高了螢幕縱橫比,17:9、19:10、18:9、18.5:9的手機開始進入市場,這些手機的螢幕縱橫比大大超過了1.86,這些手機被稱為全面屏手機。

為何需要適配

我們將targetSdkVersion的值改為小於等於23,執行程式,我們會發現螢幕底部出現一個黑條。

Android APP全面屏適配技術要點

如何適配

targetSdkVersion<=23,更大的螢幕縱橫比

在Galaxy S8釋出之後,Android官方提供了適配方案,即提高App所支援的最大螢幕縱橫比,實現很簡單,在AndroidManifest.xml中可做如下配置:

<meta-data android:name="android.max_aspect"
    android:value="ratio_float"/>
複製程式碼

其中ratio_float為浮點數,官方建議為2.1或更大,因為18.5:9=2.055555555……,如果日後出現縱橫比更大的手機,此值將會更大。

<meta-data android:name="android.max_aspect" 
    android:value="2.1" />
複製程式碼

max_aspect值也可以在Java程式碼中動態地設定,通過下面的方法即可實現:

public void setMaxAspect() {
        ApplicationInfo applicationInfo = null;
        try {
            applicationInfo = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        if(applicationInfo == null){
            throw new IllegalArgumentException(" get application info = null, has no meta data! ");
        }
        applicationInfo.metaData.putString("android.max_aspect", "2.1");
    }
複製程式碼

如果targetSdkVersion的值的值大於23,那麼應該不用設定max_aspect即可。

檢視適配之後的截圖:

Android APP全面屏適配技術要點

android-developers.googleblog.com/2017/03/upd…

圖片資源適配

我們看一下啟動頁,在16:9螢幕中適配的圖片,到了18:9的螢幕中就會被拉伸了。

16:9螢幕中顯示 18:9螢幕中顯示
Android APP全面屏適配技術要點
Android APP全面屏適配技術要點

解決這個問題無非就是兩種方法,換圖片或者是換佈局

換圖片

不能依賴單一廠商的解決方案,只能從Android系統屬性出發。考慮到目前大部分全面屏手機只是在高度上拉長,且大多為6.0英寸左右,畫素密度對比xxhdpi並沒有多大區別,那我們可以在專案中增加一組資源drawable-xxhdpi-2160x1080 、drawable-long 這樣解決圖片的拉伸問題,當然這樣的方法肯定是不太好的,會增加app的容量。這裡就不演示了。

優化佈局

當然最好的方法還是用相對佈局採用XML的方式,或者.9圖的解決方案。

我總結的就是少量多切,儘量減少尺寸對佈局的影響。比如這裡,使用正方形的切圖,讓他居中顯示,無論螢幕縱橫比如何,都不會拉伸這個圖片,拉伸的只是背景而已。

Android APP全面屏適配技術要點

<ImageView
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:scaleType="fitCenter"
        android:src="@drawable/bz002"/>
複製程式碼
適配前 適配後
Android APP全面屏適配技術要點
Android APP全面屏適配技術要點

全面屏高度問題適配

首先解釋一下window,decorview,rootview這幾個概念

Android APP全面屏適配技術要點

Window官方文件:Window

public abstract class Window. Abstract base class for a top-level window look and behavior policy. An instance of this class should be used as the top-level view added to the window manager. It provides standard UI policies such as a background, title area, default key processing, etc.

The only existing implementation of this abstract class is android.view.PhoneWindow, which you should instantiate when needing a Window.

翻譯一下:每一個 Activity 都持有一個 Window 物件,但是 Window 是一個抽象類,這裡 Android 為 Window 提供了唯一的實現類 PhoneWindow。也就是說 Activity 中的 window 例項就是一個 PhoneWindow 物件。

但是 PhoneWindow 終究是 Window,它並不具備多少 View 相關的能力。不過 PhoneWindow 中持有一個 Android 中非常重要的一個 View 物件 DecorView.

現在的關係就很明確了,每一個 Activity 持有一個 PhoneWindow 的物件,而一個 PhoneWindow 物件持有一個 DecorView 的例項,所以 Activity 中 View 相關的操作其實大都是通過 DecorView 來完成

DecorView就可以理解為手機的內屏,就是那塊玻璃,可以發光的螢幕。

這裡通過程式碼,列印出我們頁面中的高度的各項資料

int decorviewHeight = decorView.getHeight();
int screenHeight = FullScreenManager.getScreenHeight();
int nativeBarHeight = FullScreenManager.getNativeBarHeight();
int contentViewHeight = rootView.getHeight();
int navigationBarHeight1 = FullScreenManager.getNavigationBarHeight();

Log.d("shijiacheng","=======================================");
Log.d("shijiacheng","DecorView height: " + decorviewHeight + " px");
Log.d("shijiacheng","Screen height: " + screenHeight + " px");
Log.d("shijiacheng","NativeBar height: " + nativeBarHeight + " px");
Log.d("shijiacheng","ContentView height: " + contentViewHeight + " px");
Log.d("shijiacheng","NavigationBar height: " + navigationBarHeight + " px");
Log.d("shijiacheng","---------------------------------------");
複製程式碼

獲取decorView的高度

final View decorView = getWindow().getDecorView();
int decorviewHeight = decorView.getHeight();
複製程式碼

獲得螢幕高度

/**
 * 獲得螢幕高度
 * @return
 */
public static int getScreenHeight() {
    Resources resource = AppContext.getInstance().getResources();
    DisplayMetrics displayMetrics = resource.getDisplayMetrics();
    return displayMetrics.heightPixels;
}
複製程式碼

獲取狀態列的高度

/**
 * 獲取狀態列的高度
 *
 * @return
 */
public static int getNativeBarHeight() {
    Resources resource = AppContext.getInstance().getResources();
    int result = 0;
    int resourceId = resource.getIdentifier("status_bar_height", 
            "dimen", "android");
    if (resourceId > 0) {
        result = resource.getDimensionPixelSize(resourceId);
    }
    return result;
}
複製程式碼

獲取contentView的高度

LinearLayout contentView = findViewById(R.id.root);
int contentViewHeight = contentView.getHeight();
複製程式碼

獲取NavigationBar的高度

public static int getNavigationBarHeight() {
    Resources resources =  AppContext.getInstance().getResources();
    int resourceId = resources.getIdentifier("navigation_bar_height","dimen", "android");
    int height = resources.getDimensionPixelSize(resourceId);
    return height;
}
複製程式碼

為了更加直觀的展示各個資料,這裡我們使用佈局的方式將各個資料展示出來,佈局程式碼比較簡單,這裡就不展示了。

Android APP全面屏適配技術要點

先展示一下正常的螢幕高度的各項資料

10-08 09:52:03.636 23818-23818/? D/shijiacheng: =========================
10-08 09:52:03.637 23818-23818/? D/shijiacheng: DecorView height: 1280 px
10-08 09:52:03.637 23818-23818/? D/shijiacheng: Screen height: 1280 px
10-08 09:52:03.637 23818-23818/? D/shijiacheng: NativeBar height: 50 px
10-08 09:52:03.637 23818-23818/? D/shijiacheng: ContentView height: 1230 px
10-08 09:52:03.637 23818-23818/? D/shijiacheng: NavigationBar height: 96 px
10-08 09:52:03.637 23818-23818/? D/shijiacheng: -------------------------
複製程式碼

Android APP全面屏適配技術要點

DecorView = Screen height = NativeBar height + ContentView height

看一下小米mix全面屏的情況

2018-10-08 09:54:15.640 /? D/shijiacheng: =========================
2018-10-08 09:54:15.640 /? D/shijiacheng: DecorView height: 2160 px
2018-10-08 09:54:15.641 /? D/shijiacheng: RootView height: 2094 px
2018-10-08 09:54:15.641 /? D/shijiacheng: Screen height: 2030 px
2018-10-08 09:54:15.641 /? D/shijiacheng: NativeBar height: 66 px
2018-10-08 09:54:15.641 /? D/shijiacheng: ContentView height: 2094 px
2018-10-08 09:54:15.641 /? D/shijiacheng: NavigationBar height: 130 px
2018-10-08 09:54:15.641 /? D/shijiacheng: -------------------------
複製程式碼

Android APP全面屏適配技術要點

問題出現了,可以發現contentView的高度比screen螢幕的高度還要大,不禁要懷疑,我們的獲取螢幕高度的方法在全面屏下計算錯誤了。

問題1:獲取螢幕高度方法計算不準確

我們一直都是使用如下方法進行螢幕高度測量的:

public static int getScreenHeight() {
    Resources resource = AppContext.getInstance().getResources();
    DisplayMetrics displayMetrics = resource.getDisplayMetrics();
    return displayMetrics.heightPixels;
}
複製程式碼

但是這個方法卻是一個十分古老的方法,沒有與時俱進,雖然說在普通螢幕上這種方法沒有問題,但是在全面屏手機上來說,這種方法就不靈了。

下面我們就來研究一下獲取螢幕尺寸的方法的演進。

獲取螢幕寬高

獲取螢幕的寬高是我們開發中經常遇到的問題,而且相信大家都已經非常熟悉,最常用的為以下兩種:

public static int getScreenHeight1(Activity activity) {
    return activity.getWindowManager().getDefaultDisplay().getHeight();
}
public static int getScreenHeight2(Activity activity) {
    DisplayMetrics displayMetrics = new DisplayMetrics();
    activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
    return displayMetrics.heightPixels;
}
複製程式碼

其實以上兩種方式是一樣的,只不過第二種是把資訊封裝到 DesplayMetrics中,再從DesplayMetrics得到資料。

在 Android 3.2(Api 13) 之後又提供瞭如下的一個方法,將資料封裝到Point中,然後返回寬度高度資訊。

@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
public static int getScreenHeight3(Activity activity) {
    Point point = new Point();
    activity.getWindowManager().getDefaultDisplay().getSize(point);
    return point.y;
}
複製程式碼

在 Android 4.2(Api17) 之後提供瞭如下方法,與第三種類似也是將資料封裝到Point中,然後返回款高度資訊。

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public static int getScreenHeight4(Activity activity) {
    Point realSize = new Point();
    activity.getWindowManager().getDefaultDisplay().getRealSize(realSize);
    return realSize.y;
}
複製程式碼

其實getRealSize這個方法在Android Api15的時候就已經加入了,不過是被隱藏了,通過查閱原始碼我們可以看到。

Android APP全面屏適配技術要點

Android Api15 Display.java原始碼中getRealSize()方法被標記為@hide

Android APP全面屏適配技術要點

因此,我們可以重寫獲取高度的方法,適配所有機型,所有系統。

適配所有螢幕的獲取螢幕尺寸的方法

public static int[] getScreenSize(Context context) {
        int[] size = new int[2];

        WindowManager w = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        Display d = w.getDefaultDisplay();
        DisplayMetrics metrics = new DisplayMetrics();
        d.getMetrics(metrics);
        // since SDK_INT = 1;
        int widthPixels = metrics.widthPixels;
        int heightPixels = metrics.heightPixels;

        // includes window decorations (statusbar bar/menu bar)
        if (Build.VERSION.SDK_INT >= 14 && Build.VERSION.SDK_INT < 17)
            try {
                widthPixels = (Integer) Display.class.getMethod("getRawWidth").invoke(d);
                heightPixels = (Integer) Display.class.getMethod("getRawHeight").invoke(d);
            } catch (Exception ignored) {
            }
        // includes window decorations (statusbar bar/menu bar)
        if (Build.VERSION.SDK_INT >= 17)
            try {
                Point realSize = new Point();
                Display.class.getMethod("getRealSize", Point.class).invoke(d, realSize);
                widthPixels = realSize.x;
                heightPixels = realSize.y;
            } catch (Exception ignored) {
            }
        size[0] = widthPixels;
        size[1] = heightPixels;
        return size;
    }
複製程式碼

使用新的獲取高度的方法,重新執行程式,執行結果已經正常顯示了。

2018-10-08 13:19:32.389 /? D/shijiacheng: ==========================
2018-10-08 13:19:32.390 /? D/shijiacheng: DecorView height: 2160 px
2018-10-08 13:19:32.390 /? D/shijiacheng: Screen height: 2160 px
2018-10-08 13:19:32.390 /? D/shijiacheng: NativeBar height: 66 px
2018-10-08 13:19:32.390 /? D/shijiacheng: ContentView height: 2094 px
2018-10-08 13:19:32.390 /? D/shijiacheng: NavigationBar height: 130 px
2018-10-08 13:19:32.390 /? D/shijiacheng: --------------------------
複製程式碼

Android APP全面屏適配技術要點

問題2:小米mix切為經典導航鍵模式下的計算問題

我們在MIUI設定中將全面屏導航樣式修改為“經典導航鍵”樣式。

Android APP全面屏適配技術要點

重新執行程式,執行結果如下:

Android APP全面屏適配技術要點

可以發現又出問題了,DecorView = Screen height > NativeBar height + ContentView height

這裡不難發現,Screen height將底部虛擬導航欄的高度也算進裡面了。

很多情況下,我們都用如下方法獲取導航欄的高度:

public static int getNavigationBarHeight() {
        Resources resources =  AppContext.getInstance().getResources();
        int resourceId = resources.getIdentifier("navigation_bar_height","dimen", "android");
        int height = resources.getDimensionPixelSize(resourceId);
        return height;
    }
複製程式碼

這種方法得到的導航欄的高度數值是沒問題的,但是在全面屏的手機上,即使隱藏了導航欄,也是可以獲取到導航欄的高度的。通過上面的logcat日誌可以看到,即使沒有導航欄,導航欄的高度的計算也是有值的。

適配小米mix虛擬導航欄

小米mix的機型中,我們可以“force_fsg_nav_bar”來判斷小米手機是否開啟了全面屏手勢。

public static int getHeightOfNavigationBar(Context context) {
        //如果小米手機開啟了全面屏手勢隱藏了導航欄則返回 0
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            if (Settings.Global.getInt(context.getContentResolver(), "force_fsg_nav_bar", 0) != 0) {
                return 0;
            }
        }
        int realHeight = getScreenSize(context)[1];

        Display d = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
        DisplayMetrics displayMetrics = new DisplayMetrics();
        d.getMetrics(displayMetrics);

        int displayHeight = displayMetrics.heightPixels;

        return realHeight - displayHeight;
    }
複製程式碼

因此可以通過這個方法來判斷是否顯示了底部導航欄,並且可以計算導航欄的高度。

int navigationBarHeight = FullScreenManager.getHeightOfNavigationBar(MainActivity.this);
if (navigationBarHeight > 0){
    container_navigationview.setVisibility(View.VISIBLE);
}else {
    container_navigationview.setVisibility(View.GONE);
}
複製程式碼

正常的顯示效果如下:

有虛擬導航欄 沒有虛擬導航欄
Android APP全面屏適配技術要點
Android APP全面屏適配技術要點

沒有虛擬導航欄Log

2018-10-08 13:19:32.389 /? D/shijiacheng: ==========================
2018-10-08 13:19:32.390 /? D/shijiacheng: DecorView height: 2160 px
2018-10-08 13:19:32.390 /? D/shijiacheng: Screen height: 2160 px
2018-10-08 13:19:32.390 /? D/shijiacheng: NativeBar height: 66 px
2018-10-08 13:19:32.390 /? D/shijiacheng: ContentView height: 2094 px
2018-10-08 13:19:32.390 /? D/shijiacheng: NavigationBar height: 0 px
2018-10-08 13:19:32.390 /? D/shijiacheng: --------------------------

複製程式碼

有虛擬導航欄Log

2018-10-08 13:38:03.229 /? D/shijiacheng: ==========================
2018-10-08 13:38:03.230 /? D/shijiacheng: DecorView height: 2160 px
2018-10-08 13:38:03.230 /? D/shijiacheng: Screen height: 2160 px
2018-10-08 13:38:03.230 /? D/shijiacheng: NativeBar height: 66 px
2018-10-08 13:38:03.230 /? D/shijiacheng: ContentView height: 1964 px
2018-10-08 13:38:03.230 /? D/shijiacheng: NavigationBar height: 130 px
2018-10-08 13:38:03.230 /? D/shijiacheng: --------------------------
複製程式碼

相關文章