上一篇文章發到掘金後,發現省略了內容後,竟然很多人說看不懂;這就尷尬了。我原本覺得DP這個東西說起來實在布一樣長,而且是入門的東西,不用在討論。但發現很多人對DP適配存在誤區,尤其還有人任務不同裝置螢幕畫素不一樣需要適配的問題。所以這裡說一下DP究竟怎麼來的。
畫素(pixel)
先直接看Wiki 畫素。簡單說,畫素就是表示一個點的RGB顏色;這個點,是數學上的概念,是沒有大小的(回顧一下z初中數學,我沒記錯吧)。就是說,我們要描述一幅影象(比如清明上河圖),可以轉換成X*Y畫素的點陣圖(Bitmap),但轉換後我們是不知道這個Bitmap原來的物理尺寸,就這樣丟失了。為什麼會這樣?因為現實世界是連續的,不可能記錄下來(有限的儲存空間放不下無限量的資料)。Bitmap也是沒有物理大小的。
螢幕解析度
把Bitmap重新表示出來,我們接觸到最多的就是螢幕和列印了。要展示畫素,最簡單的就是在螢幕上取一個點對應一個畫素的點就行了。假如把螢幕分為1080*1920個點,這樣同樣1080*1920畫素的Bitmap剛好展示。螢幕上的點雖然對應畫素的點,但它是有物理尺寸的(通過螢幕寬高除以份數得到)。知道了展示規則,雖然畫素本身是沒有大小的,但我們也可以用畫素來表示一個圖形展示時究竟有多大。比如一個字型是16px,這不是說它的物理尺寸有多大,而是它顯示時會佔據多少個點。
體驗問題
螢幕點和畫素對應,很長時間內都是這樣過來的,直到後來手機從奢侈品變成日用品再變成消耗品。手機的普及,每個人整天都抱著手機螢幕盯著看,然後發現了問題:螢幕顆粒看起來太大。螢幕上每英寸上點的數量,我們叫做DPI(dots per inch,因為螢幕上面每個點對應一個畫素,所以也有叫PPI;兩者大部分情況混用,有時候又不一樣,可以參考,不展開討論)。當DPI小的時候,每個點的物理尺寸就變大(點的大小理解為 DPI 分之一 英寸;比如DPI是160,每個點就是 1/160 英寸);所以要解決螢幕顆粒大的問題,提高DPI值就可以了。但DPI提高後,又出現了另外一個問題:同樣畫素的Bitmap在新的螢幕上看起來小了。假設把DPI從160提高到320,原來160畫素在新螢幕上就只覆蓋了1/4英寸。
DP方案
無規矩不成方圓。要完整的解決問題,需要訂立新的標準,不能再讓螢幕點和畫素一一對應了。這裡是螢幕展示大小,為什麼我們不直接以尺寸為標準呢?簡單粗暴直接規定一英寸就是160dp,如果是160dpi的裝置,1dp對應一個點(畫素);如果是320dpi的裝置,1dp對應兩個點(畫素)。這樣我們要描述一個控制元件究竟多大,原來用畫素的地方,就改成DP。比如一個使用者頭像可以是48dp,表示它的大小是48/160英寸。注意,這裡DP只是取代了畫素作為描述控制元件展示大小的作用,實際展示時螢幕上還是點,系統內部Bitmap用的還是Px。使用DP的好處是把控制元件大小轉換為物理上的尺寸,讓不同dpi上的控制元件可以看起來大小一樣。
這個DP當然也可以從其他方面理解,原來用px表示大小(一個px對應一個點),現在螢幕dpi提高了一倍,之前一個點的大小現在就要對應4個點。這樣用px表示大小就不合適了,得重新取個名字(dp),當然也有叫pt的(iOS大小單位)。不管這個單位叫什麼,它表示的都是一個物理尺寸的單位,把物理尺寸大小和和最終展示點的數量進行了剝離。
用DP作為描述大小的單位後,Bitmap展示的問題還沒有解決。在160dpi上鋪滿一平方英寸需要160*160畫素,但320dpi上需要320*320畫素(依然使用160*160只能鋪滿1/4,前面提到的問題)。這個問題現在解決起來也簡單,我們把160作為標準(mdpi),把320作為兩倍圖(xhdpi),為每個標準建立一個資料夾,同樣的圖片在mdpi裡面放標準的,在xhdpi放mdpi 2倍大的。再擴充套件一下,還可以支援0.75倍的,1.5倍的,3倍的,4倍的。
到這裡,我們用dp解決了大小單位的表示問題,還相容了原來的Bitmap展示。DP是Android方案,實際上iOS用的pt也是同樣的思路,mmdpi、xhdpi也對應iOS的1倍圖、2倍圖。需要注意的是,上面說1英寸為160dp,不是強制的,而是靈活的;硬體廠商可以是一定範圍為調整,但總的1dp的視覺大小並不會差距太大;這也是DP的出發點,讓同樣單位(dp)的控制元件再不同裝置上看起來一樣大。
就這樣,新的單位訂好了,也解決了和畫素直接的轉換問題。
原型設計
引入了DP,這是對開發而已的。對於設計師來說,他面對的仍然是px。對於手機App的設計而已,設計師會取720p、1080p或者750*1334作為原型大小開發。對應到市面上的手機,我們可以直接任務720p、750p是xhdpi的,1080是xxxhdpi的。一個控制元件的dp單位的大小用它px的大小除以2或者3就可以得到;現在有原型工具也支援這種換算。這可以理解成是一個約定好的尺寸,或者一個實踐得出的結果。實際上,設計師也是遵循設計指導的,比如Material Design(雖然官方建議margin padding用8的倍數,但很多設計師用各種奇怪的大小)。
螢幕適配
螢幕適配是任何UI設計需要面對的問題,從移動裝置出來之前,PC軟體和Web就積累了螢幕適配的方案了。我們先說螢幕適配的來源,再說之前的經驗,最後說點實踐。
引入了DP概念後,對開發而已,螢幕的大小就是以dp來看的。Android是開放系統,裝置眾多,比較通用的可以分為兩類:手機和平板。這些裝置的都是ldpi-xxxxhdpi的(實際上Google自己還弄出了420dpi和560dpi的裝置);因為現在UI設計上一般認為垂直方向是可以無限延長的(上下滾動),高是一定滿足展示的,更多的我們要注意不同寬度的螢幕適配。以手機為例,一般寬度介於320到411dp之間。這就是說開發是適配手機(不包括平板),佈局必須能適應320到411dp之間的任何寬度,這就是開發要面對的適配問題。
在PC軟體和Web上,它們不僅要適配不同螢幕的畫素,還要處理同個螢幕上父視窗的不同大小;這個在核心上是和移動適配是一致的,但是要不同尺寸的外部約束下,很好的處理內部的控制元件位置。這個適配處理,實際上是個設計問題。主要的思路是,設計時以螢幕上下左右作為錨點擺放控制元件,規定好控制元件的margin和padding,寬高會變化的控制元件自適應(TextView,各種父佈局),寬高固定的控制元件寫死大小(頭像控制元件等)。開發者還原出設計師的設計思路,就能做出滿足螢幕適配的佈局。這裡我強調還原設計思路,而不是還原設計。設計圖不能展示在不同尺寸上的效果,但設計思路已經設計到了。還是上一篇文章的例子,頂部的tab設計圖上是308dp,設計思路是要表示它距離左右兩側22dp(這樣左側剛好和返回按鈕右側對齊)。
在具體實踐上,還原一個設計最好的父佈局是RelativeLayout,它本身就是表面不同控制元件之間的位置關係的,和設計思路一致。這裡直接實現設計圖中列表的item
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:padding="16dp"
android:background="@color/white"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/iv_avatar"
android:layout_marginRight="15dp"
android:layout_width="40dp"
android:layout_height="40dp"
android:scaleType="fitXY"
android:src="@drawable/img_avatar" />
<TextView
android:id="@+id/tv_name"
android:layout_toRightOf="@+id/iv_avatar"
android:layout_toLeftOf="@+id/tv_price"
android:layout_marginRight="15dp"
android:singleLine="true"
android:maxEms="8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="朱霸天"
android:textColor="#FF222222"
android:textSize="15sp"
/>
<TextView
android:id="@+id/tv_price"
android:gravity="center_vertical"
android:layout_alignParentRight="true"
android:singleLine="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="+50"
android:textColor="@color/red"
android:textSize="15sp"
/>
<TextView
android:layout_alignBottom="@+id/iv_avatar"
android:layout_toRightOf="@+id/iv_avatar"
android:singleLine="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#FF9C9FA2"
android:textSize="13sp"
tools:text="2018-08-28"
/>
</RelativeLayout>
複製程式碼
上面的程式碼,在320dp到411dp的手機上都是適配了的;程式碼也很簡單。這裡我的確是做了適配的,還原了控制元件之間的關係(價格在螢幕右側,標題和時間在頭像右側);設計圖上名字“朱霸天”是三個字,實踐上TextView都要考慮內容過多換行或擠到右側的問題(名字擠到右側紅色價格,會重疊),所以需要限制為SingleLine和maxEms,名字控制元件必須是在價格控制元件左側(android:layout_toLeftOf="@+id/tv_price")。
更多
上面說到手機螢幕適配就是適配320到411dp的裝置,這個資料可能不準確。手機螢幕眾多,這只是我看到的大部分的螢幕。但這個並不影響適配的效果,就算是擴充套件到480dp甚至平板的600dp,按dp適配也是能相容的。當然有些效果可能不是太好,這更應該是設計問題,而不是適配問題,畢竟有些UI的確不適合在大屏上顯示,內容會顯得太空。雖然不是很準確,但知道這個很重要。比如你不能看到一行可以顯示50個文字寫死了一個TextView的寬度,畢竟你看的效果可能是360dp的,在320dp上一行50個字會放不下。
DP適配後完全不用管螢幕畫素也不太準確,畢竟該做的還是要做的。該做的也說過了,給不同xxxxdpi的資料夾放對應的資原始檔。
在高度的處理上,前面也說到現在UI一般認為高度是無限延長的。如果是一個特別長的列表,適配時會使用ListView或ScrollView實現無限延長效果。要小心的是一個看起來是一個螢幕大小的佈局,不能假設頁面一定有640dp高或者其他。本來Android螢幕的高度就不一樣;有些裝置的系統按鍵可能是虛擬的,會佔據一部分螢幕;Android 新增了分屏效果(multi-window)後,螢幕高度可能不是App視窗的高度了。
DP適配當然不是什麼問題都沒有。比如實現一個啟動頁面上放一張滿屏的圖片,要相容畫素和寬高比例的螢幕的話很難保證不拉伸。這些問題又變成了一個設計問題,我近來看到像網易雲音樂或QQ音樂啟動廣告頁底部都留了一截顯示App名稱,讓圖片自頻寬高比例,這種設計就很好。對於Android而已,整個系統是基於DP機制的,甚至還有sp機制,整個適配也並不複雜。
最後
沒有了;看情況待續。