Android技能樹 — 屏幕適配小結

前言:

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: 當然想不看的可以直接跳過。)

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

1.1 px

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


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

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



類似這種,表示橫著有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 將所有屏幕密度分組為六種通用密度: 低、中、高、超高、超超高和超超超高。

六種通用的密度:

  • 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分類:


我們的某臺手機的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 屏幕適配方案
我們可以看下面的圖:

我們可以看到列舉了所有可能的屏幕分辨率的values,然后手動按照倍數,進行相應的賦值。當然這些文件不可能手寫,通過Java自動生成相應的文件:


這樣最終影響結果的就只是分辨率的了,分辨率越大的,x1的值越大。

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


2.2 生成values -sw 文件夾

可以參考:Android 目前最穩定和高效的UI適配方案

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

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


我們看到其實就是把上面具體的分辨率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.......大家輕噴即可。。。。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容