Android 屏幕适配问题

Android 屏幕适配问题

基本概念

ppi

ppi(Pixels Per Inch)硬件像素密度:在物理设备上,每英寸包含的物理像素点数量,ppi是不能修改的。

dpi

dpi(Dots Per Inch)屏幕像素密度:是软件概念,每英寸包含多少个点,dpi一般情况下是能修改的。

density

density 密度:1个dp对应多少个px。

dp

dp(density-independent pixel)密度无关像素:与设备的像素密度无关的像素,Google推荐使用。

sp

sp(scale-independent pixel)比例无关像素:与dp类似,但会随着系统的字体大小改变而调整。

密度限定符

不同屏幕像素密度大设备对应了不同的密度限定符。

密度类型说明代表分辨率(px)像素密度(dpi)density转换
ldpi低密度屏幕240*3201200.751dp=0.75px
mdpi中密度屏幕
(基准密度)
320*48016011dp=1px
hdpi高密度屏幕480*8002401.51dp=1.5px
xhdpi超高密度屏幕720*128032021dp=2px
xxhdpi超超高密度屏幕1080*192048031dp=3px

logo图标大小

屏幕密度对应图片大小图片资源目录
120dpi36px*36pxmipmap-dpi
160dpi(基准)48px*48pxmipmap/mipmap-mdpi
240dpi(1.5倍)72px*72mipmap-hdpi
320dpi(2倍)96px*96pxmipmap-xhdpi
480dpi(3倍)144px*144pxxmipmap-xxhdpi

方向限定符

限定符说明
land横屏
port竖屏

转换公式

px = density * dp

density = dpi /160

px = dp * (dpi / 160)

获取屏幕信息

不同的手机屏幕上,1pd对应的px值可能忽悠很大差异。如,在小屏幕上1dp可能对应1px,在大屏幕圣桑可能对应1px

可以通过displayMetrics获取详细信息:

val displayMetrics = applicationContext.resources.displayMetrics

//输出: DisplayMetrics{density=3.0, width=1080, height=1920, scaledDensity=3.0, xdpi=480.0, ydpi=480.0}

说明:

  • 屏幕像素密度为480dpi
  • density为3,表示在这个设备上1dp=3px
  • 屏幕宽高为1080*1920px,也就是360*640dp

Android系统定义的屏幕像素密度基准值是160dp,也就是1dp=1px,因此480dp下1dp=3px

适配问题

在布局文件中使用的单位值,最终都会被系统转换为px。Google官方推荐使用dp作为单位值,系统会根据屏幕的实际情况自动完成dp与px之间的转换。

如:将一个View的宽度设置为180dp,在标准屏幕下如540*960px/240dpi 即360*640dp720*1280px/320dpi 即360*640dp1080*1920px/480dpi 即360*640dp中都是显示一半空间。由于屏幕像素密度的存在,使得同一套dp在不同的屏幕下显示相同的效果。但是dp值只适用于大部分正常情况。

在这里插入图片描述

屏幕适配问题的根源是设备碎片化:系统碎片化、屏幕尺寸碎片化、屏幕像素密度碎片化。

屏幕尺寸碎片化问题,如:dpi都为320,但屏幕尺寸不相同时,同样的180dp在720*1280px/320dpi下占据50%的空间,但在900*1600px/320dpi下占据40%的空间,两边的显示效果就不一样了。这是用dp值无法解决的。

在这里插入图片描述

屏幕像素密度碎片化问题,如:尺寸是720*1280px/320dpi 即360*640dp,尺寸为720*1280px/360dpi 即320*568dp,这时相同的屏幕尺寸但是dpi不同,导致同样的180dp所占据的空间是不一样的。

适配方案

在布局文件中声明的dp值,最终都通过TypedValue#applyDimension()方法转换为px值,即:density * dp

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;
}

今日头条方案

详细文档

本质是通过获取屏幕宽度,再除以基准dp值,获取新的density,通过修改旧的density,最终改变View的尺寸。

fun setCustomDensity(activity: Activity, application: Application, designWidthDp: Int) {
    val appDisplayMetrics = application.resources.displayMetrics
    val targetDensity = 1.0f * appDisplayMetrics.widthPixels / designWidthDp
    val targetDensityDpi = (targetDensity * 160).toInt()
    appDisplayMetrics.density = targetDensity
    appDisplayMetrics.densityDpi = targetDensityDpi
    val activityDisplayMetrics = activity.resources.displayMetrics
    activityDisplayMetrics.density = targetDensity
    activityDisplayMetrics.densityDpi = targetDensityDpi
}
override fun onCreate(savedInstanceState: Bundle?) {
    val displayMetrics = applicationContext.resources.displayMetrics
    log("修改前:${displayMetrics.toString()}")
    setCustomDensity(this, application, 360)
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    log("修改后:${displayMetrics.toString()}")
}

//修改前:DisplayMetrics{density=2.0, width=900, height=1600, scaledDensity=2.0, xdpi=320.0, ydpi=320.0}
//修改后:DisplayMetrics{density=2.5, width=900, height=1600, scaledDensity=2.0, xdpi=320.0, ydpi=320.0}

smallestWidth 最小宽度限定

smallestWidth即最小宽度适配,是系统原生支持的一种适配方案。

它是不考虑屏幕方向,指的是最短的那个边,按比例分配,如:设计稿是720*1280px/360dpi,那么基准分辨率就是设计稿的宽度宽度为360dp,表示把宽度分为360份,得到以下尺寸:

<dimen name="dp_2">1dp</dimen>
<dimen name="dp_1">2dp</dimen>
...
<dimen name="dp_360 ">360dp</dimen>

360dp为基准,在不同最小宽度的文件夹下按比例缩放尺寸,如sw480dp

<dimen name="dp_2">1.333dp</dimen>
<dimen name="dp_1">2.666dp</dimen>
...
<dimen name="dp_360 ">480dp</dimen>

在这里插入图片描述

在这里插入图片描述

使用最小宽度限定符适配解决大部分情况下的适配问题。

布局适配

  • 可以使用LinearLayout(线性布局)和百分比布局控制权重比例。

  • 使用RelativeLayout(相对布局)和ConstraintLayout(约束布局)控制关联关系。

其他

dimens 文件生成工具

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 = """<?xml version="1.0" encoding="utf-8"?>"""
private const val XML_RESOURCE_START = """<resources>"""
private const val XML_SW_DP_TAG = """<string name="sw_dp">%ddp</string>"""
private const val XML_DIMEN_TEMPLATE_TO_DP = """<dimen name="DIMEN_%ddp">%.2fdp</dimen>"""
private const val XML_DIMEN_TEMPLATE_TO_PX = """<dimen name="DIMEN_%dpx">%.2fdp</dimen>"""
private const val XML_RESOURCE_END = """</resources>"""

private const val DESIGN_WIDTH_DP = 360
private const val DESIGN_HEIGHT_DP = 640

private const val DESIGN_WIDTH_PX = 720
private const val DESIGN_HEIGHT_PX = 1280

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()
}

dimens 文件生成插件

一、需要在AndroidStudio中安装ScreenMatch 插件。

二、在默认values文件夹下准备一份dimens.xml文件。

三、鼠标右键ScreenMatch选项,立即会生成多套dimens文件。

四、可以打开工程目录下screenMatch.properties文件,配置一些其他信息。

在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值