前言:
Android技能樹系列:
Android基礎知識
Android技能樹 — Android存儲路徑及IO操作小結
數據結構基礎知識
算法基礎知識
Rx系列相關
Android技能樹 — Rxjava取消訂閱小結(1):自帶方式
Android技能樹 — Rxjava取消訂閱小結(2):RxLifeCycle
關于屏幕適配,幾乎每隔一段時間就會看見有人發出來說XXX方案,實現超級簡單的適配方式等等。所以我把我目前了解過的常用的適配方案做個總結,并簡單說說原理,從而讓大家也初步了解各個方案的實現。(其實很多人都是看見別人寫的適配方案,雖然可能實際在使用了,但是卻從來沒有去了解過這個方案的原理,而且遇到一些簡單的坑的時候,因為不知道原理,也無法自己解決。)
常見適配方案:
- 生成分辨率values文件夾
- 生成values -sw 文件夾
- 谷歌百分比布局庫
- AutoLayout
- 動態更改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 文件夾
騷年你的屏幕適配方式該升級了!-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,然后寫在了文件里面,然后我們去讀數據。
那它的實現原理是什么呢?簡單來說就是二步:
- 獲取用戶到底填了多少的百分比數值
- 獲取父布局的空間,然后乘以用戶填的百分比數值,或者一個新數值,然后賦值給該控件。
我們一步步來看源碼:
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全新的適配方式 堪稱適配終結者
使用方式很簡單:
- 注冊設計圖尺寸
將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>
- 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 屏幕適配從未如斯簡單
騷年你的屏幕適配方式該升級了!-今日頭條適配方案
- 假如設計圖是按1920px * 1080px來設計,以density為3來標注,也就是屏幕其實是640dp * 360dp。這時候如果我們的Button想要占據一半,是不是寬度需要設置成180dp。
- 那假如我們的手機屏幕是1280X 720,density是2 ,則寬度是360dp,的確當設置成180dp的時候也正好占據一半。
- 但是萬一1280X 720的手機的density是3呢,則寬度為240dp, 這時候設置成180dp,實際的px值為: 180 * 3 = 540px ,但是我們想要的是360px ,也就是 180 * density = 360px , 既然我們設置成的180dp不能改變(也就是設置一個值,適配各種手機),那么我們只能改變這個density值。
- 換成公式就是: 180 * density = 360,那么density是多少。哈哈。沒錯是2 ,我們動態把density從 3變成2,是不是就符合了。
- 比如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.......大家輕噴即可。。。。