-
问题
刚刚接手项目的时候,发现很多UI布局都是直接写的多少dp,完全没有考虑到适配问题,所以发现app的安装到手机上时,有些手机的ui显示是完全变形了,所以不得不想办法解决屏幕的适配问题,因为android平台碎片化的原因,需要对许多不同的机型进行适配,查了一些资料,整理了一些解决方案
-
解决方案
-
方案1
针对不同的机型,我们创建不同的屏幕尺寸的的values文件夹,然后创建dimens.xml文件。那这样了就能解决针对不同的机型进行适配,但是市面上的机型太多,而且随之后面出现新的分辨率的机型时,我们又要写一套dimens,这样不仅工作量会增大,而且app的体积也会随之增大,并不是一个很好的方案
-
-
方案2
这种方案也就是很多人提到的swdp限定符的方式,即SmllestWidth限定符适配方案
-
什么是SmllestWidth?
移动设备是允许屏幕旋转的,当屏幕旋转时,屏幕的宽高会互换,而使用SmllestWidth方案是不区分屏幕方向的, 它只会把屏幕宽高最小的一方当做最小宽度,这个最小宽度是根据屏幕而定的,是固定不变的。也就是无论你如何旋转, 只需要宽高相比,把小的那一边当做最小宽度
-
-
在SmllestWidth方案中 需要在dimens.xml写入dp值(即将对应的px转换为dp),那么这个dp值是如何计算出来的呢?
首先我们需要直到屏幕的密度density ,可根据 density=dpi/160 获得 dpi是每英寸像素 然后我们就可以根据密度(density),像素(px),计算出对应的dp dp=px/density=px/(dpi/160)
-
举个例子 屏幕的像素 1080*1920 dpi:480
我们会发现最小宽度为1080,它的密度为:480/160=3,而最小的像素为1080px, 所以我们最终算出的dp为:1080/3=360dp,这个360dp就被认为是最小宽度,那么屏幕会适配values-sw360dp里面的尺寸, 若没有该尺寸,它将找小于等于他的离得最近的尺寸,以此来减少适配的误差问题
-
-
不同的values-swdp文件夹下的dimens如何生成的?
-
对于不同尺寸的dimens文件的生成,需要考虑到两个因素:最小宽度基准值是多少/需要适配哪些最小宽度尺寸
-
最小宽度基准值
以某个屏幕尺寸为基准,确定把设备的屏幕分为多少份,然后所有的屏幕都将分为一样的份数,最后根据比例确定每一份的尺寸是多
少,这样就会生成不同对应尺寸的dimens文件。就比如说若以360dp为基准,把屏幕分为360份,每份为1dp,那屏幕的最小宽度为
400dp,也是要将其分为360份,那么每一份的尺寸为:400/360=1.1dp。 -
适配哪些最小宽度
比如你想适配的最小宽度有320dp、360dp、400dp、480dp,那么方案将在res下生成values-sw320dp,values-sw360dp、
values-sw400dp、values-sw480dp这几个资源文件夹。当在不同的尺寸的手机上运行时,将找对应的尺寸资源进行适配,若没有找
到将会找到相近的尺寸资源进行适配,即使有一些误差,但也不会差距很大,但是每生成对应的一种尺寸,都会占用一定的app的体
积,所以要合理的分配想要适配的尺寸
-
-
优缺点
-
优点
* 适配率高,极低概率出现意外 * 不会有任何性能损耗 * 适配范围可自由控制,不会影响其他第三方库 * 学习成本低
-
缺点
* 维护成本比较高 * 侵入性高,每一个布局中都会引入dimen,所以后续切换其他方案时成本非常高 * 无法覆盖全部机型,若需要全部适配,将以增大app的体积为代价
-
-
使用代码生成对应问文件和尺寸资源
-
DimenTypes 使用DimenTypes确认要生成那些尺寸
public enum DimenTypes {
//适配Android 3.2以上 大部分手机的sw值集中在 300-460之间 DP_sw__300(300), // values-sw300 DP_sw__310(310), DP_sw__320(320), DP_sw__330(330), DP_sw__340(340), DP_sw__350(350), DP_sw__360(360), DP_sw__370(370), DP_sw__380(380), DP_sw__390(390), DP_sw__410(410), DP_sw__420(420), DP_sw__430(430), DP_sw__440(440), DP_sw__450(450), DP_sw__460(460), DP_sw__470(470), DP_sw__480(480), DP_sw__490(490), DP_sw__400(400); // 想生成多少自己以此类推 /** * 屏幕最小宽度 */ private int swWidthDp; DimenTypes(int swWidthDp) { this.swWidthDp = swWidthDp; } public int getSwWidthDp() { return swWidthDp; } public void setSwWidthDp(int swWidthDp) { this.swWidthDp = swWidthDp; }
}
-
使用MakeUtils生成values文件夹以及dimens文件
public class MakeUtils { private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"; private static final String XML_RESOURCE_START = "<resources>\r\n"; private static final String XML_RESOURCE_END = "</resources>\r\n"; private static final String XML_DIMEN_TEMPLETE = "<dimen name=\"qb_%1$spx_%2$d\">%3$.2fdp</dimen>\r\n"; private static final String XML_BASE_DPI = "<dimen name=\"base_dpi\">%ddp</dimen>\r\n"; private static final int MAX_SIZE = 720; /** * 生成的文件名 */ private static final String XML_NAME = "dimens.xml"; public static float px2dip(float pxValue, int sw,int designWidth) { float dpValue = (pxValue/(float)designWidth) * sw; BigDecimal bigDecimal = new BigDecimal(dpValue); float finDp = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP).floatValue(); return finDp; } /** * 生成所有的尺寸数据 * * @param type * @return */ private static String makeAllDimens(DimenTypes type, int designWidth) { float dpValue; String temp; StringBuilder sb = new StringBuilder(); try { sb.append(XML_HEADER); sb.append(XML_RESOURCE_START); //备份生成的相关信息 temp = String.format(XML_BASE_DPI, type.getSwWidthDp()); sb.append(temp); for (int i = 0; i <= MAX_SIZE; i++) { dpValue = px2dip((float) i,type.getSwWidthDp(),designWidth); temp = String.format(XML_DIMEN_TEMPLETE,"", i, dpValue); sb.append(temp); } sb.append(XML_RESOURCE_END); } catch (Exception e) { e.printStackTrace(); } return sb.toString(); } /** * 生成的目标文件夹 * 只需传宽进来就行 * * @param type 枚举类型 * @param buildDir 生成的目标文件夹 */ public static void makeAll(int designWidth, DimenTypes type, String buildDir) { try { //生成规则 final String folderName; if (type.getSwWidthDp() > 0) { //适配Android 3.2+ folderName = "values-sw" + type.getSwWidthDp() + "dp"; }else { return; } //生成目标目录 File file = new File(buildDir + File.separator + folderName); if (!file.exists()) { file.mkdirs(); } //生成values文件 FileOutputStream fos = new FileOutputStream(file.getAbsolutePath() + File.separator + XML_NAME); fos.write(makeAllDimens(type,designWidth).getBytes()); fos.flush(); fos.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
}
-
客户端调用
public class DimenGenerator { /** * 设计稿尺寸(将自己设计师的设计稿的宽度填入) */ private static final int DESIGN_WIDTH = 375; /** * 设计稿的高度 (将自己设计师的设计稿的高度填入) */ private static final int DESIGN_HEIGHT = 667; public static void main(String[] args) { int smallest = DESIGN_WIDTH>DESIGN_HEIGHT? DESIGN_HEIGHT:DESIGN_WIDTH; // 求得最小宽度 DimenTypes[] values = DimenTypes.values(); for (DimenTypes value : values) { File file = new File(""); MakeUtils.makeAll(smallest, value, file.getAbsolutePath()); } } }
-
运行后生成的文件
-
-
方案3
-
该方案是根据今日头条的适配方案而来的,即今日头条适配方案,该方案已经开源,可直接依赖使用
-
方案的使用( https://github.com/JessYanCoding/AndroidAutoSiz)
-
1.添加依赖库
implementation 'me.jessyan:autosize:1.1.2'
-
-
2.在清单文件中注册设计图的宽高尺寸(dp)
<manifest> <application> <meta-data android:name="design_width_in_dp" android:value="360"/> <meta-data android:name="design_height_in_dp" android:value="640"/> </application> </manifest>
-
3.根据对应的屏幕尺寸在对应的布局中布局控件
-
4.若某个Activity的设计尺寸与清单文件中的尺寸不一样时,可以实现CustomAdapt接口,指定特定的屏幕尺寸(dp)
public class CustomAdaptActivity : AppCompatActivity , CustomAdapt { //是否以屏幕宽度为适配基准 @Override public boolean isBaseOnWidth() { return false; } //适配的尺寸是多少(dp) @Override public float getSizeInDp() { return 667; } }
-
5.若某个Activity不想适配,可以实现CancelAdapt接口
public class CancelAdaptActivity : AppCompatActivity , CancelAdapt { }
-
6.可以在 pt、in、mm 这三个冷门单位中,选择一个作为副单位,副单位是用于规避修改 DisplayMetrics#density 所造成的对于其他使用 dp 布局的系统控件或三方库控件的不良影响,使用副单位后可直接填写设计图上的像素尺寸,不需要再将像素转化为 dp
AutoSizeConfig.getInstance().getUnitsManager() .setSupportDP(false) .setSupportSP(false) .setSupportSubunits(Subunits.MM);
-
代码混淆
-keep class me.jessyan.autosize.** { *; } -keep interface me.jessyan.autosize.** { *; }
-
-
优缺点
-
优点
* 使用成本低,操作简单,无需在布局时添加额外的操作和代码 * 侵入性低,该方案和项目完全解耦 * 由于修改的density在整个项目是全局的,所以只要一次修改,项目中所有地方都会受益,包含第三方控件和系统控件 * 没有任何性能的损耗
-
缺点
* 因只修改一次density,项目中所有的地方都会自动适配,所以也会作用于第三方控件,但是若第三方控件的尺寸和当前项目的设计尺寸差距非常大时,那么适配会出现很严重的问题,若需要解决,则可以取消当前Activity的适配效果,采用其他适配方案
-
-
原理解析
* px:即像素,1px代表屏幕上一个物理像素点 * dp(dip):设备无关像素,只要针对控件尺寸大小 dp=px/density * sp:与缩放无关的抽象像素,类似于dp,但它主要针对文字尺寸大小 * density:屏幕密度,每一个设备的density都是固定的 density=dpi/160
-
适配方案最核心的思想是:根据公式算出density
dp=px/density density=dpi/160 public static float applyDimension(int unit, float value, DisplayMetrics metrics) { switch (unit) { case COMPLEX_UNIT_PX://单位为px 直接返回value return value; case COMPLEX_UNIT_DIP://单位为dip px=value*metrics.desity px=dp*density return value * metrics.density; case COMPLEX_UNIT_SP://单位为sp px=sp*metrics.scaledDensity 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; }
-
根据源码可知,无论使用任何单位,最终都会转化为px