Android10填坑适配指南,实际经验代码,拒绝翻译

作者: 黄海彬 | 来源:发表于2019-11-25 16:22 被阅读0次

    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); 引起的异常,参考源码如下:

    @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 得到兼容,几乎所有的博客解决方案都是如下简单粗暴:

    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中的媒体资源读写

    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){
            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到公有目录):

    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、保存图片相关

     /**
         * 通过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中加入如下代码,可获得焦点,如需要弹出键盘可延迟一下:

    mEditText.post(() -> {
           mEditText.requestFocus();
           mEditText.setFocusable(true);
           mEditText.setFocusableInTouchMode(true);
    });
    

    5.安装APK Intent及其它共享文件相关Intent

    /*
    * 自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来操作

    目前我们实际遇到的问题就这些

    相关文章

      网友评论

        本文标题:Android10填坑适配指南,实际经验代码,拒绝翻译

        本文链接:https://www.haomeiwen.com/subject/kudjwctx.html