Android中有一个类:DisplayMetrics,官方文档在此:https://developer.android.google.cn/reference/android/util/DisplayMetrics?hl=en
DisplayMetrics类描述有关显示器的一般信息的结构,例如其大小,密度和字体缩放。
DisplayMetrics实例对象的获取方式:context.getResources().getDisplayMetrics();
属性并不多,对于屏幕密度官方描述不够详细,所以这里记录一下,描述详细一点。DisplayMetrics常用属性如下:
一般我们在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模式,估计要清除数据才能恢复了。所以这个最小宽度不要随便设置,有风险!!
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组件里面的,所以要想使用这个类,需要引入依赖,如下:
implementation "androidx.window:window:1.0.0-alpha02"
这两个函数的区别,看不太懂,英文不行。在我的手机上,这两个函数都能获取到屏幕的宽高,代码如下:
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 config = getResources().getConfiguration();
这个类上的一些感觉能用得上的属性如下:
代码示例如下:
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 "竖屏"}")
}
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