android 适配aoto_android 屏幕适配 auto - CSDN
  • 转载请标明出处: ... 本文出自:【张鸿洋的博客】 一、概述相信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面板,选择分辨率一致的设备:

    然后你就可以看到最为精确的预览了:

    两个注意事项:

    1. 你们UI给的设计图的尺寸并非是主流的设计图,该尺寸没找到,你可以拿显示器砸他自己去新建一个设备。
    2. 不要在PreView中去查看所有分辨率下的显示,是看不出来适配效果的,因为有些计算是动态的。

    (2)关于TextView

    TextView这个控件呢,可能和设计稿上会有一些出入,并非是此库的原因,而是与生俱来的特性。

    比如:

    <TextView
     textSize="32px"
     layout_height="wrap_contnt"
     />

    你去运行肯定不是32px的高度,文字的上下方都会有一定的空隙。如何你将高度写死,也会发现文字显示不全。

    恩,所以呢,灵活应对这个问题,对于存在字体标识很精确的值,你可以选择:对于TextView与其他控件的上下边距呢,尽可能的稍微写小一点。

    其实我上面的例子,几乎都是TextView,所有我在编写Item里面的时候,也有意缩小了一下marginTop值等。不过,对于其他控件是不存在这样的问题的。

    ps:因为TextView的上述问题:所以对于居中,虽然可以使用本库通过编写margin_left,margin_top等很轻松的完成居中。但是为了精确起见,还是建议使用gravitycenterInXXX等属性完成。

    (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
    (欢迎关注,第一时间推送博文信息)

    展开全文
  • A low-cost Android screen adaptation solution (今日头条屏幕适配方案终极版,一个极低成本的 Android 屏幕适配方案)
  • 在不久之前,我才发布了一篇Android 10适配的文章,讲的是作用域存储的相关内容。 而除了作用域存储之外,深色主题也是Android 10中的一大亮点,并且是需要开发者进行适配的。因此本篇文章我们就来探讨关于深色主题...

    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新。

    各位小伙伴们大家早上好,今天给大家带来一篇关于Android 10适配的原创文章。

    在不久之前,我才发布了一篇Android 10适配的文章,讲的是作用域存储的相关内容,详见链接 Android 10适配要点,作用域存储

    而除了作用域存储之外,深色主题也是Android 10中的一大亮点,并且是需要开发者进行适配的。因此本篇文章我们就来探讨关于深色主题的内容。

    另外说明一下,本篇文章主要摘自《第一行代码——Android 第3版》的第14章,在此基础之上,我又扩展了一些内容。

    为什么要将书中的内容再发一份到博客上呢?主要是因为深色主题的运行效果对于色彩性有一定的要求,书中的黑白印刷模式不太容易给大家直观地展示深色主题的效果,所以就额外再借助一篇文章来呈现了。另外对于还没看过《第一行代码》的朋友,这也是一篇非常好的学习文章。

    我们一直以来使用的操作系统都是以浅色主题为主的,这种主题模式在白天或者是光线充足的情况下使用起来没有任何问题,可是在夜晚灯光关闭的情况下使用就会显得非常刺眼。

    于是,许多应用程序为了能够让用户在光线昏暗的环境下更加舒适地使用,会在应用内部提供一个一键切换夜间模式的按钮。当用户开启了夜间模式,就会将应用程序的整体色调都调整成更加适合于夜间浏览的颜色。

    不过,这种由应用程序自发实现夜间模式的方式很难做到全局统一,即有些应用可能支持夜间模式,有些应用却不支持。而且重复操作的问题也很让人头疼,比如说我在一个应用中开启了夜间模式,在另外一个应用中还需要再开启一次,关闭夜间模式也需要进行同样重复的操作。

    因此,一直以来都有强烈的呼声,希望Android能够在系统层面支持夜间模式功能。终于在Android 10.0系统中,Google引入了深色主题这一特性,从而让夜间模式正式成为了官方支持的功能。

    或许你会有些疑惑,这种看上去并没有太多技术难度的功能,为什么Android直到10.0系统中才进行支持呢?这是因为仅仅操作系统自身支持深色主题是没有用的,还得让所有的应用程序都能够支持才行,而这从来都不是一件容易的事情。

    为此,我们以后开发的应用程序都应该尽量按照Android系统的要求对深色主题进行支持,不然当用户开启了深色主题之后,只有你的应用还使用的是浅色主题的话,就会显得格格不入。

    除了让眼部在夜间使用时更加舒适之外,深色主题还可以减少电量消耗,从而延长手机续航,是一项非常有用的功能。那么接下来,我们就开始学习如何才能让应用程序支持深色主题功能。

    首先,Android 10.0及以上系统的手机,都可以在Settings -> Display -> Dark theme中对深色主题进行开启和关闭。开启深色主题后,系统的界面风格包括一些内置的应用程序都会变成深色主题的色调,如下图所示。

    不过,如果这时你打开我们自己编写的应用程序,你会发现目前界面的风格还是使用的浅色主题模式,这就和系统的主题风格不同了,说明我们需要对此进行适配。

    这里我准备使用在第12章中编写的MaterialTest项目来作为示例,看看如何才能让它更加完美地适配深色主题模式。(示例下载地址见链接随书下载部分 https://www.ituring.com.cn/book/2744)。

    首先看一下MaterialTest项目的初始运行效果。

    接下来我们开始学习如何深色主题模式进行适配。

    最简单的一种适配方式就是使用Force Dark,它是一种能让应用程序快速适配深色主题,并且几乎不用编写额外代码的方式。Force Dark的工作原理是系统会分析浅色主题应用下的每一层View,并且在这些View绘制到屏幕之前,自动将它们的颜色转换成更加适合深色主题的颜色。注意,只有原本使用浅色主题的应用才能使用这种方式,如果你的应用原本使用的就是深色主题,Force Dark将不会起作用。

    这里我们尝试对MaterialTest项目使用Force Dark转换来进行举例。启用Force Dark功能需要借助android:forceDarkAllowed属性,不过这个属性是从API 29,也就是Android 10.0系统开始才有的,之前的系统无法指定这个属性。因此,我们得进行一些系统差异型编程才行。

    右击res目录 -> New -> Directory,创建一个values-v29目录,然后右击values-v29目录 -> New -> Values resource file,创建一个styles.xml文件。接着对这个文件进行编写,代码如下所示:

    <resources>
        <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
            <item name="colorPrimary">@color/colorPrimary</item>
            <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
            <item name="colorAccent">@color/colorAccent</item>
            <item name="android:forceDarkAllowed">true</item>
        </style>
    </resources>
    

    除了android:forceDarkAllowed属性之外,其他的内容都是从之前的styles.xml文件中复制过来的。这里给AppTheme主题增加了android:forceDarkAllowed属性并设置为true,说明现在我们是允许系统使用Force Dark将应用强制转换成深色主题的。另外,values-v29目录是只有Android 10.0及以上的系统才会去读取的,因此这是一种系统差异型编程的实现方式。

    现在重新运行MaterialTest项目,效果如下图所示。

    可以看到,虽然整体的界面风格好像确实变成了深色主题的模式,可是却并不怎么美观,尤其是卡片式布局的效果,经过Force Dark之后已经完全看不出来了。

    Force Dark就是这样一种简单粗暴的转换方式,并且它的转换效果通常是不尽如人意的。因此,这里我并不推荐你使用这种自动化的方式来实现深色主题,而是应该使用更加传统的实现方式——手动实现。

    是的,要想实现最佳的深色主题效果,不要指望有什么神奇魔法能够一键完成,而是应该针对每一个界面都进行浅色和深色两种主题的界面设计。这听上去好像有点复杂,不过我们仍然有一些好用的技巧能让这个过程变得简单。

    在第12章中我们曾经学习过,AppCompat库内置的主题恰好主要分为浅色主题和深色主题两类,比如MaterialTest项目中目前使用的Theme.AppCompat.Light.NoActionBar就是浅色主题,而Theme.AppCompat.NoActionBar就是深色主题。选用不同的主题,在控件的默认颜色等方面会有完全不同的效果。

    下面我们动手来尝试一下吧。首先删除values-v29目录及其目录下的内容,然后修改values/styles.xml中的代码,如下所示:

    <resources>
    
        <!-- Base application theme. -->
        <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
            <!-- Customize your theme here. -->
            <item name="colorPrimary">@color/colorPrimary</item>
            <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
            <item name="colorAccent">@color/colorAccent</item>
        </style></resources>
    

    可以看到,这里我们将AppTheme的parent主题指定成了Theme.AppCompat.DayNight.NoActionBar ,这是一种DayNight主题。因此,在普通情况下MaterialTest项目仍然会使用浅色主题,和之前并没有什么区别,但是一旦用户在系统设置中开启了深色主题,MaterialTest项目就会自动使用相应的深色主题。

    现在我们就可以重新运行一下程序,看看使用DayNight主题之后,MaterialTest项目默认的界面效果是什么样的,如下图所示。

    很明显,现在的界面比之前使用Force Dark转换后的界面要好看很多,至少卡片式布局的效果得到了保留。

    然而,虽然现在界面中的主要内容都已经自动切换成了深色主题,但是你会发现标题栏和悬浮按钮仍然保持着和浅色主题时一样的颜色。这是因为标题栏以及悬浮按钮使用的是我们定义在colors.xml中的几种颜色值,代码如下所示:

    <resources>
        <color name="colorPrimary">#008577</color>
        <color name="colorPrimaryDark">#00574B</color>
        <color name="colorAccent">#D81B60</color>
    </resources>
    

    这种指定颜色值引用的方式相当于对控件的颜色进行了硬编码,DayNight主题是不能对这些颜色进行动态转换的。

    好在解决方案也并不复杂,我们只需要进行一些主题差异型编程就可以了。右击res目录 -> New -> Directory,创建一个values-night目录,然后右击values-night目录 -> New -> Values resource file,创建一个colors.xml文件。接着在这个文件中指定深色主题下的颜色值,如下所示:

    <resources>
        <color name="colorPrimary">#303030</color>
        <color name="colorPrimaryDark">#232323</color>
        <color name="colorAccent">#008577</color>
    </resources>
    

    这样的话,在普通情况下,系统仍然会读取values/colors.xml文件中的颜色值,而一旦用户开启了深色主题,系统就会去读取values-night/colors.xml文件中的颜色值了。

    现在重新运行一下程序,效果如下图所示。

    这种深色主题的效果,在夜间使用的时候明显会好上很多,对不对?

    虽说使用主题差异型的编程方式几乎可以帮你解决所有的适配问题,但是在DayNight主题下,我们最好还是尽量减少通过硬编码的方式来指定控件的颜色,而是应该更多地使用能够根据当前主题自动切换颜色的主题属性。比如说黑色的文字通常应该衬托在白色的背景下,反之白色的文字通常应该衬托在黑色的背景下,那么此时我们就可以使用主题属性来指定背景以及文字的颜色,示例写法如下:

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="?android:attr/colorBackground">
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="Hello world"
            android:textSize="40sp"
            android:textColor="?android:attr/textColorPrimary" />
    
    </FrameLayout>
    

    这些主题属性会自动根据系统当前的主题模式选择最合适的颜色值呈现给用户,效果如下图所示。

    另外,或许你还会有一些特殊的需求,比如要在浅色主题和深色主题下分别执行不同的代码逻辑。对此Android也是支持的,你可以使用如下代码在任何时候判断当前系统是否是深色主题:

    fun isDarkTheme(context: Context): Boolean {
        val flag = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
        return flag == Configuration.UI_MODE_NIGHT_YES
    }
    

    调用isDarkTheme()方法,判断当前系统是浅色主题还是深色主题,然后根据返回值执行不同的代码逻辑即可。

    由于Kotlin取消了按位运算符的写法,改成了使用英文关键字,因此上述代码中的and关键字其实就对应了Java中的&运算符,而Kotlin中的or关键字对应了Java中的|运算符,xor关键字对应了Java中的^运算符,非常好理解。

    我个人认为,在绝大多数情况下,让应用程序跟随系统的设置来决定使用浅色主题还是深色主题是最合适的一种做法。然而如果你一定想要脱离系统设置,让自己的应用程序独立控制使用浅色主题还是深色主题,Android对此也是支持的,只要使用AppCompatDelegate.setDefaultNightMode()方法即可。

    setDefaultNightMode()方法接收一个mode参数,用于控制当前应用程序的夜间模式。mode参数主要有以下值可供选择:

    • MODE_NIGHT_FOLLOW_SYSTEM:默认模式,表示让当前应用程序跟随系统设置来决定使用浅色主题还是深色主题。

    • MODE_NIGHT_YES:脱离系统设置,强制让当前应用程序使用深色主题。

    • MODE_NIGHT_NO:脱离系统设置,强制让当前应用程序使用浅色主题。

    • MODE_NIGHT_AUTO_BATTERY:根据手机的电池状态来决定使用浅色主题还是深色主题,如果开启了节点模式,则使用深色主题。

    在MaterialTest当中,我们只需要使用如下代码就可以实现浅色主题和深色主题动态切换的功能:

    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            ...
            fab.setOnClickListener { view ->
                if (isDarkTheme(this)) {
                    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
                } else {
                    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
                }
            }
        }
    	
        ...
    	
    }
    

    运行之后的效果如下图所示。

    需要注意的是,当调用setDefaultNightMode()方法并成功切换主题时,应用程序中所有处于started状态的Activity都会被重新创建(不在started状态的Activity则会在恢复started状态时再重新创建,关于started状态的解释可以参见《第一行代码 第3版》的第13章,Lifecycles部分)。这很好理解,因为Activity不重新创建Activity怎么切换主题呢?

    可是如果你当前所处的界面不想被重新创建,比如一个正在播放视频的界面。这个时候可以在Activity的configChanges属性当中配置uiMode来让当前Activity避免被重新创建,如下所示:

    <activity
        android:name=".MainActivity"
        android:configChanges="uiMode" />
    

    现在当应用程序的主题发生变化时,MainActivity并不会重新创建,而是会触发onConfigurationChanged()方法的回调,你可以在回调当中手动做一些逻辑处理。

    override fun onConfigurationChanged(newConfig: Configuration) {
        val currentNightMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK
        when (currentNightMode) {
            Configuration.UI_MODE_NIGHT_NO -> {} // 夜间模式未启用,使用浅色主题
            Configuration.UI_MODE_NIGHT_YES -> {} // 夜间模式启用,使用深色主题
        }
    }
    

    如果什么都不做的话,当前的Activity就好像并没有切换主题一样,界面上也不会有任何变化。

    好了,关于Android 10深色主题的适配内容就讲到这里。想要学习更多最新的Android知识,可以阅读我的新书《第一行代码——Android 第3版》,Kotlin、Jetpack、MVVM,你所关心的知识点都在这里。详情点击这里查看

    京东购买地址

    当当购买地址

    天猫购买地址


    关注我的技术公众号,每天都有优质技术文章推送。

    微信扫一扫下方二维码即可关注:

    展开全文
  • 关于Android适配的文章,各种论坛,眼花缭乱,有的很有营养,看完收货很大;有的则完全是复制粘贴,鱼目混珠.我仅仅将自己在开发中用到的,感觉对适配有一定帮助的东西,写下来,希望能帮助到和我遇到过相同问题的...

    前言

    Android 适配问题,让每个Android程序员头疼问题,听起来可能有些夸大,但是相信每个Android开发人员都碰到过棘手的适配问题。
    关于Android适配的文章,各种论坛,眼花缭乱,有的很有营养,看完收货很大;有的则完全是复制粘贴,鱼目混珠.
    我仅仅将自己在开发中用到的,感觉对适配有一定帮助的东西,写下来,希望能帮助到和我遇到过相同问题的人

    正文

    关于Android适配,google比我们想的更多,给我们提供了DP(DIP)-device independent pixels(设备独立像素).在一些简单布局中还是有用的,碰到一些复杂布局,DP完全满足不了需求,不然也不会让大家头疼的适配问题。

    关于px/dp/ppi等概念,大家都很了解,我就不说这些,还不太明白的同学,可以看看

    八一八那些px、pt、ppi、dpi、dp、sp之间的关系
    详解Android开发中常用的 DPI / DP / SP

    其实简单的适配,我们利用LinearLayout的layout_widget就可以完成比例的适配,但这远远不够,可以利用其它方法

    这些文章已经说的很明了,所以废话不多说,直接讲讲适配问题

    1、dimens

    针对一些常见分辨率机型,设置dimens

    1240
    友盟统计Android设备分辨率

    dimens类似于drawable,drawable中我们设置

    1240
    設置不同drawable

    dimens则比drawable灵活,在values文件夹根据限定符去命名dimens

    1.屏幕分辨率

    例如1920x1080,1280x720,表示适配分辨率为1920x1080或者1280x720的设备
    res/values-1920x1080/dimens.xml

    2.屏幕像素密度

    例如hdpi,适配屏幕像素密度值近似或等于hdpi(320dpi)的设备。
    res/values-hdpi/dimens.xml

    3.屏幕尺寸

    例如 sw720dp,适配最低屏幕可用区域为720dp的设备,同理还有sw1080dp.
    res/values-sw720dp/dimens.xml

    4.屏幕方向

    分为横向(land)和纵向(port),分别适配屏幕方向为横向或纵向的屏幕
    res/values-land/dimens.xml
    类似的限定符就不一一列举,就列出常见的限定符
    但是这些限定符一般不是单独使用,而是组合使用,例如values-land-mdpi-800x480,values-xhdpi-port-1920×1080,

    所以适配时,在values-land-mdpi-800x480中,定义 px100表示100px ,
    <dimen name= "px100" >100px</ dimen>
    则在values-hdpi-port-1280×720,px100则定义为mdpi的两倍
    <dimen name= "px100" >200px</ dimen>
    在布局中,就可以灵活使用dimens

    <LinearLayout
    android:layout_width="@dimen/px100"
    android:layout_height="@dimen/px100"
    android:orientation="vertical" />

    针对不同分辨率,设置dimens实现适配,对市场上大部分主流分辨率,适配是没有问题的。
    但是!!!Android设置碎片化实在是太严重,随便哪个厂家或者山寨厂商推出奇葩的分辨率,对我们都是莫大的伤害,然并卵,我们还是逃不掉,但是很多时候,丧心病狂的分辨率是我们无法预料的,我们不可能把所以可能出现的组合情况,全部设置dimens。那么,dimens也不能完全满足我们,就得想办法寻找新途径.

    2.百分比适配

    百分比适配,显而易见,通过设置百分比,实现控件适配,在Google没有提供百分比适配之前,我们使用的android:layout_weight,其实就是一种百分比,但是只能在LinearLayout中使用。
    在Android 22开始,google提供了相当方便的百分比适配
    使用很简单,依赖compile 'com.android.support:percent:24.0.0'
    提供了PercentRelativeLayout、PercentFrameLayout两种布局,布局中,替换RelativeLayout、FrameLayout
    那就简单看看PercentRelativeLayout的使用,PercentFrameLayout同理

    <android.support.percent.PercentRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <TextView
            android:id="@+id/top_left"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_alignParentTop="true"
            android:background="#ff44aacc"
            android:gravity="center"
            android:text="70%"
            app:layout_heightPercent="20%"
            app:layout_widthPercent="70%" />
    
        <TextView
            android:id="@+id/top_right"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_alignParentTop="true"
            android:layout_toRightOf="@+id/top_left"
            android:background="#ffe40000"
            android:gravity="center"
            android:text="30%"
            app:layout_heightPercent="20%"
            app:layout_widthPercent="30%" />
    
        <View
            android:id="@+id/bottom_left"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_below="@+id/top_left"
            android:background="#ff00ff"
            app:layout_heightPercent="80%"
            app:layout_widthPercent="30%" />
    
        <View
            android:id="@+id/bottom_right_top"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_below="@+id/top_left"
            android:layout_toRightOf="@+id/bottom_left"
            android:background="#ff00ff22"
            app:layout_heightPercent="40%"
            app:layout_widthPercent="70%" />
    
        <View
            android:id="@+id/bottom_right_bottom"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_below="@+id/bottom_right_top"
            android:layout_toRightOf="@+id/bottom_left"
            android:background="#0000ff"
            app:layout_heightPercent="40%"
            app:layout_widthPercent="70%" />
    </android.support.percent.PercentRelativeLayout>
    1240
    效果图


    很简单的布局,效果也很直观
    一共有layout_widthPercent、layout_heightPercent、 layout_marginPercent 、layout_marginLeftPercent、 layout_marginTopPercent、layout_marginRightPercent 、 layout_marginBottomPercent、layout_marginStartPercent、layout_marginEndPercent属性提供使用
    使用 layout_widthPercent、layout_heightPercent
    不能删除默认的 layout_width、layout_height
    而是设置 android:layout_width="0dp" android:layout_height="0dp"

    3.代码适配

    例如,要给一个图片动态设置尺寸,宽高都为屏幕宽度的77.8%
    在Activity中,初始化 ivImgbackground;

        CircleImageView ivImgbackground = findViewById(R.id.iv_imgbackground);
    
        /**
         * 动态设置图片大小
         */
        private void setImageSize() {
            final WindowManager manager = this.getWindowManager();
            //得出屏幕宽度
            DisplayMetrics outMetrics = new DisplayMetrics();
            manager.getDefaultDisplay().getMetrics(outMetrics);
            widthPixels = outMetrics.widthPixels;
            //重新计算高度
            layoutParams = ivImgbackground.getLayoutParams();
            layoutParams.width = (int) (0.778 * widthPixels);
            layoutParams.height = (int) (0.778 * widthPixels);
            ivImgbackground.setLayoutParams(layoutParams);
        }

    其实也是一种另类的百分比适配

    展开全文
  • 一直以来android适配非常头疼,今天给大家分享一个android适配的工具类,我用这个工具类很久了,目前还没发现什么问题。 import android.app.Activity; import android.content.Context; import android.content....

    一直以来android适配非常头疼,今天给大家分享一个android适配的工具类,我用这个工具类很久了,目前还没发现什么问题。

    import android.app.Activity;
    import android.content.Context;
    import android.content.res.Resources;
    import android.util.TypedValue;
    import android.view.Display;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.TextView;
    
    /**
     * Created by fySpring
     * Date : 2019/4/11
     * To do :自适应工具类
     */
    
    public class AutoUtils {
    
        private static int displayWidth;
        private static int displayHeight;
    
        private static int designWidth;
        private static int designHeight;
    
        private static double textPixelsRate;
    
        public static void setSize(Activity act, boolean hasStatusBar, int designWidth, int designHeight) {
            if (act == null || designWidth < 1 || designHeight < 1) return;
    
            Display display = act.getWindowManager().getDefaultDisplay();
            int width = display.getWidth();
            int height = display.getHeight();
    
            if (hasStatusBar) {
                height -= getStatusBarHeight(act);
            }
    
            AutoUtils.displayWidth = width;
            AutoUtils.displayHeight = height;
    
            AutoUtils.designWidth = designWidth;
            AutoUtils.designHeight = designHeight;
    
            double displayDiagonal = Math.sqrt(Math.pow(AutoUtils.displayWidth, 2) + Math.pow(AutoUtils.displayHeight, 2));
            double designDiagonal = Math.sqrt(Math.pow(AutoUtils.designWidth, 2) + Math.pow(AutoUtils.designHeight, 2));
            AutoUtils.textPixelsRate = displayDiagonal / designDiagonal;
        }
    
        public static int getStatusBarHeight(Context context) {
            int result = 0;
            try {
                int resourceId = context.getResources().getIdentifier(
                        "status_bar_height", "dimen", "android");
                if (resourceId > 0) {
                    result = context.getResources().getDimensionPixelSize(
                            resourceId);
                }
            } catch (Resources.NotFoundException e) {
                e.printStackTrace();
            }
            return result;
        }
    
        public static void auto(Activity act) {
            if (act == null || displayWidth < 1 || displayHeight < 1) return;
    
            View view = act.getWindow().getDecorView();
            auto(view);
        }
    
        public static void auto(View view) {
            if (view == null || displayWidth < 1 || displayHeight < 1) return;
    
            AutoUtils.autoTextSize(view);
            AutoUtils.autoSize(view);
            AutoUtils.autoPadding(view);
            AutoUtils.autoMargin(view);
    
            if (view instanceof ViewGroup) {
                auto((ViewGroup) view);
            }
    
        }
    
        private static void auto(ViewGroup viewGroup) {
            int count = viewGroup.getChildCount();
    
            for (int i = 0; i < count; i++) {
    
                View child = viewGroup.getChildAt(i);
    
                if (child != null) {
                    auto(child);
                }
            }
        }
    
        public static void autoMargin(View view) {
            if (!(view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams))
                return;
    
            ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
            if (lp == null) return;
    
            lp.leftMargin = getDisplayWidthValue(lp.leftMargin);
            lp.topMargin = getDisplayHeightValue(lp.topMargin);
            lp.rightMargin = getDisplayWidthValue(lp.rightMargin);
            lp.bottomMargin = getDisplayHeightValue(lp.bottomMargin);
    
        }
    
        public static void autoPadding(View view) {
            int l = view.getPaddingLeft();
            int t = view.getPaddingTop();
            int r = view.getPaddingRight();
            int b = view.getPaddingBottom();
    
            l = getDisplayWidthValue(l);
            t = getDisplayHeightValue(t);
            r = getDisplayWidthValue(r);
            b = getDisplayHeightValue(b);
    
            view.setPadding(l, t, r, b);
        }
    
        public static void autoSize(View view) {
            ViewGroup.LayoutParams lp = view.getLayoutParams();
    
            if (lp == null) return;
    
            if (lp.width > 0) {
                lp.width = getDisplayWidthValue(lp.width);
            }
    
            if (lp.height > 0) {
                lp.height = getDisplayHeightValue(lp.height);
            }
    
        }
    
        public static void autoTextSize(View view) {
            if (view instanceof TextView) {
                double designPixels = ((TextView) view).getTextSize();
                double displayPixels = textPixelsRate * designPixels;
                ((TextView) view).setIncludeFontPadding(false);
                ((TextView) view).setTextSize(TypedValue.COMPLEX_UNIT_PX, (float) displayPixels);
            }
        }
    
        public static int getDisplayWidthValue(int designWidthValue) {
            if (designWidthValue < 2) {
                return designWidthValue;
            }
            return designWidthValue * displayWidth / designWidth;
        }
    
        public static int getDisplayHeightValue(int designHeightValue) {
            if (designHeightValue < 2) {
                return designHeightValue;
            }
            return designHeightValue * displayHeight / designHeight;
        }
    }
    

    通常UI给你的图会有一个标准的尺寸的,我的一般是按1280*800来,添加下面的代码来设置标准尺寸:(我通常加到SplashActivity中,因为这段代码只需写一次就可以了)

    AutoUtils.setSize(this, true, 1280, 800);//参数二  表示设计尺寸的宽高是否包含状态栏
    

    下面这一段代码就要在BaseActivity、BaseFragment、以及你的自定义View中添加:

    AutoUtils.auto(this);

    接下来也是最关键的,在android中设置宽高的单位 为  dp,字体的单位为 sp,在这里 我们把单位全部换成  px! ,是的,你没有看错  是 px !  px !  px !  所以在标注的时候,让UI 把单位标注为 px  。

    如果你哪里的控件大小不对,那一定是你的 单位忘了设置,或者忘了加上    AutoUtils.auto(this);

    好了,就是这些。使用中有问题的话可以私信我。

     

    展开全文
  • Android 10 适配攻略

    2020-03-20 11:08:34
    文章目录准备工作1.Scoped Storage(分区存储)说明适配补充2.权限变化1.在后台运行时访问设备位置信息需要权限2.一些电话、蓝牙和WLAN的API需要精确位置权限3.ACCESS_MEDIA_LOCATION4.PROCESS_OUTGOING_CALLS3.后台...

    准备工作

    老规矩,首先将我们项目中的targetSdkVersion改为 29。

    1.Scoped Storage(分区存储)

    说明

    在Android 10之前的版本上,我们在做文件的操作时都会申请存储空间的读写权限。但是这些权限完全被滥用,造成的问题就是手机的存储空间中充斥着大量不明作用的文件,并且应用卸载后它也没有删除掉。为了解决这个问题,Android 10 中引入了Scoped Storage的概念,通过添加外部存储访问限制来实现更好的文件管理。

    首先明确一个概念,外部储存和内部储存。

    • 内部储存:/data目录。一般我们使用getFilesDir()getCacheDir()方法获取本应用的内部储存路径,读写该路径下的文件不需要申请储存空间读写权限,且卸载应用时会自动删除。

    • 外部储存:/storage/mnt目录。一般我们使用getExternalStorageDirectory()方法获取的路径来存取文件。

    因为不同厂商、系统版本的原因,所以上述的方法并没有一个固定的文件路径。了解了上面的概念,那我们所说的外部储存访问限制,可以认为是针对getExternalStorageDirectory()路径下的文件。具体的规则如下表:
    在这里插入图片描述

    上图将外部存储空间分为了三部分:

    • 特定目录(App-specific),使用getExternalFilesDir()或 getExternalCacheDir()方法访问。无需权限,且卸载应用时会自动删除。

    • 照片、视频、音频这类媒体文件。使用MediaStore 访问,访问其他应用的媒体文件时需要READ_EXTERNAL_STORAGE权限。

    • 其他目录,使用存储访问框架SAF(Storage Access Framwork)

    所以在Android 10上即使你拥有了储存空间的读写权限,也无法保证可以正常的进行文件的读写操作。

    适配

    最简单粗暴的方法就是在AndroidManifest.xml中添加 android:requestLegacyExternalStorage="true"来请求使用旧的存储模式。

    但是我不推荐此方法。因为在下一个版本的Android中,此条配置将会失效,将强制采用外部储存限制。其实早在Android Q Beta 3之前都是强制的,但为了给开发者适配的时间才没有强制执行。所以如果你不抓住这段时间去适配,那么今年下半年出了Android 11。。。直接开花~~

    如果你已经适配Android 10,这里有个现象要注意一下:

    如果应用通过升级安装,那么还会使用以前的储存模式(Legacy View)。只有通过首次安装或是卸载重新安装才能启用新模式(Filtered View)。

    所以在适配时,我们的判断代码如下:

    	// 使用Environment.isExternalStorageLegacy()来检查APP的运行模式
    	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&  !Environment.isExternalStorageLegacy()) {
              
        }
    

    这样的好处是你可以在用户升级后,能方便的将用户的数据移动至应用的特定目录。否则你只能通过SAF去移动,这样会非常麻烦。如果你要移动数据注意只适用于Android 10下,所以现在适配反而是一个好时机。当然如果你不需要迁移数据,那适配会更省事。

    下面就说说推荐适配方案:

    • 对于应用中涉及的文件操作,修改一下你的文件路径。

    以前我们习惯使用Environment.getExternalStorageDirectory()方法,那么现在可以使用getExternalFilesDir()方法(包括下载的安装包这类的文件)。如果是缓存类型文件,可以放到getExternalCacheDir()路径下。

    或者使用MediaStore,将文件存至对应的媒体类型中(图片:MediaStore.Images ,视频:MediaStore.Video,音频:MediaStore.Audio),不过仅限于多媒体文件。

    下面代码将图片保存到公共目录下,返回Uri:

       public static Uri createImageUri(Context context) {
            ContentValues values = new ContentValues();
            // 需要指定文件信息时,非必须
            values.put(MediaStore.Images.Media.DESCRIPTION, "This is an image");
            values.put(MediaStore.Images.Media.DISPLAY_NAME, "Image.png");
            values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
            values.put(MediaStore.Images.Media.TITLE, "Image.png");
            values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/test");
            
            return context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
        }
    
    • 对于媒体资源的访问:比如图片选择器这类的场景。无法直接使用File,而应使用Uri。否则报错如下:
    java.io.FileNotFoundException: open failed: EACCES (Permission denied)
    

    比如我在适配项目中使用的图片选择器时,首先修改了Glide 通过加载File的方式显示图片。改为加载Uri的方式,否则图片无法显示出来。

    Uri的获取方式还是使用MediaStore:

    String id = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
    
    Uri uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
    

    其次为了便于不影响之前选择图片返回File的逻辑(因为一般都是上传File,没有直接上传Uri的操作),所以我将最终选择的文件又转存进了getExternalFilesDir(),主要代码如下:

    
    	File imgFile = this.getExternalFilesDir("image");
        if (!imgFile.exists()){
            imgFile.mkdir();
        }
    	try {
            File file = new File(imgFile.getAbsolutePath() + File.separator + 
            	System.currentTimeMillis() + ".jpg");
            // 使用openInputStream(uri)方法获取字节输入流
            InputStream fileInputStream = getContentResolver().openInputStream(uri);
            FileOutputStream fileOutputStream = new FileOutputStream(file);
            byte[] buffer = new byte[1024];
            int byteRead;
            while (-1 != (byteRead = fileInputStream.read(buffer))) {
                fileOutputStream.write(buffer, 0, byteRead);
            }
            fileInputStream.close();
            fileOutputStream.flush();
            fileOutputStream.close();
            // 文件可用新路径 file.getAbsolutePath()
        } catch (Exception e) {
            e.printStackTrace();        
        }
    
    • 如果你要获取图片中的地理位置信息,需要申请ACCESS_MEDIA_LOCATION权限,并使用MediaStore.setRequireOriginal()获取。下面是官方的示例代码:
    	Uri photoUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    		 cursor.getString(idColumnIndex));
    
        final double[] latLong;
    
        // 从ExifInterface类获取位置信息
        photoUri = MediaStore.setRequireOriginal(photoUri);
        InputStream stream = getContentResolver().openInputStream(photoUri);
        if (stream != null) {
            ExifInterface exifInterface = new ExifInterface(stream);
            double[] returnedLatLong = exifInterface.getLatLong();
    
            // If lat/long is null, fall back to the coordinates (0, 0).
            latLong = returnedLatLong != null ? returnedLatLong : new double[2];
    
            // Don't reuse the stream associated with the instance of "ExifInterface".
            stream.close();
        } else {
            // Failed to load the stream, so return the coordinates (0, 0).
            latLong = new double[2];
        }
    

    这样下来,一个图片选择器就基本适配完了。

    补充

    应用在卸载后,会将App-specific目录下的数据删除,如果在AndroidManifest.xml中声明:android:hasFragileUserData="true"用户可以选择是否保留。

    对于SAF的使用,可以查看我之前写的SAF使用攻略,这里就不展开说了。

    最后这里有一个介绍Scoped Storage的视频,推荐观看:

    准备好使用分区存储 | ADS 中文字幕视频

    准备好使用分区存储

    2.权限变化

    从6.0开始,基本每次都会有权限方面变动,这次也不例外。(前几天发布了Android 11的预览版,看来也有权限方面的变化。。。单次权限即将到来)

    1.在后台运行时访问设备位置信息需要权限

    Android 10 引入了 ACCESS_BACKGROUND_LOCATION 权限(危险权限)。

    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
    

    该权限允许应用程序在后台访问位置。如果请求此权限,则还必须请求ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION权限。只请求此权限无效果。

    在Android 10的设备上,如果你的应用的 targetSdkVersion < 29,则在请求ACCESS_FINE_LOCATION 或ACCESS_COARSE_LOCATION权限时,系统会自动同时请求ACCESS_BACKGROUND_LOCATION。在请求弹框中,选择“始终允许”表示同意后台获取位置信息,选择“仅在应用使用过程中允许”或"拒绝"选项表示拒绝授权。

    如果你的应用的 targetSdkVersion >= 29,则请求ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION权限表示在前台时拥有访问设备位置信息的权。在请求弹框中,选择“始终允许”表示前后台都可以获取位置信息,选择“仅在应用使用过程中允许”只表示拥有前台的权限。

    总结一下就是下图:
    在这里插入图片描述
    其实官方不推荐你使用申请后台访问权的方式,因为这样的结果无非就是多请求一个权限,那么这像变更还有什么意义?申请过多的权限,也会造成用户的反感。所以官方推荐使用前台服务来实现,在前台服务中获取位置信息。

    • 首先在清单中对应的service中添加 android:foregroundServiceType=“location”:
     	<service
            android:name="MyNavigationService"
            android:foregroundServiceType="location" ... >
            ...
        </service>
    
    • 启动前台服务前检查是否具有前台的访问权限:
    	boolean permissionApproved = ActivityCompat.checkSelfPermission(this, 
    		Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
    
        if (permissionApproved) {
           // 启动前台服务
        } else {
           // 请求前台访问位置权限
        }
    

    如此一来就可以在Service中获取位置信息。

    2.一些电话、蓝牙和WLAN的API需要精确位置权限

    下面列举了Android 10中必须具有 ACCESS_FINE_LOCATION 权限才能使用类和方法:

    • 电话
      TelephonyManager
      getCellLocation()
      getAllCellInfo()
      requestNetworkScan()
      requestCellInfoUpdate()
      getAvailableNetworks()
      getServiceState()
      TelephonyScanManager
      requestNetworkScan()
      TelephonyScanManager.NetworkScanCallback
      onResults()
      PhoneStateListener
      onCellLocationChanged()
      onCellInfoChanged()
      onServiceStateChanged()
    • WLAN
      WifiManager
      startScan()
      getScanResults()
      getConnectionInfo()
      getConfiguredNetworks()
      WifiAwareManager
      WifiP2pManager
      WifiRttManager
    • 蓝牙
      BluetoothAdapter
      startDiscovery()
      startLeScan()
      BluetoothAdapter.LeScanCallback
      BluetoothLeScanner
      startScan()
      我们可以根据上面提供的具体类和方法,在适配项目中检查是否有使用到并及时处理。

    3.ACCESS_MEDIA_LOCATION

    Android 10新增权限,上面有提到,不赘述了。

    4.PROCESS_OUTGOING_CALLS

    Android 10上该权限已废弃。

    3.后台启动 Activity 的限制

    简单解释就是应用处于后台时,无法启动Activity。比如点开一个应用会进入启动页或者广告页,一般会有几秒的延时再跳转至首页。如果这期间你退到后台,那么你将无法看到跳转过程。而在之前的版本中,会强制弹出页面至前台。

    既然是限制,那么肯定有不受限的情况,主要有以下几点:

    • 应用具有可见窗口,例如前台 Activity。

    • 应用在前台任务的返回栈中已有的 Activity。

    • 应用在 Recents 上现有任务的返回栈中已有的 Activity。Recents 就是我们的任务管理列表。

    • 应用收到系统的 PendingIntent 通知。

    • 应用收到它应该在其中启动界面的系统广播。示例包括 ACTION_NEW_OUTGOING_CALL 和 SECRET_CODE_ACTION。应用可在广播发送几秒钟后启动 Activity。

    • 用户已向应用授予 SYSTEM_ALERT_WINDOW 权限,或是在应用权限页开启后台弹出页面的开关。

    因为此项行为变更适用于在 Android 10 上运行的所有应用,所以这一限制导致最明显的问题就是点击推送信息时,有些应用无法进行正常的跳转(具体的实现问题导致)。所以针对这类问题,可以采取PendingIntent的方式,发送通知时使用setContentIntent方法。

    当然你也可以申请相应权限或者白名单:
    在这里插入图片描述

    不过申请白名单这种方法受各种手机厂商所限,很麻烦。感觉还不如引导用户手动开启权限。。。

    对于全屏 intent,注意设置最高优先级和添加USE_FULL_SCREEN_INTENT权限,这是一个普通权限。比如微信来语音或者视频通话时,弹出的接听页面就是使用这一功能。

    	<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
    
    
    	Intent fullScreenIntent = new Intent(this, CallActivity.class);
        PendingIntent fullScreenPendingIntent = PendingIntent.getActivity(this, 0,
                fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);
    
        NotificationCompat.Builder notificationBuilder =
                new NotificationCompat.Builder(this, CHANNEL_ID)
            .setSmallIcon(R.drawable.notification_icon)
            .setContentTitle("Incoming call")
            .setContentText("(919) 555-1234")
            .setPriority(NotificationCompat.PRIORITY_HIGH) // <--- 高优先级
            .setCategory(NotificationCompat.CATEGORY_CALL)
    
            // Use a full-screen intent only for the highest-priority alerts where you
            // have an associated activity that you would like to launch after the user
            // interacts with the notification. Also, if your app targets Android 10
            // or higher, you need to request the USE_FULL_SCREEN_INTENT permission in
            // order for the platform to invoke this notification.
            .setFullScreenIntent(fullScreenPendingIntent, true); // <--- 全屏 intent
    
        Notification incomingCallNotification = notificationBuilder.build();
    

    注意:在部分手机上,直接设置setPriority无效(或者说以渠道优先级为准)。所以需要创建通知渠道时将重要性设置为IMPORTANCE_HIGH。

    NotificationChannel channel = new NotificationChannel(channelId, "xxx", NotificationManager.IMPORTANCE_HIGH);
    

    后台启动 Activity 的限制的目的是为了减少对用户操作的中断。如果你有要弹出的页面,推荐你先弹出通知,让用户自己选择接下来的操作,而不是一股脑的强制弹出。(如果你的全屏intent都让用户反感,那他也可以关掉你的通知,不至于任你摆布。)

    4.深色主题

    Android 10 新增了一个系统级的深色主题(在系统设置中开启)。虽然深色主题并不是强制适配项,但是它可以带给用户更好的体验:

    • 可大幅减少耗电量。 OLED 屏幕中每个像素都是自主发光,所以在显示深色元素时像素所消耗的电流更低,尤其在纯黑颜色时像素点可以完全关闭来达到省电的效果。

    • 为弱视以及对强光敏感的用户提高可视性。深色可以降低屏幕的整体视觉亮度,减少对眼睛的视觉压力。

    • 让所有人都可以在光线较暗的环境中更轻松地使用设备。

    适配方法有两种:

    1.手动适配(资源替换)

    官方文档中提到的继承Theme.AppCompat.DayNight 或者 Theme.MaterialComponents.DayNight的方法,但这只是将我们使用的各种View的默认样式进行了适配,并不太适用于实际项目的适配。因为具体的项目中的View都按照设计的风格进行了重定义。

    其实适配的方法很简单,类似屏幕适配、国际化的操作,并不需要继承上面的主题。比如你要修改颜色,就在res 下新建 values-night目录,创建对应的colors.xml文件。将具体要修改的色值定义在里面。图标之类的也是一个思路,创建对应的 drawable-night目录。

    只要你之前的代码不是硬编码且代码规范,那么适配起来还是很轻松。

    2.自动适配(Force Dark)

    Android 10 提供 Force Dark 功能。一如其名,此功能可让开发者快速实现深色主题背景,而无需明确设置 DayNight 主题背景。

    如果您的应用采用浅色主题背景,则 Force Dark 会分析应用的每个视图,并在相应视图在屏幕上显示之前,自动应用深色主题背景。有些开发者会混合使用 Force Dark 和本机实现,以缩短实现深色主题背景所需的时间。

    应用必须选择启用 Force Dark,方法是在其主题背景中设置 android:forceDarkAllowed=“true”。此属性会在所有系统及 AndroidX 提供的浅色主题背景(例如 Theme.Material.Light)上设置。使用 Force Dark 时,您应确保全面测试应用,并根据需要排除视图。

    如果您的应用使用Dark Theme主题(例如Theme.Material),则系统不会应用 Force Dark。同样,如果应用的主题背景继承自 DayNight 主题(例如Theme.AppCompat.DayNight),则系统不会应用 Force Dark,因为会自动切换主题背景。

    您可以通过 android:forceDarkAllowed 布局属性或 setForceDarkAllowed(boolean) 在特定视图上控制 Force Dark。

    上述内容我直接照搬文档的说明。总结一下,使用Force Dark需要注意几点:

    • 如果使用的是 DayNight 或 Dark Theme 主题,则设置forceDarkAllowed 不生效。

    • 如果有需要排除适配的部分,可以在对应的View上设置forceDarkAllowed为false。

    这里说说我实际使用此方法的感受:整体还是不错的,设置的色值会自动取反。但也因此颜色不受控制,能否达到预期效果是个需要注意的问题。追求快速适配可以采取此方案。

    手动切换主题

    使用 AppCompatDelegate.setDefaultNightMode(@NightMode int mode)方法,其中参数mode有以下几种:

    • 浅色 - MODE_NIGHT_NO
    • 深色 - MODE_NIGHT_YES
    • 由省电模式设置 - MODE_NIGHT_AUTO_BATTERY
    • 系统默认 - MODE_NIGHT_FOLLOW_SYSTEM

    下面的代码是官方Demo中的使用示例:

    public class ThemeHelper {
    
        public static final String LIGHT_MODE = "light";
        public static final String DARK_MODE = "dark";
        public static final String DEFAULT_MODE = "default";
    
        public static void applyTheme(@NonNull String themePref) {
            switch (themePref) {
                case LIGHT_MODE: {
                    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
                    break;
                }
                case DARK_MODE: {
                    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
                    break;
                }
                default: {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                        AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
                    } else {
                        AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
                    }
                    break;
                }
            }
        }
    }
    

    通过AppCompatDelegate.getDefaultNightMode()方法,可以获取到当前的模式,这样便于代码中去适配。

    监听深色主题是否开启

    首先在清单文件中给对应的Activity配置 android:configChanges=“uiMode”:

    	<activity
        	android:name=".MyActivity"
        	android:configChanges="uiMode" />
    

    这样在onConfigurationChanged方法中就可以获取:

    	@Override
        public void onConfigurationChanged(@NonNull Configuration newConfig) {
            super.onConfigurationChanged(newConfig);
            int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
            switch (currentNightMode) {
                case Configuration.UI_MODE_NIGHT_NO:
                    // 关闭
                    break;
                case Configuration.UI_MODE_NIGHT_YES:
                    // 开启
                    break;
                default:
                    break;    
            }
        }
    

    详细的内容你可以参看官方文档和官方Demo。

    判断深色主题是否开启

    其实和上面onConfigurationChanged方法同理:

        public static boolean isNightMode(Context context) {
            int currentNightMode = context.getResources().getConfiguration().uiMode & 
            	Configuration.UI_MODE_NIGHT_MASK;
            return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
        }
    

    5.标识符和数据

    对不可重置的设备标识符实施了限制
    受影响的方法包括:

    • Build
      getSerial()
    • TelephonyManager
      getImei()
      getDeviceId()
      getMeid()
      getSimSerialNumber()
      getSubscriberId()
      从 Android 10 开始,应用必须具有 READ_PRIVILEGED_PHONE_STATE 特许权限才能正常使用以上这些方法。

    如果你的应用没有该权限,却仍然使用了以上的方法,则返回的结果会因目标 SDK 版本而异:

    • 如果应用以 Android 10 或更高版本为目标平台,则会发生 SecurityException。
    • 如果应用以 Android 9(API 级别 28)或更低版本为目标平台,则相应方法会返回 null 或占位符数据(如果应用具有 READ_PHONE_STATE 权限)。否则,会发生 SecurityException。

    这项改动表示第三方应用无法获取Device ID这类唯一标识。如果你需要唯一标识符,请参阅文档:唯一标识符的最佳做法。

    当然你也可以试试移动安全联盟(MSA)联合多家厂商共同开发的统一补充设备标识调用SDK。据说还有点不稳定,因为我暂时还没有尝试过,所以不做评价。

    限制了对剪贴板数据的访问权限

    除非您的应用是默认输入法 (IME) 或是目前处于焦点的应用,否则它无法访问 Android 10 或更高版本平台上的剪贴板数据。

    对启用和停用 WLAN 实施了限制

    以 Android 10 或更高版本为目标平台的应用无法启用或停用 WLAN。WifiManager.setWifiEnabled()方法始终返回 false。

    如果您需要提示用户启用或停用 WLAN,请使用设置面板。

    6.其他

    • Android10上对折叠屏设备有了更好的支持,对于有折叠屏适配的需求,可以参看为可折叠设备构建应用 和 华为折叠屏应用开发指导。

    • 以上内容只是Android 10中比较大的几项变化,完整的内容可以查看官方文档。

    展开全文
  • 由于Android的碎片化,导致app的界面元素在不同屏幕尺寸上显示不一致,所以Android适配问题一直都比较让人蛋疼。一般来说,常见的屏幕适配方式有以下几种: 1)布局适配 -避免写死控件,采用wrap_content,match_...
  • android 屏幕适配第三方 添加链接描述
  • 文章目录暗黑模式为什么我们需要暗黑模式如何适配暗黑模式Force Dark自动适配自定义适配手动切换暗黑模式 暗黑模式 在 2019 年的 Google I/O上,谷歌新发布的android 10终于从系统层级支持暗黑模式,那么为什么我们...
  • 小猪浅谈Android屏幕适配tags: Tutorial引言 国庆前在微信群里看到有人在问Android屏幕适配的问题,凑巧自己最近时间 略有闲暇,索性来谈谈Android中屏幕适配相关的一些内容吧,鄙人才疏学浅, 所说的都是自己...
  • 面试问你屏幕适配,那么你要知道为什么Android要做屏幕适配,因为Android是开源的, 各大厂商不仅可以对软件定制,还可以对硬件定制,这样就造成市场上不同分辨率的手机超多,现在估计得有几万或者几十万种,这就导致...
  • Android的屏幕适配一直以来都在折磨着我们Android开发者,本文将结合: Google的官方权威适配文档 郭霖: Android官方提供的支持不同屏幕大小的全部方法 Stormzhang:Android 屏幕适配 鸿洋:Android 屏幕适配...
  • android10的适配

    2020-06-23 11:44:33
    市场上android10的用户越来越多了,作为资深的安卓开发人员,如果不及时给你的项目适配,将会出现很多问题,相比较去年的写的Android 9适配,这次Android 10的内容有点多,我整整写了一个星期,痛苦中。。。 首先...
  • Android 屏幕适配总结

    2020-04-19 20:37:48
    Android 适配相关的文章有很多了,我阐述一下自己总结后的理解与解决方式,本文从为什么需要适配为起点,到官方推荐的适配方式的用法为结束。 一、为什么需要适配 1.与屏幕相关的概念 1)屏幕尺寸 屏幕大小...
  • Android 机型适配之百分比适配 ConstraintLayout 由于Android的品类繁杂,机型适配向来是一个难题,常见的通过LinearLayout+RelativeLayout的方式进行适配具有较大的局限。而相比之下,百分比适配就强大很多了。
  • Android适配 首先屏幕适配这个问题已经困扰了我很久,一直没有找到很好的解决办法,看了鸿洋大神的博客感觉发现了新大陆一样,以后适配可以像web开发一样利用百分比来控制控件的大小和排版了,谷歌给我们提供了...
  • 转载请标明出处: ... 本文出自:【张鸿洋的博客】 一、概述相信Android的开发者对于设配问题都比较苦恼,Google官方虽然给出了一系列的建议,但是...个人也比较关注适配的问题,之前也发了几篇关于适配的文章,大致有:
1 2 3 4 5 ... 20
收藏数 10,286
精华内容 4,114
关键字:

android 适配aoto