-
Android AutoLayout全新的适配方式 堪称适配终结者
2015-11-23 09:27:03转载请标明出处: ... 本文出自:【张鸿洋的博客】 一、概述相信Android的开发者对于设配问题都比较苦恼,Google官方虽然给出了一系列的建议,但是...个人也比较关注适配的问题,之前也发了几篇关于适配的文章,大致有:转载请标明出处:
http://blog.csdn.net/lmj623565791/article/details/49990941;
本文出自:【张鸿洋的博客】一、概述
相信Android的开发者对于设配问题都比较苦恼,Google官方虽然给出了一系列的建议,但是想要单纯使用这些建议将设备很轻松的做好,还是相当困难的。个人也比较关注适配的问题,之前也发了几篇关于适配的文章,大致有:
ok,我大致说一下,没看过的先看完这篇,再考虑看不看以上几篇,本篇的灵感是来自以上几篇,但是适配的方便程度、以及效果远比上面几篇效果要好。
既然灵感来源于上述几篇,就大体介绍下:
第一篇:主要是根据设计图的尺寸,然后将设计图上标识的px尺寸,转化为百分比,为所有的主流屏幕去生成对应百分比的值,每个尺寸都会有一个values文件夹。存在一些问题:产生大量的文件夹,适配不了特殊的尺寸(必须建立默认的文件夹)
第二篇和第三篇:这两篇属于一样的了,主要是基于Google推出的百分比布局,已经很大程度解决了适配的问题。存在一些问题:使用起来比较反人类,因为设计图上标识的都是px,所以需要去计算百分比,然后这个百分比还是依赖父容器的,设计图可能并不会将每个父容器的尺寸都标识出来,所有很难使用(当然,有人已经采用自动化的工具去计算了)。还有个问题就是,因为依赖于父容器,导致ScrollView,ListView等容器内高度无法使用百分比。
可以看到都存在一些问题,或多或少都需要进行一些额外的工作,然而我希望适配是这样的:
- 拿到设计图,meta信息中填入设计图的尺寸,然后不需要额外计算,布局直接抄设计图上的尺寸,不产生任何多余的资源文件,完成各种分辨率的适配!
二、直观的体验
假设我们拿到一张设计图:
这样的设计图开发中很常见吧,有些公司可能需要自己去测量。
按照我们的思想:
布局直接抄设计图上的尺寸
对于,
新增旅客
我们的布局文库应该这么写:<RelativeLayout android:layout_width="match_parent" android:layout_height="86px" android:layout_marginTop="26px" android:background="#ffffffff"> <ImageView android:id="@+id/id_tv_add" android:layout_width="34px" android:layout_height="34px" android:layout_gravity="center_vertical" android:layout_marginLeft="276px" android:layout_marginTop="26px" android:src="@mipmap/add" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="26px" android:layout_toRightOf="@id/id_tv_add" android:text="新增旅客" android:textColor="#1fb6c4" android:textSize="32px" /> </RelativeLayout>
来张组合图,感受一下:
感受完了,想一想,按照这种方式去写布局你说爽不爽。
ok,那么对于Item的布局文件,就是这么写:
<RelativeLayout android:layout_width="match_parent" android:layout_height="108px" android:layout_marginTop="26px" android:background="#ffffffff" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="22px" android:layout_marginTop="16px" android:text="王大炮 WANG.DAPAO" android:textColor="#333" android:textSize="28px" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_marginBottom="16px" android:layout_marginLeft="22px" android:text="护照:G50786449" android:textColor="#999" android:textSize="26px" /> </RelativeLayout>
看到这,我相信,你现在最大的疑问就是:你用的px,px能完成适配?搞笑吧?
那么首先说一下:这个px并不代表1像素,我在内部会进行百分比化处理,也就是说:720px高度的屏幕,你这里填写72px,占据10%;当这个布局文件运行在任何分辨率的手机上,这个72px都代表10%的高度,这就是本库适配的原理。
接下来:看下不同分辨率下的效果:
768*1280,Andriod 4.4.4
480*800,Android 2.3.7
上述两个机器的分辨率差距相当大了,按照百分比的规则,完美实现了适配,最为重要的是:
- 再也不用拿着设计稿去想这控件的宽高到底取多少dp
- 再也不用去为多个屏幕去写多个dimens
- 再也不用去计算百分比了(如果使用百分比控件完成适配)
- 再也不用去跟UI MM去解释什么是dp了
接下来说下用法。
本库的地址:https://github.com/hongyangAndroid/AndroidAutoLayout
三、用法
用法
(1)注册设计图尺寸
将autolayout引入
dependencies { compile project(':autolayout') }
对于eclipse的伙伴,只有去copy源码了~~
在你的项目的AndroidManifest中注明你的
设计稿
的尺寸。<meta-data android:name="design_width" android:value="768"></meta-data> <meta-data android:name="design_height" android:value="1280"></meta-data>
(2)Activity中开启设配
- 让你的Activity去继承
AutoLayoutActivity
ok,非常简单的两部即可引入项目,然后,然后干嘛?
然后就按照上个章节的编写方式开始玩耍吧~
ok,上面是最简单的用法,当然你也可以不去继承
AutoLayoutActivity
来使用。AutoLayoutActivity
的用法实际上是完成了一件事:- LinearLayout -> AutoLinearLayout
- RelativeLayout -> AutoRelativeLayout
- FrameLayout -> AutoFrameLayout
如果你不想继承
AutoLayoutActivity
,那么你就得像Google的百分比库一样,去用AutoXXXLayout代替系统原有的XXXLayout。当然,你可以放心的是,所有的系统属性原有的属性都会支持,不过根布局上就不支持px的自动百分比化了,但是一般根布局都是MATCH_PARENT,而上述的方式,根布局也是可以直接px的百分比化的。四、注意事项
(1)如何开启PreView
大家都知道,写布局文件的时候,不能实时的去预览效果,那么体验真的是非常的不好,也在很大程度上降低开发效率,所以下面教大家如何用好,用对PreView(针对该库)。
首先,你要记得你设计稿的尺寸,比如
768 * 1280
然后在你的PreView面板,选择分辨率一致的设备:
然后你就可以看到
最为精确的
预览了:两个注意事项:
- 你们UI给的设计图的尺寸并非是主流的设计图,该尺寸没找到,你可以
拿显示器砸他自己去新建一个设备。 - 不要在PreView中去查看所有分辨率下的显示,是看不出来适配效果的,因为有些计算是动态的。
(2)关于TextView
TextView这个控件呢,可能和设计稿上会有一些出入,并非是此库的原因,而是与生俱来的特性。
比如:
<TextView textSize="32px" layout_height="wrap_contnt" />
你去运行肯定不是32px的高度,文字的上下方都会有一定的空隙。如何你将高度写死,也会发现文字显示不全。
恩,所以呢,灵活应对这个问题,对于存在字体标识很精确的值,你可以选择:对于TextView与其他控件的上下边距呢,尽可能的稍微写小一点。
其实我上面的例子,几乎都是TextView,所有我在编写Item里面的时候,也有意缩小了一下marginTop值等。不过,对于其他控件是不存在这样的问题的。
ps:因为TextView的上述问题:所以对于居中,虽然可以使用本库通过编写
margin_left
,margin_top
等很轻松的完成居中。但是为了精确起见,还是建议使用gravity
,centerInXXX
等属性完成。(3) 指定设置的值参考宽度或者高度
由于该库的特点,布局文件中宽高上的1px是不相等的,于是如果需要宽高保持一致的情况,布局中使用属性:
app:layout_auto_basewidth="height"
,代表height上编写的像素值参考宽度。app:layout_auto_baseheight="width"
,代表width上编写的像素值参考高度。如果需要指定多个值参考宽度即:
app:layout_auto_basewidth="height|padding"
用|隔开,类似gravity的用法,取值为:
- width,height
- margin,marginLeft,marginTop,marginRight,marginBottom
- padding,paddingLeft,paddingTop,paddingRight,paddingBottom
- textSize.
(4)将状态栏区域作为内容区域
如果某个Activity需要将状态栏区域作为实际的内容区域时,那么可用高度会变大,你所要做的只有一件事:让这个Activity实现
UseStatusBar
接口(仅仅作为标识左右,不需要实现任何方法),当然你肯定要自己开启windowTranslucentStatus
或者设置FLAG_TRANSLUCENT_STATUS
。注意:仅仅是改变状态栏颜色,并不需要实现此接口,因为并没有实际上增加可用高度。
五、其他
目前支持属性
- layout_width
- layout_height
- layout_margin(left,top,right,bottom)
- pading(left,top,right,bottom)
- textSize
- 不会影响系统所有的其他属性,以及不会影响dp,sp的使用
性能的提升
通过本库的方式去编写代码,可以在很大程序上使用
margin
,也就是说,对于View的位置非常好控制,从而能够减少非常多的嵌套,甚至任何一个复杂的界面做到无嵌套。以及,几乎不需要去使用RelativeLayout的规则了,比如rightOf,完全可以由
marginLeft
完成,其他的rule同理。对于LinearLayout的weight,几乎也不需要使用了,比如屏幕宽度720px,想要四个控件横向均分,完全可以写
layout_width="180px"
我相信通过上述的介绍,你已经了解的本库适配的做法,而且可以说是我见过的最方便的适配方案,最大化的减轻了适配的负担,甚至比你不适配时编写UI都方便。目前本库,已经尝试用于项目中,尽可能去发现一些潜在的问题。
本库的地址:https://github.com/hongyangAndroid/AndroidAutoLayout,欢迎各位一起完善,让
适配问题消失在我们的痛苦中。ok,最后,只有去体验了,才能发现优点和缺点~~
ps:对于使用,尽可能参考github上的readme,博文没办法做到一直修改适应新的变化,使用版本也尽可能使用github上提供的最新版本。
欢迎关注我的微博:
http://weibo.com/u/3165018720群号:514447580,欢迎入群
微信公众号:hongyangAndroid
(欢迎关注,第一时间推送博文信息) -
iOS14适配汇总:【1、隐私权限相关:定位、相册、IDFA 2、KVC相关: UIPageControl的pageImage3、UIView...
2020-09-19 16:46:02文章目录前言I、权限相关的适配II、KVC相关的适配III、UIView相关的适配 前言 I、权限相关的适配 iOS14 隐私适配:【定位授权新增了精确和模糊定位 可根据不同的需求设置不同的定位精确度】向用户申请临时开启一次...文章目录
前言
I、隐私权限相关的适配
iOS14相册权限适配 :Limited Photo Access模式、PHAccessLevel(请求查询limited权限在 accessLevel 为readAndWrite 生效)、图片选择器
-
iOS14 适配:【IDFA Identity for Advertisers 广告标识符】(请求用户授权获取到正确信息)
-
【 读取用户剪切板数据会弹出提示】 查找哪些SDK使用了剪切板,及时升级SDK。比如发现了JCore iOS SDK在iOS 14引用剪贴板,该行为导致APP被用户怀疑隐私泄露,请予以重视
II、KVC相关的适配
III、UIView相关的适配
iOS14适配:【解决iOS14下pop多层控制器至首页时,tabbar不显示问题】方案1:重写pushViewController;方案2: hook hidesBottomBarWhenPush
因此问题涉及的是添加子视图cell.addSubView方法,因此与之对应的方法(UITableViewCell *)[SubView superview] 和cell.subviews方法 都要注意谨慎使用和处理
IV、第三方框架相关
4.1 QMUIKit在iOS14 下首次唤起键盘卡住主线程]
Main Thread Checker: UI API called on a background thread: -[UIWindow windowScene]
================================================================= Main Thread Checker: UI API called on a background thread: -[UIWindow windowScene] PID: 580, TID: 21138, Thread name: (none), Queue name: com.apple.root.user-initiated-qos, QoS: 25 Backtrace: 4 retail 0x000000010576b628 __62+[UIWindow(QMUIUserInterfaceStyleWillChangeNotification) load]_block_invoke_3 + 296 Main Thread Checker: UI API called on a background thread: -[UIWindow traitCollection] PID: 509, TID: 22376, Thread name: (none), Queue name: com.apple.root.user-initiated-qos, QoS: 25 Backtrace: 4 Housekeeper 0x0000000100f3c000 __62+[UIWindow(QMUIUserInterfaceStyleWillChangeNotification) load]_block_invoke_3 + 92
- 解决方案:如果你没使用QMUITheme,就直接注释掉代码即可。
@implementation UIWindow (QMUIUserInterfaceStyleWillChangeNotification) #ifdef IOS13_SDK_ALLOWED + (void)load { return ; }
如果你使用QMUITheme,则及时你更新4.2.1版本也无法根本性解决
这是因为系统自己在子线程访问了这些方法,只是 Main Thread Checker 对其做了兼容,发现 App 自己修改了这些方法的实现,才报错,没修改则不报错。
检测方式可以打条件符号断点,然后把 QMUI 那段代码注释掉,运行起来后会发现依然能命中这个断点,说明系统自身确实是在子线程访问了(UIKit 这种行为特别多,不只是这里)。
所以从原理上看,QMUI 命中这个主线程检测是不可避免的,目前只是做了一些优化,只有真正使用了 QMUITheme 组件时才会出现这个情况,没使用的时候就不会命中,以减少一部分的出错场景。这个优化将会跟随 4.2.1 版本发布。V、网络相关
iOS14 开启 encrypted DNS 提高安全性,防止DNS 劫持
VI、Mac Catalyst 相关
本来iOS不越狱的情况下是没权限看沙盒文件,这下到好了,macOS倒是打开了大门
5.1 判断App是运行在Mac还是iPhone上
- 使用iOS14 新增的API进行判断
+ (void)deviceisMac{ BOOL isMac = false; if (@available(iOS 14.0, *)) { isMac = NSProcessInfo.processInfo.isiOSAppOnMac; } NSLog(@"App %s on Mac:",isMac ? "":"not");// }
- 获取设备型号进行判断App是运行在Mac还是iPhone上
/** #import <sys/utsname.h> 获取设备型号 应用场景:判断App是运行在Mac还是iPhone上 "MacBookAir10,1": "MacBook Air (M1, 2020)", "MacBookPro17,1": "MacBook Pro (13-inch, M1, 2020)", "Macmini9,1": "Mac mini (M1, 2020)", */ + (void)deviceModel { struct utsname systemInfo; uname(&systemInfo); NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; NSLog(@"deviceModel:%@", deviceModel);//iPhone8,1 }
-
-
Android 屏幕适配方案
2015-05-04 13:08:471、概述大家在Android开发时,肯定会觉得屏幕适配是个尤其痛苦的事,各种屏幕尺寸适配起来蛋疼无比。如果我们换个角度我们看下这个问题,不知道大家有没有了解过web前端开发,或者说大家对于网页都不陌生吧,其实...转载请标明出处:
http://blog.csdn.net/lmj623565791/article/details/45460089;
本文出自:【张鸿洋的博客】1、概述
大家在Android开发时,肯定会觉得屏幕适配是个尤其痛苦的事,各种屏幕尺寸适配起来蛋疼无比。如果我们换个角度我们看下这个问题,不知道大家有没有了解过web前端开发,或者说大家对于网页都不陌生吧,其实适配的问题在web页面的设计中理论上也存在,为什么这么说呢?电脑的显示器的分辨率、包括手机分辨率,我敢说分辨率的种类远超过Android设备的分辨率,那么有一个很奇怪的现象:
为什么Web页面设计人员从来没有说过,尼玛适配好麻烦?
那么,到底是什么原因,让网页的设计可以在千差万别的分辨率的分辨率中依旧能给用户一个优质的体验呢?带着这个疑惑,我问了下媳妇(前端人员),媳妇睁大眼睛问我:什么叫适配?fc,尼玛,看来的确没有这类问题。后来再我仔细的追问后,她告诉我,噢,这个尺寸呀,我都是设置为20%的~~追根到底,其实就是一个原因,网页提供了百分比计算大小。
同样的,大家拿到UI给的设计图以后,是不是抱怨过尼玛你标识的都是px,我项目里面用dp,这什么玩意,和UI人员解释,UI妹妹也不理解。那么本例同样可以解决Android工程师和UI妹妹间的矛盾~UI给出一个固定尺寸的设计稿,然后你在编写布局的时候不用思考,无脑照抄上面标识的像素值,就能达到完美适配,理想丰不丰满~~。
然而,Android对于不同的屏幕给出的适配方案是dp,那么dp与百分比的差距到底在哪里?
2、dp vs 百分比
- dp
我们首先看下dp的定义:
Density-independent pixel (dp)独立像素密度。标准是160dip.即1dp对应1个pixel,计算公式如:px = dp * (dpi / 160),屏幕密度越大,1dp对应 的像素点越多。
上面的公式中有个dpi,dpi为DPI是Dots Per Inch(每英寸所打印的点数),也就是当设备的dpi为160的时候1px=1dp;好了,上述这些概念记不记得住没关系,只要记住一点dp是与像素无关的,在实际使用中1dp大约等于1/160inch。
那么dp究竟解决了适配上的什么问题?可以看出1dp = 1/160inch;那么它至少能解决一个问题,就是你在布局文件写某个View的宽和高为160dp*160dp,这个View在任何分辨率的屏幕中,显示的尺寸大小是大约是一致的(可能不精确),大概是 1 inch * 1 inch。
但是,这样并不能够解决所有的适配问题:
- 呈现效果仍旧会有差异,仅仅是相近而已
- 当设备的物理尺寸存在差异的时候,dp就显得无能为力了。为4.3寸屏幕准备的UI,运行在5.0寸的屏幕上,很可能在右侧和下侧存在大量的空白。而5.0寸的UI运行到4.3寸的设备上,很可能显示不下。
以上两点,来自参考链接1
一句话,总结下,dp能够让同一数值在不同的分辨率展示出大致相同的尺寸大小。但是当设备的尺寸差异较大的时候,就无能为力了。适配的问题还需要我们自己去做,于是我们可能会这么做:
<?xml version="1.0" encoding="utf-8"?> <resources> <!-- values-hdpi 480X800 --> <dimen name="imagewidth">120dip</dimen> </resources> <resources> <!-- values-hdpi-1280x800 --> <dimen name="imagewidth">220dip</dimen> </resources> <?xml version="1.0" encoding="utf-8"?> <resources> <!-- values-hdpi 480X320 --> <dimen name="imagewidth">80dip</dimen> </resources>
上述代码片段来自网络,也就是说,我们为了优质的用户体验,依然需要去针对不同的dpi设置,编写多套数值文件。
可以看出,dp并没有能解决适配问题。下面看百分比。
- 百分比
这个概念不用说了,web中支持控件的宽度可以去参考父控件的宽度去设置百分比,最外层控件的宽度参考屏幕尺寸设置百分比,那么其实中Android设备中,只需要支持控件能够参考屏幕的百分比去计算宽高就足够了。
比如,我现在以下几个需求:
- 对于图片展示的Banner,为了起到该有的效果,我希望在任何手机上显示的高度为屏幕高度的1/4
- 我的首页分上下两栏,我希望每个栏目的屏幕高度为11/24,中间间隔为1/12
- slidingmenu的宽度为屏幕宽度的80%
当然了这仅仅是从一个大的层面上来说,其实小范围布局,可能百分比将会更加有用。
那么现在不支持百分比,实现上述的需求,可能需要1、代码去动态计算(很多人直接pass了,太麻烦);2、利用weight(weight必须依赖Linearlayout,而且并不能适用于任何场景)
再比如:我的某个浮动按钮的高度和宽度希望是屏幕高度的1/12,我的某个Button的宽度希望是屏幕宽度的1/3。
上述的所有的需求,利用dp是无法完成的,我们希望控件的尺寸可以按照下列方式编写:
<Button android:text="@string/hello_world" android:layout_width="20%w" android:layout_height="10%h"/>
利用屏幕的宽和高的比例去定义View的宽和高。
好了,到此我们可以看到dp与百分比的区别,而百分比能够更好的解决我们的适配问题。
- some 适配tips
我们再来看看一些适配的tips
- 多用match_parent
- 多用weight
- 自定义view解决
其实上述3点tip,归根结底还是利用百分比,match_parent相当于100%参考父控件;weight即按比例分配;自定义view无非是因为里面多数尺寸是按照百分比计算的;
通过这些tips,我们更加的看出如果能在Android中引入百分比的机制,将能解决大多数的适配问题,下面我们就来看看如何能够让Android支持百分比的概念。
3、百分比的引入
1、引入
其实我们的解决方案,就是在项目中针对你所需要适配的手机屏幕的分辨率各自简历一个文件夹。
如下图:
然后我们根据一个基准,为基准的意思就是:
比如480*320的分辨率为基准
- 宽度为320,将任何分辨率的宽度分为320份,取值为x1-x320
- 高度为480,将任何分辨率的高度分为480份,取值为y1-y480
例如对于800*480的宽度480:
可以看到x1 = 480 / 基准 = 480 / 320 = 1.5 ;
其他分辨率类似~~
你可能会问,这么多文件,难道我们要手算,然后自己编写?不要怕,下文会说。那么,你可能有个疑问,这么写有什么好处呢?
假设我现在需要在屏幕中心有个按钮,宽度和高度为我们屏幕宽度的1/2,我可以怎么编写布局文件呢?
<FrameLayout > <Button android:layout_gravity="center" android:gravity="center" android:text="@string/hello_world" android:layout_width="@dimen/x160" android:layout_height="@dimen/x160"/> </FrameLayout>
可以看到我们的宽度和高度定义为x160,其实就是宽度的50%;
那么效果图:可以看到不论在什么分辨率的机型,我们的按钮的宽和高始终是屏幕宽度的一半。
- 对于设计图
假设现在的UI的设计图是按照480*320设计的,且上面的宽和高的标识都是px的值,你可以直接将px转化为x[1-320],y[1-480],这样写出的布局基本就可以全分辨率适配了。
你可能会问:设计师设计图的分辨率不固定怎么办?下文会说~
- 对于上文提出的几个dp做不到的
你可以通过在引入百分比后,自己试试~~
好了,有个最主要的问题,我们没有说,就是分辨率这么多,尼玛难道我们要自己计算,然后手写?
2、自动生成工具
好了,其实这样的文件夹手写也可以,按照你们需要支持的分辨率,然后编写一套,以后一直使用。
当然了,作为程序员的我们,怎么能做这么low的工作,肯定要程序来实现:
那么实现需要以下步骤:
- 分析需要的支持的分辨率
对于主流的分辨率我已经集成到了我们的程序中,当然对于特殊的,你可以通过参数指定。关于屏幕分辨率信息,可以通过该网站查询:http://screensiz.es/phone
- 编写自动生成文件的程序
代码如下
import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.PrintWriter; /** * Created by zhy on 15/5/3. */ public class GenerateValueFiles { private int baseW; private int baseH; private String dirStr = "./res"; private final static String WTemplate = "<dimen name=\"x{0}\">{1}px</dimen>\n"; private final static String HTemplate = "<dimen name=\"y{0}\">{1}px</dimen>\n"; /** * {0}-HEIGHT */ private final static String VALUE_TEMPLATE = "values-{0}x{1}"; private static final String SUPPORT_DIMESION = "320,480;480,800;480,854;540,960;600,1024;720,1184;720,1196;720,1280;768,1024;800,1280;1080,1812;1080,1920;1440,2560;"; private String supportStr = SUPPORT_DIMESION; public GenerateValueFiles(int baseX, int baseY, String supportStr) { this.baseW = baseX; this.baseH = baseY; if (!this.supportStr.contains(baseX + "," + baseY)) { this.supportStr += baseX + "," + baseY + ";"; } this.supportStr += validateInput(supportStr); System.out.println(supportStr); File dir = new File(dirStr); if (!dir.exists()) { dir.mkdir(); } System.out.println(dir.getAbsoluteFile()); } /** * @param supportStr * w,h_...w,h; * @return */ private String validateInput(String supportStr) { StringBuffer sb = new StringBuffer(); String[] vals = supportStr.split("_"); int w = -1; int h = -1; String[] wh; for (String val : vals) { try { if (val == null || val.trim().length() == 0) continue; wh = val.split(","); w = Integer.parseInt(wh[0]); h = Integer.parseInt(wh[1]); } catch (Exception e) { System.out.println("skip invalidate params : w,h = " + val); continue; } sb.append(w + "," + h + ";"); } return sb.toString(); } public void generate() { String[] vals = supportStr.split(";"); for (String val : vals) { String[] wh = val.split(","); generateXmlFile(Integer.parseInt(wh[0]), Integer.parseInt(wh[1])); } } private void generateXmlFile(int w, int h) { StringBuffer sbForWidth = new StringBuffer(); sbForWidth.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); sbForWidth.append("<resources>"); float cellw = w * 1.0f / baseW; System.out.println("width : " + w + "," + baseW + "," + cellw); for (int i = 1; i < baseW; i++) { sbForWidth.append(WTemplate.replace("{0}", i + "").replace("{1}", change(cellw * i) + "")); } sbForWidth.append(WTemplate.replace("{0}", baseW + "").replace("{1}", w + "")); sbForWidth.append("</resources>"); StringBuffer sbForHeight = new StringBuffer(); sbForHeight.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); sbForHeight.append("<resources>"); float cellh = h *1.0f/ baseH; System.out.println("height : "+ h + "," + baseH + "," + cellh); for (int i = 1; i < baseH; i++) { sbForHeight.append(HTemplate.replace("{0}", i + "").replace("{1}", change(cellh * i) + "")); } sbForHeight.append(HTemplate.replace("{0}", baseH + "").replace("{1}", h + "")); sbForHeight.append("</resources>"); File fileDir = new File(dirStr + File.separator + VALUE_TEMPLATE.replace("{0}", h + "")// .replace("{1}", w + "")); fileDir.mkdir(); File layxFile = new File(fileDir.getAbsolutePath(), "lay_x.xml"); File layyFile = new File(fileDir.getAbsolutePath(), "lay_y.xml"); try { PrintWriter pw = new PrintWriter(new FileOutputStream(layxFile)); pw.print(sbForWidth.toString()); pw.close(); pw = new PrintWriter(new FileOutputStream(layyFile)); pw.print(sbForHeight.toString()); pw.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } } public static float change(float a) { int temp = (int) (a * 100); return temp / 100f; } public static void main(String[] args) { int baseW = 320; int baseH = 400; String addition = ""; try { if (args.length >= 3) { baseW = Integer.parseInt(args[0]); baseH = Integer.parseInt(args[1]); addition = args[2]; } else if (args.length >= 2) { baseW = Integer.parseInt(args[0]); baseH = Integer.parseInt(args[1]); } else if (args.length >= 1) { addition = args[0]; } } catch (NumberFormatException e) { System.err .println("right input params : java -jar xxx.jar width height w,h_w,h_..._w,h;"); e.printStackTrace(); System.exit(-1); } new GenerateValueFiles(baseW, baseH, addition).generate(); } }
同时我提供了jar包,默认情况下,双击即可生成,使用说明:
下载地址见文末,内置了常用的分辨率,默认基准为480*320,当然对于特殊需求,通过命令行指定即可:
例如:基准 1280 * 800 ,额外支持尺寸:1152 * 735;4500 * 3200;
按照
java -jar xx.jar width height width,height_width,height
上述格式即可。
到此,我们通过编写一个工具,根据某基准尺寸,生成所有需要适配分辨率的values文件,做到了编写布局文件时,可以参考屏幕的分辨率;在UI给出的设计图,可以快速的按照其标识的px单位进行编写布局。基本解决了适配的问题。
本方案思想已经有公司投入使用,个人认为还是很不错的,如果大家有更好的方案来解决屏幕适配的问题,欢迎留言探讨或者直接贴出好文链接,大家可以将自己的经验进行分享,这样才能壮大我们的队伍~~。
注:本方案思想来自Android Day Day Up 一群的【blue-深圳】,经其同意编写此文,上述程序也很大程度上借鉴了其分享的源码。在此标识感谢,预祝其创业成功!
===>后期更新
Google已经添加了百分比支持库,详情请看:Android 百分比布局库(percent-support-lib) 解析与扩展
ok~
群号:463081660,欢迎入群
微信公众号:hongyangAndroid
(欢迎关注,第一时间推送博文信息)参考链接
-
iOS14适配【 采用hook全局性地解决UITableViewCell兼容问题】往cell添加子视图的方式不规范,导致...
2020-09-18 19:10:02今天升级最新IDE Xcode,准备适配iOS14 API,结果发现app首页的cell中按钮也无法点击了。 I、问题分析 iOS14 UITableViewCell的子试图不能点击或者滑动等手势响应问题,发现有问题的cell基本都是直接 cell....文章目录
前言
今天升级最新IDE Xcode,准备适配iOS14 API,结果发现app首页的cell中按钮也无法点击了。
I、问题分析
iOS14 UITableViewCell的子试图不能点击或者滑动等手势响应问题,发现有问题的cell基本都是直接
cell.addSubView(tempView1)
这种方式添加的,通过Xcode自带的DebugViewHierarchy视图分析发现问题的原因是:
被系统自带的UITableViewCellContentView遮挡在底部了
所以需要改规范的做法
cell.contentView.addSubView(tempView1)
温馨提示:如果你用旧版的Xcode打包,而非使用Xcode12以上版本编译打包的话,是不会有问题。一旦你使用了Xcode12打包,就会出现此问题。(
但是苹果迟早会限制高于Xcode12才可以上传appstore,所以一旦使用了不规范的代码,早晚都要面临这个问题
)1.0 其他分析视图层级的方法:私有API _printHierarchy 和recursiveDescription
关于视图层级分析你也可以使用私有API
_printHierarchy
和recursiveDescription 在
lldb 窗口进行分析:例如先打印VC层级
(lldb) po [[[UIWindow keyWindow] rootViewController] _printHierarchy]
再使用目标View的地址进行
recursiveDescription
打印子视图的层级。po [0x10ff5e5e0 recursiveDescription]
(lldb) po [0x10ff5e5e0 recursiveDescription] <UITableViewCell: 0x10ff5e5e0; frame = (0 767.5; 375 120); hidden = YES; autoresize = W; layer = <CAGradientLayer: 0x280b80860>> | <_UISystemBackgroundView: 0x10fe2d170; frame = (0 0; 375 120); layer = <CAGradientLayer: 0x280c58500>; configuration = <UIBackgroundConfiguration: 0x283aa54a0; Base Style = List Grouped Cell; backgroundColor = <UIDynamicSystemColor: 0x2818d3140; name = tableCellGroupedBackgroundColor>>> | | <UIView: 0x10fe2d310; frame = (0 0; 375 120); clipsToBounds = YES; layer = <CAGradientLayer: 0x280c58640>> | <UIView: 0x10ff9a820; frame = (0 0; 375 120); layer = <CAGradientLayer: 0x280b9db60>> | | <UIButton: 0x10ff9ab10; frame = (17 0; 170.5 60); opaque = NO; layer = <CAGradientLayer: 0x280b9dc40>> | | | <UIImageView: 0x10fe70710; frame = (0 16; 28 28); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280be9520>> | | | <UIButtonLabel: 0x10ff9af70; frame = (38 21.5; 86 17); text = '商户交易汇总'; opaque = NO; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280b9dc80>> | | <UIButton: 0x10ff9bd40; frame = (187.5 0; 170.5 60); opaque = NO; tag = 1; layer = <CAGradientLayer: 0x280b9e1c0>> | | | <UIImageView: 0x10ffacfd0; frame = (0 16; 28 28); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280b93220>> | | | <UIButtonLabel: 0x10ff9c1a0; frame = (38 21.5; 100 17); text = '代理商交易汇总'; opaque = NO; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280b9e340>> | | <UIButton: 0x10ff9cda0; frame = (17 60; 170.5 60); opaque = NO; tag = 2; layer = <CAGradientLayer: 0x280b9e540>> | | | <UIImageView: 0x10ffab1f0; frame = (0 16; 28 28); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280b92f40>> | | | <UIButtonLabel: 0x10ff9d200; frame = (38 21.5; 86 17); text = '终端激活汇总'; opaque = NO; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280b9e680>> | | <UIButton: 0x10ff9db20; frame = (187.5 60; 170.5 60); opaque = NO; tag = 3; layer = <CAGradientLayer: 0x280b9ea20>> | | | <UIImageView: 0x10ffa95d0; frame = (0 16; 28 28); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280b92ce0>> | | | <UIButtonLabel: 0x10ff9df80; frame = (38 21.5; 86 17); text = '商户终端汇总'; opaque = NO; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280b9eb80>> | | <UIView: 0x10ff9e8a0; frame = (15 60; 345 0.5); layer = <CAGradientLayer: 0x280b9ef00>> | | <UIView: 0x10ff9ea10; frame = (15 120; 345 0.5); layer = <CAGradientLayer: 0x280b9f0c0>> | <UITableViewCellContentView: 0x10ffaafa0; frame = (0 0; 375 120); gestureRecognizers = <NSArray: 0x281fc73c0>; layer = <CAGradientLayer: 0x280b808e0>> | | <UIImageView: 0x110f07d00; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280b7e220>>
1.1 注意事项
因为此问题涉及的是添加子视图
cell.addSubView
,因此与之对应的方法(UITableViewCell *)[SubView superview]
和cell.subviews
都要注意谨慎使用和处理II、使用方法交换,全局修改
如果错误代码比较多,可以采用hook,进行便捷的方法进行修改。
例如125个文件的1452个地方使用错误的方法,这个如果不使用hook高质工作量有点大
所以通过Runtime hook cell的addSubView 方法强制修改为正确的添加cell 子视图的方式2.1 全局修改
- 只允许添加 UITableViewCellContentView,其余都直接添加到self.contentView
// // UITableViewCell+CRMaddSubView.m // Housekeeper // // Created by mac on 2020/9/18. // Copyright © 2020 QCT. All rights reserved. // #import "UITableViewCell+CRMaddSubView.h" @implementation UITableViewCell (CRMaddSubView) + (void)load { // Swizzle addSubView [UITableViewCell sensorsdata_swizzleMethod:@selector(addSubview:) withMethod:@selector(kunnan_addSubview:)]; } - (void)kunnan_addSubview:(UIView *)view { if ([view isKindOfClass:NSClassFromString(@"UITableViewCellContentView")]) {//允许 addSubView UITableViewCellContentView [self kunnan_addSubview:view];//实现方法,因为已经进行了 swizzle,相当于调用原来的方法 } else { [self.contentView addSubview:view]; } } @end
2.2 注意事项
因为此问题涉及的是添加子视图cell.addSubView,因此与之对应的方法(UITableViewCell *)[SubView superview] 和cell.subviews 都要注意谨慎使用和处理
具体例子如下2.2.1 和2.2.2
2.2.1 cell.subviews
因为这是针对全局的,所以测试的覆盖面也要广。 比如获取子视图采用cell.subviews 也要记得修改为
cell.contentView.subviews
.UIButton * btn = cell.contentView.subviews[2-1];
2.2.2 通过superview 获取cell的也需做相关修改
-
经过全局hook之后,以下的代码就是错误的
(UITableViewCell *)[textField superview]
-
全局搜索进行修改
UITableViewCell * myCell = (UITableViewCell *)[textField superview].superview;
所以使用class的时候,最好写得健壮性强点,进行类型判断,避免一旦类型错误,就会找不到对应的方法,发送闪退
UIView * textFieldsuperview = [textField superview]; UITableViewCell * myCell = nil; if([textFieldsuperview isKindOfClass:NSClassFromString(@"UITableViewCellContentView")]){ myCell= (UITableViewCell *)[textFieldsuperview superview]; }else{ return; }
能遇见这样的奇葩代码,只能说之前的同事很”牛逼啊。。。。“
2.3 使用到的工具类
- h
// // NSObject+CRMSwizzling.h // Housekeeper // // Created by mac on 2020/9/18. // Copyright © 2020 QCT. All rights reserved. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN typedef IMP *IMPPointer; /** 让所有继承自NSObject的子类,都具有Method Swizzling的能力。 */ @interface NSObject (CRMSwizzling) /** 交换方法名为 originalSEL 和方法名为 alternateSEL 两个方法的实现 @param originalSEL 原始方法名 @param alternateSEL 要交换的方法名称 */ + (BOOL)sensorsdata_swizzleMethod:(SEL)originalSEL withMethod:(SEL)alternateSEL; /** 方式二 */ + (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(out IMPPointer)store; @end NS_ASSUME_NONNULL_END
- m
// // NSObject+CRMSwizzling.m // Housekeeper // // Created by mac on 2020/9/18. // Copyright © 2020 QCT. All rights reserved. // #import <objc/runtime.h> #import <objc/message.h> #import "NSObject+CRMSwizzling.h" BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store); BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) { IMP imp = NULL; Method method = class_getInstanceMethod(class, original); if (method) { const char *type = method_getTypeEncoding(method); imp = class_replaceMethod(class, original, replacement, type); if (!imp) { imp = method_getImplementation(method); } } if (imp && store) { *store = imp; } return (imp != NULL); } @implementation NSObject (CRMSwizzling) + (BOOL)sensorsdata_swizzleMethod:(SEL)originalSEL withMethod:(SEL)alternateSEL { // 获取原始方法 Method originalMethod = class_getInstanceMethod(self, originalSEL); // 当原始方法不存在时,返回 NO,表示 Swizzling 失败 if (!originalMethod) { return NO; } // 获取要交换的方法 Method alternateMethod = class_getInstanceMethod(self, alternateSEL); // 当要交换的方法不存在时,返回 NO,表示 Swizzling 失败 if (!alternateMethod) { return NO; } // 获取 originalSEL 方法的实现 IMP originalIMP = method_getImplementation(originalMethod); // 获取 originalSEL 方法的类型 const char * originalMethodType = method_getTypeEncoding(originalMethod); // 往类中添加 originalSEL 方法,如果已经存在会添加失败,并返回 NO if (class_addMethod(self, originalSEL, originalIMP, originalMethodType)) { // 如果添加成功了,重新获取 originalSEL 实例方法 originalMethod = class_getInstanceMethod(self, originalSEL); } // 获取 alternateIMP 方法的实现 IMP alternateIMP = method_getImplementation(alternateMethod); // 获取 alternateIMP 方法的类型 const char * alternateMethodType = method_getTypeEncoding(alternateMethod); // 往类中添加 alternateIMP 方法,如果已经存在会添加失败,并返回 NO if (class_addMethod(self, alternateSEL, alternateIMP, alternateMethodType)) { // 如果添加成功了,重新获取 alternateIMP 实例方法 alternateMethod = class_getInstanceMethod(self, alternateSEL); } // 交换两个方法的实现 method_exchangeImplementations(originalMethod, alternateMethod); // 返回 YES,表示 Swizzling 成功 return YES; } + (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(out IMPPointer)store { return class_swizzleMethodAndStore(self, original, replacement, store); } @end
III、逆向相关
3.1 iOS 恢复调用栈(适配iOS14)
原理:objective-c 函数信息除了保存在符号表中,还保存在其他段中
https://github.com/zhangkn/restore-symbol4iOS14
__TEXT.__objc_methname - Method names for locally implemented methods
__TEXT.__objc_classname - Names for locally implemented classes
__TEXT.__objc_methtype - Types for locally implemented method types
__DATA.__objc_classlist - An array of pointers to ObjC classes
__DATA.__objc_nlclslist - An array of pointers to classes who implement +load
__DATA.__objc_catlist - List of ObjC categories
__DATA.__objc_protolist - List of ObjC protocols
__DATA.__objc_imageinfo - Version info, not really useful
__DATA.__objc_const - Constant data, i.e. class_ro_t data
__DATA.__objc_selrefs - External references to selectors
__DATA.__objc_protorefs - External references to protocols
__DATA.__objc_classrefs - External references to other classes
__DATA.__objc_superrefs - External references to super classes
__DATA.__objc_ivar - Offsets to ObjC properties
__DATA.__objc_data - Misc ObjC storage, notably ObjC classessee also
extension UITableViewCell { class func ios14Bug() { let sel1 = #selector(UITableViewCell.runtime_addSubview(_:)) let sel2 = #selector(UITableViewCell.addSubview(_:)) let method1 = class_getInstanceMethod(UITableViewCell.self, sel1)! let method2 = class_getInstanceMethod(UITableViewCell.self, sel2)! let isDid: Bool = class_addMethod(self, sel2, method_getImplementation(method1), method_getTypeEncoding(method1)) if isDid { class_replaceMethod(self, sel1, method_getImplementation(method2), method_getTypeEncoding(method2)) } else { method_exchangeImplementations(method2, method1) } } @objc func runtime_addSubview(_ view: UIView) { // 判断不让 UITableViewCellContentView addSubView自己 if view.isKind(of: NSClassFromString("UITableViewCellContentView")!) { runtime_addSubview(view) } else { self.contentView.addSubview(view) } } }
-
flutter 屏幕适配 字体大小适配
2018-09-21 00:08:21比如我们的设计稿一个View的大小是300px,如果直接写300px,可能在当前设备显示正常,但到了其他设备可能就会偏小或者偏大,这就需要我们对屏幕进行适配。 安卓原生的话有自己的适配规则,可以根据不同的尺寸建立... -
-
关于Android 10.0适配,看这篇就够了
2019-07-09 10:48:47本文将从三个角度介绍Android Q的部分适配问题,也是大家开发适配过程中大概率会遇到的问题: Q 行为变更:所有应用 (不管targetSdk是多少,对所有跑在Q设备上的应用均有影响) Q 行为变更:以 Android Q 为目标... -
RecyclerView 通用适配 BaseQuickAdapter
2017-04-05 11:11:15RecyclerView 是Android L版本中新添加的一个用来取代ListView的SDK,它的灵活性与可替代性比...RecyclerView 同样也用到适配,枯燥重复的适配肯定会让你不胜其烦,下面让我们一起来打造一款通用的适配(BaseQui... -
Android 10适配要点,作用域存储
2020-04-14 08:42:48距离Android 10系统正式发布已经过去大半年左右的时间了,你的应用程序已经对它进行适配了吗?在Android 10众多的行为变更当中,有一点是非常值得引起我们重视的,那就是作用域存储。这个新功能直接颠覆了长久以来... -
iPhone屏幕尺寸、分辨率及适配
2014-12-26 18:57:59从初代iPhone3GS到现如今的...如何适配不同的屏幕尺寸,使UI更加协调美观,这给iPhone/iOS应用开发者带来了挑战。 本文结合个人在iOS UI开发和适配方面的粗浅经验,对常用屏幕适配相关因素做个梳理盘点,以备日后查阅。 -
Android通知栏微技巧,8.0系统中通知栏的适配
2018-04-17 07:39:11大家好,今天我们继续来学习Android 8.0系统的适配。 之前我们已经讲到了,Android 8.0系统最主要需要进行适配的地方有两处:应用图标和通知栏。在上一篇文章当中,我们学习了Android 8.0系统应用图标的适配,那么... -
移动端适配
2018-11-04 21:16:12移动端适配移动设备分辨率前言主流的移动端屏幕以及一些概念viewport视口手机的各种屏幕参数移动端适配rem适配方案vw单位适配 移动设备分辨率 前言 一直以来,我都觉得前端要做移动端的适配都是比较麻烦的事情,现在... -
android 平板适配,今日头条适配(同时适配手机和平板)
2020-11-23 11:59:10新项目,做平板适配,看了网上的很多适配方案,感觉都不合适,最后,干脆用今日头条适配. 经几个月的临床经验,果然能用,但是要注意几个问题: (顺便说一下,如果身边有丝袜,短裙,哈哈哈哈,好姑娘.... 记得给我介绍一下,... -
Android 屏幕适配之dimens适配
2017-05-23 10:23:02在过去多个项目中一直使用dimens适配,这种适配方式的优点很明显,不用管什么dp还是dpi这些东西,只需要以一种屏幕分辨率为基准(例如1280x720,相当于把屏幕宽分成720份,高分成1280份),生成对应屏幕分辨率的的dimens文件... -
Android 机型适配之百分比适配 ConstraintLayout
2018-03-14 15:07:21Android 机型适配之百分比适配 ConstraintLayout 由于Android的品类繁杂,机型适配向来是一个难题,常见的通过LinearLayout+RelativeLayout的方式进行适配具有较大的局限。而相比之下,百分比适配就强大很多了。 -
屏幕适配之图片适配
2017-09-19 09:19:56屏幕适配总共有6种(我知道的):图片适配,dimens适配,布局(Layout)适配,权重适配,百分比适配; 今天只说图片适配:图片适配主要是根据不同的手机密度,设置显示不同大小的图片; 下面首先说明下我们为什么要... -
IOS 暗黑模式适配---基础适配
2020-04-08 14:54:59IOS 暗黑模式 前言 适配DarkMode 图片适配 颜色适配 单页面适配 模式配置 总结 -
-
Android 10 适配攻略
2020-02-26 11:20:52相比较去年的写的Android 9适配,这次Android 10的内容有点多。没想到写了我整整两天,吐血中。。。 -
-
Android屏幕适配之dimen适配
2017-09-19 09:22:09Android屏幕适配 xml文件屏幕适配 -
屏幕适配,rom适配和版本适配
2017-10-19 09:53:001,不同android api版本的兼容 ...1、屏幕适配。(网上讲的最多的就是这个。) 由于Android碎片化严重,导致开发中一套代码在不同手机上运行起来效果不是很好,兼容性不是很好,这就需要对不同分辨率,不 -
vue实现PC端分辨率适配
2019-07-25 16:12:23} style> 可以看到,适配依然生效。 以上是vue-cli2配置px2rem的方式, 如果脚手架用的是vue-cli3的话,由于vue-cli3 生成的目录结构没有build文件,所以在配置上也有一些不同。 vue-cli3配置方式: 找到文件 node_... -
Android Q 适配
2019-09-16 20:10:51因为项目在华为部分手机有预装,应华为要求,适配 Android Q(Android 10) 版本,因为华为那边要求,新版本系统出来不久就会适配,项目是一步步适配上来的,Android M、Android N、Android O、Android P ,所以本次... -
iOS13适配指南:1、present半屏问题2、禁止 KVC访问UI控件私有API 3、 深色模式适配(设置状态栏背景颜色的...
2020-03-05 13:43:46I、 适配第三方库在iOS13的问题: 1.1) : 升级腾讯的第三方UI框架,解决iOS13 无法访问私有属性的问题 II、 适配iOS13UI控件的API :(涉及的模块有登录) 2.1)UITextField的_placeholderLabel属性:无法直接访问_... -
移动端(二)——移动端适配方案之viewport适配、比例缩放适配和百分比适配
2017-12-09 20:20:05适配:在不同尺寸的手机上,页面相对性的达到合理的展示(自适应)或这保持统一效果的等比缩放(看起来差不多),例如一个占满屏幕宽度的元素,在不同尺寸的手机下所显示都是占满宽度。 适配的元素 字体 宽高 ... -
android再谈屏幕适配之smallestwidth适配
2018-04-29 15:45:16Android发展至今,功能越来越多,越来越强大,不过碎片化的问题也是众所周知的越来越严重,如果是做大众应用,我相信屏幕适配肯定是我们不可回避的一个问题,当然了,网上的适配方案有很多,比如: Google的百度... -
android屏幕适配之dimens适配
2018-07-20 14:36:00安卓的适配方式有很多种,今天讲一下dimens适配: 一般来讲,一个项目里会有一个values文件夹,如下图所示: 做好dimens适配后的项目图如下: 效果图已经发给大家看了,那我们来看看如何写dimens适配 也许会... -
Android 9.0 适配指南
2019-08-05 22:24:32又到了我一年一度写Android适配文章的时间,本身这篇应该会早几个月发出来,但是前两三个月主要忙于Flutter的项目,所以这篇文章才姗姗来迟。不过毕竟是9.0的适配,还不算太晚哈! 1.前言 从去年开始就有消息说... -
Laya的屏幕适配,UI组件适配
2019-09-16 06:40:49屏幕适配API概述 版本2.1.1.1 目录 一 适配模式 二 UI组件适配 一 适配模式 基本和白鹭的适配模式一样。 Laya官方也推荐了竖屏使用fiexedwidth,横屏使用fixedheight。这也是我自己常用的适配模式。 ...