不知不觉,Android系统已经发展了10年之久,按照谷歌的命名风格,每个安卓版本都会以英文字母的顺序来进行命名,并且它们都有一个好吃的甜品代号,从最初的纸杯蛋糕(cupcake)到现在的奥利奥(pie)以及最新的beta版本的Android Q。但据说从Android Q开始Google将以数字的形式去命名接下来的版本,android甜品时代即将落幕。。。
牛皮吹到这里,下面进入正题。既然Android版本的迭代如此之快,那么系统的适配工作也必须同步进行。系统适配对于项目的发展尤为重要,只有不断地提升性能与体验才能得到用户的认可。但即使这样,相信还是有一部分的app还没来得及做系统的适配工作,那么这次的适配内容就从2015年推出的的6.0的版本开始讲起,一共是四个大版本,6.0(Marshmallow)、7.0(Nougat)、8.0(Oreo),9.0(Pie)。先看下这四个版本有哪些主要的变化:
发布时间:2015年5月28日
主要变化:
1.运行时权限
2. 增加低电耗模式和应用待机模式
3. 取消支持 Apache HTTP 客户端
4. 移除硬件标识符访问权
5. WLAN 和网络连接变更
6. 相机服务变更
发布时间: 2016年8月22日
主要变化:
私有文件访问权限更改
多窗口支持(分屏显示)
发布时间:2017年8月22日
主要变化:
通知渠道
启动图标
发布时间:2018年 8 月 7 日
主要变化:
网络安全变化
可以看到,每个版本都有比较多的变化,但并不是所有内容都需要适配,红色标注就是最需要进行系统适配的内容,也是文章所讲的内容。如果已经准备好去适配某个版本,那么请将targetSdkVersion改为对应的版本号,点击sync Now,开启填坑之路吧。
在6.0之前的版本中,我们在使用某项权限时,只需要在Manifest中声明就可以使用了,但是在6.0之后,某些权限(例如:通讯录、位置、相机)不仅需要在Manifest中声明,还要在app运行的时候动态地申请并被用户允许才能正常使用,这类权限称为危险权限
(Dangerous Permission)。与其对应的是正常权限
(Normal Permissions),正常权限(例如:蓝牙,网络)只需要在清单文件声明即可。具体的权限列表可以参考谷歌的官方文档
适配流程:
适配过程相对比较简单,下面以相机权限CAMERA
为例子说明,分为以下几个步骤:
1.在Manifest中声明所需权限:
<uses-permission android:name="android.permission.CAMERA"/>
2.调用*ContextCompat.checkSelfPermission()*检查权限:
if(ContextCompat.checkSelfPermission(this,Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
//权限未授予,调用requestPermission()申请权限
}else{
//权限已授予
}
*checkSelfPermission()*方法接受两个参数。第一个参数为Context对象,第二个参数为需要进行检查的权限,类型为String。返回值是一个int常量,返回PackageManager.PERMISSION_GRANTED
表示权限已经被授予,返回PackageManager.PERMISSION_DENIED
表示权限未被授予。
3.如果权限未被授予,调用*ActivityCompat.requestPermissions()*申请权限:
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA}, MY_REQUEST_CODE);
*requestPermissions()*接受三个参数,第一个参数是Context对象,第二个参数是一个String数组,可以同时申请多个权限,第三个参数是请求码。
4.重写Activity的*onRequestPermissionsResult()*处理申请回调:
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case MY_REQUEST_CODE: {
if (grantResults.length > 0 &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//用户允许该权限
} else {
//用户拒绝该权限
}
return;
}
}
}
其中requestCode就是请求码,在这里就是上面的MY_REQUEST_CODE,permissions就是申请的权限,grantResults是请求的结果,数组大小与permissions对应。由于我们只请求了一个CAMERA
权限,所以只需要看grantResults的第一个值就好了,根据结果做出相应的操作。至此,运行时权限申请就完成了。整体看起来,流程比较清晰和简单,就是有点繁琐(每个权限都要这么写的话),那这里推荐一个比较好用的封装库RxPermission。
多窗口模式是7.0版本的一个新特性,顾名思义,也就是让一个屏幕分成多个窗口,可以在一个屏幕上显示多个应用,多窗口模式共分为三种(分屏模式、画中画、自由形状),在手机上比较多用的是分屏模式
,如下图所示(个人感觉:在屏幕不是特别大的手机上效果一般)。画中画模式在Android TV上比较适用,自由模式在TNT上比较适用…这里不展开讨论。提供一个官方文档的链接。
如何进入分屏模式?每个厂家的设置都可能不一样,有的长按菜单键,有的长按返回键,具体可以百度下。
分屏模式具有以下几个特点:
onConfigurationChanged
android:screenOrientation
属性所作的更改。适配流程:
1.如果布局外层是ScrollView、RecycleView、ListView的界面,那么在分屏模式下是比较正常,可以尝试让这些界面支持分屏模式。
2.如果是固定宽高的界面,设置android:minimalHeight
、android:minimalWidth
来设置分屏模式下的最小宽高。
<activity android:name=".MyActivity">
<layout
android:minimalHeight="450dp"
android:minimalWidth="300dp" />
activity>
(编译出错,问题未知)
3.如果不想适配分屏模式,可以在Manifest的activity标签下设置属性android:resizeableActivity="false"
在7.0版本之前,我们可以通过File://
这一类Uri访问其他应用的私有文件或者让其他应用访问自己的私有文件。什么是私有文件?如果不太清楚这个概念的话,可以看这一篇博客,讲解的非常好,另外他还有一篇关于7.0的适配也讲解的非常详细。
http://yifeng.studio/2017/04/27/android-app-file-storage-directory/
从7.0版本开始,这么做就不行了,如果尝试传递 file:// Uri来访问其他应用的私有文件会触发 FileUriExposedException
异常,分享私有文件内容的推荐方法是使用 FileProvider
,FileProvider是v4包下的一个类,继承自ContentProvider。所以可以猜测通过FileProvider对外公开的uri就是content://开头的。
说是这么说,什么场景下我们会需要访问其他应用的私有文件或让其他应用访问自己的私有文件呢?有几个场景还是比较常见的,比如,调用系统的安装程序安装apk文件(假设这个apk文件存放在私有目录中),那么就需要提供此apk的路径让安装程序来访问(这就是让其他应用访问自己的私有文件),又或者调用系统相机拍照时,需要提供一个拍照后的相片的存放路径等。下面以拍照为例子讲解具体的适配流程。
适配流程:
1.导入v4包,在主module的build.gradle下的dependencies节点下添加依赖,版本号视情况而定。
implementation 'com.android.support:support-v4:27.1.1'
2.在 res/xml 目录下新建一个 xml 文件,用于存放应用需要共享的文件目录,这里我命名为file_paths.xml
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path
name="my_images"
path="./images/"/>
paths>
name是自定义的一个别名,path是这个共享的目录,这里的path值表示共享外部私有目录file/images下的文件。如果是输入的是"."
则表示共享外部私有目录下的file/目录下的所有文件。
其中
元素有以下几个子节点:
:内部存储空间应用私有目录下的 files/ 目录,等同于 Context.getFilesDir() 所获取的目录路径
:内部存储空间应用私有目录下的 cache/ 目录,同于 Context.getCacheDir() 所获取的目录路径;
:外部存储空间根目录,等同于 Environment.getExternalStorageDirectory() 所获取的目录路径;
外部存储空间应用私有目录下的 files/ 目录,等同于 Context.getExternalFilesDir(null) 所获取的目录路径;
:外部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getExternalCacheDir();3.在AndroidManifest中声明FileProvide:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
provider>
authorities的名字可自定义,一般为包名+FileProvide,resource就是刚刚新建的共享文件。
完成以上三步就ok了,下面来尝试调用系统相机进行拍照(注意CAMERA权限的申请)。
下面是测试代码:
private void testCapture() {
String path = getExternalFilesDir(null) + File.separator + "images" + File.separator +
System.currentTimeMillis() + ".jpg";
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
Uri uri = FileProvider.getUriForFile(this,
BuildConfig.APPLICATION_ID + ".FileProvider", file);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult(intent, MY_REQUEST_CODE);
} else {
//7.0之前的使用
}
}
点击✓后照片就会保存在指定的目录中,我们通过adb找到该文件:
完美!!!这样就可以在7.0以上的设备访问应用私有文件了。
由于不同的厂商对应用图标的形状有了一定的规范(圆角、圆形等),如果不遵循它们的规范,您的应用图标可能就会被强制改成它们要求的形状,有时候改过的图标可能你的期望落差太大 。所以从Android 8.0系统开始,google对应用图标进行统一的规范。在8.0版本之后,应用程序的图标被分为了两层:前景层和背景层。前景层是一个背景透明的logo,背景层一般是一张纯色或带纹理的图片。前景层和背景层组合之后,会被盖上一层mask,这层mask是厂商决定的,这样一来,不管这一层mask是圆角还是圆形,都可以完整地显示您的logo了。下面是官方文档的一张gif演示图。
适配流程
如果您的Android Studio版本在3.0以上,那么基本在新建工程的时候就会默认创建res/mipmap-anydpi-v26文件夹,如下:
mipmap-anydpi-v26下有两个xml文件代表8.0以上版本的启动图标,没错是xml文件。打开ic_launcher.xml可以看到以下内容:
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
adaptive-icon>
这是8.0版本的标准写法,由标签名即可知道其意思,
就是背景
就是前景。这是新建工程后Android Studio为我们创建的,那么,我们可以根据项目的进行简单的更改即可。Android Studio3.0以上版本还为我们提供了一个可视化的工具Image Asset,windows下可用快捷键ctrl+shift+a搜索
这里提供了可以直接操作的可视化界面
这个没什么好说的,非常的简单。唯一要注意的是Preview界面上的黑色圆圈,如果您不想您的logo在mark的时候被覆盖掉,那么前景层必须显示在黑色圆圈内。这里我将默认的icon的背景层用一个黄色的
来替代。下载到手机上看下效果:
Android 8.0对通知栏进行了比较大的改动,引入了通知渠道的概念,就好像为每种不同的消息分类别一样,例如,某个新闻app,将通知分为两种,一种为新闻类通知,一种为广告类通知。这样,就可以创建两个不同的渠道,发出通知的时候,可以根据渠道来发送,这样做的好处是便于用户去管理每个渠道的通知(选择他们感兴趣的内容)。同时,Android用户具有管理每个渠道的权限(设置声音、震动等),并且可以关闭某个渠道的通知,附上官方文档。我们先来看一下最右app的通知渠道体验一下。
好了,现在我们来进行通知栏的适配。
适配流程
1.创建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel newsChannel = new NotificationChannel(CHANNEL_NEWS, "新闻",
NotificationManager.IMPORTANCE_HIGH);
NotificationChannel adsChannel = new NotificationChannel(CHANNEL_ADS, "广告",
NotificationManager.IMPORTANCE_DEFAULT);
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.createNotificationChannel(newsChannel);
manager.createNotificationChannel(adsChannel);
}
NotificationChannel对象就代表着一个通知渠道,构造方法接受三个参数,第一个参数是全局唯一的渠道id,String类型。第二个是渠道名称,注意这个名称给用户看的。第三个是通知的重要等级。这里我们调用*createNotificationChannel()*创建了两个渠道,新闻和广告。注意:通知渠道不会重复创建,创建过程可以放在Application中进行。运行上面的代码后,渠道就创建好了,如下图:
2.发送通知
我们定义三个按钮,一个用来发送新闻类的通知,一个用来发送广告类的通知,一个用来发送没有通知渠道的通知:
代码如下:
public void sendNews(View view) {
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Notification notification = new NotificationCompat.Builder(this, CHANNEL_NEWS)
.setContentTitle("法国获得2018世界杯冠军")
.setContentText("法国夺冠克罗地亚虽败犹荣比利时季军")
.setAutoCancel(true)
.build();
manager.notify(1, notification);
}
public void sendAds(View view) {
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ADS)
.setContentTitle("收到某新闻来自拼多多的广告")
.setContentText("这是它的广告内容...")
.setAutoCancel(true)
.build();
manager.notify(1, notification);
}
public void sendNoChannel(View view) {
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle("没有渠道的通知")
.setContentText("通内容")
.setAutoCancel(true)
.setSmallIcon(R.mipmap.ic_launcher)
.build();
manager.notify(1, notification);
}
运行效果:
可以看到,在8.0之后(targetSdkVersion=26)发送通知如果不指定渠道的话是无法发送出去的,只有指定了通知渠道的通知才可以正常发送,通知展示的方式取决于用户的设置。
从9.0开始,google禁止App使用未加密的连接,简单来说,网络请求必须使用https开头的,http的网络请求被禁止了,如果想在9.0之后使用http的网络请求,解决方法如下:
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
network-security-config>
android:networkSecurityConfig="@xml/network_security_config"
ok,这样就可以继续使用http了。
到这里,6.0到9.0的系统适配内容就讲完了,由于一次性要讲四个大版本的适配,所以对一些内容叙述的不太完整,见谅。本次适配内容,也是参考了多个博主的文章,下面把链接放上来。
1. 关于 Android 7.0 适配中 FileProvider 部分的总结
2 .Android应用图标微技巧,8.0系统中应用图标的适配
3 .Android通知栏微技巧,8.0系统中通知栏的适配