Android技能樹 — 螢幕適配小結

青蛙要fly發表於2018-07-21

前言:

Android技能樹系列:

Android基礎知識

Android技能樹 — 動畫小結

Android技能樹 — View小結

Android技能樹 — Activity小結

Android技能樹 — View事件體系小結

Android技能樹 — Android儲存路徑及IO操作小結

Android技能樹 — 多程式相關小結

Android技能樹 — Drawable小結

Android技能樹 — 螢幕適配小結

資料結構基礎知識

Android技能樹 — 陣列,連結串列,雜湊表基礎小結

Android技能樹 — 樹基礎知識小結(一)

演算法基礎知識

Android技能樹 — 排序演算法基礎小結

Rx系列相關

Android技能樹 — RxPermission分析

Android技能樹 — Rxjava取消訂閱小結(1):自帶方式

Android技能樹 — Rxjava取消訂閱小結(2):RxLifeCycle

關於螢幕適配,幾乎每隔一段時間就會看見有人發出來說XXX方案,實現超級簡單的適配方式等等。所以我把我目前瞭解過的常用的適配方案做個總結,並簡單說說原理,從而讓大家也初步瞭解各個方案的實現。(其實很多人都是看見別人寫的適配方案,雖然可能實際在使用了,但是卻從來沒有去了解過這個方案的原理,而且遇到一些簡單的坑的時候,因為不知道原理,也無法自己解決。)

常見適配方案:

  1. 生成解析度values資料夾
  2. 生成values -sw 資料夾
  3. 谷歌百分比佈局庫
  4. AutoLayout
  5. 動態更改density

1. 基礎知識

其實本來不想寫這塊,因為基本大家都懂什麼dp, dpi ,px , inch ,density等,但是後面的一些適配都會涉及到這些原理,外加有時候面試別人,都是感覺知道這個知識點,但並不是真正的瞭解,所以我這邊還是重新提一下,我會用通俗易懂的例子來讓大家更好的理解。 (PS: 當然想不看的可以直接跳過。)

這邊直接放一個腦圖講下基本的基礎知識:

Android技能樹 — 螢幕適配小結

1.1 px

我們可以看到現在市面上的手機解析度截止到2018-05月,統計為:

Android技能樹 — 螢幕適配小結

這裡額外提一下,類似1080 x 1812,720 x 1184 等看著很奇怪的結尾不是0的解析度,大部分是因為有虛擬鍵的原因,虛擬鍵佔去了一部分高度。

以1080 X 1920為例,它代表的是手機上的畫素點,

Android技能樹 — 螢幕適配小結
類似這種,表示橫著有1080個畫素點,豎著有1920個畫素點,所以1080 X 1920 代表了手機在橫向、縱向上的畫素點數總和

所以如果我們寫了一個Button,假設高度和寬度都為10px , 則說明在這個螢幕點上高寬都佔了10個點。

1.2 inch(螢幕尺寸)

手機螢幕的物理尺寸,我們經常聽到有人說我買的是iPhone 8 plus,尺寸是5.5的螢幕,iPhone 8尺寸是 4.7的。其實它們所帶的單位都是inch(英寸), 1(inch)≈2.54(cm)

百度搜到的圖

所以螢幕尺寸就是按螢幕對角測量的實際物理尺寸。 為簡便起見,Android 將所有實際螢幕尺寸分組為四種通用尺寸:小、 正常、大和超大。

1.3 dpi

螢幕物理區域中的畫素量;通常稱為 dpi(Dots Per Inch 每英寸 點數)。所以看標題就知道,他更像是在求一個密度。那我們既然知道了手機螢幕對角線的尺寸,我們只要知道了手機對角線上的px數量,除一下就知道了每英寸上的畫素點數了。

所以我們只需要通過勾股定理獲取對角線上的畫素值,再除以螢幕尺寸值就可以了。

Android技能樹 — 螢幕適配小結

為簡便起見,Android 將所有螢幕密度分組為六種通用密度: 低、中、高、超高、超超高和超超超高。

六種通用的密度:

  • ldpi(低)~120dpi
  • mdpi(中)~160dpi
  • hdpi(高)~240dpi
  • xhdpi(超高)~320dpi
  • xxhdpi(超超高)~480dpi
  • xxxhdpi(超超超高)~640dpi

1.4 dp 和 density

其實dp 本來是叫dip (Density Independent Pixels),所有有時候面試的別人,面試者會弄錯,把dip當做了dpi,所以你問他請說下 dp 和 dip ,他會把 dip說成dpi的內容。

我們舉例說下這塊知識點: 要畫一個 高和寬各為螢幕的一般的按鈕,我們假設有二塊螢幕,一塊是100 X 100 ,一塊是 200 X 200 ,那這時候第一塊的螢幕上我們寫Button 應該為:

<Button 
     layout_height = "50px"
     layout_width = "50px"/>
複製程式碼

第二個螢幕的Button應該為:

<Button 
     layout_height = "100px"
     layout_width = "100px"/>
複製程式碼

這樣是不是都各自佔了螢幕的高寬的一半,但是假如有第三個螢幕 300 X 300 呢,難不成再寫一個Button的高寬值? 所以我們可以用一種單位來代替,但是這種單位可以在不同的螢幕環境下,值是不同的。比如我們就把這個單位當做“haha”。

比如我們現在都這麼寫:

<Button 
     layout_height = "50haha"
     layout_width = "50haha"/>
複製程式碼

這時候在100 x 100的時候, 50haha = 50px ,在200 X 200 螢幕的時候 , 50 haha = 100px , 在 300 X 300 螢幕的時候,50haha 等150px。

這個感覺就很像你跟別人說我欠你50 money,如果在中國,代表你欠別人50元人民幣,但是如果在美國,你這麼說,指你欠50美元,也就是欠了三百多元人民幣。(這個例子不要跟我較真,我就意思意思而已)

所以dp就是類似我們上面自己定義的haha這個單位。

比如50dp = 50px ,這時候1dp = 1px , 50dp = 100px的時候 是 1dp = 2px ,所以我們可以看到倍數分別為 1 和 2 ,我們用density來代表這個倍數。也就是說: dp * density = px,這時候就是 50 dp * 1 = 50px , 50dp * 2 = 100px

(就像是我說我欠你50 money,在中國,這個density就是1 , 也就是欠你50元人民幣,在美國可能就是指300多人民幣,這個density也就是 美元換算成人民幣的倍數)

那麼這個density具體是怎麼來的呢?其實很簡單,記不記得我們前面說過dpi ,也就是螢幕的密度,我們就用這個密度來做比較,比如我們 把160dpi 作為標準,那另外一個手機是320dpi ,那麼這個density就是 (320/160 = 2)。 所以我們再次把公式 : dp * density = px 轉變為: dp * (dpi / 160) = px

那麼為什麼用160dpi作為標準呢,以前看到文章提過:mdpi基於第一款 Android 裝置 ″T-Mobile G1″ 的螢幕配置(縮放係數scale=1)。

1.5 基礎知識小結

所以假如我們現在的手機解析度知道了,手機螢幕尺寸也知道了。我們通過公式求出 dpi ,然後 dpi / 160 就是當前手機的density,然後我們就知道我寫了1dp 在這臺手機上具體是多少px了。

具體的安卓手機尺寸四個分類及6中dpi分類:

Android技能樹 — 螢幕適配小結

我們的某臺手機的dpi,density,解析度等如何獲取呢,:

DisplayMetrics mDisplayMetrics = getResources().getDisplayMetrics();    
//橫向解析度
int width = mDisplayMetrics.widthPixels;  
//豎向解析度
int height = mDisplayMetrics.heightPixels;  
//density值
float density = mDisplayMetrics.density;  
//dpi的值就等於density * 160
float dpi = density * 160;
複製程式碼

也許有人說,那我們使用dp不是已經完美的實現了各種相容性嗎,就像我們上面提到過的,100 X 100 ,200X200 , 300 X 300的螢幕,我們都只要寫50haha, 就分別代表了50,100,150,不是就佔了各自螢幕的一半了麼。理論上的確是這樣,但是我們剛提過我們的density是等於 (dpi / 160),而dpi又由解析度和螢幕尺寸同時決定,安卓手機的碎片化太過嚴重,所以很多手機雖然解析度不同及螢幕尺寸不同,造成最後的dpi一樣,所以最後的density也一樣,就造成了適配實現不全。假設我們多了一個400X400 的裝置,因為它的螢幕尺寸也同時變大了很多,所以最終的density和300X300一樣,那這時候我們寫了50haha,也就代表了150px,這時候明顯在400X400上面並沒有顯示為一半,甚至當這個400X400的設定的螢幕尺寸超級大,反而可能算下來的density與100X100的一樣,那這時候50haha可能就只有50px,則顯示差距就更大了。 (其實主要原因就是dpi不是單獨由解析度來決定,同時還有螢幕尺寸影響,所以二個變數同時作用,造成不同解析度的手機最後的density也可能相同。這樣dp轉換成的px也就相同了,但是手機的解析度本身有不同,這時候就會出現適配不對。)

2 各類適配方案

2.1 生成解析度values資料夾

因為我們上面提過 , px = (dpi / 160) * dp, 但是dpi又是同時由解析度和螢幕尺寸同時決定,造成了不同的解析度,dpi可能一樣,這樣最終得到的px一樣,比如都是佔螢幕的一半,300X300得到的可能是150,但是400X 400得到的也是150,這時候就不對了。

那我們就想到了。我們能不能不是同時受到解析度和螢幕尺寸決定,而是隻受一個因素來影響,這樣就是真正的按比例來了。比如300X300是150,400X400是200,500X500是250,是隻受解析度的影響,所以解析度大的,最終得到的結果一定就大。所以我們就不能使用dp了。而是一個新的單位,而這個單位是根據不同的解析度,得到不同的值,那怎麼計算呢,就是窮舉法,比如剛才的300X300,我們規定1 haha等於1 px,然後再600 X 600裡面,1 haha 等2 px , 1200X1200裡面是 1 haha 等於 3 px 。所以我們在不同解析度下的values資料夾下寫上不同的值:

300X300下
<dimens name = "1haha"> 1px </dimens>
600X600下
<dimens name = "1haha"> 2px </dimens>
1200X1200下
<dimens name = "1haha"> 3px </dimens>
複製程式碼

所以這個就是方案1 ,附上文章連結。

Android 螢幕適配方案 我們可以看下面的圖:

Android技能樹 — 螢幕適配小結

我們可以看到列舉了所有可能的螢幕解析度的values,然後手動按照倍數,進行相應的賦值。當然這些檔案不可能手寫,通過Java自動生成相應的檔案:

Android技能樹 — 螢幕適配小結

這樣最終影響結果的就只是解析度的了,解析度越大的,x1的值越大。

但是這個方案有一個致命的缺陷,那就是需要精準命中才能適配,比如1920x1080的手機就一定要找到1920x1080的限定符,否則就只能用統一的預設的dimens檔案了。而使用預設的尺寸的話,UI就很可能變形,簡單說,就是容錯機制很差。


2.2 生成values -sw 資料夾

可以參考:

Android 目前最穩定和高效的UI適配方案

騷年你的螢幕適配方式該升級了!-smallestWidth 限定符適配方案

其實這個方式跟上面的2.1方法原理可以說一模一樣。唯一的區別就是使用了sw來保證一定的容錯性。

Android技能樹 — 螢幕適配小結

Android技能樹 — 螢幕適配小結

我們看到其實就是把上面具體的解析度values改成了values - sw而已。


2.3 百分比佈局庫

Android 百分比佈局庫(percent-support-lib) 解析與擴充套件 Android 增強版百分比佈局庫 為了適配而擴充套件

其實這個也是很簡單的,字面意思,我寫了這個Button寬度為父佈局的百分之50,則在不同手機上,都是佔據了百分之50。使用過過百分比佈局的人都應該知道,我們寫的時候是這麼寫的:

<android.support.percent. PercentRelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_heightPercent="20%"
        app:layout_widthPercent="50%"
        android:gravity="center"
        />

</PercentRelativeLayout >
複製程式碼

其實原理很簡單,就是動態計算實際的百分之50在不同機器的時候到底佔了多少px,2.1,2.2則是等於提前幫我們計算好了具體的px,然後寫在了檔案裡面,然後我們去讀資料。

那它的實現原理是什麼呢?簡單來說就是二步:

  1. 獲取使用者到底填了多少的百分比數值
  2. 獲取父佈局的空間,然後乘以使用者填的百分比數值,或者一個新數值,然後賦值給該控制元件。

我們一步步來看原始碼:

2.3.1 獲取使用者到底填了多少的百分比數值:

我們知道我們的百分比佈局中的核心屬性是子控制元件填寫:

app:layout_heightPercent="20%"
app:layout_widthPercent="30%"
複製程式碼

所以我們需要在PercentRelativeLayout中遍歷它下面的子控制元件,然後分別獲取每個子控制元件的百分比數值。 其實很簡單,寫過自定義View的人應該都知道,因為這個其實就是自定義屬性而已。

TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.PercentLayout_Layout);
float value = array.getFraction(R.styleable.PercentLayout_Layout_layout_widthPercent, 1, 1,-1f);
複製程式碼

2.3.2 獲取計算後的值並且賦值:

因為要動態獲取父控制元件的控制元件,同時把新的值賦值給子控制元件,所以該行為在onMeasure方法中執行。

//傳入的ViewGroup.LayoutParams params是遍歷的每個子View的LayoutParams
public void fillLayoutParams(ViewGroup.LayoutParams params, int widthHint,
                int heightHint) {
            // Preserve the original layout params, so we can restore them after the measure step.
            mPreservedParams.width = params.width;
            mPreservedParams.height = params.height;

            if (widthPercent >= 0) {
                params.width = (int) (widthHint * widthPercent);
            }
            if (heightPercent >= 0) {
                params.height = (int) (heightHint * heightPercent);
            }
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "after fillLayoutParams: (" + params.width + ", " + params.height + ")");
            }
}
複製程式碼

當然具體原始碼會更多,我不會大篇幅完整講流程,更多的是講解思路。


2.4 AutoLayout

Android AutoLayout全新的適配方式 堪稱適配終結者

使用方式很簡單:

  1. 註冊設計圖尺寸

autolayout引入

dependencies {
    compile project(':autolayout')
}
複製程式碼

在你的專案的AndroidManifest中註明你的設計稿的尺寸。

<meta-data android:name="design_width" android:value="768"></meta-data>
<meta-data android:name="design_height" android:value="1280"></meta-data>
複製程式碼
  1. Activity中開啟設配 讓你的Activity去繼承AutoLayoutActivity

我們想到的原理,肯定也是把填在AndroidManifest.xml裡面的數值讀取出來,然後作為參考值。然後在不同手機上動態的計算出來數值,是不是感覺和百分比佈局有點相似。

我們來看下AutoLayoutActivity原始碼:

public class AutoLayoutActivity extends AppCompatActivity
{
    private static final String LAYOUT_LINEARLAYOUT = "LinearLayout";
    private static final String LAYOUT_FRAMELAYOUT = "FrameLayout";
    private static final String LAYOUT_RELATIVELAYOUT = "RelativeLayout";


    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs)
    {
        View view = null;
        if (name.equals(LAYOUT_FRAMELAYOUT))
        {
            view = new AutoFrameLayout(context, attrs);
        }

        if (name.equals(LAYOUT_LINEARLAYOUT))
        {
            view = new AutoLinearLayout(context, attrs);
        }

        if (name.equals(LAYOUT_RELATIVELAYOUT))
        {
            view = new AutoRelativeLayout(context, attrs);
        }

        if (view != null) return view;

        return super.onCreateView(name, context, attrs);
    }
}
複製程式碼

我們發現把我們寫在Layout.xml裡面的佈局控制元件替換成AutoXXXX等自定義控制元件。那我們以AutoLinearLayout來分析:其實看過百分比佈局的原始碼,就會發現基本架構都一樣,所以百分比佈局的程式碼看得懂,再去看AutoLayout相關程式碼會很快。


2.5 動態更改density

一種極低成本的Android螢幕適配方式

Android螢幕適配很麻煩嗎?不!太簡單了。

Android 螢幕適配從未如斯簡單

騷年你的螢幕適配方式該升級了!-今日頭條適配方案

  1. 假如設計圖是按1920px * 1080px來設計,以density為3來標註,也就是螢幕其實是640dp * 360dp。這時候如果我們的Button想要佔據一半,是不是寬度需要設定成180dp。
  2. 那假如我們的手機螢幕是1280X 720,density是2 ,則寬度是360dp,的確當設定成180dp的時候也正好佔據一半。
  3. 但是萬一1280X 720的手機的density是3呢,則寬度為240dp, 這時候設定成180dp,實際的px值為: 180 * 3 = 540px ,但是我們想要的是360px ,也就是 180 * density = 360px , 既然我們設定成的180dp不能改變(也就是設定一個值,適配各種手機),那麼我們只能改變這個density值。
  4. 換成公式就是: 180 * density = 360,那麼density是多少。哈哈。沒錯是2 ,我們動態把density從 3變成2,是不是就符合了。
  5. 比如960X540 的手機,density是2 ,因為我們的Button寬度設定成了180dp,寬度為180 X 2 = 360px,超過了一半,我們只需要動態更改density滿足 180X density = 270px即可,所以我們的density算出來是1.5。

那麼density具體怎麼得出來呢,很簡單,我們剛才假設的是有一個按鈕,佔了螢幕的一半,那我們假設佔了整個手機螢幕不就可以了。 設計圖的寬度是360dp,而960X540的手機,只要540/360 = 1.5就可以得到,所以 density = 裝置真實寬(單位px) / 360

if (orientation.equals("height")) {
        targetDensity = (appDisplayMetrics.heightPixels - barHeight) / 667f;
} else {
        targetDensity = appDisplayMetrics.widthPixels / 360f;
}
複製程式碼

所以本方案就是動態更改density以滿足設計圖方案。

結語:

emm.......大家輕噴即可。。。。

相關文章