Android 學習筆記核心篇

薛定貓的諤發表於2019-01-22

基礎知識

底層原理

  • Android 作業系統是一個多使用者 Linux 作業系統,每個應用都是一個使用者
  • 作業系統一般會給每個應用分配一個唯一的 Linux 使用者 ID,這個 ID 對應用是不可見的。但有些情況下兩個應用可以共享同一個 Linux 使用者 ID,此時他們可以訪問彼此的檔案,甚至還可以執行在同一個 Linux 程式中,共享同一個虛擬機器。但兩個應用的簽名必須是一樣的
  • 每個程式都有自己的虛擬機器,一般每個應用都執行在自己的 Linux 程式中

應用元件

  • 應用沒有唯一的入口,沒有 main() 函式,因為應用是由多個元件拼湊在一起的,每個元件都是系統或者使用者進入應用的入口,元件之間既可以是相互獨立的,也可以是相互依賴的。系統和其它應用在被允許的情況下可以啟動/啟用一個應用的任意一個元件
  • 元件有四種型別: ActivityServiceBroadcastReceiverContentProvider

Activity

  • Activity 表示一個新的使用者介面,只能由系統進行建立和銷燬,應用只能監聽到一些生命週期回撥,這些回撥通常也被叫作生命週期方法
  • Activity 的名字一旦確定好就不要再更改了,否則可能會引發一系列問題

Service

  • Service 表示一個後臺服務,Service 可以是獨立的,可以在應用退出後繼續執行。也可以繫結到其他程式或 Activity,表示其他程式想使用這個 Service,像輸入法、動態桌布、屏保等系統功能都是以 Service 的形式存在的,在需要執行的時候進行繫結
  • 大部分情況下,更建議使用 JobScheduler,因為 JobSchedulerDoze API 配合下一般會比簡單使用 Service 更省電

BroadcastReceiver

  • BroadcastReceiver 是一個事件傳遞的元件,通過它應用可以響應系統範圍的廣播通知。系統的包管理器會在安裝應用時將應用中的靜態廣播接收器註冊好,所以即使應用沒在執行,系統也能把事件傳遞到該元件。
  • 通過 BroadcastReceiver 可以實現程式間通訊

ContentProvider

  • ContentProvider 是在多個應用間共享資料的元件,如果應用的一些資料想要被其它應用使用,必須通過 ContentPrivider 進行管理,不過應用的私有資料也可以通過 ContentProvider 進行管理,主要還是因為 ContentProvider 提供了共享資料的抽象,使用者不需要知道資料究竟是以檔案形式還是資料庫等其他形式儲存的,只需要通過 ContentProvider 提供的 統一的 API 進行資料的增刪改查即可。同時 ContentProvider 還提供了 安全 環境,可以根據需要方便地控制資料的訪問許可權,不需要手動控制檔案許可權或資料庫許可權
  • 為了安全,也為了方便,一般需要通過 ContentResolver 操作 ContentProvider
  • 通過 ContentProvider 可以實現程式間通訊

啟用元件

  • 應用不能也不應該直接啟用其它應用的任意一個元件,但是系統可以,所以要想啟用一個元件,需要給系統發一個訊息詳細說明你的意圖( Intent ),之後系統就會為你啟用這個元件
  • ActivityServiceBroadcastReceiver 都需要通過被稱為 Intent 的非同步訊息啟用
  • 被啟用元件返回的結果也是 Intent 形式的
  • ContentProvider 只有在收到 ContentResolver 的請求時才會被啟用
  • 只有 BroadcastReceiver 可以不在 manifest 檔案中註冊,因為有些 BroadcastReceiver 需要在程式執行時動態地註冊和登出。而其它元件必須在 manifest 檔案中註冊,否則無法被系統記錄,也就無法被啟用
  • 如果 Intent 通過元件類名顯式指明瞭唯一的目標元件,那麼這個 Intent 就是顯式的,否則就是隱式的。隱式 Intent 一般只描述要執行動作的型別,必要時可以攜帶資料,系統會根據這個隱式 Intent 的描述決定啟用哪個元件,如果有多個元件符合啟用條件,系統一般會彈出選擇框讓使用者選擇到底啟用哪個元件
  • Service 必須使用顯式 Intent 啟用,不能宣告 IntentFilter
  • 啟動指定的 Activity 使用顯式 Intent,啟動隨便一個能完成指定工作的 Activity 使用隱式 Intent。能完成指定工作的那些想要被隱式 Intent 啟用的 Activity 需要事先宣告好 IntentFilter 表示自己有能力處理什麼工作,IntentFilter 一般通過 能完成的動作 、意圖型別 和 額外資料 來描述
  • 要想被隱式 Intent 啟用,意圖型別至少要包含 android.intent.category.DEFAULT 的意圖型別
  • 在使用隱式 Intent 啟用 Activity 之前一定要檢查一下有沒有 Activity 能處理這個 Intent :
if (sendIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(sendIntent);
}
複製程式碼
PackageManager packageManager = getPackageManager();
List<ResolveInfo> activities = packageManager.queryIntentActivities(intent,
        PackageManager.MATCH_DEFAULT_ONLY);
boolean isIntentSafe = activities.size() > 0;
複製程式碼
  • 使用隱式 Intent 時每次都強制使用者選擇一個元件啟用:
Intent intent = new Intent(Intent.ACTION_SEND);
String title = getResources().getString(R.string.chooser_title);
Intent chooser = Intent.createChooser(intent, title);
if (intent.resolveActivity(getPackageManager()) != null) {
    startActivity(chooser);
}
複製程式碼
  • 如果想要你的 Activity 能被隱式 Intent 啟用,如果想要某個 連結 能直接跳轉到你的 Activity,必須配置好 IntentFilter。這種連結分為兩種: Deep linksAndroid App Links
  • Deep links 對連結的 scheme 沒有要求,對系統版本也沒有要求,也不會驗證連結的安全性,不過需要一個 android.intent.action.VIEW 的 action 以便 Google Search 能直接開啟,需要 android.intent.category.DEFAULT 的 category 才能響應隱式 Intent,需要 android.intent.category.BROWSABLE 的 category 瀏覽器開啟連結時才能跳轉到應用,所以經典用例如下。一個 intent filter 最好只宣告一個 data 描述,否則你得考慮和測試所有變體的情況。系統處理這個連結的流程為: 如果使用者之前指定了開啟這個連結的預設應用就直接開啟這個應用 → 如果只有一個應用可以處理這個連結就直接開啟這個應用 → 彈窗讓使用者選擇用哪個應用開啟
<activity
    android:name="com.example.android.GizmosActivity"
    android:label="@string/title_gizmos" >
    <intent-filter android:label="@string/filter_view_http_gizmos">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <!-- Accepts URIs that begin with "http://www.example.com/gizmos” -->
        <data android:scheme="http"
              android:host="www.example.com"
              android:pathPrefix="/gizmos" />
        <!-- note that the leading "/" is required for pathPrefix-->
    </intent-filter>
    <intent-filter android:label="@string/filter_view_example_gizmos">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <!-- Accepts URIs that begin with "example://gizmos” -->
        <data android:scheme="example"
              android:host="gizmos" />
    </intent-filter>
</activity>
複製程式碼
  • Android App Links 是一種特殊的 Deep links,要求連結必須是你自己網站的 HTTP URL 連結,系統版本至少是 Android 6.0 (API level 23),優點是安全且具體,其他應用不能使用你的連結,不過你得先 驗證你的連結,由於連結和網站連結一致所以可以無縫地在應用和網站間切換,可以支援 Instant App,可以通過瀏覽器、谷歌搜尋 APP、系統螢幕搜尋、甚至 Google Assistant 的連結直接跳轉到應用。驗證連結的流程為: 將 <intent-filter> 標籤的 android:autoVerify 設定為 true 以告訴系統自動驗證你的應用屬於這個 HTTP URL 域名 → 填寫好網站域名和應用 ID 並使用簽名檔案生成 Digital Asset Links JSON 檔案 → 將檔案上傳到伺服器,訪問路徑為 https://domain.name/.well-known/assetlinks.json ,響應格式為 application/json,子域名也需要存在對應的檔案,一個域名可以關聯多個應用,一個應用也可以關聯多個域名,且可以使用相同的簽名 → 利用編輯器外掛完成關聯並驗證
  • 使用 Intent Scheme URL 需要做過濾。如果瀏覽器支援 Intent Scheme Uri 語法,如果過濾不當,那麼惡意使用者可能通過瀏 覽器 js 程式碼進行一些惡意行為,比如盜取 cookie 等。如果使用了 Intent#parseUri() 方法,獲取的 intent 必須嚴格過濾,intent 至少包含 addCategory(“android.intent.category.BROWSABLE”)setComponent(null)setSelector(null) 3 個策略
  • 開放的 Activity/Service/BroadcastReceiver 等需要對傳入的 intent 做合法性校驗

應用資源

  • 新增資源限定符的順序為: SIM 卡所屬的國家程式碼和移動網程式碼 → 語言區域程式碼 → 佈局方向 → 最小寬度 → 可用寬度 → 可用高度 → 螢幕大不大 → 螢幕長不長 → 螢幕圓不圓 → 螢幕色域寬不寬 → 螢幕支援的動態範圍高不高 → 螢幕方向 → 裝置的 UI 模式 → 夜間模式 → 螢幕畫素密度 → 觸控式螢幕型別 → 鍵盤型別 → 主要的文字輸入方式 → 導航鍵是否可用 → 主要的非觸控導航方式 → 支援的 API level
  • 一個資源目錄的每種資源限定符最多隻能出現一次
  • 必須提供預設的資原始檔
  • 資源目錄名是大小寫不敏感的
  • drawable 資源取別名:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <drawable name="icon">@drawable/icon_ca</drawable>
</resources>
複製程式碼
  • 佈局檔案取別名:
<?xml version="1.0" encoding="utf-8"?>
<merge>
    <include layout="@layout/main_ltr"/>
</merge>
複製程式碼
  • 只有動畫、選單、raw 資源 以及 xml/ 目錄中的資源不能使用別名
  • 尋找使用最優資源的流程:
    res
  • 在應用程式執行時,裝置的配置可能會發生變化(如螢幕方向變化、切換到多視窗模式,切換了系統語言),預設情況下系統會銷燬重建正在執行的 Activity ,所以應用程式必須保證銷燬重建的過程中使用者的資料和頁面狀態完好無損地恢復。如果不想系統銷燬重建你的 Activity 只需要在 manifest 檔案的 <activity> 標籤的 android:configChanges 屬性中新增你想自己處理的配置更改,多個配置使用 "|" 隔開,此時系統就不會在這些配置更改後銷燬重建你的這個 Activity 而是直接呼叫它的 onConfigurationChanged() 回撥方法,你需要在這個回撥中自己處理配置更改後的行為。
  • Activity 的銷燬重建不但發生在裝置配置更改後,只要使用者離開了某個 Activity,那麼那個 Activity 就隨時可能被系統銷燬。所以銷燬重建是無法避免的,也不應該逃避,而是應該想辦法儲存和恢復狀態
  • 由於各種各樣的硬體都能安裝 Android 作業系統,Android 作業系統之間也可能千差萬別,而應用程式的一些功能是與這些軟硬體息息相關的,如拍照應用需要裝置必須有攝像頭才能正常工作。應用可以通過 <uses-feature> 標籤宣告只有滿足這些軟硬體要求的裝置才能安裝,通過它的 android:required 屬性設定該要求是不是必須的,程式中可以通過 PackageManager.hasSystemFeature() 方法判斷

核心知識

Activity 相關

生命週期方法

  • Activity 變得對使用者可見時,將會回撥 onStart(), 當 Activity 變得可以和使用者互動時,將會回撥 onResume()
  • onPause() 被呼叫時 Activity 可能依然對使用者全部可見,如多視窗模式下沒有獲得焦點時,所以在 onResume() 中申請資源在 onPause() 中釋放資源的想法並不總是合理的
  • onStop() 被呼叫時表示 Activity 已經完全不可見了,此時應該儘量停止包含動畫在內的 UI 更新,儘量釋放暫時不用的資源。對於 stopped 的 Activity,系統隨時可能殺掉包含這個 Activity 的程式,如果沒有合適的機會可以在 onStop() 中儲存一些資料
  • 如果系統在未經使用者允許的情況下銷燬了 Activity(殺掉了該 Activity 例項所在的程式),那麼系統肯定記得這個例項存在過,在使用者重新回到這個 Activity 時會重新建立一個新的例項,並將之前儲存好的例項狀態傳遞給這個新的例項。這個系統之前儲存好的用來恢復 Activity 狀態的資料被稱為例項狀態(Instance state),例項狀態是以鍵值對的形式儲存在 Bundle 物件中的,預設系統只能自動儲存和恢復有 ID 的 View 的簡單狀態(如輸入框的文字,滾動控制元件的滾動位置),但由於在主執行緒中序列化或反序列化 Bundle 物件既消耗時間又消耗系統程式記憶體,所以最好只用它儲存簡單、輕量的資料
  • onSaveInstanceState() 被呼叫的時機: 對於 Build.VERSION_CODES.P 及之後的系統該方法會在 onStop() 之後隨時可能被呼叫,對於之前的系統該方法會在 onStop() 之前隨時被呼叫
  • onRestoreInstanceState() 被呼叫的時機: 如果有例項狀態要恢復那麼一定會在 onStart() 之後被呼叫
  • onActivityResult() 被呼叫時機: onResume() 之前。目標 Activity 沒有顯式返回任何結果或者崩潰那麼 resultCode 就會是 RESULT_CANCELED
  • 在儲存例項狀態之後恢復例項狀態之前的一些操作(如 Fragment 的事務提交)是不允許的,Android 系統會不惜一切代價避免狀態丟失。Activity#onCreate() 方法中提交事務是沒問題的,因為你可以在裡面根據儲存的狀態重建,但是在其他生命週期回撥中提交事務就可能會出現問題了。FragmentActivity#onPostResume() 方法中呼叫了 FragmentActivity#onResumeFragments() 方法完成其關聯的所有的 Fragment 的 resume 事件的分發,執行完這兩個方法 Activity 和它關聯的所有 Fragment 才算真正的 resumed,才算恢復了狀態,才可以提交事務,所以如果非要在 Activity#onCreate() 之外的回撥中提交事務那麼 FragmentActivity#onPostResume()FragmentActivity#onResumeFragments() 是最好的選擇。避免在非同步的回撥中提交事務: 因為在這些回撥執行的時候很難確定當前 Activity 正處於什麼生命週期狀態,而且突然地提交事務更改大量 UI 會產生糟糕的使用者體驗,所以如果遇到這樣的場景可以考慮換一種實現思路,不要隨便使用 commitAllowingStateLoss() 方法
  • 如非必須,避免使用多層巢狀的 Fragment,否則容易出現 Bug

任務和返回棧

  • Activity 可以在 manifest 檔案中定義自己應該如何與當前任務相關聯,Activity 也可以在啟動其它 Activity 時通過 Intent 的 flag 要求其它 Activity 應該如何與當前任務相關聯,如果兩者同時出現,那麼 Intent 的 flag 要求獲勝
  • launchMode 屬性預設是 standard,每次啟動這樣的 Activity 都會新建一個新的例項放入啟動它的任務中。一個新的 Intent 總會建立一個新的例項。一個任務可以有多個該 Activity 的例項,每個該 Activity 的例項可以屬於不同的任務
  • launchMode 屬性是 singleTopActivity : 如果當前任務頂部已經是這個 Activity 的例項那麼就直接將 Intent 傳遞給這個例項的 onNewIntent() 方法。一個任務可以有多個該 Activity 的例項,每個該 Activity 的例項可以屬於不同的任務
  • launchMode 屬性是 singleTaskActivity : 如果這個 Activity 的例項已經在某個任務中存在了那麼就直接將 Intent 傳遞給這個例項的 onNewIntent() 方法,並將其所在的任務移到前臺即當前任務頂部,否則會新建一個任務並例項化一個這個 Activity 的例項放在棧底
  • launchMode 屬性是 singleInstanceActivity : 和 singleTask 類似,不過它會保證新的任務中有且僅有一個這個 Activity 的例項
  • FLAG_ACTIVITY_NEW_TASK : 行為和 singleTask 一樣,不過在新建任務之前會先尋找是否已經存在和這個 Activity 有相同 affinity 的任務,如果已經存在就不新建任務了,而是直接在那個任務中啟動
  • FLAG_ACTIVITY_SINGLE_TOP : 行為和 singleTop 一樣
  • FLAG_ACTIVITY_CLEAR_TOP : 如果當前任務中已經有要啟動的 Activity 的例項了,那麼就銷燬它上面所有的 Activity(甚至包括它自己),由於 launchMode 屬性是 standardActivity 一個新的 Intent 總會建立一個新的例項,所以如果要啟動的 ActivitylaunchMode 屬性是 standard 的並且沒有 FLAG_ACTIVITY_SINGLE_TOP 的 flag,那麼這個 flag 會銷燬它自己然後建立一個新的例項
  • FLAG_ACTIVITY_CLEAR_TOPFLAG_ACTIVITY_NEW_TASK 結合使用可以直接定位指定的 Activity 到前臺
  • 不管要啟動的 Activity 是在當前任務中啟動還是在新任務中啟動,點選返回鍵都可以直接或間接回到之前的 Activity,間接的情況像 singleTask 是將整個任務而不是隻有一個 Activity 移到前臺,任務中的所有的 Activity 在點選返回鍵的時候都要依次彈出
  • 如果離開了任務,系統可能會清除任務中除了最底層 Activity 外的的所有 Activity。將最底層 Activity<activity> 標籤的 alwaysRetainTaskState 屬性設定為 true 可以保留任務中所有的 Activity。將最底層 Activity<activity> 標籤的 clearTaskOnLaunch 屬性設定為 true 可以在無論何時進入或離開這個任務都清除任務中除了最底層 Activity 外的的所有 Activity。包含最底層 Activity 在內的任何 Activity 只要 finishOnTaskLaunch 屬性設定為 true 那麼離開任務再回來都不會出現了
  • Activity 作為新文件新增到最近任務中需要設定 newDocumentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);launchMode 必須是 standard 的,如果此時又設定了 newDocumentIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) 那麼系統每次都會建立新的任務並將目標 Activity 作為根 Activity,如果沒有設定 FLAG_ACTIVITY_MULTIPLE_TASK,那麼 Activity 例項會被重用到新的任務中(如果已經存在這樣的任務就不會重建,而是直接將任務移到前臺並呼叫 onNewIntent()
  • <activity> 標籤的 android:documentLaunchMode 屬性預設是 none : 不會為新文件建立新的任務。intoExisting 與設定了 FLAG_ACTIVITY_NEW_DOCUMENT 但沒設定 FLAG_ACTIVITY_MULTIPLE_TASK 一樣。always 與設定了 FLAG_ACTIVITY_NEW_DOCUMENT 同時設定了 FLAG_ACTIVITY_MULTIPLE_TASK 一樣。nevernone 一樣不過會覆蓋 FLAG_ACTIVITY_NEW_DOCUMENTFLAG_ACTIVITY_MULTIPLE_TASK
  • 使用 Intent.FLAG_ACTIVITY_NEW_DOCUMENT|android.content.Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS; 同時 <activity> 標籤的 android:autoRemoveFromRecents 屬性設定為 false 可以讓文件 Activity 即使結束了也可以保留在最近任務中
  • 使用 finishAndRemoveTask() 方法可以移除當前任務

動態申請許可權

  • Android 6.0 (API level 23) 開始 targetSdkVersion >= 23 的應用必須在執行時動態申請許可權
  • 許可權請求對話方塊是作業系統進行管理的,應用無法也不應該干預。
  • 系統對話方塊描述的是許可權組而不是某個具體許可權。
  • 如果使用者授予了許可權組中的一個許可權,那麼再申請該許可權組的其它許可權時系統會自動授予,不需要使用者再授權。但這並不意味著該許可權組中的其它許可權就不用申請了,因為許可權處於哪個許可權組將來有可能會發生變化。
  • 呼叫 requestPermissions() 並不意味著系統一定會彈出許可權請求對話方塊,也就是說不能假設呼叫該方法後就發生了使用者互動,因為如果使用者之前勾選了 “禁止後不再詢問” 或者系統策略禁止應用獲取許可權,那麼系統會直接拒絕此次許可權請求,沒有任何互動。
  • 如果某個許可權跟應用的主要功能無關,如應用中廣告可能需要位置許可權,使用者可能很費解,此時在申請許可權之前彈出對話方塊向使用者解釋為什麼需要這個許可權是個不錯的選擇。但不要在所有申請許可權之前都彈出對話方塊解釋,因為頻繁地打斷使用者的操作或讓使用者進行選擇容易讓使用者不耐煩。
  • Fragment 中的 onRequestPermissionsResult() 方法只有在使用 Fragment#requestPermissions() 方法申請許可權時才可能接收到回撥,建議將許可權放在所屬 Activity 中申請和處理。
private void showContactsWithPermissionsCheck() {
    if (ContextCompat.checkSelfPermission(MainActivity.this,
            Manifest.permission.READ_CONTACTS)
            != PackageManager.PERMISSION_GRANTED) {
        if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
                Manifest.permission.READ_CONTACTS)) {
            // TODO: 彈框解釋為什麼需要這個許可權. 【下一步】 -> 再次請求許可權
        } else {
            ActivityCompat.requestPermissions(MainActivity.this,
                    new String[]{Manifest.permission.READ_CONTACTS},
                    RC_CONTACTS);
        }
    } else {
        showContacts();
    }
}
private void showContacts() {
    startActivity(ContactsActivity.getIntent(MainActivity.this));
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                       @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    switch (requestCode) {
        case RC_CONTACTS:
            if (grantResults.length > 0
                    && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                showContacts();
            } else {
                if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
                        Manifest.permission.READ_CONTACTS)) {
                    // TODO: 彈框引導使用者去設定頁主動授予該許可權. 【去設定】 -> 應用資訊頁
                } else {
                    // TODO: 彈框解釋為什麼需要這個許可權. 【下一步】 -> 再次請求許可權
                }
            }
            break;
        default:
            break;
    }
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == RC_SETTINGS) {
        // TODO: 在使用者主動授予許可權後重新檢查許可權,但不要在這裡進行事務提交等生命週期敏感操作
    }
}
複製程式碼

Shortcut

  • 類似於 iOS 的 3D Touch,長按啟動圖示彈出幾個快捷入口,入口最好不要超過 4 個,像搜尋、掃描二維碼、發帖等應用程式最常用功能的入口被稱為靜態 shortcut,不會隨著使用者不同或隨著使用者使用而改變。還有一種像從某個存檔點繼續遊戲、任務進度等與使用者相關的上下文敏感入口被稱為動態 shortcut,會因使用者不同或隨著使用者使用不斷變化。還有一種在 Android 8.0 (API level 26) 及以上系統版本上像固定網頁標籤等使用者主動固定到桌面的快捷方式被稱為固定 shortcut
  • 靜態 shortcut 系統可以自動備份和恢復,動態 shortcut 需要應用自己備份和恢復,固定 shortcut 的圖示系統無法備份和恢復因此需要應用自己完成
  • android:shortcutIdandroid:shortcutShortLabel 屬性是必須的,android:shortcutShortLabel 不能超過 10 個字元,android:shortcutLongLabel 不能超過 25 個字元,android:icon 不能包含 tint
  • 獲取 ShortcutManager 的方式有兩個: getSystemService(ShortcutManager.class)getSystemService(Context.SHORTCUT_SERVICE)
  • 建立固定 shortcut:
ShortcutManager mShortcutManager =
        context.getSystemService(ShortcutManager.class);
if (mShortcutManager.isRequestPinShortcutSupported()) {
    ShortcutInfo pinShortcutInfo =
            new ShortcutInfo.Builder(context, "my-shortcut").build();
    Intent pinnedShortcutCallbackIntent =
            mShortcutManager.createShortcutResultIntent(pinShortcutInfo);
    PendingIntent successCallback = PendingIntent.getBroadcast(context, 0,
            pinnedShortcutCallbackIntent, 0);
    mShortcutManager.requestPinShortcut(pinShortcutInfo,
            successCallback.getIntentSender());
}
複製程式碼

其它

  • Parcelable 物件用來在程式間、Activity 間傳遞資料,儲存例項狀態也是用它,Bundle 是它的一個實現,最好只用它儲存和傳遞少量資料,別超過 50k,否則既可能影響效能又可能導致崩潰
  • Android 9 (API level 28) 開始廢棄了 Loader API,包括 LoaderManagerCursorLoader 等類的使用。推薦使用 ViewModelLiveDataActivityFragment 生命週期中載入資料
  • Activity 可以通過 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 保持螢幕常亮,這是最推薦、最簡單、最安全的保持螢幕常亮的方法,給 view 新增 android:keepScreenOn="true" 也是一樣的。這個只在這個 Activity 生命週期內有效,所以大可放心,如果想提前解除常亮,只需要清除這個 flag 即可
  • WAKE_LOCK 可以阻止系統睡眠,保持 CPU 一直執行,需要 android.permission.WAKE_LOCK 許可權,通過 powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::MyWakelockTag") 建立例項,通過 wakeLock.acquire() 方法請求鎖,通過 wakelock.release() 釋放鎖
  • WakefulBroadcastReceiver 結合 IntentService 也可以阻止系統睡眠

UI 相關

系統欄適配

  • Android 4.1 (API level 16) 開始可以通過 setSystemUiVisibility() 方法在各個 view 層次中(一般是在 DecorView 中)配置 UI flag 實現系統欄(狀態列、導航欄統稱)配置,最終彙總體現到 window 級
  • View.SYSTEM_UI_FLAG_FULLSCREEN 可以隱藏狀態列,View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 可以隱藏導航欄。但是: 使用者的任何互動包括觸控螢幕都會導致 flag 被清除進而系統欄保持可見,一旦離開當前 Activity flag 就會被清除,所以如果在 onCreate() 方法中設定了這個 flag 那麼按 HOME 鍵再回來狀態列又保持可見了,非要這樣設定的話一般要放在 onResume()onWindowFocusChanged() 方法中,而且這樣設定只有在目標 View 可見時才會生效,狀態列/導航欄的顯示隱藏會導致顯示內容的大小尺寸跟著變化。
  • View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 可以讓內容顯示在狀態列後面,View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 可以讓內容顯示在導航欄後面,這樣無論系統欄顯示還是隱藏內容都不會跟著變化,但不要讓可互動的內容出現在系統欄區域內,通過將 android:fitsSystemWindows 屬性設定為 true 可以讓父容器調整 padding 以便為系統欄留出空間,如果想自定義這個 padding 可以通過覆寫 fitSystemWindows(Rect insets) 方法完成
  • lean back 全屏模式: View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION,隱藏狀態列和導航欄,任何互動都會清除 flag 使系統欄保持可見
  • Immersive 全屏模式: View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION,隱藏狀態列和導航欄,從被隱藏的系統欄邊緣向內滑動會使系統欄保持可見,應用無法響應這個手勢
  • sticky immersive 全屏模式: View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION,隱藏狀態列和導航欄,從被隱藏的系統欄邊緣向內滑動會使系統欄暫時可見,flag 不會被清除,且系統欄的背景是半透明的,會覆蓋應用的內容,應用也可以響應這個手勢,在使用者沒有任何互動或者沒有系統欄互動幾秒鐘後系統欄會自動隱藏
  • 真正的沉浸式全屏體驗需要 6 個 flag: View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN
  • 監聽系統欄可見性(sticky immersive 全屏模式無法監聽):
decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
    @Override
    public void onSystemUiVisibilityChange(int visibility) {
        if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
            // TODO: The system bars are visible. Make any desired
        } else {
            // TODO: The system bars are NOT visible. Make any desired
        }
    }
});
複製程式碼
  • 全面屏適配只需要指定支援的最大寬高比即可: <meta-data android:name="android.max_aspect" android:value="2.4"/>
  • Android 9 (API level 28) 開始支援劉海屏 cutout 的配置,window 的屬性 layoutInDisplayCutoutMode 預設是 LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT,豎屏時可以渲染到劉海區,橫屏時不允許渲染到劉海區。LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 橫豎屏都可以渲染到劉海區。LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER 橫豎屏都不允許渲染到劉海區,可以在 values-v28/styles.xml 檔案中通過 android:windowLayoutInDisplayCutoutMode 指定預設的劉海區渲染模式
  • 華為手機通過 <meta-data android:name="android.notch_support" android:value="true" /> 屬性宣告應用是否已經適配了劉海屏,如果沒適配,那麼在橫屏或者豎屏不顯示狀態列時會禁止渲染到劉海區,開發者文件: 《華為劉海屏手機安卓O版本適配指導》
  • 小米手機通過 <meta-data android:name="notch.config" android:value="portrait|landscape" /> 設定預設的劉海區渲染模式,開發者文件: 《小米劉海屏 Android O 適配》《小米劉海屏 Android P 適配》
  • 其他手機的開發者文件有: OPPO 手機的 《OPPO凹形屏適配說明》,VIVO 手機的 《異形屏應用適配指南》,錘子手機的 《Smartisan 開發者文件》
  • Android 5.0 (API level 21) 開始支援通過 window 的 setStatusBarColor() 方法設定狀態列背景色,要求 window 必須新增 WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 的 flag 並且清除 WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS 的 flag
  • Android 6.0 (API level 23) 開始可以通過 setSystemUiVisibility() 方法設定 View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR flag 相容亮色背景的狀態列,同樣要求 window 必須新增 WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 的 flag 並且清除 WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS 的 flag
  • 小米手機在 MIUI 開發版 7.7.13 之前需要通過反射相容亮色背景的狀態列,開發者文件: 《MIUI 9 & 10“狀態列黑色字元”實現方法變更通知》
  • 魅族手機同樣需要通過反射相容亮色背景的狀態列,開發者文件: 《狀態列變色》

動畫

  • view 動畫系統只能作用於 view 物件,只能改變 view 的部分樣式,只是簡單改變了 view 繪製,並沒有改變 view 真正的位置和屬性。核心類是 android.view.animation.Animation 和它的 ScaleAnimation 等子類,一般使用 AnimationUtils.loadAnimation() 方法載入。不建議使用,除非為了方便又能滿足現在和將來的需求
  • 屬性動畫系統是一個健壯的、優雅的動畫系統,可以對任意物件的屬性做動畫。核心類是 android.animation.Animator 的子類 ValueAnimatorObjectAnimatorAnimatorSet
  • 通過呼叫 ValueAnimatorofInt()ofFloat() 等工廠方法獲取 ValueAnimator 物件,通過它的 addUpdateListener() 方法可以監聽動畫值並在裡面進行自定義操作
  • ObjectAnimator 作為 ValueAnimator 的子類可以自動地為目標物件的命名屬性設定動畫,但是對目標物件有嚴格的要求: 目標物件必須有對應屬性的 setter 方法,如果在工廠方法中只提供了一個動畫值那麼它會作為終止值,起始值為目標物件的當前值,此時為了獲取當前屬性值目標物件必須有對應屬性的 getter 方法。有些屬性的更改不會導致 view 重新渲染,此時需要主動呼叫 invalidate() 方法強制觸發重繪
  • AnimatorListenerAdapter 提供了 Animator.AnimatorListener 介面的空實現
  • 多數情況下可以直接使用系統提供的幾個動畫 duration,如 getResources().getInteger(android.R.integer.config_shortAnimTime)
  • 可以呼叫任意 view 物件的 animate() 方法獲取 ViewPropertyAnimator 物件,鏈式呼叫這個物件的 scaleX()alpha() 等方法可以簡單方便地同時對 view 的多個屬性做動畫
  • 為了更好地重用和管理屬性動畫,最好使用 XML 檔案來描述動畫並放到 res/animator/ 目錄下,ValueAnimator 對應 <animator>ObjectAnimator 對應 <objectAnimator>AnimatorSet 對應 <set>,使用 AnimatorInflater.loadAnimator() 可以載入這些動畫
  • 動態 Drawable 的實現有兩種,最傳統最簡單的就是像電影關鍵幀一樣依次指定關鍵幀和每一幀的停留時間,AnimationDrawable 對應於 XML 檔案中的 <animation-list>,儲存目錄為 res/drawable/AnimationDrawablestart() 方法可以在 onStart() 中呼叫。還有一種是 AnimatedVectorDrawable,需要 res/drawable/ 中的 <animated-vector> 引用 res/drawable/ 中的 <vector> 對其使用 res/animator/ 中的 <objectAnimator> 動畫
  • 突然更改顯示的內容會讓視覺感受非常突兀不和諧,而且可能意識不到哪些內容突然變了,所以很多場景下需要使用動畫過渡一下,而不是突然更改顯示的內容
  • 顯示隱藏 view 的常用動畫有三個: crossfade 動畫,card flip 動畫,circular reveal 動畫
  • crossfade 動畫就是內容淡出另一個內容淡入交叉進行,也被稱為溶入動畫。實現方式為: 事先將淡入 view 的 visibility 設定為 GONE → 開始動畫時將淡入 view 的 alpha 設定為 0,visibility 設定為 VISIBLE → 將淡入 view 的 alpha 動畫到 1,將淡出 view 的 alpha 動畫到 0 並在動畫結束時將淡出 view 的 visibility 設定為 GONE
  • card flip 動畫就是卡片翻轉動畫,需要四個動畫描述: card_flip_right_incard_flip_right_outcard_flip_left_incard_flip_left_out
  • Android 5.0 (API level 21) 開始支援 circular reveal 圓形裁剪動畫,實現方式為: 事先將 view 的 visibility 設定為 INVISIBLE → 利用 ViewAnimationUtils.createCircularReveal() 方法建立半徑從 0 到 Math.hypot(cx, cy) 的圓形裁剪動畫 → 將 view 的 visibility 設定為 VISIBLE 然後開啟動畫
  • 直線動畫移動 view 只需要藉助 ObjectAnimator.ofFloat() 方法動畫設定 view 的 translationXtranslationY 屬性即可
  • 曲線動畫移動 view 還需要藉助 Android 5.0 (API level 21) 開始提供的 PathInterpolator 插值器(對應於 XML 檔案中的 <pathInterpolator>),他需要個 Path 物件描述運動的貝塞爾曲線。可以使用 ObjectAnimator.ofFloat(view, "translationX", 100f) 同時設定 PathInterpolator 也可以直接設定 view 動畫路徑 ObjectAnimator.ofFloat(view, View.X, View.Y, path)。系統提供的 fast_out_linear_in.xmlfast_out_slow_in.xmllinear_out_slow_in.xml 三個基礎的曲線插值器可以直接使用
  • 基於物理的動畫需要引用 support-dynamic-animation 支援庫,最常見的就是 FlingAnimationSpringAnimation 動畫,物理動畫主要是模擬現實生活中的物理世界,利用經典物理學的知識和原理實現動畫過程,其中最關鍵的就是的概念。FlingAnimation 就是使用者通過手勢給動畫元素一個力,動畫元素在這個力的作用下運動,之後由於摩擦力的存在慢慢減速直到結束,當然這個力也可以通過程式直接指定(指定固定的初始速度)。SpringAnimation 就是彈簧動畫,動畫元素的運動與彈簧有關
  • FlingAnimation 通過 setStartVelocity() 方法設定初始速度,通過 setMinValue()setMaxValue() 約束動畫值的範圍,通過 setFriction() 設定摩擦力(如果不設定預設為 1)。如果動畫的屬性不是以畫素為單位的,那麼需要通過 setMinimumVisibleChange() 方法設定使用者可察覺到動畫值的最小更改,如對於 TRANSLATION_XTRANSLATION_YTRANSLATION_ZSCROLL_XSCROLL_Y 1 畫素的更改就對使用者可見了,而對於 ROTATIONROTATION_XROTATION_Y 最小可見更改是 MIN_VISIBLE_CHANGE_ROTATION_DEGREES 即 1/10 畫素,對於 ALPHA 最小可見更改是 MIN_VISIBLE_CHANGE_ALPHA 即 1/256 畫素,對於 SCALE_XSCALE_Y 最小可見更改是 MIN_VISIBLE_CHANGE_SCALE 即 1/500 畫素,計算公式為: 自定義屬性值的範圍 / 動畫的變化畫素範圍。
  • SpringAnimation 需要先鞏固一下彈簧的知識,彈簧有一個屬性叫阻尼比 ζ(damping ratio),是實際的粘性阻尼係數 C 與臨界阻尼係數 Cr 的比。ζ = 1 時為臨界阻尼,這是最小的能阻止系統震盪的情況,系統可以最快回到平衡位置。0 < ζ < 1 時為欠阻尼,物體會作對數衰減振動。ζ > 1 時為過阻尼,物體會沒有振動地緩慢回到平衡位置。ζ = 0 表示不考慮阻尼,震動會一直持續下去不會停止。預設是 SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY 即 0.5,可以通過 getSpring().setDampingRatio() 設定。彈簧另一個屬性叫剛度(stiffness),剛度越大形變產生的力就越大,預設是 SpringForce.STIFFNESS_MEDIUM 即 1500.0,可以通過 getSpring().setStiffness() 設定
    damping ratio
  • FlingAnimationSpringAnimation 動畫通過 setStartVelocity() 設定固定的初始速度時最好用 dp/s 轉成 px/s : TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond, getResources().getDisplayMetrics()),使用者手勢的初始速度可以通過 GestureDetector.OnGestureListenerVelocityTracker 計算
  • SpringAnimation 動畫使用 start() 方法開始動畫時屬性值不會馬上變化,而是在每次動畫脈衝即繪製之前更改。animateToFinalPosition() 方法會馬上設定最終的屬性值,如果動畫沒開始就開始動畫,這在鏈式依賴的彈簧動畫中非常有用。cancel() 方法可以結束動畫在其當前位置,skipToEnd() 方法會跳轉至終止值再結束動畫,可以通過 canSkipToEnd() 方法判斷是否是阻尼動畫
  • 放大預覽動畫只需要同時動畫更改目標 view 的 XYSCALE_XSCALE_Y 屬性即可,不過要先計算好兩個 view 最終的位置和初始縮放比
  • Android 提供了預載入的佈局改變動畫,可以通過 android:animateLayoutChanges="true" 屬性告訴系統開啟預設動畫,或者通過 LayoutTransition API 設定
  • Activity 內部的佈局過渡動畫: 過渡動畫框架可以在開始 Scene 和結束 Scene 開始過渡動畫,Scene 儲存著 view hierarchy 狀態,包括所有 view 和其屬性值,開始 Scene 可以通過 setExitAction() 定義過渡動畫開始前要執行的操作,結束 Scene 可以通過 Scene.setEnterAction() 定義過渡動畫完成後要執行的操作。如果 view hierarchy 是靜態不變的,可以通過佈局檔案描述和載入 Scene.getSceneForLayout(mSceneRoot, R.layout.a_scene, this),否則可以手動建立 new Scene(mSceneRoot, mViewHierarchy)Transition 的內建子類包括 AutoTransitionFadeChangeBounds,可以在 res/transition/ 目錄下定義內建的 <fade xmlns:android="http://schemas.android.com/apk/res/android" />,多個組合包裹在 <transitionSet> 標籤中,然後使用 TransitionInflater.from(this).inflateTransition(R.transition.fade_transition) 載入。還可以手動建立 new Fade()。開始過渡動畫時只需要執行 TransitionManager.go(mEndingScene, mFadeTransition) 即可。預設是對 Scene 中所有的 view 作動畫,可以通過 addTarget()removeTarget() 在開始過渡動畫前進行調整。如果不想在兩個 view hierarchy 間進行過渡,而是在同一個 view hierarchy 狀態更改後執行過渡動畫,那就不需要使用 Scene 了,先利用 TransitionManager.beginDelayedTransition(mRootView, mFade) 讓系統記錄 view 的更改,然後增刪 view 來更改 view hierarchy 的狀態,系統會在重繪 UI 時執行延遲過渡動畫。由於 SurfaceView 由非 UI 執行緒更新,所以它的過渡可能有問題,TextureView 在一些過渡型別上可能有問題,AdapterView 與過渡動畫框架不相容,TextView 的大小過渡動畫可能有問題
  • Activity 之間的過渡動畫: 需要 Android 5.0 (API level 21) ,內建的進入退出過渡動畫包括: explode 從中央進入或退出,slide 從一邊進入或退出,fade 透明度漸變進入或退出。內建的共享元素過渡動畫包括: changeBounds 動態更改目標 view 的邊界,changeClipBounds 動態裁剪目標 view 的邊界,changeTransform 動態更改目標 view 的縮放和旋轉,changeImageTransform 動態更改目標 view 的縮放和尺寸。過渡動畫需要兩個 Activity 都要開啟 window 的內容過渡,通過 android:windowActivityTransitions 屬性設定為 true 或者手動 getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS),通過 setExitTransition()setSharedElementExitTransition() 方法可以為起始 Activity 設定退出過渡動畫,通過 setEnterTransition()setSharedElementEnterTransition() 方法可以為目標 Activity 設定進入過渡動畫。啟用目標 Activity 的時候需要攜帶 ActivityOptions.makeSceneTransitionAnimation(this).toBundle() 的 Bundle,返回的時候要使用 Activity.finishAfterTransition() 方法。共享元素需要使用 android:transitionName 屬性或者 View.setTransitionName() 方法指定名字,多個共享元素使用 Pair.create(view1, "agreedName1") 傳遞資訊
  • 自定義過渡動畫需要繼承 Transition,實現 captureStartValues()captureEndValues() 方法捕獲過渡的 view 屬性值並告訴過渡框架,具體實現為通過 transitionValues.view 檢索當前 view,通過 transitionValues.values.put(PROPNAME_BACKGROUND, view.getBackground()) 儲存屬性值,為了避免衝突 key 的格式必須為 package_name:transition_name:property_name。同時還要實現 createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) 方法,框架呼叫這個方法的次數取決於開始和結束 scene 需要更改的元素數
  • 動畫可能會影響效能,必要時可以啟用 Profile GPU Rendering 進行除錯

其它

  • Android 8.0 (API level 26) 開始支援自適應啟動圖示,自適應啟動圖示必須由前景和背景兩部分組成,尺寸必須都是 108 x 108 dp,其中內部的 72 x 72 dp 用來顯示圖示,靠近四個邊緣的 18 dp 是保留區域,用來進行視覺互動
  • 對於字型大小自適應的 TextView 寬和高都不能是 wrap_contentautoSizeTextType 預設是 none,設定為 uniform 開啟自適應,預設最小 12sp,最大 112sp,粒度 1pxautoSizePresetSizes 屬性可以設定預置的一些大小
  • Android 8.0 (API level 26) 開始支援 XML 自定義字型,相容庫可以相容到 Android 4.1 (API level 16),字型檔案路徑為 res/font/,使用屬性為 fontFamily,獲取 TypefacegetResources().getFont(R.font.myfont);,相容庫使用 ResourcesCompat.getFont(context, R.font.myfont)
  • Android 9 (API level 28) 支援控制元件放大鏡功能,Magnifiershow() 方法的引數是相對於被放大 View 的左上角的座標
  • 工程中的 Drawable 資源只能有一個狀態,你不應該手動更改它的任何屬性,否則會影響到其它使用這個 Drawable 資源的地方
  • Android 7.0 (API level 24) 開始支援在 XML 檔案中使用自定義 Drawable,公共頂級類使用全限定名作為標籤名即可 <com.myapp.MyDrawable>,公共靜態內部類可以使用 class 屬性 class="com.myapp.MyTopLevelClass$MyDrawable"
  • Android 5.0 (API level 21) 開始支援為 Drawable 設定 tint
  • Android 5.0 (API level 21) 開始支援向量圖,支援庫可以支援到 Android 2.1 (API level 7+),相容低版本是需要 Gradle 外掛版本大於 2.0+ 時新增 vectorDrawables.useSupportLibrary = true 並使用 VectorDrawableCompatAnimatedVectorDrawableCompat

BroadcastReceiver 相關

  • Android 9 (API level 28) 開始 NETWORK_STATE_CHANGED_ACTION 廣播不再包含 SSID,BSSID 等資訊
  • Android 8.0 (API level 26) 開始限制應用靜態註冊一些非當前應用專屬的隱式廣播的 BroadcastReceiver,免除這項限制的廣播包括 ACTION_LOCKED_BOOT_COMPLETED 不太可能影響使用者體驗的廣播
  • Android 7.0 (API level 24) 開始不能傳送 ACTION_NEW_PICTUREACTION_NEW_VIDEO 系統廣播,不能註冊 CONNECTIVITY_ACTION 的靜態廣播
  • 應該儘量在程式碼中動態註冊登出 BroadcastReceiver
  • onReceive() 方法中不能進行復雜工作否則會導致 ANR,onReceive() 方法一旦執行完,系統可能就認為這個廣播接收器已經沒用了,隨時會殺掉包含這個廣播接收器的程式,包括這個程式啟動的執行緒。使用 goAsync() 方法可以在 PendingResult#finish() 方法執行前為廣播接收器的存活爭取更多的時間,但最好還是使用 JobScheduler 等方式進行長時間處理工作
  • 使用 sendBroadcast() 方法發的廣播屬於常規廣播,所有能接收這個廣播的廣播接收器接收到廣播的順序是不可控的
  • 使用 sendOrderedBroadcast() 方法發的廣播屬於有序廣播,根據廣播接收器的優先順序一個接一個地傳遞這條廣播,相同優先順序的順序不可控,廣播接收器可以選擇繼續傳遞給下一個,也可以選擇直接丟掉
  • 使用 LocalBroadcastManager.getInstance(this).sendBroadcast() 方法發的廣播屬於應用程式內的本地廣播,這樣的廣播只有應用自己知道,比系統級的全域性廣播更安全更有效率
  • 為了保證廣播的 action 全域性唯一,action 的名字最好使用應用的包名作為字首,最好宣告成靜態字串常量

資料儲存與共享

儲存方式

  • 系統會在安裝應用時在內部儲存器的檔案系統中為應用生成一個私有檔案目錄,一般是 /data/data/your.application.package//data/user/0/your.application.package/,當解除安裝應用時這個目錄也會被刪除。這個目錄除了系統和應用自己誰都無法訪問,除非擁有許可權。這個路徑下有個 files/ 子目錄用來儲存應用的檔案,可以通過 getFilesDir() 方法獲取這個路徑表示,可以通過 openFileOutput(filename, Context.MODE_PRIVATE) 寫這個目錄下的檔案。還有一個 cache/ 子目錄用來儲存臨時快取檔案,系統可能會在儲存空間不足時清理這個目錄,可以通過 getCacheDir() 方法獲取這個路徑表示,可以通過 File#createTempFile(fileName, null, context.getCacheDir()) 方法在這個目錄下建立一個臨時檔案。還有一個 shared_prefs/ 子目錄用來以 XML 檔案的形式儲存簡單的鍵值對資料,需要使用 SharedPreferences API 進行管理
  • 讀寫外存(外存是指可以被移除的外部儲存器)檔案需要先動態申請 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 許可權,然後檢查外存是否可用: Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) 表示可寫,Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state) 表示可讀。外存根目錄可以使用 Environment.getExternalStorageDirectory() 方法獲取,一般是 /storage/emulated/0/,使用 new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), albumName) 可以讀寫外存公有目錄的檔案。使用 getExternalFilesDir(null) 可以獲取該應用的外存根目錄,一般是 /storage/emulated/0/Android/data/your.application.package/files,使用 new File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), albumName) 可以讀寫檔案,應用的外存目錄也會在解除安裝應用時被刪除。使用 getExternalCacheDir() 可以獲取應用的外存快取目錄。
  • 使用 myFile.delete()myContext.deleteFile(fileName) 刪除檔案
  • 直接使用 SQLite API 進行資料庫操作既麻煩又容易出錯,建議使用 Room 等其它 ORM 庫進行資料庫操作
  • 獲取 SharedPreferences 的方式有三個: 通過 PreferenceManager.getDefaultSharedPreferences() 可以獲取或建立名字為 context.getPackageName() + "_preferences" 模式為 Context.MODE_PRIVATE 的檔案。通過 MainActivity.this.getPreferences(Context.MODE_PRIVATE) 可以獲取或建立名字為當前 Activity 類名的檔案。使用 context.getSharedPreferences("file1", Context.MODE_PRIVATE) 可以獲取或建立名字是 file1 的檔案。MODE_WORLD_READABLEMODE_WORLD_WRITEABLE 從 Android 7.0 (API level 24) 開始被禁止使用了。commit() 方法會將資料同步寫到磁碟所以可能會阻塞 UI,而 apply() 方法會非同步寫到磁碟。

分享檔案

  • 為了安全地共享檔案,分享的檔案必須通過 content URI 表示,必須授予這個 content URI 臨時訪問許可權。FileProvider 作為 ContentProvider 的特殊子類,它的 getUriForFile() 方法可以為檔案生成 content URI。
<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.example.myapp.fileprovider"
    android:grantUriPermissions="true"
    android:exported="false">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/filepaths" />
</provider>
複製程式碼
<paths>
    <files-path path="images/" name="myimages" />
</paths>
複製程式碼
  • android:authorities 屬性一般是以當前應用包名為字首的字串,用來標誌資料的所有者,多個的話用分號隔開
  • <root-path/> 代表根目錄
  • <files-path/> 代表 getFilesDir()
  • <cache-path/> 代表 getCacheDir()
  • <external-path/> 代表 Environment.getExternalStorageDirectory()
  • <external-files-path> 代表 getExternalFilesDir(null)
  • <external-cache-path> 代表 getExternalCacheDir()
  • <external-media-path> 代表 getExternalMediaDirs()
File imagePath = new File(getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), "com.example.myapp.fileprovider", newFile);
複製程式碼
  • 給 Intent 新增 FLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSION 的 flag 授予對這個 content URI 的臨時訪問許可權,該許可權會被目標 Activity 所在應用的其它元件繼承,會在所在的任務結束時自動撤銷授權
  • 呼叫 Context.grantUriPermission(package, Uri, mode_flags) 方法也可以授予 FLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSION 許可權,但只有在主動呼叫 revokeUriPermission() 方法後或者重啟系統後才會撤銷授權
mResultIntent.setDataAndType(
        fileUri,
        getContentResolver().getType(fileUri));
MainActivity.this.setResult(Activity.RESULT_OK,
        mResultIntent);
複製程式碼
Uri returnUri = returnIntent.getData();
try {
    mInputPFD = getContentResolver().openFileDescriptor(returnUri, "r");
} catch (FileNotFoundException e) {
    e.printStackTrace();
    Log.e("MainActivity", "File not found.");
    return;
}
FileDescriptor fd = mInputPFD.getFileDescriptor();
複製程式碼

ContentProvider

  • ContentProvider 的資料形式和關係型資料庫的表格資料類似,因此 API 也像資料庫一樣包含增刪改查(CRUD)操作,但為了更好地組織管理一個或多個 ContentProvider,最好通過 ContentResolver 操作 ContentProvider
  • 對於 ContentProvider 的增刪改查操作,不能直接在 UI 執行緒上執行
  • UriContentUris 類的靜態方法可以方便地構造 content URI
SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;
複製程式碼
mCursor = getContentResolver().query(
        UserDictionary.Words.CONTENT_URI,
        mProjection,
        mSelectionClause,
        mSelectionArgs,
        mSortOrder);
複製程式碼
  • 為了防止 SQL 注入,禁止拼接 SQL 語句,如 mSelectionClause 不能直接包含 selectionArgs 引數值
  • ContentProvider 所在應用本身的元件可以隨便訪問它,不需要授權
  • 如果 ContentProvider 的應用不指定任何許可權,那麼其它應用就無法訪問這個 ContentProvider 的資料
  • 使用者需要事先通過 <uses-permission> 標籤獲取訪問許可權
  • 建立 ContentProvider 需要繼承 ContentProvider 並實現增刪改查等一系列方法: onCreate() 在系統建立 provider 後馬上呼叫,可以在這裡建立資料庫,但不要在這裡做耗時操作。getType() 返回 content URI 的 MIME 型別。query()insert()update()delete() 進行增刪改查。除了 onCreate() 方法其它方法必須要保證是執行緒安全的

其它

  • Android 7.0 (API level 24) 開始禁止使用 file URI 進行檔案共享
  • Android 7.1.1 (API level 25) 開始安裝 APK 時必須宣告 REQUEST_INSTALL_PACKAGES 許可權,資料必須通過 FileProvider 形式共享,資料型別是 application/vnd.android.package-archive,必須給 Intent 新增 FLAG_GRANT_READ_URI_PERMISSION flag 授予臨時訪問許可權
Intent installIntent = new Intent(Intent.ACTION_VIEW);
File apkPath = new File(Environment.getExternalStorageDirectory(), "apks");
File apkFile = new File(apkPath, "myapp.apk");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    Uri contentUri = FileProvider.getUriForFile(MainActivity.this, "com.example.myapp.fileprovider", apkFile);
    installIntent.setDataAndType(contentUri, "application/vnd.android.package-archive");
    installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
    installIntent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
}
installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (installIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(installIntent);
}
複製程式碼

Notification 相關

  • Android 5.0 (API level 21) 開始通知可以出現在鎖屏頁面
  • Android 7.0 (API level 24) 開始可以在通知中直接輸入文字或執行一些自定義操作,如直接回覆按鈕
  • Android 8.0 (API level 26) 開始所有的通知必須屬於一個 channel,channel 被使用者看作是 categories,即通知類別,使用者通過通知類別來精確管理各個應用或一個應用內的通知。一個應用可以有多個通知類別,如私信類別、好友請求類別、應用更新類別等等。可以給每個通知類別指定通知的 importance,即重要程度,Urgent(緊急)會發出提示音並在螢幕上彈出通知,High(高)會發出提示音,Medium(中)不發出提示音,Low(低)不發出提示音並且不會出現在狀態列中。在 Android 8.0 (API level 26) 以下的系統中通知的重要程度表現為 priority,即優先順序。對應關係分別為: IMPORTANCE_HIGH 對應 PRIORITY_HIGHPRIORITY_MAXIMPORTANCE_DEFAULT 對應 PRIORITY_DEFAULTIMPORTANCE_LOW 對應 PRIORITY_LOWIMPORTANCE_MIN 對應 PRIORITY_MIN。在應用啟動時可以執行下面的程式碼建立通知類別,可以無副作用地多次執行
private void createNotificationChannel() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        CharSequence name = getString(R.string.channel_name);
        String description = getString(R.string.channel_description);
        int importance = NotificationManager.IMPORTANCE_DEFAULT;
        NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
        channel.setDescription(description);
        NotificationManager notificationManager = getSystemService(NotificationManager.class);
        notificationManager.createNotificationChannel(channel);
    }
}
複製程式碼
  • 通過 NotificationChannelenableLights()setLightColor() 等方法可以指定該通知類別預設的通知行為,但是一旦建立了應用就不能再對它做任何更改了,只有使用者自己可以更改設定。可以通過 Intent 引導使用者跳轉至對應設定頁
Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
intent.putExtra(Settings.EXTRA_CHANNEL_ID, myNotificationChannel.getId());
startActivity(intent);
複製程式碼
  • 查詢使用者當前的通知類別的設定可以通過 getNotificationChannel()getNotificationChannels()getVibrationPattern()getImportance() 等方法獲取
  • 使用 deleteNotificationChannel(id) 可以刪除通知類別,但是在開發模式下可能需要重灌應用或者清除資料才會完全刪除
  • 通知類別也可以分組
// The id of the group.
String groupId = "my_group_01";
// The user-visible name of the group.
CharSequence groupName = getString(R.string.group_name);
NotificationManager mNotificationManager =
        (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.createNotificationChannelGroup(new NotificationChannelGroup(groupId, groupName));
複製程式碼
  • Android 5.0 (API level 21) 開始支援勿擾模式(Do Not Disturb)以禁止任何通知產生的聲音和震動。Total silence(完全阻止)會阻止包括鬧鐘視訊遊戲在內的所有聲音和震動,Alarms only(僅限鬧鐘)會阻止除了鬧鐘外的所有聲音和震動,Priority only(自訂)可以定製要遮蔽的資訊通話等系統範圍內的通知。setCategory() 方法可以設定所屬的系統範圍的勿擾類別
  • 每個通知類別可以選擇是否覆蓋勿擾模式的設定,當勿擾模式設定為“僅限優先事項”時,可以允許繼續接收此類通知
  • Android 8.1 (API level 27) 開始每秒最多播放一次通知提示音,如果一秒內有多個通知那麼只播放一秒內的第一個通知提示音,如果一秒內多次頻繁更新一個通知,那麼系統可能會丟棄一些通知更新
  • 最好使用 NotificationCompatNotificationManagerCompat 等相容庫中的類以便方便地適配低版本系統
  • setSmallIcon() 方法可以設定小圖示,應用名和時間是由系統設定的,setLargeIcon() 方法可以設定右邊大圖示,setContentTitle()setContentText() 方法可以設定通知的標題和內容,setPriority() 方法可以為 Android 8.0 (API level 26) 以下的系統設定通知優先順序。系統範圍的預定義通知類別包括 NotificationCompat.CATEGORY_ALARMNotificationCompat.CATEGORY_REMINDER 等類別,這個類別在勿擾模式中有用,可以通過 setCategory() 方法指定所屬的系統範圍通知類別
  • 預設的通知內容會收縮成一行,可以通過 setStyle() 方法設定其他可展開的通知樣式,.setStyle(new NotificationCompat.BigTextStyle().bigText(emailObject.getSubjectAndSnippet())) 可以設定大文字塊樣式。.setStyle(new NotificationCompat.InboxStyle().addLine(messageSnippet1) 可以設定多行的 inbox 樣式。.setStyle(new NotificationCompat.MessagingStyle(resources.getString(R.string.reply_name)).addMessage(message1)) 可以設定訊息樣式,但是此樣式會忽略 setContentTitle()setContentText() 方法的設定,但可以通過 setConversationTitle() 方法設定該聊天所屬的群組名。setStyle(new android.support.v4.media.app.Notification.MediaStyle().setShowActionsInCompactView(1).setMediaSession(mMediaSession.getSessionToken())) 可以設定媒體樣式的通知,屬於 CATEGORY_TRANSPORT 類別。
  • 通知的點選事件可以通過 setContentIntent() 方法設定 PendingIntent 物件完成,setAutoCancel(true) 可以在點選後自動移除通知
Intent intent = new Intent(this, AlertDetails.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, CHANNEL_ID)
        .setSmallIcon(R.drawable.notification_icon)
        .setContentTitle("My notification")
        .setContentText("Hello World!")
        .setLargeIcon(myBitmap)
        .setStyle(new NotificationCompat.BigPictureStyle()
                .bigPicture(myBitmap)
                .bigLargeIcon(null))
        .setPriority(NotificationCompat.PRIORITY_DEFAULT)
        .setContentIntent(pendingIntent)
        .setAutoCancel(true);
複製程式碼
  • 通過 NotificationManagerCompat#notify() 方法可以顯示通知,你需要定義一個唯一的 int 值的 ID 作為這個通知的 ID,儲存這個 ID 以便之後更新或移除這個通知
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(notificationId, mBuilder.build());
複製程式碼
  • 通過 addAction() 方法可以新增操作按鈕
  • 新增回覆按鈕的流程為:
private static final String KEY_TEXT_REPLY = "key_text_reply";

String replyLabel = getResources().getString(R.string.reply_label);
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
        .setLabel(replyLabel)
        .build();

PendingIntent replyPendingIntent =
        PendingIntent.getBroadcast(getApplicationContext(),
                conversation.getConversationId(),
                getMessageReplyIntent(conversation.getConversationId()),
                PendingIntent.FLAG_UPDATE_CURRENT);

NotificationCompat.Action action =
        new NotificationCompat.Action.Builder(R.drawable.ic_reply_icon,
                getString(R.string.label), replyPendingIntent)
                .addRemoteInput(remoteInput)
                .build();

Notification newMessageNotification = new Notification.Builder(mContext, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_message)
        .setContentTitle(getString(R.string.title))
        .setContentText(getString(R.string.content))
        .addAction(action)
        .build();

NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(notificationId, newMessageNotification);
複製程式碼
private CharSequence getMessageText(Intent intent) {
    // 在 BroadcastReceiver 接收的 Intent 中可以根據之前的 KEY 拿到文字框的內容
    Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
    if (remoteInput != null) {
        return remoteInput.getCharSequence(KEY_TEXT_REPLY);
    }
    return null;
 }
複製程式碼
// 在回覆完成後更新通知表示已經處理這次回覆,也可以呼叫 setRemoteInputHistory() 方法附加回復的內容
Notification repliedNotification = new Notification.Builder(context, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_message)
        .setContentText(getString(R.string.replied))
        .build();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(notificationId, repliedNotification);
複製程式碼
  • 通過 setProgress(PROGRESS_MAX, PROGRESS_CURRENT, false) 可以給通知新增確定進度條,通過 setProgress(0, 0, true) 可以新增不確定進度條,通過 setProgress(0, 0, false) 可以在完成後移除進度條
  • setVisibility() 方法可以設定鎖屏時的通知顯示策略,VISIBILITY_PUBLIC(顯示所有通知)表示完整地顯示通知內容,VISIBILITY_SECRET(完全不顯示內容)表示不顯示通知的任何資訊,VISIBILITY_PRIVATE(隱藏敏感通知內容)表示只顯示圖示標題等基本資訊
  • NotificationManagerCompat#notify() 方法傳遞之前的通知 ID 可以更新之前的通知,呼叫 setOnlyAlertOnce() 方法以便只在第一次出現通知時提示使用者
  • 使用者可以主動清除通知,建立通知時呼叫 setAutoCancel() 方法可以在使用者點選通知後清除通知,建立通知時呼叫 setTimeoutAfter() 方法可以在超時後由系統自動清除通知,可以隨時呼叫 cancel()cancelAll() 方法清除之前的通知
  • 點選通知後啟動的 Activity 分為兩種,一種是應用的正常使用者體驗流中的常規 Activity,擁有任務完整的返回棧。還有一種是僅僅用來展示通知的詳細內容的特殊Activity,它不需要返回棧。
  • 對於常規 Activity 需要先通過 android:parentActivityName 屬性或者 android.support.PARENT_ACTIVITY<meta-data> 標籤指定層級關係,然後
Intent resultIntent = new Intent(this, ResultActivity.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addNextIntentWithParentStack(resultIntent);
PendingIntent resultPendingIntent =
        stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
複製程式碼
  • 對於特殊 Activity 需要先指定 android:taskAffinity=""android:excludeFromRecents="true" 以避免在之前的任務中啟動,然後
Intent notifyIntent = new Intent(this, ResultActivity.class);
notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent notifyPendingIntent = PendingIntent.getActivity(
        this, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT
);
複製程式碼
  • Android 7.0 (API level 24) 開始,如果一個應用同時有 4 個及以上的通知,那麼系統會自動將它們合併成一組,應用也可以自己定義和組織分組,使用者點選後可以展開成一些單獨的通知,老版本可以考慮使用 inbox 樣式代替。每個通知可以通過 setGroup() 方法指定所屬分組,通過 setSortKey() 方法排序,通過 setGroupAlertBehavior() 指定通知行為,預設是 GROUP_ALERT_ALL 表示組內所有的通知都可能產生聲音和震動。系統預設會自動生成通知組的摘要,你也可以單獨建立一個表示通知組摘要的通知
int SUMMARY_ID = 0;
String GROUP_KEY_WORK_EMAIL = "com.android.example.WORK_EMAIL";

Notification newMessageNotification1 =
    new NotificationCompat.Builder(MainActivity.this, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_notify_email_status)
        .setContentTitle(emailObject1.getSummary())
        .setContentText("You will not believe...")
        .setGroup(GROUP_KEY_WORK_EMAIL)
        .build();

Notification newMessageNotification2 =
    new NotificationCompat.Builder(MainActivity.this, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_notify_email_status)
        .setContentTitle(emailObject2.getSummary())
        .setContentText("Please join us to celebrate the...")
        .setGroup(GROUP_KEY_WORK_EMAIL)
        .build();

Notification summaryNotification =
    new NotificationCompat.Builder(MainActivity.this, CHANNEL_ID)
        .setContentTitle(emailObject.getSummary())
        //set content text to support devices running API level < 24
        .setContentText("Two new messages")
        .setSmallIcon(R.drawable.ic_notify_summary_status)
        //build summary info into InboxStyle template
        .setStyle(new NotificationCompat.InboxStyle()
                .addLine("Alex Faarborg  Check this out")
                .addLine("Jeff Chang    Launch Party")
                .setBigContentTitle("2 new messages")
                .setSummaryText("janedoe@example.com"))
        //specify which group this notification belongs to
        .setGroup(GROUP_KEY_WORK_EMAIL)
        //set this notification as the summary for the group
        .setGroupSummary(true)
        .build();

NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(emailNotificationId1, newMessageNotification1);
notificationManager.notify(emailNotificationId2, newMessageNotification2);
notificationManager.notify(SUMMARY_ID, summaryNotification);
複製程式碼
  • Android 8.0 (API level 26) 開始應用啟動圖示可以自動新增一個小圓點表示有新的通知,使用者長按應用啟動圖示可以檢視和處理通知,呼叫 mChannel.setShowBadge(false) 可以禁用小圓點標誌,呼叫 setNumber(messageCount) 可以設定長按後顯示給使用者的訊息數,呼叫 setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL) 可以設定長按後的圖示樣式,通過 setShortcutId() 可以隱藏重複的 shortcut
  • 自定義通知內容的樣式需要 setStyle(new NotificationCompat.DecoratedCustomViewStyle()) 樣式,然後呼叫 setCustomContentView()setCustomBigContentView() 方法指定自定義的摺疊和展開佈局(一般摺疊佈局限制高度為 64 dp,展開佈局高度限制為 256 dp),佈局中的控制元件要使用相容庫的樣式,如 style="@style/TextAppearance.Compat.Notification.Title"。如果不想使用標準通知模板,不呼叫 setStyle() 只呼叫 setCustomBigContentView() 即可
  • Android 5.0 (API level 21) 開始支援頂部彈出的 heads-up 通知,可能觸發 heads-up 通知的條件有: 使用者的 Activity 正處於全屏模式並使用了 fullScreenIntent;Android 8.0 (API level 26) 及更高的裝置上通知的重要程度為 IMPORTANCE_HIGH;Android 8.0 (API level 26) 以下的裝置上通知的優先順序為 PRIORITY_HIGHPRIORITY_MAX 並且開啟了鈴聲或震動

小技巧

  • 測試 Deep links:
adb shell am start
    -W -a android.intent.action.VIEW
    -d "example://gizmos" com.example.android
複製程式碼
  • 測試 Android App Links:
adb shell am start -a android.intent.action.VIEW
    -c android.intent.category.BROWSABLE
    -d "http://domain.name:optional_port"
複製程式碼
  • 應用安裝完 20s 後獲取所有應用的連結處理策略:
adb shell dumpsys package domain-preferred-apps
複製程式碼
  • 模擬系統殺掉應用程式:
adb shell am kill com.some.package
複製程式碼
  • 將檔案匯入手機:
adb push com.some.package /sdcard/
複製程式碼
  • .nomedia 檔案會導致其所在目錄不被 Media Scanner 掃描到

附錄

系統欄適配

/**
 * 華為手機劉海屏適配
 *
 * @author frank
 * @see <a href="https://developer.huawei.com/consumer/cn/devservice/doc/50114">《華為劉海屏手機安卓O版本適配指導》</a>
 */
public class HwNotchSizeUtil {

    private static final int FLAG_NOTCH_SUPPORT = 0x00010000;

    /**
     * 是否是劉海屏手機
     *
     * @param context Context
     * @return true:劉海屏 false:非劉海屏
     */
    public static boolean hasNotchInScreen(Context context) {
        boolean ret = false;
        try {
            ClassLoader cl = context.getClassLoader();
            Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
            Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen");
            ret = (boolean) get.invoke(HwNotchSizeUtil);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;
    }

    /**
     * 獲取劉海尺寸
     *
     * @param context Context
     * @return int[0]值為劉海寬度 int[1]值為劉海高度
     */
    public static int[] getNotchSize(Context context) {
        int[] ret = new int[]{0, 0};
        try {
            ClassLoader cl = context.getClassLoader();
            Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
            Method get = HwNotchSizeUtil.getMethod("getNotchSize");
            ret = (int[]) get.invoke(HwNotchSizeUtil);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;
    }

    /**
     * 設定應用視窗在華為劉海屏手機使用劉海區
     *
     * @param window Window
     */
    public static void setFullScreenWindowLayoutInDisplayCutout(Window window) {
        if (window == null) {
            return;
        }
        WindowManager.LayoutParams layoutParams = window.getAttributes();
        try {
            Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx");
            Constructor con = layoutParamsExCls.getConstructor(ViewGroup.LayoutParams.class);
            Object layoutParamsExObj = con.newInstance(layoutParams);
            Method method = layoutParamsExCls.getMethod("addHwFlags", int.class);
            method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 設定應用視窗在華為劉海屏手機不使用劉海區顯示
     *
     * @param window Window
     */
    public static void setNotFullScreenWindowLayoutInDisplayCutout(Window window) {
        if (window == null) {
            return;
        }
        WindowManager.LayoutParams layoutParams = window.getAttributes();
        try {
            Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx");
            Constructor con = layoutParamsExCls.getConstructor(ViewGroup.LayoutParams.class);
            Object layoutParamsExObj = con.newInstance(layoutParams);
            Method method = layoutParamsExCls.getMethod("clearHwFlags", int.class);
            method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
複製程式碼
/**
 * 小米手機劉海屏適配
 *
 * @author frank
 * @see <a href="https://dev.mi.com/console/doc/detail?pId=1293">《小米劉海屏 Android O 適配》</a>
 * @see <a href="https://dev.mi.com/console/doc/detail?pId=1341">《小米劉海屏 Android P 適配》</a>
 */
public class XiaomiNotchSizeUtil {

    private static final int FLAG_NOTCH_OPEN = 0x00000100;
    private static final int FLAG_NOTCH_PORTRAIT = 0x00000200;
    private static final int FLAG_NOTCH_LANDSCAPE = 0x00000400;

    /**
     * 是否是劉海屏手機
     *
     * @param context Context
     * @return true:劉海屏 false:非劉海屏
     */
    public static boolean hasNotchInScreen(Context context) {
        boolean ret = false;
        try {
            ret = "1".equals(getSystemProperty("ro.miui.notch"));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;
    }

    /**
     * 獲取劉海尺寸
     *
     * @param context Context
     * @return int[0]值為劉海寬度 int[1]值為劉海高度
     */
    public static int[] getNotchSize(Context context) {
        int[] ret = new int[]{0, 0};
        try {
            int widthResId = context.getResources().getIdentifier("notch_width", "dimen", "android");
            if (widthResId > 0) {
                ret[0] = context.getResources().getDimensionPixelSize(widthResId);
            }
            int heightResId = context.getResources().getIdentifier("notch_height", "dimen", "android");
            if (heightResId > 0) {
                ret[1] = context.getResources().getDimensionPixelSize(heightResId);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;
    }

    /**
     * 橫豎屏都繪製到耳朵區
     *
     * @param window Window
     */
    public static void setFullScreenWindowLayoutInDisplayCutout(Window window) {
        if (window == null) {
            return;
        }
        try {
            Method method = Window.class.getMethod("addExtraFlags",
                    int.class);
            method.invoke(window, FLAG_NOTCH_OPEN | FLAG_NOTCH_PORTRAIT | FLAG_NOTCH_LANDSCAPE);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 橫豎屏都不會繪製到耳朵區
     *
     * @param window Window
     */
    public static void setNotFullScreenWindowLayoutInDisplayCutout(Window window) {
        if (window == null) {
            return;
        }
        try {
            Method method = Window.class.getMethod("clearExtraFlags",
                    int.class);
            method.invoke(window, FLAG_NOTCH_OPEN | FLAG_NOTCH_PORTRAIT | FLAG_NOTCH_LANDSCAPE);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static String getSystemProperty(String key) {
        String ret = null;
        BufferedReader bufferedReader = null;
        try {
            Process process = Runtime.getRuntime().exec("getprop " + key);
            bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            StringBuilder stringBuilder = new StringBuilder();
            while ((line = bufferedReader.readLine()) != null) {
                stringBuilder.append(line);
            }
            ret = stringBuilder.toString();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return ret;
    }

}
複製程式碼
/**
 * OPPO手機劉海屏適配
 *
 * @author frank
 * @see <a href="https://open.oppomobile.com/wiki/doc#id=10159">《OPPO凹形屏適配說明》</a>
 */
public class OppoNotchSizeUtil {

    /**
     * 是否是劉海屏手機
     *
     * @param context Context
     * @return true:劉海屏 false:非劉海屏
     */
    public static boolean hasNotchInScreen(Context context) {
        return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
    }

}
複製程式碼
/**
 * VIVO手機劉海屏適配
 *
 * @author frank
 * @see <a href="https://dev.vivo.com.cn/documentCenter/doc/103">《異形屏應用適配指南》</a>
 */
public class VivoNotchSizeUtil {

    private static final int MASK_NOTCH_IN_SCREEN = 0x00000020;
    private static final int MASK_ROUNDED_IN_SCREEN = 0x00000008;

    /**
     * 是否是劉海屏手機
     *
     * @param context Context
     * @return true:劉海屏 false:非劉海屏
     */
    public static boolean hasNotchInScreen(Context context) {
        boolean ret = false;
        try {
            ClassLoader cl = context.getClassLoader();
            Class FtFeature = cl.loadClass("android.util.FtFeature");
            Method get = FtFeature.getMethod("isFeatureSupport", int.class);
            ret = (boolean) get.invoke(FtFeature, MASK_NOTCH_IN_SCREEN);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;
    }

}
複製程式碼
/**
 * 錘子手機劉海屏適配
 *
 * @author frank
 * @see <a href="https://resource.smartisan.com/resource/61263ed9599961d1191cc4381943b47a.pdf">《Smartisan 開發者文件》</a>
 */
public class SmartisanNotchSizeUtil {

    private static final int MASK_NOTCH_IN_SCREEN = 0x00000001;

    /**
     * 是否是劉海屏手機
     *
     * @param context Context
     * @return true:異形屏 false:非異形屏
     */
    public static boolean hasNotchInScreen(Context context) {
        boolean ret = false;
        try {
            ClassLoader cl = context.getClassLoader();
            Class DisplayUtilsSmt = cl.loadClass("smartisanos.api.DisplayUtilsSmt");
            Method get = DisplayUtilsSmt.getMethod("isFeatureSupport", int.class);
            ret = (boolean) get.invoke(DisplayUtilsSmt, MASK_NOTCH_IN_SCREEN);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;
    }

}
複製程式碼

相關文章