[Android] 使用Gradle Plugin实现自动化屏幕适配

屏幕适配的必要性

  1. 处理虚拟尺寸和设计稿之间的比例,如设计稿为375 x 667,我们需要将这个尺寸缩放到我们的虚拟尺寸上。
  2. 处理个别手机虚拟尺寸不同的问题,让虚拟尺寸不同的手机显示相同的视觉效果。

何为虚拟尺寸

我们采用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 =[]
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值