您当前的位置:首页 > 计算机 > 编程开发 > 安卓(android)开发

Android屏幕密度的深刻理解

时间:02-05来源:作者:点击数:

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会变成160,密度比例就是1,所以此时看到的桌面图标会非常小,因为此时1dp = 1px,就像电脑一样了,你可以想像把1920 x 1080的电脑屏幕放到手机上显示,内容看起来肯定小啊。平时我们看手机上的内容不觉得小,是因为我们使用的单位是dp,而1个dp是等于多个px的,所以内容看起来就大,不会觉得小,打个比方,比如手机屏幕宽度为360dp,我们设置一个按钮的宽为300dp,则此时看效果屏幕右边就只有60个dp空闲出现,如果我们把最小宽改为1080dp,则右边就空闲了780dp出来,那按钮看起来肯定比之前小了。理论上说,dpi应该是固定的,比如dpi = 480,分辨率为1920 x 1080,当你dpi变为160时,则密度变低了,则如果你还想分辨率不变的话,理论上你是要扩大显示屏的真实大小,因为你密度小了,屏幕需要更多的尺寸才能有和原来一样的分辨率啊! 至于为什么能修改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

后来想了一下,修改dpi可以这样理解,默认的dpi是真实的dpi,也就是代表1英寸有多少个像素的,当你修改屏幕最小宽度后,其实1英寸有多少个像素是固定了不可能再变了的,修改只是模拟而已,是模拟的行为,比如宽为1080,标准密度是480,1dp = 3px,则宽度为360dp,现在把最小宽度改为1080dp,则1dp = 1px,密度为160,此时的1英寸中包含有160个像素,这是不可能的,因为真实的话是包含有480个,所以这里密度为160的1英寸是模拟的1英寸,是比原来小3倍的1英寸,所以,最小宽度改为1080dp后,可以理解为手机的英寸变多了(手机尺寸变大了),当然了,这只是模拟的或者想像的变大了,其实并没有变大,此时的1英寸并不是真实物理上的1英寸了,它是物理1英寸的三分之一大小,因为物理1英寸是装有480像素的,而它只有160,480 / 160 = 3。

所以,当你在两台分辨率相同的手机上运行同一个app时,如果发现显示效果不一样,则你要想到应该是它们的dpi不相同导致的,你可以到设置里面修改最小宽度,改成一样的,这样效果肯定就一样了。

如果是分辨率一样,密度也一样,但是字体大小看起来不一样,则你要想到应该是字体密度比例不相同导致的,你可以到设置里面修改字体大小,调到字体密度比例一样时,效果肯定就一样了。

在公司的一台定制机上,分辨率是1080P的,默认最小宽度是360dp,可以改到160dp,但是在小米6和华为mate30上,相同的分辨率,默认最小宽也是360dp,但是修改时最小只能改到320dp。改大时,华为mate30可以改到1080dp没问题,而小米6改1080dp后,直接黑屏重启了,而且再也起不来了,自动进入了Recovery模式,估计要清除数据才能恢复了。所以这个最小宽度不要随便设置,有风险!!

常见分辨率:

drawable-xhdpi 文件夹: 超高密度屏幕:dpi = 320dp,scale = 2.0,分辨率:720 x 1280

drawable-xxhdpi文件夹: 超超高密度屏幕:dpi = 480dp,scale= 3.0,分辨率:1080 x 1920

为什么按照720 x 1280写的界面,只要单位是dp,就能在1080 x 1920中完美适配呢?这是因为他们的dp数量是一个的:

720 x 1280

宽度:720 / 2 = 360dp

高度:1280 / 2 = 640dp

1080 x 1920

宽度:1080 / 3 = 360dp

高度:1920 / 3 = 640dp

所以,虽然他们的分辨率不一样,但是只要确保720P的手机密度为320dp,1080P的手机密度为480dp,则他们宽高的dp数是一样的。其实高度的dp数不一样影响不大,因为高度上可以增加垂直滚动,上下滚动就好了,关键宽度的dp数,如果宽度上dp数一样,则适配就是完美的,所以对于适配,一般看看手机设置里的最小宽度的dp数量是不是一样的就知道了,比如在720P或1080P手机上,最小宽度dp为360dp,而在我的另一台小米手机,它的宽度最小dp为411dp,假如美工给你的图是以720P设计的,在宽度上放了各种控件,放满宽度最多也就使用360个dp,假设用的是线性布局水平排序,当运行到411dp的手机上时,你会发现手机右侧多了很多空白,因为你的空间总共的宽最多360dp,而手机有411dp。所以在做开发时,尽量不要写这种太绝对的布局,比如标题栏,标题在中间,左右两侧有图标,使用相对布局就比较好,运行到360dp和411dp上,能保证标题栏各位控件的相对位置,只是效果上,411dp的手机标题栏的标题和左右两边的图标距离看着会感觉比较大一些。

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组件里面的,所以要想使用这个类,需要引入依赖,如下:

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实例获取方式:

Configuration config = getResources().getConfiguration();

这个类上的一些感觉能用得上的属性如下:

  • 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

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门