Android10填坑適配指南(實際經驗程式碼)
今天看到一篇好的文章,分享給大家,膜拜大佬。
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(),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,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();判斷公有目錄檔案是否存在,正確方式如下:
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,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,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來操作
這些是我們根據sdk升級到29時遇到的實際問題而羅列出來的,不是翻譯AndroidQ中的行為變更,具體問題請根據自身實際自行解決。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。