AndroidDevelop|屏幕兼容性概览
为什么会出现屏幕适配,首先我们思考一个问题,如果我们用dp作为单位,假设屏幕密度一样的两台手机,比如都是320dpi,但是他们的尺寸一个是4.3英寸,一个是5.0英寸,那么原先为4.3英寸适配的UI放到5.0中,则会出现差异,可能原先全屏适配的到了5.0会出现留白。
假设我们UI设计图是按屏幕宽度为360dp来设计的,那么在1080x1920,5英寸的设备上,屏幕宽度其实为1080/(440/160)=392.7dp,也就是屏幕是比设计图要宽的。
所以对于Android设备来说,屏幕适配的根本原因在于,Android屏幕碎片化太严重,没有遵循1:1.5:2:3:4
的比例进行屏幕设计,所以即使用dp作为单位,也会出现上面那种情况。
屏幕尺寸大全
屏幕尺寸指屏幕的对角线的长度,单位是英寸,1英寸=2.54厘米,比如常见的屏幕尺寸有2.4、2.8、3.5、3.7、4.2、5.0、5.5、6.0等
屏幕分辨率是指在 纵横向上的像素点数,单位是px,1px=1个像素点。一般以纵向像素 x 横向像素
,如1920 x1080。
分辨率和屏幕尺寸之间的关系
由于density在长和宽方向都是一致的,所以 屏幕尺寸长宽比 = 分辨率长宽比
比如 1080 * 1920 ——> 1080/x = 1920/y —— > 即:屏幕尺寸长宽比 = 分辨率之比;
综上:分辨率 和 尺寸定好,手机的长宽就定死了;
屏幕像素密度是指 每英寸上的像素点数,单位是dpi,即"dot per inch”的缩写。屏幕像素密度与屏幕 尺寸 和屏幕 分辨率 有关,在单一变化条件下,屏幕尺寸越小、分辨率越高,像素密度越大,反之越小。
设备密度:density = px / dpi
, 比如: 2 = 1080px / 540dp
px = dp x (dpi/160) = dp x density
# 1080px = 540dp x (320dpi/160) = 540dp x 2
px我们应该是比较熟悉的,前面的分辨率就是用的像素为单位,大多数情况下,比如 UI设计、Android原生API
都会以px作为统一的计量单位,像是获取屏幕宽高等。**像素(点)是组成图片的基本要素,单位面积上的像素点越多,这张图就越清晰;分辨率是指长和宽两个方向上各自拥有的像素点的个数;
dp,Density Independent Pixels的缩写,即密度无关像素,上面我们说过,dpi是屏幕像素密度,假如一英寸里面有160个像素,这个屏幕的像素密度就是160dpi,那么在这种情况下,dp和px如何换算呢?在Android中,规定以160dpi为基准(1英寸160px),1dp=1px,如果像素密度是320dpi(设备密度2),则1dp=2px,以此类推。
假如同样都是画一条160px的线,在480x800分辨率(标准设备密度1.5)手机上显示为1/3屏幕宽度,在320x480(标准设备密度1)的手机上则是1/2,如果换算成dp,则前者分辨率是 320 x 533,后者分辨率是320x 480,在这两种分辨率下,160dp都显示为屏幕一半的长度。这也是为什么在Android开发中,写布局的时候要尽量使用dp而不是px的原因。
而sp,即scale-independent pixels,与dp类似,但是可以根据文字大小首选项进行放缩,是设置字体大小的御
用单位。
// android\util\TypedValue.java
public static float applyDimension( int unit, float value, DisplayMetrics metrics) {
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}
mdpi、 hdpi、 xdpi、xxdpi用来修饰Android中的 drawable
文件夹及values
文件夹,用来区分不同像素密度下的 图片和dimen值。
按照 AndroidDevelopers|提供备用位图 按下列来划分
名称 | 像素密度范围 |
---|---|
mdpi | 120dpi~160dpi (0.75~1)——1 |
hdpi | 160dpi~240dpi (1~1.5) ——1.5 |
xhdpi | 240dpi~320dpi (1.5~2) ——2 |
xxhdpi | 320dpi~480dpi (2~3) ——3 |
xxxhdpi | 480dpi~640dpi (3~4) ——4 |
在设计图标时,对于五种主流的像素密度(MDPI、HDPI、XHDPI、XXHDPI和XXXHDPI)应按照2:3:4:6:8
的比例进行缩放。例如,一个启动图标的尺寸为48x48 dp,在mdpi的屏幕上其实际尺寸应为 48x48
pX,在hdmi的屏幕上其实际大小是mdpi的1.5倍(72x72 px),在xdpi的屏幕上其实际大小是mdpi的2
倍(96x96 px),依此类推。
屏幕密度 | 图标尺寸 |
---|---|
mdpi | 48x48px |
hdpi | 72x72px |
xhdpi | 96x96px |
xxhdpi | 144x144px |
xxxhdpi | 192x192px |
也有说 宽高限定符
可以参考鸿神提供的思路,即每种屏幕分辨率的设备需要定义一套 dimens.xml文件。
用的是百分比的思路,src\main\res
下创建一些常见的手机像素比例,系统会自动去找对应的dimens文件
# - src
# - main
# - res
# - values-480x320
# - values-800x480
# - values-854x480
# - values-xxxxxxx
# - values-2560x1440
假设设计图是以480x320为基准作图,那么
宽度为320,将任何分辨率的宽度分为320份,取值为x1-x320
高度为480,将任何分辨率的高度分为480份,取值为y1-y480
<resources>
<dimen name="x1">1pxdimen>
<dimen name="x2">2pxdimen>
...
<dimen name="x320">320pxdimen>
resources>
对于800x480来说,它对应的就是,480/320 = 1.5,纵向同样的思路创建
<resources>
<dimen name="x1">1.5pxdimen>
<dimen name="x2">3pxdimen>
...
<dimen name="x320">480pxdimen>
resources>
其它的依次类推
wildma/ScreenAdaptation
Android 3.2 版引入。最小宽度限定符可让您通过指定某个最小宽度(以dp为单位)来定位屏幕。
一般来说,UI设计师提供的设计图分以下几种
可以借助 ScreenMatch 插件(原理可以参考鸿洋屏幕分辨率限定符)快速生成。安装插件后,会自动在根目录生成screenMatch.properties
和screenMatch_example_dimens.xml
示例文件
base_dp=360
# System default values is 240,320,384,392,400,410,411,480,533,592,600,640,662,720,768,800,811,820,960,961,1024,1280,1365
# 可根据实际情况修改下值
match_dp=392.7272
ignore_dp=240,320,384,392,400,410,411,480,533,592,600,640,662
# 以哪个module为基准module生成相应的dimens.xml
match_module=app
base_dp
:最小宽度基/准值,填写设计图的最小宽度值即可。system default...
:插件默认适配的最小宽度值,默认生成的一系列的dimens.xml文件。match_dp
:需要适配的最小宽度值(如果是小数,则保留4位小数。例如 392.727272…,则取392.7272),即你想生成哪些dimens.xml文件。ignore_dp
:忽略不需要适配的最小宽度值,即忽略掉插件默认生成的 dimens.xml文件。match_module
:适配其它module时候,不需要每份都生成一套dimens.xml,只需要相应的module中的values文件夹下有一套与match_module一样的dimens文件即可按照上面示例生成的文件夹为下图:
当然也可以自己写一个方法来生成这些文件,以宽高的最小值为基准,此处我们以宽370为基准,在根目录下生成相应src-dp
和src-px
目录,并生成相应的dimens.xml
。
import java.io.File
import java.io.FileOutputStream
import kotlin.math.min
private const val XML_FILE_NAME = """dimens.xml"""
private const val XML_HEADER = """"""
private const val XML_RESOURCE_START = """"""
private const val XML_SW_DP_TAG = """%ddp """
private const val XML_DIMEN_TEMPLATE_TO_DP = """%.2fdp """
private const val XML_DIMEN_TEMPLATE_TO_PX = """%.2fdp """
private const val XML_RESOURCE_END = """"""
private const val DESIGN_WIDTH_DP = 370
private const val DESIGN_HEIGHT_DP = 667
private const val DESIGN_WIDTH_PX = 1080
private const val DESIGN_HEIGHT_PX = 1920
fun main() {
val designWidthDp = min(DESIGN_WIDTH_DP, DESIGN_HEIGHT_DP)
val srcDirFileDp = File("src-dp")
makeDimens(designWidthDp, srcDirFileDp, XML_DIMEN_TEMPLATE_TO_DP)
val designWidthPx = min(DESIGN_WIDTH_PX, DESIGN_HEIGHT_PX)
val srcDirFilePx = File("src-px")
makeDimens(designWidthPx, srcDirFilePx, XML_DIMEN_TEMPLATE_TO_PX)
}
private fun makeDimens(designWidth: Int, srcDirFile: File, xmlDimenTemplate: String) {
if (srcDirFile.exists() && !srcDirFile.deleteRecursively()) {
return
}
srcDirFile.mkdirs()
val smallestWidthList = mutableListOf<Int>().apply {
for (i in 320..460 step 10) {
add(i)
}
}.toList()
for (smallestWidth in smallestWidthList) {
makeDimensFile(designWidth, smallestWidth, xmlDimenTemplate, srcDirFile)
}
}
private fun makeDimensFile(designWidth: Int, smallestWidth: Int, xmlDimenTemplate: String, srcDirFile: File) {
val dimensFolderName = "values-sw" + smallestWidth + "dp"
val dimensFile = File(srcDirFile, dimensFolderName)
dimensFile.mkdirs()
val fos = FileOutputStream(dimensFile.absolutePath + File.separator + XML_FILE_NAME)
fos.write(generateDimens(designWidth, smallestWidth, xmlDimenTemplate).toByteArray())
fos.flush()
fos.close()
}
private fun generateDimens(designWidth: Int, smallestWidth: Int, xmlDimenTemplate: String): String {
val sb = StringBuilder()
sb.append(XML_HEADER)
sb.append("\n")
sb.append(XML_RESOURCE_START)
sb.append("\n")
sb.append(" ")
sb.append(String.format(XML_SW_DP_TAG, smallestWidth))
sb.append("\n")
for (i in 1..designWidth) {
val dpValue = i.toFloat() * smallestWidth / designWidth
sb.append(" ")
sb.append(String.format(xmlDimenTemplate, i, dpValue))
sb.append("\n")
}
sb.append(XML_RESOURCE_END)
return sb.toString()
}
但要注意的是,这种方式只适合 Android 3.2版本之前。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment android:id="@+id/headlines"
android:layout_height="match_parent"
android:layout_width="match_parent" />
LinearLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal">
<fragment android:id="@+id/headlines"
android:layout_height="fill_parent"
android:layout_width="400dp"
android:layout_marginRight="10dp"/>
<fragment android:id="@+id/article"
android:layout_height="match_parent"
android:layout_width="match_parent" />
LinearLayout>
图片资源匹配,.9图片类型
布局组件,使用wrap_content
、match_parent
、weight
布局别名
一种极低成本的Android屏幕适配方式
由于使用的是 Application.getResources
,这会导致最后计算状态栏高度使用的是修改过后的 density,如果换成 Resources.getSystem()
来获取系统的 Resources
,果不其然可以获取到正确高度的状态栏高度,代码如下所示:
public static int getStatusBarHeight() {
Resources resources = Resources.getSystem();
int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
return resources.getDimensionPixelSize(resourceId);
}
三种Resources
的获取
// 比如状态栏、导航栏
Resources systemRes = Resources.getSystem();
// applicaiton
Resources appRes = Application.getResources();
// acttivity
Resources actRes = activity.getResources();
JessYanCoding/AndroidAutoSize
其实这里并没有用到什么 黑科技,原理反而非常简单,只需要声明—个ContentProvider
,在它的onCreate
方法中启动框架即可,在App 启动时,系统会在App 的主进程中自动实例化你声明的这个ContentProvider,并调用它的 onCreate 方法,执行时机比 Application#onCreate 还靠前,可以做一些初始化的工作
// me.jessyan.autosize.InitProvider.java
public class InitProvider extends ContentProvider {
@Override
public boolean onCreate() {
Context application = getContext().getApplicationContext();
if (application == null) {
application = AutoSizeUtils.getApplicationByReflect();
}
AutoSizeConfig.getInstance()
.setLog(true)
.init((Application) application)
.setUseDeviceSize(false);
return true;
}
}
这里是今日头条适配方案的核心代码, 核心在于根据当前设备的实际情况做自动计算并转换 DisplayMetrics.density、 DisplayMetrics.scaledDensity、DisplayMetrics.densityDpi 这三个值, 额外增加 DisplayMetrics.xdpi 以支持单位 pt、in、mm
// me.jessyan.autosize\AutoSize.java
public static void autoConvertDensity(Activity act, float sizeInDp, boolean isBaseOnWidth){
//...
if (displayMetricsInfo == null) {
if (isBaseOnWidth) {
targetDensity = AutoSizeConfig.getInstance().getScreenWidth() * 1.0f / sizeInDp;
} else {
targetDensity = AutoSizeConfig.getInstance().getScreenHeight() * 1.0f / sizeInDp;
}
}
//...
setDensity(activity, targetDensity, targetDensityDpi, targetScaledDensity, targetXdpi);
setScreenSizeDp(activity, targetScreenWidthDp, targetScreenHeightDp);
}
setDensity
中有两种类型的DisplayMetrics,分别获取并设置其属性
private static void setDensity(Activity activity, float density, int densityDpi, float scaledDensity, float xdpi) {
// Activity 的 DM
DisplayMetrics activityDM = activity.getResources().getDisplayMetrics();
setDensity(activityDisplayMetrics, density, densityDpi, scaledDensity, xdpi);
// App 的 DM
DisplayMetrics appDM = AutoSizeConfig.getInstance().getApplication().getResources().getDisplayMetrics();
setDensity(appDisplayMetrics, density, densityDpi, scaledDensity, xdpi);
}
setScreenSizeDp
给Configuration
赋值,在Activity配置变化时能够同步变更
private static void setScreenSizeDp(Configuration configuration, int screenWidthDp, int screenHeightDp) {
configuration.screenWidthDp = screenWidthDp;
configuration.screenHeightDp = screenHeightDp;
}
scaleDensity
显示器上显示的字体的缩放因子。这与density
是相同的,只是在运行时可以根据用户对字体大小的偏好以较小的增量进行调整。
与 density
本质上是同一个值,为了单独调整字体而设置
// density 和 scaledDensity 的关系
public static void autoConvertDensity(Activity act, float sizeInDp, boolean isBaseOnWidth){
//...
// 是否屏蔽系统字体大小对AutoSize的影响,否的话就随系统改变,设置字体大小后,计算缩放比(scaledDensity/density)
float systemFontScale = AutoSizeConfig.getInstance().isExcludeFontScale() ? 1 : AutoSizeConfig.getInstance().getInitScaledDensity() * 1.0f / AutoSizeConfig.getInstance().getInitDensity();
//
targetScaledDensity = targetDensity * systemFontScale;
}
我们以 pixel2xl 为验证机型,当字体大小调节为默认时候,调整显示大小时,我们看输出:设备密度与字体密度都会变,但比例不变
# 小
E/测试: scanledDensity/density=1.0
E/测试: 设备屏幕密度:density:2.9750001 scaledDensity:2.9750001 densityDpi(dpi):476
# 默认
E/测试: scanledDensity/density=1.0
E/测试: 设备屏幕密度:density:3.5 scaledDensity:3.5 densityDpi(dpi):560
# 大
E/测试: scanledDensity/density=1.0
E/测试: 设备屏幕密度:density:3.825 scaledDensity:3.825 densityDpi(dpi):612
当显示大小为默认时,调节字体大小,我们看输出:设备密度不变,字体密度会变
# 小
E/测试: scanledDensity/density=0.85
E/测试: 设备屏幕密度:density:3.5 scaledDensity:2.9750001 densityDpi(dpi):560
# 默认
E/测试: scanledDensity/density=1.0
E/测试: 设备屏幕密度:density:3.5 scaledDensity:3.5 densityDpi(dpi):560
# 大
E/测试: scanledDensity/density=1.15
E/测试: 设备屏幕密度:density:3.5 scaledDensity:4.025 densityDpi(dpi):560