原标题:走心的Android 10 适配指南,手把手上代码
作者:黄海彬
链接: https://my.oschina.net/huanghaibin/blog/3106432
我们App的适配从targetSdkVersion = 26跨版本升级到29,因此会遇到大量的坑,最终的版本配置如下:
现在进入填坑适配指南,包含实际经验代码,绝不照搬翻译文档
1.Region.Op相关异常
java.lang.IllegalArgumentException: Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed
当 targetSdkVersion >= Build.VERSION_CODES.P时调用 canvas.clipPath(path, Region.Op.XXX); 引起的异常,参考源码如下:
@Deprecated
publicbooleanclipPath(@NonNull Path path, @NonNull Region.Op op){
checkValidClipOp(op);
returnnClipPath(mNativeCanvasWrapper, path.readOnlyNI, op.nativeInt);
}
privatestaticvoidcheckValidClipOp(@NonNull Region.Op op){
if(sCompatiblityVersion >= Build.VERSION_CODES.P
&& op != Region.Op.INTERSECT && op != Region.Op.DIFFERENCE) {
thrownewIllegalArgumentException(
"Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed");
}
}
我们可以看到当目标版本从Android P开始,Canvas.clipPath(@NonNull Path path, @NonNull Region.Op op) ; 已经被废弃,而且是包含异常风险的废弃API,只有 Region.Op.INTERSECT 和 Region.Op.DIFFERENCE 得到兼容,目前不清楚google此举目的如何,仅仅如此简单就抛出异常提示开发者适配,几乎所有的博客解决方案都是如下简单粗暴:
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
canvas.clipPath(path);
} else{
canvas.clipPath(path, Region.Op.XOR); // REPLACE、UNION 等
}
但我们一定需要一些高级逻辑运算效果怎么办?如小说的仿真翻页阅读效果,解决方案如下,用Path.op代替,先运算Path,再给canvas.clipPath:
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
Path mPathXOR = newPath;
mPathXOR.moveTo( 0, 0);
mPathXOR.lineTo(getWidth, 0);
mPathXOR.lineTo(getWidth,getHeight);
mPathXOR.lineTo( 0,getHeight);
mPathXOR.close;
//以上根据实际的Canvas或View的大小,画出相同大小的Path即可
mPathXOR.op(mPath0, Path.Op.XOR);
canvas.clipPath(mPathXOR);
} else{
canvas.clipPath(mPath0, Region.Op.XOR);
}
2.明文HTTP限制
当 targetSdkVersion >= Build.VERSION_CODES.P时,默认限制了HTTP请求,并出现相关日志:
java.net.UnknownServiceException: CLEARTEXT communication to xxx not permitted by network security policy
第一种解决方案:在AndroidManifest.xml中Application添加如下节点代码
第二种解决方案:在res目录新建xml目录,已建的跳过 在xml目录新建一个xml文件network_security_config.xml,然后在AndroidManifest.xml中Application添加如下节点代码
android:networkSecurityConfig="@xml/network_config"
名字随机,内容如下:
< network-security-config>
< base-configcleartextTrafficPermitted= "true"/>
network-security-config>
3. Android Q(10)中的媒体资源读写
相关的Android Q 行为变更不做细说,网上大部分博客关于Android Q 适配都在说行为变更,我们将根据实际遇到的问题,实际解决
1、扫描系统相册、视频等,图片、视频选择器都是通过ContentResolver来提供,主要代码如下:
privatestaticfinalString[] IMAGE_PROJECTION = {
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media._ID,
MediaStore.Images.Media.BUCKET_ID,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME};
Cursor imageCursor = mContext.getContentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
IMAGE_PROJECTION, null, null, IMAGE_PROJECTION[ 4] + " DESC");
String path = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[ 0]));
String name = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[ 1]));
intid = imageCursor.getInt(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[ 2]));
String folderPath = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[ 3]));
String folderName = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[ 4]));
//Android Q 公有目录只能通过Content Uri + id的方式访问,以前的File路径全部无效,如果是Video,记得换成MediaStore.Videos
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
path = MediaStore.Images.Media
.EXTERNAL_CONTENT_URI
.buildUpon
.appendPath(String.valueOf(id)).build.toString;
}
2、判断公有目录文件是否存在,自Android Q开始,公有目录File API都失效,不能直接通过new File(path).exists;判断公有目录文件是否存在,正确方式如下:
publicstaticbooleanisAndroidQFileExists(Context context, String path){
if(context == null) {
returnfalse;
}
AssetFileDeor afd = null;
ContentResolver cr = context.getContentResolver;
try{
Uri uri = Uri.parse(path);
afd = cr.openAssetFileDeor(Uri.parse(path), "r");
if(afd == null) {
returnfalse;
} else{
close(afd);
}
} catch(FileNotFoundException e) {
returnfalse;
} finally{
close(afd);
}
returntrue;
}
3、保存或者下载文件到公有目录,保存Bitmap同理,如Download,MIME_TYPE类型可以自行参考对应的文件类型,这里只对APK作出说明
publicstaticvoidcopyToDownloadAndroidQ(Context context, String sourcePath, String fileName, String saveDirName){
ContentValues values = newContentValues;
values.put(MediaStore.Downloads.DISPLAY_NAME, fileName);
values.put(MediaStore.Downloads.MIME_TYPE, "application/vnd.android.package-archive");
values.put(MediaStore.Downloads.RELATIVE_PATH, "Download/"+ saveDirName.replaceAll( "/", "") + "/");
Uri external = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
ContentResolver resolver = context.getContentResolver;
Uri insertUri = resolver.insert(external, values);
if(insertUri == null) {
return;
}
String mFilePath = insertUri.toString;
InputStream is = null;
OutputStream os = null;
try{
os = resolver.openOutputStream(insertUri);
if(os == null){
return;
}
intread;
File sourceFile = newFile(sourcePath);
if(sourceFile.exists) { // 文件存在时
is = newFileInputStream(sourceFile); // 读入原文件
byte[] buffer = newbyte[ 1444];
while((read = is.read(buffer)) != - 1) {
os.write(buffer, 0, read);
}
is.close;
os.close;
}
} catch(Exception e) {
e.printStackTrace;
}
finally{
close(is,os);
}
}
4、保存图片相关
/**
* 通过MediaStore保存,兼容AndroidQ,保存成功自动添加到相册数据库,无需再发送广告告诉系统插入相册
*
* @param context context
* @param sourceFile 源文件
* @param saveFileName 保存的文件名
* @param saveDirName picture子目录
* @return 成功或者失败
*/
publicstaticbooleansaveImageWithAndroidQ(Context context,
File sourceFile,
String saveFileName,
String saveDirName) {
String extension = BitmapUtil.getExtension(sourceFile.getAbsolutePath);
ContentValues values = newContentValues;
values.put(MediaStore.Images.Media.DEION, "This is an image");
values.put(MediaStore.Images.Media.DISPLAY_NAME, saveFileName);
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/"+ saveDirName);
Uri external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver resolver = context.getContentResolver;
Uri insertUri = resolver.insert(external, values);
BufferedInputStream inputStream = null;
OutputStream os = null;
booleanresult = false;
try{
inputStream = newBufferedInputStream( newFileInputStream(sourceFile));
if(insertUri != null) {
os = resolver.openOutputStream(insertUri);
}
if(os != null) {
byte[] buffer = newbyte[ 1024* 4];
intlen;
while((len = inputStream.read(buffer)) != - 1) {
os.write(buffer, 0, len);
}
os.flush;
}
result = true;
} catch(IOException e) {
result = false;
} finally{
Util.close(os, inputStream);
}
returnresult;
}
4.EditText默认不获取焦点,不自动弹出键盘
该问题出现在 targetSdkVersion >= Build.VERSION_CODES.P情况下,且设备版本为Android P以上版本,目前我们没有从源码中查到相关判断改动,解决方法在onCreate中加入如下代码:
mEditText.post( -> {
mEditText.requestFocus;
mEditText.setFocusable( true);
mEditText.setFocusableInTouchMode( true);
});
5.Only fullscreen activities can request orientation 异常
该问题出现在 targetSdkVersion >= Build.VERSION_CODES.O_MR1,也就是 API 27,当设备为Android 26时(27以上已经修复,也许google觉得不妥当,又改回来了),如果非全面屏透明activity固定了方向,则出现该异常,但是当我们在小米、魅族等Android 26机型测试的时候,并没有该异常,华为机型则报该异常,这是何等的卧槽。。。没办法,去掉透明style或者去掉固定方向代码即可,其它无解
6.安装APK Intent及其它文件相关Intent
/*
* 自Android N开始,是通过FileProvider共享相关文件,但是Android Q对公有目录 File API进行了限制
* 从代码上看,又变得和以前低版本一样了,只是必须加上权限代码Intent.FLAG_GRANT_READ_URI_PERMISSION
*/
privatevoidinstallApk{
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
//适配Android Q,注意mFilePath是通过ContentResolver得到的,上述有相关代码
Intent intent = newIntent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(mFilePath) , "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
return;
}
File file = newFile(saveFileName + "osc.apk");
if(!file.exists)
return;
Intent intent = newIntent(Intent.ACTION_VIEW);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri contentUri = FileProvider.getUriForFile(getApplicationContext, "net.oschina.app.provider", file);
intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
} else{
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
startActivity(intent);
}
7.Activity透明相关,windowIsTranslucent属
Android10又一个天坑,如果你要显示一个半透明的Activity,这在android10之前普通样式Activity只需要设置windowIsTranslucent=true即可,但是到了Android10,它没有效果了,而且如果动态设置View.setVisibility,界面还会多出残影...
解决办法:使用Dialog样式Theme,且设置windowIsFloating=true,此时问题又来了,如果Activity根布局没有设置fitsSystemWindow=true,默认会对根布局 padddingTop一个状态栏高度。使界面看上去正常,所以如果你的Activity代码有动态适配状态栏高度,需要对根布局设置fitsSystemWindow=true,否则会发现多出来一个padddingTop状态栏高度
8.剪切板兼容
只有当应用处于可交互情况才能访问剪切板和监听剪切板变化,在onResume回调也无法直接访问剪切板,这么做的好处是避免了一些后台应用疯狂监听响应剪切板的内容。
我们APK开发实践中暂时遇到的坑就这些,当然Android Q的改动是相当大的,例如还有App私有沙箱文件、定位权限和后台弹出Activity限制,这些都必须根据自身实践去踩坑适配,有条件的尽可能去阅读官方文档,参考改进。
」
责任编辑: