天天看點

Android螢幕密度的深刻了解context.getResources().getDisplayMetrics()WindowMetrics.getBounds()擷取螢幕寬高Configuration擷取螢幕密度、方向、最小寬度Context.getDisplay()

context.getResources().getDisplayMetrics()

Android中有一個類:DisplayMetrics,官方文檔在此:https://developer.android.google.cn/reference/android/util/DisplayMetrics?hl=en

DisplayMetrics類描述有關顯示器的一般資訊的結構,例如其大小,密度和字型縮放。

DisplayMetrics執行個體對象的擷取方式:context.getResources().getDisplayMetrics();

屬性并不多,對于螢幕密度官方描述不夠詳細,是以這裡記錄一下,描述詳細一點。DisplayMetrics常用屬性如下:

  • widthPixels 螢幕寬。當手機發生旋轉時,之前的寬會變成高。
  • heightPixels 螢幕高。當手機發生旋轉時,之前的高會變成寬。
  • densityDpi 螢幕密度,即每英寸的螢幕中包含的像素數量,英寸為國外的長度機關,它換算為國内的機關為:1英寸 = 2.54厘米,是以每英寸螢幕,就是說每2.54厘米螢幕。比如densityDpi為160,則表示每英寸螢幕中的像素點有160個,也就是說真實手機螢幕上,你可以拿尺去量一量,螢幕上,每2.54厘米就包含有160個像素點在裡面,當然了,你量一量長度還可以,像素點你是看不見的,因為像素點非常非常的小。dpi為480的手機,理論是要比dpi為160的手機清晰很多很多的,因為同樣的1英寸的螢幕大小,一個手機可以使用480個像素點來顯示圖像,一個隻能用160個像素點來顯示圖像,效果肯定是差很多的。
  • density 官方稱它為顯示的邏輯密度,這不太好了解,我把它了解為密度的比例,也可以了解為dp換算為像素的比例(即1個dp等于幾個像素)。标準的螢幕密度為160,它的密度比例就是1,即1個dp就等于1個像素。如果你手機的densityDpi為320,則它是标準螢幕密度的兩倍(320 / 160 = 2),則density = 2,表示1個dp就等于2個像素。舉個例子,比如你手機的densityDpi為320,然後你設定了一個控件的寬為60dp,則它顯示到螢幕上的實際寬度為120像素,因為density = 2,是以60 * 2px = 120px。
  • scaledDensity 字型比例。它的預設值也是densityDpi / 160,也就是說預設和density值是一樣的,但是我們在手機上是可以設定字型大小的,這時候的scaledDensity就會發生改變。舉個例子,假設densityDpi = 320,則density = 2,預設的scaledDensity也為2,這時我們在手機設定中修改字型大小,設定為最大号,則scaledDensity的值肯定大于2,我假設此時scaledDensity = 3,然後我們在布局中添加兩個TextView,一個TextView的大小設定為10dp,另一個TextView的大小設定為10sp,則它們顯示到手機上時,真實大小為:一個是20像素(10dp x 2px = 20px),另一個是30像素(10sp x 3px = 30px),是以平時我們說,在設定字型大小時應該使用sp,而不是dp,了解到這裡之後,我們就有了自己的了解了,字型大小并不一定要設定為sp的,設定為dp也可以,如何選擇呢?就看需求了,比如,有一個地方的TextView,我希望它的大小是固定不變的,因為如果設定的太大了會影響到我其它控件的顯示,界面就不美觀了,則此時應該使用dp,這樣的話,使用者在設定裡面不論如何設定字型大小都不會影響到我的這個TextView的大小了。而一些文章類的頁面,我們為了照顧視力不好的使用者,應該使用sp,這樣,視力不好的使用者,它覺得我界面上的字型太小了,但是我界面上又沒有增加設定字型大小的功能,則此時使用者可以自己到系統的設定裡面去修改字型大小,這樣我的界面上使用sp為機關的字型大小就會随之改變。

一般我們在xml中寫布局控件的大小時,直接使用dp或sp即可,但是如果在代碼中,設定控件大小,它預設機關是px,此時就需要動手把dp轉換為px,比如在代碼中想把20dp換算為對應的像素值,則:

val dpCount = 20 // 表示20dp
val pxCount = context.getResources().getDisplayMetrics().density * dpCount // dp換算為像素
mButton.setWidth(pxCount) // 設定按鈕的寬為20dp
           

在代碼中設定TextView或Button等控件的字型大小時,預設機關就是sp的,是以不需要在換算處理,如:mTextView.setTextSize(10),即表示設定字型大小為10sp。

是以,其實平時開發中,常用到的是desity屬性,而densityDpi和scaledDensity很少會使用。

之前,我還寫過一篇有點相關螢幕密度的文章,可以了解一下:getDimension與getDimensionPixelOffset與getDimensionPixelSize的差別

Android Jetpack元件中的Compose用于寫UI,以後應該會成為主流,它裡面有一個類叫Dp,官方文檔在此:https://developer.android.google.cn/reference/kotlin/androidx/compose/ui/unit/Dp?hl=en

添加了Compose依賴之後,在代碼中想要表示20dp,可以這樣:val width = 20.dp,它其實就是Kotlin的擴充函數,給Int添加了擴充,自動把20轉換為對應的像素。

後來,有發現手機的dpi是可以改變的,這就神奇了,這不應該是固定不變的嗎?比如,你手機顯示器分辨率為1080 x 1920,dpi為480,然後我在手機設定的開發者選項裡面,修改一個叫“最小寬度”的設定,預設最小寬度為360dp,為什麼是360dp呢?因為dpi為480,則密度的比例desity = 480 / 160 = 3,是以1dp = 3px,是以1080px就相當于360dp(1080 / 3 = 360dp),我把最小寬度設定為1080dp,此時的dpi會變成120,密度比例就是1,是以此時看到的桌面圖示會非常小,因為此時1dp = 1px,就像電腦一樣了,你可以想像把1920 x 1080的電腦螢幕放到手機上顯示,内容看起來肯定小啊。平時我們看手機上的内容不覺得小,是因為我們使用的機關是dp,而1個dp是等于多個px的,是以内容看起來就大,不會覺得小,打個比方,比如手機螢幕寬度為360dp,我們設定一個按鈕的寬為300dp,則此時看效果螢幕右邊就隻有60個dp空閑出現,如果我們把最小寬改為1080dp,則右邊就空閑了780dp出來,那按鈕看起來肯定比之前小了。理論上說,dpi應該是固定的,比如dpi = 480,分辨率為1920 x 1080,當你dpi變為120時,則密度變低了,則如果你還想分辨率不變的話,理論上你是要擴大顯示屏的真實大小,因為你密度小了,螢幕需要更多的尺寸才能有和原來一樣的分辨率啊! 至于為什麼能修改dpi我就不深究了,我們隻要記得dpi是可以修改的就行了,當你修改了最小寬度的時候,dpi就會發生變化,最小寬度可以通過分辨率的寬和dpi算出來,dpi也可以通過分辨率和最小寬度算出來,如下:

最小寬度(機關dp)= 分辨率寬 / (dpi / 160)
dpi = 分辨率寬 / 最小寬度 * 160
           

比如,寬是1080px,dpi是480,則最小寬為360dp,1080 / (480 / 160) = 360dp

比如,寬是1080px,最小寬度為360dp,則dpi為480,1080 / 360 * 160 = 480

是以,當你在兩台分辨率相同的手機上運作同一個app時,如果發現顯示效果不一樣,則你要想到應該是它們的dpi不相同導緻的,你可以到設定裡面修改最小寬度,改成一樣的,這樣效果肯定就一樣了。

如果是分辨率一樣,密度也一樣,但是字型大小看起來不一樣,則你要想到應該是字型密度比例不相同導緻的,你可以到設定裡面修改字型大小,調到安慰體密度比例一樣時,效果肯定就一樣了。

在公司的一台定制機上,分辨率是1080P的,預設最小寬度是360dp,可以改到160dp,但是在小米6和華為mate30上,相同的分辨率,預設最小寬也是360dp,但是修改時最小隻能改到320dp。改大時,華為mate30可以改到1080dp沒問題,而小米6改1080dp後,直接黑屏重新開機了,而且再也起不來了,自動進入了Recovery模式,估計要清除資料才能恢複了。是以這個最小寬度不要随便設定,有風險!!

WindowMetrics.getBounds()擷取螢幕寬高

DisplayMetrics dm = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(dm);
           

通過這種方式也能擷取到一個DisplayMetrics對象,但是我們發現getMetrics函數已經過時了,推薦使用WindowMetrics.getBounds()擷取應用程式視窗區域的尺寸,使用Configuration.densityDpi擷取目前密度。

于是我就查了WindowMetrics的官方文檔,上面推薦了兩各方式擷取此對象的執行個體,如下:

mWindowManager.getCurrentWindowMetrics()
mWindowManager.getMaximumWindowMetrics()
           

細看WindowMetrics類的完全形式為:androidx.window.WindowMetrics,這不是SDK裡面的類,這是Jetpack元件裡面的,是以要想使用這個類,需要引入依賴,如下:

這兩個函數的差別,看不太懂,英文不行。在我的手機上,這兩個函數都能擷取到螢幕的寬高,代碼如下:

val wm = WindowManager(this)
val metrics = wm.currentWindowMetrics
val bounds = metrics.bounds
val width = bounds.width()
val height = bounds.height()
           

哦呵,這裡的WindowManager竟然可以直接new出來,真實神奇了!

getBounds()函數官方文檔:https://developer.android.google.cn/reference/kotlin/android/view/WindowMetrics?hl=en#getBounds()

文檔描述說,此方法傳回的寬高是包括系統欄區域的,而Display.getSize(Point)擷取的寬高不包括。getSize函數擷取的寬高也可通過下面的方式獲得:

final WindowMetrics metrics = windowManager.getCurrentWindowMetrics();
  // Gets all excluding insets
  final WindowInsets windowInsets = metrics.getWindowInsets();
  Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()
          | WindowInsets.Type.displayCutout());
 
  int insetsWidth = insets.right + insets.left;
  int insetsHeight = insets.top + insets.bottom;
 
  // Legacy size that Display#getSize reports
  final Rect bounds = metrics.getBounds();
  final Size legacySize = new Size(bounds.width() - insetsWidth,
          bounds.height() - insetsHeight);
  
           

注:此windowManager要使用SDK自帶的,不能使用jetpack中的。且此insets.width()是在API29才出來的,在低版本無法使用。

Configuration擷取螢幕密度、方向、最小寬度

Configuration執行個體擷取方式:

這個類上的一些感覺能用得上的屬性如下:

  • densityDpi 螢幕密度
  • fontScale 字型比例,實際上我發現這個并不是我們之前了解的那個字型密度比例,不論我如何在設定中修改字型大小,它的值始終是1。
  • locale 語言環境
  • mcc IMSI MCC(移動國家/地區代碼)
  • mnc IMSI MNC(移動網絡代碼)
  • orientation 螢幕的總體方向
  • screenWidthDp 可用螢幕空間的目前寬度,以dp為機關。
  • screenHeightDp 可用螢幕空間的目前高度,以dp為機關。經實驗,我發現這個值并不是螢幕的完整高,應該是扣掉了系統狀态欄的高度了。
  • smallestScreenWidthDp 應用程式在正常操作中将看到的最小螢幕尺寸,這是什麼意思呢?意思是不管你是橫屏還是豎屏顯示,它始終是最小的那條邊的尺寸,比如1080 x 1920的手機,豎屏時寬是1080,最小寬,1080px為360dp,當橫屏時,高為1080px,最小寬的值還是這個1080px對應的360dp。

代碼示例如下:

resources.configuration.apply {
    Timber.i("densityDpi = ${this.densityDpi}")
    Timber.i("screenWidthDp = ${this.screenWidthDp}")
    Timber.i("screenHeightDp = ${this.screenHeightDp}")
    Timber.i("smallestScreenWidthDp = ${this.smallestScreenWidthDp}")
    Timber.i("orientation = ${if (this.orientation == Configuration.ORIENTATION_LANDSCAPE) "橫屏" else "豎屏"}")
}
           

Context.getDisplay()

Display:提供一個邏輯顯示的關于大小和密度的資訊。

Display擷取方式:

context.getWindowManager().getDefaultDisplay() // 在API30版本過時
context.getDisplay() // API30才出來的函數
DisplayManager.getDisplay(displayId)
DisplayManager.getDisplays()
DisplayManager.getDisplays(category)
           

示例如下:

windowManager.defaultDisplay?.let {
                Timber.i("displayId = ${it.displayId}")
                Timber.i("name = ${it.name}")
                Timber.i("width = ${it.width}")   // 過時,推薦WindowMetrics.getBounds()
                Timber.i("height = ${it.height}") // 過時,推薦WindowMetrics.getBounds()
                val metrics = DisplayMetrics()
                it.getMetrics(metrics) // 過時,推薦WindowMetrics.getBounds()擷取寬高,Configuration.densityDpi擷取螢幕密度
                Timber.i("width2 = ${metrics.widthPixels}")
                Timber.i("height2 = ${metrics.heightPixels}")

                Timber.i("orientation = ${it.orientation}") // 過時,推薦rotation
                Timber.i("rotation = ${it.rotation}")
                Timber.i("state = ${it.state}") // 1-顯示器關閉,2-顯示器打開
                Timber.i("supportedRefreshRates = ${it.supportedRefreshRates.contentToString()}")

                val metrics2 = DisplayMetrics()
                it.getRealMetrics(metrics2)
                Timber.i("width3 = ${metrics2.widthPixels}")
                Timber.i("height3 = ${metrics2.heightPixels}")

                val point = Point()
                it.getRealSize(point)
                Timber.i("width4 = ${point.x}")
                Timber.i("height4 = ${point.y}")

                val rect = Rect()
                it.getRectSize(rect) // 過時,推薦WindowMetrics#getBounds()
                Timber.i("width5 = ${rect.width()}")
                Timber.i("height5 = ${rect.height()}")

                val point2 = Point()
                it.getSize(point2) // 過時,推薦WindowManager#getCurrentWindowMetrics()
                Timber.i("width6 = ${point2.x}")
                Timber.i("height6 = ${point2.y}")
            }
           

列印結果如下:

displayId = 0

name = 内置螢幕

width = 1080

height = 1920

width2 = 1080

height2 = 1920

orientation = 0

rotation = 0

state = 2

supportedRefreshRates = [55.55]

width3 = 1080

height3 = 1920

width4 = 1080

height4 = 1920

width5 = 1080

height5 = 1920

width6 = 1080

height6 = 1920