屏幕适配的必要性
- 处理虚拟尺寸和设计稿之间的比例,如设计稿为375 x 667,我们需要将这个尺寸缩放到我们的虚拟尺寸上。
- 处理个别手机虚拟尺寸不同的问题,让虚拟尺寸不同的手机显示相同的视觉效果。
何为虚拟尺寸
我们采用Android中的dp来作为单位来描述虚拟尺寸。下面先来解释下dp。
概念解释
我们需要先看下下面的几个定义:
- 像素(PX): 屏幕上的物理点,是显示图像的最小单元。
- 每英寸像素密度(DPI): 每英寸中的像素个数。
- 密度无关像素(dp, dip): Android中定义的虚拟尺寸
Android设备的历史发展
在最早的手机上(即一倍屏 Size=360px * 640px, Density=160dpi),1dp=1px。
所以我们的虚拟尺寸为360dp * 640dp。
由此我们推倒出虚拟尺寸的计算公式:
宽/高度 = Size(宽/高度) / (Density / 标准Density(160))
之后又出现了二倍屏(Size=720px * 1280px, Density=320dpi)和三倍屏(Size=1080px * 1920px, Density=480dpi)
经过计算,他们的虚拟尺寸同为360dp * 640dp。
所以,一定程度上dp这个设计还是成功的,它屏蔽了物理尺寸之间的差异,在不同尺寸屏幕的设备上显示了相同的效果。
存在虚拟尺寸不是360dp*640dp的手机么?
存在的,比如坚果Pro2,它的虚拟尺寸进过计算为430dp*800dp,一个360dp的色块在小米和坚果手机上显示如下:
这样直接写dp就会变小了,我们下面讲下虚拟尺寸的适配,把这个坚果手机的显示效果适配的和小米一样。
适配虚拟尺寸
使用资源描述符机制
根本原理还是得通过Android提供的资源描述符机制进行,这里我们选用sw,即需要生成一个values-swXXXdp的资源目录。
我们的程序跑在这样的手机上时,会去先寻找sw和自身属性匹配的目录。找不到的话在从
统一的values.xml进行寻找。
如一个宽度为400dp的手机,会去寻找sw-400dp或者w-400dp的资源目录。
这样的方式需要我们手动将写多个dimens.xml文件,能否自动化呢? 下面介绍下。
使用Gradle Plugin自动生成sw文件
原理
先了解下Android资源的编译流程,在编译时,资源会被merge到统一的目录,然后在被AAPT所处理,我们在AAPT处理之前,在资源Merge之后将尺寸文件进行生成即可。
下面看下我们的Hook点,AAPT处理资源的Task叫ProcessVariantResource。我们将自定义的Task先于它执行即可。
将我们资源处理Task Hook进编译流程
代码也非常简单
// 将Merge资源的Task Hook到APG中的processVariantResource
def dependencies = processResTask.taskDependencies.getDependencies(processResTask)
target.tasks.findByName(mergeDimensTask)
.dependsOn(dependencies)
processResTask.dependsOn target.tasks.findByName(mergeDimensTask)
我们的Task目前处于这个位置
暂且命名为MergeVariantDimensResource
通过自定义Task处理资源
然后使用Groovy将values.xml中的标签读取处理,在指定sw的目录生成重新生成values.xml文件,并重新计算尺寸。
适配效果如下,这样就和之前的小米手机显示一样了:
对于我们来说侵入性并不强,只是需要在dimens.xml中指定下尺寸而已。
Task代码
并没有什么很复杂的逻辑,大部分都是xml操作。
定义一个Ext,供传入参数
SwDimens {
// 设计稿的宽度
designWidth = 360
// 需要适配的SW值,根据公式计算
// 这个随便测试,适配的是sw-430,sw-432,sw433。
// 实际根据需适配手机情况自行填写
autoGenerateSwList = ['430','432','433']
}
插件代码:
class DimensGenerator implements Plugin<Project> {
@Override
void apply(Project target) {
target.afterEvaluate {
target.android.applicationVariants.all {
variant ->
def processResTask = target.tasks.findByName("process${variant.name.capitalize()}Resources")
def mergeDimensTask = "merge${variant.name.capitalize()}DimensResource"
target.task(mergeDimensTask) {
doLast {
def path = "${target.projectDir}/build/intermediates/res/merged/${variant.name}/"
def dir = new File(path)
//println "#path: ${path}"
dir.eachFile {
file ->
//println "#file ${file.name}"
if (file.name == 'values') {
file.eachFile {
xml ->
def dimensSet = new HashMap()
final def resources = new XmlSlurper().parse(xml)
for (dimensNode in resources.dimen) {
def nodeValue = dimensNode.toString()
def nodeName = dimensNode.@name.toString()
//println "@dimen node name ${nodeName} value=${nodeValue}"
if (nodeValue.startsWith('@')) {
continue
}
dimensSet.put(nodeName, nodeValue)
}
// 生成新的目录
if (!dimens.autoGenerateSwList?.isEmpty()) {
println 'create dir' + dimens.autoGenerateSwList
dimens.autoGenerateSwList.each {
def sw =it
File targetDir = new File(path, "values-sw${it}dp")
// 创建文件夹
if (!targetDir.exists()) {
targetDir.mkdir()
}
// 将尺寸写入values-swXXXdp.xml文件
// 创建资源文件
File targetXml = new File(targetDir, "${targetDir.name}.xml")
if (!targetXml.exists()) {
targetXml.createNewFile()
// 写入根标签
BufferedWriter bf = new BufferedWriter(new FileWriter(targetXml));
bf.writeLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>")
bf.writeLine("<resources>")
bf.writeLine("</resources>")
bf.flush()
bf.close()
}
// 移除重复的资源项
def originResources = new XmlSlurper().parse(targetXml)
// println "#dimens node ${dimensSet.keySet()}"
for(originNode in originResources.dimen){
// println "origin dimen : ${originNode.@name} keyset constain? ${dimensSet.containsKey(originNode.@name.toString())}"
def nodeName = originNode.@name?.toString()
if (dimensSet.keySet().contains(nodeName)){
//println "remove dimen in map: ${nodeName}"
dimensSet.remove(nodeName)
}
}
dimensSet.each {
k, v ->
//println "##### sw ${sw}"
def value = dpStr2dpx(dimens, sw as float, v)
originResources.appendNode {
dimen(name: k, value)
}
}
def output = XmlUtil.serialize(originResources)
BufferedWriter bf = new BufferedWriter(new FileWriter(targetXml));
bf.write(output)
bf.flush()
bf.close()
}
}
}
}
}
}
}
println "depend task: $processResTask.name"
println("variant: ${variant.name.capitalize()}")
// 将Merge资源的Task Hook到APG中的processVariantResource
def dependencies = processResTask.taskDependencies.getDependencies(processResTask)
println "#### dependencies: ${dependencies}"
target.tasks.findByName(mergeDimensTask)
.dependsOn(dependencies)
processResTask.dependsOn target.tasks.findByName(mergeDimensTask)
}
}
}
private static String formatDPXValue(SwDimens dimens, def dp) {
return String.format("%.2f", dp2dpx(dimens, dp))
}
static def dp2dpx(SwDimens dimens,def sw, def dp) {
println "sw ${sw} , dw ${dimens.designWidth}, dp ${dp}"
return (sw / ((float) dimens.designWidth)) * dp
}
static def dp2dpx(SwDimens dimens, def dp) {
return dp2dpx(dimens, dimens.sw, dp)
}
static def dpStr2dpx(SwDimens dimens, def sw, String string) {
def pattern = "^\\d+\\.?\\d+"
Matcher matcher = Pattern.compile(pattern).matcher(string)
if (matcher.find()) {
def dp = matcher.group() as float
// println "find num in ${string} value is ${dp}"
string = string.replaceAll(pattern, formatDPXValue(dimens, dp2dpx(dimens, sw, dp)))
// println "changed string is${string}"
}
return string
}
static class SwDimens {
int designWidth
List<Integer> autoGenerateSwList =[]
}
}