今天看到一篇好的文章,分享给大家,膜拜大佬。
Android10填坑适配指南,包含实际经验代码,绝不照搬翻译文档
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); 引起的异常,参考源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Deprecated public boolean clipPath( @NonNull Path path, @NonNull Region.Op op) { checkValidClipOp(op); return nClipPath(mNativeCanvasWrapper, path.readOnlyNI(), op.nativeInt); } private static void checkValidClipOp( @NonNull Region.Op op) { if (sCompatiblityVersion >= Build.VERSION_CODES.P && op != Region.Op.INTERSECT && op != Region.Op.DIFFERENCE) { throw new IllegalArgumentException( "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 得到兼容,几乎所有的博客解决方案都是如下简单粗暴:
1
2
3
4
5
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { canvas.clipPath(path); } else { canvas.clipPath(path, Region.Op.XOR); // REPLACE、UNION 等 } |
但我们一定需要一些高级逻辑运算效果怎么办?如小说的仿真翻页阅读效果,解决方案如下,用Path.op代替,先运算Path,再
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
给canvas.clipPath: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){ Path mPathXOR = new Path(); 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添加如下节点代码
1
|
< application android:usesCleartextTraffic = "true" > |
第二种解决方案:在res目录新建xml目录,已建的跳过 在xml目录新建一个xml文件network_security_config.xml,然后在AndroidManifest.xml中Application添加如下节点代码
1
|
android:networkSecurityConfig="@xml/network_config" |
名字随机,内容如下:
1
2
3
4
|
<? xml version = "1.0" encoding = "utf-8" ?> < network-security-config > < base-config cleartextTrafficPermitted = "true" /> </ network-security-config > |
3.Android Q中的媒体资源读写
1、扫描系统相册、视频等,图片、视频选择器都是通过ContentResolver来提供,主要代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
private static final String[] 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[ 0 ] + " DESC" ); String path = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[ 0 ])); String name = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[ 1 ])); int id = 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();判断公有目录文件是否存在,正确方式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public static boolean isAndroidQFileExists(Context context, String path){ AssetFileDescriptor afd = null ; ContentResolver cr = context.getContentResolver(); try { Uri uri = Uri.parse(path); afd = cr.openAssetFileDescriptor(uri, "r" ); if (afd == null ) { return false ; } else { close(afd); } } catch (FileNotFoundException e) { return false ; } finally { close(afd); } return true ; } |
3、copy或者下载文件到公有目录,保存Bitmap同理,如Download,MIME_TYPE类型可以自行参考对应的文件类型,这里只对APK作出说明,从私有目录copy到公有目录demo如下(远程下载同理,只要拿到OutputStream即可,亦可下载到私有目录再copy到公有目录):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
public static void copyToDownloadAndroidQ(Context context, String sourcePath, String fileName, String saveDirName){ ContentValues values = new ContentValues(); 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 ; } int read; File sourceFile = new File(sourcePath); if (sourceFile.exists()) { // 文件存在时 is = new FileInputStream(sourceFile); // 读入原文件 byte [] buffer = new byte [ 1444 ]; while ((read = is.read(buffer)) != - 1 ) { os.write(buffer, 0 , read); } } } catch (Exception e) { e.printStackTrace(); } finally { close(is,os); } } |
4、保存图片相关
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
/** * 通过MediaStore保存,兼容AndroidQ,保存成功自动添加到相册数据库,无需再发送广播告诉系统插入相册 * * @param context context * @param sourceFile 源文件 * @param saveFileName 保存的文件名 * @param saveDirName picture子目录 * @return 成功或者失败 */ public static boolean saveImageWithAndroidQ(Context context, File sourceFile, String saveFileName, String saveDirName) { String extension = BitmapUtil.getExtension(sourceFile.getAbsolutePath()); ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.DESCRIPTION, "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 ; boolean result = false ; try { inputStream = new BufferedInputStream( new FileInputStream(sourceFile)); if (insertUri != null ) { os = resolver.openOutputStream(insertUri); } if (os != null ) { byte [] buffer = new byte [ 1024 * 4 ]; int len; while ((len = inputStream.read(buffer)) != - 1 ) { os.write(buffer, 0 , len); } os.flush(); } result = true ; } catch (IOException e) { result = false ; } finally { close(os, inputStream); } return result; } |
4.EditText默认不获取焦点,不自动弹出键盘
该问题出现在 targetSdkVersion >= Build.VERSION_CODES.P 情况下,且设备版本为Android P以上版本,解决方法在onCreate中加入如下代码,可获得焦点,如需要弹出键盘可延迟一下:
1
2
3
4
5
|
mEditText.post(() -> { mEditText.requestFocus(); mEditText.setFocusable( true ); mEditText.setFocusableInTouchMode( true ); }); |
5.安装APK Intent及其它共享文件相关Intent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
/* * 自Android N开始,是通过FileProvider共享相关文件,但是Android Q对公有目录 File API进行了限制,只能通过Uri来操作, * 从代码上看,又变得和以前低版本一样了,只是必须加上权限代码Intent.FLAG_GRANT_READ_URI_PERMISSION */ private void installApk() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){ //适配Android Q,注意mFilePath是通过ContentResolver得到的,上述有相关代码 Intent intent = new Intent(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 = new File(saveFileName + "demo.apk" ); if (!file.exists()) return ; Intent intent = new Intent(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); } |
6.Activity透明相关,windowIsTranslucent属性
Android Q 又一个天坑,如果你要显示一个半透明的Activity,这在android10之前普通样式Activity只需要设置windowIsTranslucent=true即可,但是到了AndroidQ,它没有效果了,而且如果动态设置View.setVisibility(),界面还会出现残影...
解决办法:使用Dialog样式Activity,且设置windowIsFloating=true,此时问题又来了,如果Activity根布局没有设置fitsSystemWindow=true,默认是没有侵入状态栏的,使界面看上去正常。
7.剪切板兼容
Android Q中只有当应用处于可交互情况(默认输入法本身就可交互)才能访问剪切板和监听剪切板变化,在onResume回调也无法直接访问剪切板,这么做的好处是避免了一些应用后台疯狂监听响应剪切板的内容,疯狂弹窗。
因此如果还需要监听剪切板,可以使用应用生命周期回调,监听APP后台返回,延迟几毫秒访问剪切板,再保存最后一次访问得到的剪切板内容,每次都比较一下是否有变化,再进行下一步操作。
8.第三方分享图片等操作,直接使用文件路径的,如QQ图片分享,都需要注意,这是不可行的,都只能通过MediaStore等API,拿到Uri来操作
这些是我们根据sdk升级到29时遇到的实际问题而罗列出来的,不是翻译AndroidQ中的行为变更,具体问题请根据自身实际自行解决。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:https://juejin.im/post/5ddb2b5b51882573461819e0