您好,登錄后才能下訂單哦!
這篇文章主要介紹了Android10適配的示例分析,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
老規(guī)矩,首先將我們項目中的 targetSdkVersion
改為 29。
在Android 10之前的版本上,我們在做文件的操作時都會申請存儲空間的讀寫權限。但是這些權限完全被濫用,造成的問題就是手機的存儲空間中充斥著大量不明作用的文件,并且應用卸載后它也沒有刪除掉。為了解決這個問題,Android 10 中引入了 Scoped Storage
的概念,通過添加外部存儲訪問限制來實現(xiàn)更好的文件管理。
首先明確一個概念,外部儲存和內部儲存。
內部儲存: /data
目錄。一般我們使用 getFilesDir()
或 getCacheDir()
方法獲取本應用的內部儲存路徑,讀寫該路徑下的文件不需要申請儲存空間讀寫權限,且卸載應用時會自動刪除。
外部儲存: /storage
或 /mnt
目錄。一般我們使用 getExternalStorageDirectory()
方法獲取的路徑來存取文件。
因為不同廠商、系統(tǒng)版本的原因,所以上述的方法并沒有一個固定的文件路徑。了解了上面的概念,那我們所說的外部儲存訪問限制,可以認為是針對 getExternalStorageDirectory()
路徑下的文件。具體的規(guī)則如下表:
上圖將外部存儲空間分為了三部分:
特定目錄(App-specific),使用 getExternalFilesDir()
或 getExternalCacheDir()
方法訪問。無需權限,且卸載應用時會自動刪除。
照片、視頻、音頻這類媒體文件。使用 MediaStore
訪問,訪問其他應用的媒體文件時需要 READ_EXTERNAL_STORAGE
權限。
其他目錄,使用 存儲訪問框架SAF (Storage Access Framwork)
所以在Android 10上即使你擁有了儲存空間的讀寫權限,也無法保證可以正常的進行文件的讀寫操作。
適配
最簡單粗暴的方法就是在 AndroidManifest.xml
中添加 android:requestLegacyExternalStorage="true"
來請求使用舊的存儲模式。
但是我不推薦此方法。因為在下一個版本的Android中,此條配置將會失效,將強制采用外部儲存限制。其實早在Android Q Beta 3之前都是強制的,但為了給開發(fā)者適配的時間才沒有強制執(zhí)行。所以如果你不抓住這段時間去適配,那么今年下半年出了Android 11。。。直接開花~~
如果你已經(jīng)適配Android 10,這里有個現(xiàn)象要 注意一下 :
如果應用通過升級安裝,那么還會使用以前的儲存模式(Legacy View)。只有通過首次安裝或是卸載重新安裝才能啟用新模式(Filtered View)。
所以在適配時,我們的判斷代碼如下:
// 使用Environment.isExternalStorageLegacy()來檢查APP的運行模式 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !Environment.isExternalStorageLegacy()) { }
這樣的好處是你可以在用戶升級后,能方便的將用戶的數(shù)據(jù)移動至應用的特定目錄。否則你只能通過SAF去移動,這樣會非常麻煩。如果你要移動數(shù)據(jù)注意只適用于Android 10下,所以現(xiàn)在適配反而是一個好時機。當然如果你不需要遷移數(shù)據(jù),那適配會更省事。
下面就說說推薦適配方案:
對于應用中涉及的文件操作,修改一下你的文件路徑。
以前我們習慣使用 Environment.getExternalStorageDirectory()
方法,那么現(xiàn)在可以使用 getExternalFilesDir()
方法(包括下載的安裝包這類的文件)。如果是緩存類型文件,可以放到 getExternalCacheDir()
路徑下。
或者使用 MediaStore
,將文件存至對應的媒體類型中(圖片: MediaStore.Images
,視頻: MediaStore.Video
,音頻: MediaStore.Audio
),不過僅限于多媒體文件。
下面代碼將圖片保存到公共目錄下,返回Uri:
public static Uri createImageUri(Context context) { ContentValues values = new ContentValues(); // 需要指定文件信息時,非必須 values.put(MediaStore.Images.Media.DESCRIPTION, "This is an image"); values.put(MediaStore.Images.Media.DISPLAY_NAME, "Image.png"); 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/test"); return context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); }
對于媒體資源的訪問:比如圖片選擇器這類的場景。無法直接使用File,而應使用Uri。否則報錯如下:
java.io.FileNotFoundException: open failed: EACCES (Permission denied)
比如我在適配項目中使用的圖片選擇器時,首先修改了 Glide
通過加載File的方式顯示圖片。改為加載Uri的方式,否則圖片無法顯示出來。
Uri的獲取方式還是使用 MediaStore
:
String id = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)); Uri uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
其次為了便于不影響之前選擇圖片返回File的邏輯(因為一般都是上傳File,沒有直接上傳Uri的操作),所以我將最終選擇的文件又轉存進了 getExternalFilesDir()
,主要代碼如下:
File imgFile = this.getExternalFilesDir("image"); if (!imgFile.exists()){ imgFile.mkdir(); } try { File file = new File(imgFile.getAbsolutePath() + File.separator + System.currentTimeMillis() + ".jpg"); // 使用openInputStream(uri)方法獲取字節(jié)輸入流 InputStream fileInputStream = getContentResolver().openInputStream(uri); FileOutputStream fileOutputStream = new FileOutputStream(file); byte[] buffer = new byte[1024]; int byteRead; while (-1 != (byteRead = fileInputStream.read(buffer))) { fileOutputStream.write(buffer, 0, byteRead); } fileInputStream.close(); fileOutputStream.flush(); fileOutputStream.close(); // 文件可用新路徑 file.getAbsolutePath() } catch (Exception e) { e.printStackTrace(); }
如果你要獲取圖片中的地理位置信息,需要申請 ACCESS_MEDIA_LOCATION
權限,并使用MediaStore.setRequireOriginal()獲取。下面是官方的示例代碼:
Uri photoUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cursor.getString(idColumnIndex)); final double[] latLong; // 從ExifInterface類獲取位置信息 photoUri = MediaStore.setRequireOriginal(photoUri); InputStream stream = getContentResolver().openInputStream(photoUri); if (stream != null) { ExifInterface exifInterface = new ExifInterface(stream); double[] returnedLatLong = exifInterface.getLatLong(); // If lat/long is null, fall back to the coordinates (0, 0). latLong = returnedLatLong != null ? returnedLatLong : new double[2]; // Don't reuse the stream associated with the instance of "ExifInterface". stream.close(); } else { // Failed to load the stream, so return the coordinates (0, 0). latLong = new double[2]; }
這樣下來,一個圖片選擇器就基本適配完了。
補充
應用在卸載后,會將 App-specific
目錄下的數(shù)據(jù)刪除,如果在 AndroidManifest.xml
中聲明: android:hasFragileUserData="true"
用戶可以選擇是否保留。
對于 SAF
的使用,可以查看我之前寫的 SAF使用攻略 ,這里就不展開說了。
最后這里有一個介紹Scoped Storage的視頻,推薦 觀看 :
從6.0開始,基本每次都會有權限方面變動,這次也不例外。(前幾天發(fā)布了Android 11的預覽版,看來也有權限方面的變化。。。單次權限即將到來)
1.在后臺運行時訪問設備位置信息需要權限
Android 10 引入了 ACCESS_BACKGROUND_LOCATION
權限(危險權限)。
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
該權限允許應用程序在后臺訪問位置。如果請求此權限,則還必須請求 ACCESS_FINE_LOCATION
或 ACCESS_COARSE_LOCATION
權限。只請求此權限無效果。
在Android 10的設備上,如果你的應用的 targetSdkVersion
< 29,則在請求 ACCESS_FINE_LOCATION
或 ACCESS_COARSE_LOCATION
權限時,系統(tǒng)會自動同時請求 ACCESS_BACKGROUND_LOCATION
。在請求彈框中,選擇“始終允許”表示同意后臺獲取位置信息,選擇“僅在應用使用過程中允許”或"拒絕"選項表示拒絕授權。
如果你的應用的 targetSdkVersion
>= 29,則請求 ACCESS_FINE_LOCATION
或 ACCESS_COARSE_LOCATION
權限表示在前臺時擁有訪問設備位置信息的權。在請求彈框中,選擇“始終允許”表示前后臺都可以獲取位置信息,選擇“僅在應用使用過程中允許”只表示擁有前臺的權限。
總結一下就是下圖:
其實官方 不推薦你使用申請后臺訪問權的方式 ,因為這樣的結果無非就是多請求一個權限,那么這像變更還有什么意義?申請過多的權限,也會造成用戶的反感。所以官方推薦使用 前臺服務
來實現(xiàn),在前臺服務中獲取位置信息。
首先在清單中對應的 service
中添加 android:foregroundServiceType="location"
:
<service android:name="MyNavigationService" android:foregroundServiceType="location" ... > ... </service>
啟動前臺服務前檢查是否具有前臺的訪問權限:
boolean permissionApproved = ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED; if (permissionApproved) { // 啟動前臺服務 } else { // 請求前臺訪問位置權限 }
如此一來就可以在 Service
中獲取位置信息。
2.一些電話、藍牙和WLAN的API需要精確位置權限
下面列舉了Android 10中必須具有 ACCESS_FINE_LOCATION
權限才能使用類和方法:
電話
TelephonyManager
getCellLocation()
getAllCellInfo()
requestNetworkScan()
requestCellInfoUpdate()
getAvailableNetworks()
getServiceState()
TelephonyScanManager
requestNetworkScan()
TelephonyScanManager.NetworkScanCallback
onResults()
PhoneStateListener
onCellLocationChanged()
onCellInfoChanged()
onServiceStateChanged()
WLAN
WifiManager
startScan()
getScanResults()
getConnectionInfo()
getConfiguredNetworks()
WifiAwareManager
WifiP2pManager
WifiRttManager
藍牙
BluetoothAdapter
startDiscovery()
startLeScan()
BluetoothAdapter.LeScanCallback
BluetoothLeScanner
startScan()
我們可以根據(jù)上面提供的具體類和方法,在適配項目中檢查是否有使用到并及時處理。
3.ACCESS_MEDIA_LOCATION
Android 10新增權限,上面有提到,不贅述了。
4.PROCESS_OUTGOING_CALLS
Android 10上該權限已廢棄。
簡單解釋就是 應用處于后臺時,無法啟動Activity 。比如點開一個應用會進入啟動頁或者廣告頁,一般會有幾秒的延時再跳轉至首頁。如果這期間你退到后臺,那么你將無法看到跳轉過程。而在之前的版本中,會強制彈出頁面至前臺。
既然是限制,那么肯定有不受限的情況,主要有以下幾點:
應用具有可見窗口,例如前臺 Activity。
應用在前臺任務的返回棧中已有的 Activity。
應用在 Recents
上現(xiàn)有任務的返回棧中已有的 Activity。 Recents
就是我們的任務管理列表。
應用收到系統(tǒng)的 PendingIntent
通知。
應用收到它應該在其中啟動界面的系統(tǒng)廣播。示例包括 ACTION_NEW_OUTGOING_CALL
和 SECRET_CODE_ACTION
。應用可在廣播發(fā)送幾秒鐘后啟動 Activity。
用戶已向應用授予 SYSTEM_ALERT_WINDOW
權限,或是在應用權限頁開啟 后臺彈出頁面
的開關。
因為此項行為變更適用于在 Android 10 上運行的所有應用,所以這一限制導致最明顯的問題就是點擊推送信息時,有些應用無法進行正常的跳轉(具體的實現(xiàn)問題導致)。所以針對這類問題,可以采取 PendingIntent
的方式,發(fā)送通知時使用 setContentIntent
方法。
當然你也可以申請相應權限或者白名單:
不過申請白名單這種方法受各種手機廠商所限,很麻煩。感覺還不如引導用戶手動開啟權限。。。
對于全屏 intent,注意設置最高優(yōu)先級和添加 USE_FULL_SCREEN_INTENT
權限,這是一個普通權限。比如微信來語音或者視頻通話時,彈出的接聽頁面就是使用這一功能。
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
Intent fullScreenIntent = new Intent(this, CallActivity.class); PendingIntent fullScreenPendingIntent = PendingIntent.getActivity(this, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT); NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.notification_icon) .setContentTitle("Incoming call") .setContentText("(919) 555-1234") .setPriority(NotificationCompat.PRIORITY_HIGH) // <--- 高優(yōu)先級 .setCategory(NotificationCompat.CATEGORY_CALL) // Use a full-screen intent only for the highest-priority alerts where you // have an associated activity that you would like to launch after the user // interacts with the notification. Also, if your app targets Android 10 // or higher, you need to request the USE_FULL_SCREEN_INTENT permission in // order for the platform to invoke this notification. .setFullScreenIntent(fullScreenPendingIntent, true); // <--- 全屏 intent Notification incomingCallNotification = notificationBuilder.build();
注意:在部分手機上,直接設置 setPriority
無效(或者說以渠道優(yōu)先級為準)。所以需要創(chuàng)建通知渠道時將重要性設置為 IMPORTANCE_HIGH
。
NotificationChannel channel = new NotificationChannel(channelId, "xxx", NotificationManager.IMPORTANCE_HIGH);
后臺啟動 Activity 的限制的目的是為了減少對用戶操作的中斷。如果你有要彈出的頁面,推薦你先彈出通知,讓用戶自己選擇接下來的操作,而不是一股腦的強制彈出。(如果你的全屏intent都讓用戶反感,那他也可以關掉你的通知,不至于任你擺布。)
Android 10 新增了一個系統(tǒng)級的深色主題(在系統(tǒng)設置中開啟)。雖然深色主題并不是強制適配項,但是它可以帶給用戶更好的體驗:
可大幅減少耗電量。 OLED
屏幕中每個像素都是自主發(fā)光,所以在顯示深色元素時像素所消耗的電流更低,尤其在純黑顏色時像素點可以完全關閉來達到省電的效果。
為弱視以及對強光敏感的用戶提高可視性。深色可以降低屏幕的整體視覺亮度,減少對眼睛的視覺壓力。
讓所有人都可以在光線較暗的環(huán)境中更輕松地使用設備。
適配方法有兩種:
1.手動適配(資源替換)
官方文檔中提到的繼承 Theme.AppCompat.DayNight
或者 Theme.MaterialComponents.DayNight
的方法,但這只是將我們使用的各種View的默認樣式進行了適配,并不太適用于實際項目的適配。因為具體的項目中的View都按照設計的風格進行了重定義。
其實適配的方法很簡單,類似屏幕適配、國際化的操作,并不需要繼承上面的主題。比如你要修改顏色,就在 res
下新建 values-night
目錄,創(chuàng)建對應的 colors.xml
文件。將具體要修改的色值定義在里面。圖標之類的也是一個思路,創(chuàng)建對應的 drawable-night
目錄。
只要你之前的代碼不是硬編碼且代碼規(guī)范,那么適配起來還是很輕松。
2.自動適配(Force Dark)
Android 10 提供 Force Dark 功能。一如其名,此功能可讓開發(fā)者快速實現(xiàn)深色主題背景,而無需明確設置 DayNight 主題背景。
如果您的應用采用淺色主題背景,則 Force Dark 會分析應用的每個視圖,并在相應視圖在屏幕上顯示之前,自動應用深色主題背景。有些開發(fā)者會混合使用 Force Dark 和本機實現(xiàn),以縮短實現(xiàn)深色主題背景所需的時間。
應用必須選擇啟用 Force Dark,方法是在其主題背景中設置 android:forceDarkAllowed="true"
。此屬性會在所有系統(tǒng)及 AndroidX 提供的淺色主題背景(例如 Theme.Material.Light)上設置。使用 Force Dark 時,您應確保全面測試應用,并根據(jù)需要排除視圖。
如果您的應用使用 Dark Theme
主題(例如Theme.Material),則系統(tǒng)不會應用 Force Dark。同樣,如果應用的主題背景繼承自 DayNight
主題(例如Theme.AppCompat.DayNight),則系統(tǒng)不會應用 Force Dark,因為會自動切換主題背景。
您可以通過 android:forceDarkAllowed
布局屬性或 setForceDarkAllowed(boolean)
在特定視圖上控制 Force Dark。
上述內容我直接照搬文檔的說明??偨Y一下,使用 Force Dark
需要注意幾點:
如果使用的是 DayNight
或 Dark Theme
主題,則設置 forceDarkAllowed
不生效。
如果有需要排除適配的部分,可以在對應的View上設置 forceDarkAllowed
為false。
這里說說我實際使用此方法的感受: 整體還是不錯的,設置的色值會自動取反。但也因此顏色不受控制,能否達到預期效果是個需要注意的問題。追求快速適配可以采取此方案。
手動切換主題
使用 AppCompatDelegate.setDefaultNightMode(@NightMode int mode)
方法,其中參數(shù) mode
有以下幾種:
淺色 - MODE_NIGHT_NO
深色 - MODE_NIGHT_YES
由省電模式設置 - MODE_NIGHT_AUTO_BATTERY
系統(tǒng)默認 - MODE_NIGHT_FOLLOW_SYSTEM
下面的代碼是官方Demo中的使用示例:
public class ThemeHelper { public static final String LIGHT_MODE = "light"; public static final String DARK_MODE = "dark"; public static final String DEFAULT_MODE = "default"; public static void applyTheme(@NonNull String themePref) { switch (themePref) { case LIGHT_MODE: { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); break; } case DARK_MODE: { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); break; } default: { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); } else { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY); } break; } } } }
通過 AppCompatDelegate.getDefaultNightMode()
方法,可以獲取到當前的模式,這樣便于代碼中去適配。
監(jiān)聽深色主題是否開啟
首先在清單文件中給對應的Activity配置 android:configChanges="uiMode"
:
<activity android:name=".MyActivity" android:configChanges="uiMode" />
這樣在 onConfigurationChanged
方法中就可以獲取:
@Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK; switch (currentNightMode) { case Configuration.UI_MODE_NIGHT_NO: // 關閉 break; case Configuration.UI_MODE_NIGHT_YES: // 開啟 break; default: break; } }
詳細的內容你可以參看官方文檔 和官方Demo 。
判斷深色主題是否開啟
其實和上面 onConfigurationChanged
方法同理:
public static boolean isNightMode(Context context) { int currentNightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; return currentNightMode == Configuration.UI_MODE_NIGHT_YES; }
對不可重置的設備標識符實施了限制
受影響的方法包括:
Build
getSerial()
TelephonyManager
getImei()
getDeviceId()
getMeid()
getSimSerialNumber()
getSubscriberId()
從 Android 10 開始,應用必須具有 READ_PRIVILEGED_PHONE_STATE
特許權限才能正常使用以上這些方法。
如果你的應用沒有該權限,卻仍然使用了以上的方法,則返回的結果會因目標 SDK 版本而異:
如果應用以 Android 10 或更高版本為目標平臺 ,則會發(fā)生 SecurityException
。
如果應用以 Android 9(API 級別 28)或更低版本為目標平臺 ,則相應方法會返回 null 或占位符數(shù)據(jù)(如果應用具有 READ_PHONE_STATE
權限)。否則,會發(fā)生 SecurityException
。
這項改動表示第三方應用無法獲取 Device ID
這類唯一標識。如果你需要唯一標識符,請參閱文檔: 唯一標識符的最佳做法 。
當然你也可以試試移動安全聯(lián)盟(MSA)聯(lián)合多家廠商共同開發(fā)的 統(tǒng)一補充設備標識調用SDK 。據(jù)說還有點不穩(wěn)定,因為我暫時還沒有嘗試過,所以不做評價。
限制了對剪貼板數(shù)據(jù)的訪問權限
除非您的應用是默認輸入法 (IME) 或是目前處于焦點的應用,否則它無法訪問 Android 10 或更高版本平臺上的剪貼板數(shù)據(jù)。
對啟用和停用 WLAN 實施了限制
以 Android 10 或更高版本為目標平臺的應用無法啟用或停用 WLAN。
WifiManager.setWifiEnabled()方法始終返回 false。
如果您需要提示用戶啟用或停用 WLAN,請使用設置面板。
Android10上對折疊屏設備有了更好的支持,對于有折疊屏適配的需求,可以參看為可折疊設備構建應用 和 華為折疊屏應用開發(fā)指導。
感謝你能夠認真閱讀完這篇文章,希望小編分享的“Android10適配的示例分析”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業(yè)資訊頻道,更多相關知識等著你來學習!
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內容。