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添加如下節(jié)點代碼
<application android:usesCleartextTraffic="true">
第二種解決方案:在res目錄新建xml目錄牢贸,已建的跳過 在xml目錄新建一個xml文件network_security_config.xml竹观,然后在AndroidManifest.xml中Application添加如下節(jié)點代碼
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潜索、掃描系統(tǒng)相冊臭增、視頻等,圖片竹习、視頻選擇器都是通過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妨退,保存成功自動添加到相冊數據庫,無需再發(fā)送廣播告訴系統(tǒng)插入相冊
*
* @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浊仆,它沒有效果了客峭,而且如果動態(tài)設置View.setVisibility(),界面還會出現殘影...
解決辦法:使用Dialog樣式Activity,且設置windowIsFloating=true氧卧,此時問題又來了桃笙,如果Activity根布局沒有設置fitsSystemWindow=true,默認是沒有侵入狀態(tài)欄的沙绝,使界面看上去正常搏明。
7.剪切板兼容
Android Q中只有當應用處于可交互情況(默認輸入法本身就可交互)才能訪問剪切板和監(jiān)聽剪切板變化,在onResume回調也無法直接訪問剪切板闪檬,這么做的好處是避免了一些應用后臺瘋狂監(jiān)聽響應剪切板的內容星著,瘋狂彈窗。
因此如果還需要監(jiān)聽剪切板粗悯,可以使用應用生命周期回調虚循,監(jiān)聽APP后臺返回,延遲幾毫秒訪問剪切板样傍,再保存最后一次訪問得到的剪切板內容横缔,每次都比較一下是否有變化,再進行下一步操作衫哥。