开源中国客户端 Android 10 经验适配指南,含代码
我们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 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 得到兼容,目前不清楚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 = 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添加如下节点代码
<application android:usesCleartextTraffic="true">
第二种解决方案:在res目录新建xml目录,已建的跳过 在xml目录新建一个xml文件network_security_config.xml,然后在AndroidManifest.xml中Application添加如下节点代码
android:networkSecurityConfig="@xml/network_config"
名字随机,内容如下:
<?xml version="1.0" encoding="utf-8"?> <network-security-config> <base-config cleartextTrafficPermitted="true" /> </network-security-config>
3.Android Q(10)中的媒体资源读写
相关的Android Q 行为变更不做细说,网上大部分博客关于Android Q 适配都在说行为变更,我们将根据实际遇到的问题,实际解决
1、扫描系统相册、视频等,图片、视频选择器都是通过ContentResolver来提供,主要代码如下:
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[4] + " 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();判断公有目录文件是否存在,正确方式如下:
public static boolean isAndroidQFileExists(Context context, String path){ if (context == null) { return false; } AssetFileDescriptor afd = null; ContentResolver cr = context.getContentResolver(); try { Uri uri = Uri.parse(path); afd = cr.openAssetFileDescriptor(Uri.parse(path), "r"); if (afd == null) { return false; } else { close(afd); } } catch (FileNotFoundException e) { return false; }finally { close(afd); } return true; }
3、保存或者下载文件到公有目录,保存Bitmap同理,如Download,MIME_TYPE类型可以自行参考对应的文件类型,这里只对APK作出说明
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); } 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 成功或者失败 */ 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 { Util.close(os, inputStream); } return result; }
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 */ 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 + "osc.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); }
我们APK开发实践中暂时遇到的坑就这些,当然Android Q的改动是相当大的,例如还有App私有沙箱文件、定位权限和后台弹出Activity限制,这些都必须根据自身实践去踩坑适配,有条件的尽可能去阅读官方文档,参考改进。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
账户系统如何应对高并发、热点账户等问题
互联网金融系统的核心是支付结算,而支付结算的基础又是账户系统。互金账户系统的特点是并发量大、响应快、交易金额大,热点账户问题突出。一个合格的账户系统既要解决上述问题,又必须绝对保证资金安全。作为宜信这家互联网金融公司的支付结算中心,其账户系统也必须具备上述特征。 一、账户体系 1.1 账户结构 宜信支付结算账户体系是客户、用户、账户三层结构,证件号和证件类型唯一确定一个客户,客户号和机构号确定一个用户,一个用户下可开多个不同类型的账户。如图: 1.2 账户属性 账户系统的基础是账户,所有的操作都围绕着账户进行,账户包含以下一些属性: 会计科目:每个账户金额的变动要体现一些会计的属性,以便会计核算。 账户类别:分为个人账户、企业账户、平台类账户。 账户明细:账户的明细是反映账户余额变动的每笔详情,采用复式记账法,包含本对方账号、账户等信息、摘要、借方的发生额及余额等信息。 账户余额:记录账户的实时余额。 1.3 会计科目 账户下挂在最底层的会计科目下,会计科目决定了账户的含义及余额变动方向。会计科目的一些属性如下: 科目类别:资产类、负债类、所有者权益、成本类、损益类等。 科目级别:会计...
- 下一篇
SBT无痛入门指南
SBT是 Scala 的构建工具,全称是 Simple Build Tool, 类似 Maven 或 Gradle。 SBT 的野心很大,采用Scala编程语言本身编写配置文件,这使得它稍显另类,虽然增强了灵活性,但是对于初学者来说同时也增加了上手难度。另外由于SBT默认从国外下载依赖,导致第一次构建非常缓慢,使用体验非常糟糕! 如果你是一名Scala初学者,本文希望帮你减轻一些第一次使用的痛苦。 本文的主要内容是帮助初学者从头到尾构建并运行一个Scala项目,重点在于讲解国内镜像仓库的配置。对于每一个操作步骤,会分别针对Windows、Mac和Linux三个主流操作系统进行讲解, 最终帮助你快速构建一个可运行的Scala开发环境。 第一步:安装SBT 单击这里下载SBT 1.3.0,下载完成后解压到指定目录,例如D:\Software\sbt-1.3.0,然后将D:\Software\sbt-1.3.0\bin添加至环境变量PATH。SBT 1.3.0 采用Coursier以无锁的方式并行下载依赖,极大地提升了使用体验! 请确认本机已安装Java运行环境。 第二步:设置国内仓库,加...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS关闭SELinux安全模块
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Linux系统CentOS6、CentOS7手动修改IP地址
- Hadoop3单机部署,实现最简伪集群
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS7,8上快速安装Gitea,搭建Git服务器