您好,登錄后才能下訂單哦!
這篇“Android10適配的方法”文章的知識點大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“Android10適配的方法”文章吧。
老規(guī)矩,首先將我們項目中的 targetSdkVersion
改為 29。
在Android 10之前的版本上,我們在做文件的操作時都會申請存儲空間的讀寫權(quán)限。但是這些權(quán)限完全被濫用,造成的問題就是手機的存儲空間中充斥著大量不明作用的文件,并且應用卸載后它也沒有刪除掉。為了解決這個問題,Android 10 中引入了 Scoped Storage
的概念,通過添加外部存儲訪問限制來實現(xiàn)更好的文件管理。
首先明確一個概念,外部儲存和內(nèi)部儲存。
內(nèi)部儲存: /data
目錄。一般我們使用 getFilesDir()
或 getCacheDir()
方法獲取本應用的內(nèi)部儲存路徑,讀寫該路徑下的文件不需要申請儲存空間讀寫權(quán)限,且卸載應用時會自動刪除。
外部儲存: /storage
或 /mnt
目錄。一般我們使用 getExternalStorageDirectory()
方法獲取的路徑來存取文件。
因為不同廠商、系統(tǒng)版本的原因,所以上述的方法并沒有一個固定的文件路徑。了解了上面的概念,那我們所說的外部儲存訪問限制,可以認為是針對 getExternalStorageDirectory()
路徑下的文件。具體的規(guī)則如下表:
上圖將外部存儲空間分為了三部分:
特定目錄(App-specific),使用 getExternalFilesDir()
或 getExternalCacheDir()
方法訪問。無需權(quán)限,且卸載應用時會自動刪除。
照片、視頻、音頻這類媒體文件。使用 MediaStore
訪問,訪問其他應用的媒體文件時需要 READ_EXTERNAL_STORAGE
權(quán)限。
其他目錄,使用 存儲訪問框架SAF (Storage Access Framwork)
所以在Android 10上即使你擁有了儲存空間的讀寫權(quán)限,也無法保證可以正常的進行文件的讀寫操作。
適配
最簡單粗暴的方法就是在 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的操作),所以我將最終選擇的文件又轉(zhuǎn)存進了 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
權(quán)限,并使用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開始,基本每次都會有權(quán)限方面變動,這次也不例外。(前幾天發(fā)布了Android 11的預覽版,看來也有權(quán)限方面的變化。。。單次權(quán)限即將到來)
1.在后臺運行時訪問設備位置信息需要權(quán)限
Android 10 引入了 ACCESS_BACKGROUND_LOCATION
權(quán)限(危險權(quán)限)。
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
該權(quán)限允許應用程序在后臺訪問位置。如果請求此權(quán)限,則還必須請求 ACCESS_FINE_LOCATION
或 ACCESS_COARSE_LOCATION
權(quán)限。只請求此權(quán)限無效果。
在Android 10的設備上,如果你的應用的 targetSdkVersion
< 29,則在請求 ACCESS_FINE_LOCATION
或 ACCESS_COARSE_LOCATION
權(quán)限時,系統(tǒng)會自動同時請求 ACCESS_BACKGROUND_LOCATION
。在請求彈框中,選擇“始終允許”表示同意后臺獲取位置信息,選擇“僅在應用使用過程中允許”或"拒絕"選項表示拒絕授權(quán)。
如果你的應用的 targetSdkVersion
>= 29,則請求 ACCESS_FINE_LOCATION
或 ACCESS_COARSE_LOCATION
權(quán)限表示在前臺時擁有訪問設備位置信息的權(quán)。在請求彈框中,選擇“始終允許”表示前后臺都可以獲取位置信息,選擇“僅在應用使用過程中允許”只表示擁有前臺的權(quán)限。
其實官方 不推薦你使用申請后臺訪問權(quán)的方式 ,因為這樣的結(jié)果無非就是多請求一個權(quán)限,那么這像變更還有什么意義?申請過多的權(quán)限,也會造成用戶的反感。所以官方推薦使用 前臺服務
來實現(xiàn),在前臺服務中獲取位置信息。
首先在清單中對應的 service
中添加 android:foregroundServiceType="location"
:
<service android:name="MyNavigationService" android:foregroundServiceType="location" ... > ... </service>
啟動前臺服務前檢查是否具有前臺的訪問權(quán)限:
boolean permissionApproved = ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED; if (permissionApproved) { // 啟動前臺服務 } else { // 請求前臺訪問位置權(quán)限 }
如此一來就可以在 Service
中獲取位置信息。
2.一些電話、藍牙和WLAN的API需要精確位置權(quán)限
下面列舉了Android 10中必須具有 ACCESS_FINE_LOCATION
權(quán)限才能使用類和方法:
電話
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新增權(quán)限,上面有提到,不贅述了。
4.PROCESS_OUTGOING_CALLS
Android 10上該權(quán)限已廢棄。
簡單解釋就是 應用處于后臺時,無法啟動Activity 。比如點開一個應用會進入啟動頁或者廣告頁,一般會有幾秒的延時再跳轉(zhuǎn)至首頁。如果這期間你退到后臺,那么你將無法看到跳轉(zhuǎn)過程。而在之前的版本中,會強制彈出頁面至前臺。
既然是限制,那么肯定有不受限的情況,主要有以下幾點:
應用具有可見窗口,例如前臺 Activity。
應用在前臺任務的返回棧中已有的 Activity。
應用在 Recents
上現(xiàn)有任務的返回棧中已有的 Activity。 Recents
就是我們的任務管理列表。
應用收到系統(tǒng)的 PendingIntent
通知。
應用收到它應該在其中啟動界面的系統(tǒng)廣播。示例包括 ACTION_NEW_OUTGOING_CALL
和 SECRET_CODE_ACTION
。應用可在廣播發(fā)送幾秒鐘后啟動 Activity。
用戶已向應用授予 SYSTEM_ALERT_WINDOW
權(quán)限,或是在應用權(quán)限頁開啟 后臺彈出頁面
的開關(guān)。
因為此項行為變更適用于在 Android 10 上運行的所有應用,所以這一限制導致最明顯的問題就是點擊推送信息時,有些應用無法進行正常的跳轉(zhuǎn)(具體的實現(xiàn)問題導致)。所以針對這類問題,可以采取 PendingIntent
的方式,發(fā)送通知時使用 setContentIntent
方法。
當然你也可以申請相應權(quán)限或者白名單:
不過申請白名單這種方法受各種手機廠商所限,很麻煩。感覺還不如引導用戶手動開啟權(quán)限。。。
對于全屏 intent,注意設置最高優(yōu)先級和添加 USE_FULL_SCREEN_INTENT
權(quán)限,這是一個普通權(quán)限。比如微信來語音或者視頻通話時,彈出的接聽頁面就是使用這一功能。
<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都讓用戶反感,那他也可以關(guān)掉你的通知,不至于任你擺布。)
Android 10 新增了一個系統(tǒng)級的深色主題(在系統(tǒng)設置中開啟)。雖然深色主題并不是強制適配項,但是它可以帶給用戶更好的體驗:
可大幅減少耗電量。 OLED
屏幕中每個像素都是自主發(fā)光,所以在顯示深色元素時像素所消耗的電流更低,尤其在純黑顏色時像素點可以完全關(guān)閉來達到省電的效果。
為弱視以及對強光敏感的用戶提高可視性。深色可以降低屏幕的整體視覺亮度,減少對眼睛的視覺壓力。
讓所有人都可以在光線較暗的環(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。
上述內(nèi)容我直接照搬文檔的說明??偨Y(jié)一下,使用 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
方法中就可以獲?。?/p>
@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: // 關(guān)閉 break; case Configuration.UI_MODE_NIGHT_YES: // 開啟 break; default: break; } }
詳細的內(nèi)容你可以參看官方文檔 和官方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
特許權(quán)限才能正常使用以上這些方法。
如果你的應用沒有該權(quán)限,卻仍然使用了以上的方法,則返回的結(jié)果會因目標 SDK 版本而異:
如果應用以 Android 10 或更高版本為目標平臺 ,則會發(fā)生 SecurityException
。
如果應用以 Android 9(API 級別 28)或更低版本為目標平臺 ,則相應方法會返回 null 或占位符數(shù)據(jù)(如果應用具有 READ_PHONE_STATE
權(quán)限)。否則,會發(fā)生 SecurityException
。
這項改動表示第三方應用無法獲取 Device ID
這類唯一標識。如果你需要唯一標識符,請參閱文檔: 唯一標識符的最佳做法 。
當然你也可以試試移動安全聯(lián)盟(MSA)聯(lián)合多家廠商共同開發(fā)的 統(tǒng)一補充設備標識調(diào)用SDK 。據(jù)說還有點不穩(wěn)定,因為我暫時還沒有嘗試過,所以不做評價。
限制了對剪貼板數(shù)據(jù)的訪問權(quán)限
除非您的應用是默認輸入法 (IME) 或是目前處于焦點的應用,否則它無法訪問 Android 10 或更高版本平臺上的剪貼板數(shù)據(jù)。
對啟用和停用 WLAN 實施了限制
以 Android 10 或更高版本為目標平臺的應用無法啟用或停用 WLAN。
WifiManager.setWifiEnabled()方法始終返回 false。
如果您需要提示用戶啟用或停用 WLAN,請使用設置面板。
以上就是關(guān)于“Android10適配的方法”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對大家有幫助,若想了解更多相關(guān)的知識內(nèi)容,請關(guān)注億速云行業(yè)資訊頻道。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。