是時候更新手裡的武器了—Jetpack最全簡析

jimuzz發表於2020-10-15

前言

Android Jetpack想必大家都耳熟能詳了,Android KTXLiveDataRoom等等一系列庫都是出自 Jetpack。那麼 Jetpack到底是什麼?又包含哪些你還沒用過的東西?Google推出這個的原因又是什麼?今天我們就一起來完善一下我們腦中的Jetpack構圖。(篇幅較長,建議點贊關注Mark哦? )

介紹

2018年穀歌I/O,Jetpack橫空出世,官方介紹如下:

Jetpack 是一套庫、工具和指南,可幫助開發者更輕鬆地編寫優質應用。這些元件可幫助您遵循最佳做法、讓您擺脫編寫樣板程式碼的工作並簡化複雜任務,以便您將精力集中放在所需的程式碼上。

好好琢磨這段介紹就能解釋我們剛才的問題。

Jetpack到底是什麼?

  • 是一套庫、工具和指南。說白了就是一系列的庫或者工具集合,而且這些工具是作為我們優質應用的指南,相當於官方推薦做法。

google推出這個系列的原因是什麼?

  • 規範開發者更快更好的開發出優質應用。一直以來,Android開發都充斥了大量的不規範的操作和重複程式碼,比如生命週期的管理,開發過程的重複,專案架構的選擇等等。所以Google為了規範開發行為,就推出這套指南,旨在讓開發者們能夠更好,更快,更規範地開發出優質應用。

當然,這兩年的實踐也確實證明了Jetpack做到了它介紹的那樣,便捷,快速,優質。所以我們作為開發者還是應該早點應用到這些工具,提高自己的開發效率,也規範我們自己的開發行為。下面我們就一起了解下Jetpack的所有工具指南。GOGOGO!

先來一張官網的總攬圖:
(溫馨提示❤️ 本文嚴格按照下圖順序對元件進行分析,有需要的可以從目錄進入或者直接搜尋檢視)

Jetpack.jpg

Jetpack-基礎元件

Android KTX

Android KTX 是包含在 Android Jetpack 及其他 Android 庫中的一組 Kotlin 擴充套件程式。KTX 擴充套件程式可以為 Jetpack、Android 平臺及其他 API 提供簡潔的慣用 Kotlin 程式碼。為此,這些擴充套件程式利用了多種 Kotlin 語言功能

所以Android KTX就是基於kotlin特性而擴充套件的一些庫,方便開發使用。

舉?:
現在有個需求,讓兩個Set陣列的資料相加,賦值給新的Set陣列。正常情況下實現功能:

    val arraySet1 = LinkedHashSet<Int>()
    arraySet1.add(1)
    arraySet1.add(2)
    arraySet1.add(3)

    val arraySet2 = LinkedHashSet<Int>()
    arraySet2.add(4)
    arraySet2.add(5)
    arraySet2.add(6)

    val combinedArraySet1 = LinkedHashSet<Int>()
    combinedArraySet1.addAll(arraySet1)
    combinedArraySet1.addAll(arraySet2)

這程式碼真是又臭又長?️,沒關係,引入Collection KTX擴充套件庫再實現試試:

    dependencies {
        implementation "androidx.collection:collection-ktx:1.1.0"
    }
    
    // Combine 2 ArraySets into 1.
    val combinedArraySet = arraySetOf(1, 2, 3) + arraySetOf(4, 5, 6)

就是這麼簡單,用到kotlin的擴充套件函式擴充套件屬性,擴充套件了集合相關的功能,簡化了程式碼。
由於kotlin的各種特性,也就促成了一系列的擴充套件庫,還包括有Fragment KTX,Lifecycle KTX等等。

官方文件
Demo程式碼地址

AppCompat

不知道大家發現沒,原來Activity繼承的Activity類都被要求改成繼承AppCompatActivity類。這個AppCompatActivity類就屬於AppCompat庫,主要包含對Material Design介面實現的支援,相類似的還包括ActionBar,AppCompatDialog和ShareActionProvider,一共四個關鍵類。

那麼AppCompatActivity類到底對比Activity類又什麼區別呢?

  • AppCompatActivity,類似於原來的ActionBarActivity,一個帶標題欄的Activity。具體就是帶Toolbar的Activity。

這裡還有個ShareActionProvider大家可能用得比較少,這個類是用於在選單欄整合分享功能。
通過setShareIntent(Intent intent)方法可以在Menu裡設定你要分享的內容。具體用法可以參考官網說明

官方文件

Auto

讓您在編寫應用時無需擔心特定於車輛的硬體差異(如螢幕解析度、軟體介面、旋鈕和觸控式控制元件)。使用者可以通過手機上的 Android Auto 應用訪問您的應用。或者,當連線到相容車輛時,執行 Android 5.0(或更高版本)的手持裝置上的應用可以與通過 Android Auto 投射到車輛的應用進行通訊。

Android Auto,這個大家估計有點陌生。但是說到 CarPlay大家是不是很熟悉呢?沒錯,Android Auto是Google出的車機手機互聯方案。國內銷售的汽車大多數沒有搭載谷歌的Android Auto牆太高,觸及不到),所以我們接觸的很少。但是國外還是應用比較廣泛的。

所以這一模組就是用於開發Android Auto相關應用的,比如音樂播放APP,即時通訊APP之類,可以與車載系統通訊。

怎麼讓你的應用支援Android Auto?

    //新增
    <meta-data android:name="com.google.android.gms.car.application"
            android:resource="@xml/automotive_app_desc"/>
            
    <automotiveApp>
        <uses name="media"/>
    </automotiveApp>        

然後就可以進行相關開發了。怎麼測試呢?總不能讓我去汽車裡面測試吧。。
放心,官方提供了模擬器—Android Auto Desktop Head Unit emulator(簡稱DHU),在SDK Tools裡面可以下載。
如果你感興趣,可以去官網文件瞭解更多

官方文件

檢測

使用 Jetpack 基準庫,您可以在 Android Studio 中快速對 Kotlin 或 Java 程式碼進行基準化分析。該庫會處理預熱,衡量程式碼效能,並將基準化分析結果輸出到 Android Studio 控制檯。

這個模組說的是一個測試效能的庫—Benchmark,其實就是測試耗時時間,所以我們可以用來測試UI效能,圖片載入效能等等。現在我們來實現一個測試圖片載入效能的?:

為了方便我們直接建立一個Benchmark模組,右鍵New > Module >Benchmark Module
這樣就會幫我們匯入好庫了,然後我們在androidTest—java目錄下建立我們的測試用例類BitmapBenchmark,並新增兩個測試用例方法。

    androidTestImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.0.0'
    
private const val JETPACK = "images/test001.jpg"

@LargeTest
@RunWith(AndroidJUnit4::class)
class BitmapBenchmark {

    @get:Rule
    val benchmarkRule = BenchmarkRule()

    private val context = ApplicationProvider.getApplicationContext<Context>()
    private lateinit var bitmap: Bitmap

    @Before
    fun setUp() {
        val inputStream = context.assets.open(JETPACK)
        bitmap = BitmapFactory.decodeStream(inputStream)
        inputStream.close()
    }


    @Test
    fun bitmapGetPixelBenchmark() {
        val pixels = IntArray(100) { it }
        benchmarkRule.measureRepeated {
            pixels.map { bitmap.getPixel(it, 0) }
        }
    }

   //測試100畫素影像繪製耗時
    @Test
    fun bitmapGetPixelsBenchmark() {
        val pixels = IntArray(100) { it }
        benchmarkRule.measureRepeated {
            bitmap.getPixels(pixels, 0, 100, 0, 0, 100, 1)
        }
    }
}    

然後右鍵BitmapBenchmark類執行,注意需要在真機執行,控制檯列印出兩個方法的耗時

Started running tests

benchmark:         2,086 ns BitmapBenchmark.bitmapGetPixelsBenchmark
benchmark:        70,902 ns BitmapBenchmark.bitmapGetPixelBenchmark
Tests ran to completion.

這就是Benchmark庫的簡單使用,我理解benchmark這個模組是在單元測試的基礎上可以提供更多效能測試的功能,比如執行時間等。但是實際使用的話好像大家都用的比較少?以後會多嘗試看看,如果有懂的老鐵也可以評論區科普下?。

官方文件
Demo程式碼地址

多dex處理

這個應該大家都很熟悉,65536方法數限制。由於 65536 等於64 X 1024,因此這一限制稱為“64K 引用限制”。意思就是單個DEX 檔案內引用的方法總數限制為65536,超過這個方法數就要打包成多個dex。

解決辦法:

  • Android5.0以下,需要新增MultiDex支援庫。具體做法就是引入庫,啟用MultiDex,修改Application。
  • Android5.0以上,預設啟動MultiDex,不需要匯入庫。

問題來了?為什麼5.0以上就預設支援這個功能了呢?

  • Android 5.0之前的平臺版本使用Dalvik執行時執行應用程式碼,Dalvik 將應用限制為每個 APK 只能使用一個 classes.dex 位元組碼檔案,為了繞過這一限制,只有我們手動新增MultiDex支援庫。
  • Android 5.0及更高版本使用名為 ART 的執行時,它本身支援從APK 檔案載入多個 DEX 檔案。ART在應用安裝時執行預編譯,掃描classesN.dex檔案,並將它們編譯成單個.oat 檔案,以供Android裝置執行。

官方文件

安全

Security 庫提供了與讀取和寫入靜態資料以及金鑰建立和驗證相關的安全最佳做法實現方法。

這裡的安全指的是資料安全,涉及到的庫為Security 庫,具體就是安全讀寫檔案以及安全設定共享偏好SharedPreferences。
不知道大家以前加密檔案都是怎麼做的,我是把資料加密後再寫入檔案的,現在用Security 庫就會方便很多。

首先程式碼匯入

    dependencies {
        implementation "androidx.security:security-crypto:1.0.0-alpha02"
    }

Security 庫主要包含兩大類:
1)EncryptedFile
讀寫一個加密檔案,生成EncryptedFile之後,正常開啟檔案是亂碼情況,也就是加密了,需要
EncryptedFile相關API才能讀取。看看怎麼實現讀寫的吧!

    // 寫入資料
    fun writeData(context: Context, directory: File) {
        val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
        val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)

        val fileToRead = "my_sensitive_data.txt"
        val encryptedFile = EncryptedFile.Builder(
            File(directory, fileToRead),
            context,
            masterKeyAlias,
            EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
        ).build()

        val fileContent = "MY SUPER-SECRET INFORMATION"
            .toByteArray(StandardCharsets.UTF_8)
        encryptedFile.openFileOutput().apply {
            write(fileContent)
            flush()
            close()
        }
    }
    
    // 讀取資料
    fun readData(context: Context, directory: File) {
        // recommended that you use the value specified here.
        val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
        val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)

        val fileToRead = "my_sensitive_data.txt"
        val encryptedFile = EncryptedFile.Builder(
            File(directory, fileToRead),
            context,
            masterKeyAlias,
            EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
        ).build()

        val inputStream = encryptedFile.openFileInput()
        val byteArrayOutputStream = ByteArrayOutputStream()
        var nextByte: Int = inputStream.read()
        while (nextByte != -1) {
            byteArrayOutputStream.write(nextByte)
            nextByte = inputStream.read()
        }

        val plaintext: ByteArray = byteArrayOutputStream.toByteArray()
    }
           

2)EncryptedSharedPreferences

    val sharedPreferences = EncryptedSharedPreferences
        .create(
        fileName,
        masterKeyAlias,
        context,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )

    val sharedPrefsEditor = sharedPreferences.edit()
    

官方文件
Demo程式碼地址

測試

測試應用在Android專案中是必不可缺的步驟,包括功能測試,整合測試,單元測試。這裡主要說的是通過程式碼的形式編寫測試用例,測試應用的的穩定性,完整性等等。

具體體現在Android Studio中有兩個測試目錄:

  • androidTest目錄應包含在真實或虛擬裝置上執行的測試。
  • test 目錄應包含在本地計算機上執行的測試,如單元測試。

具體測試的編寫可以看看這個官方專案學習:testing-samples

官方文件

TV

Android TV應用在國內還是應用比較廣泛的,市場上大部分電視都是Android系統,支援APK安裝,包括華為鴻蒙系統也支援APK安裝了。所以我們手機上的應用基本可以直接安裝到電視上,只是UI焦點等方面需要改進。
以下從四個方面簡單說下TV應用的配置,分別是配置,硬體,按鍵和測試
1)配置
首先,在Androidmanifest.xml裡面宣告Activity的時候,如果你想相容TV版和手機版,可以設定不同的啟動Activity,主要表現為設定android.intent.category.LEANBACK_LAUNCHER過濾器:

   //手機啟動Activity
   <activity
     android:name="com.example.android.MainActivity"
     android:label="@string/app_name" >

     <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
     </intent-filter>
   </activity>
   
   //TV啟動Activity
   <activity
     android:name="com.example.android.TvActivity"
     android:label="@string/app_name"
     android:theme="@style/Theme.Leanback">

     <intent-filter>
       <action android:name="android.intent.action.MAIN" />
       <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
     </intent-filter>

   </activity>   

2)硬體
硬體主要包括如何判斷當前執行環境是TV環境,以及檢查TV硬體的某些功能是否存在。

    //判斷當前執行環境是TV環境
    val uiModeManager = getSystemService(UI_MODE_SERVICE) as UiModeManager
    if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
        Log.d(TAG, "Running on a TV Device")
    } else {
        Log.d(TAG, "Running on a non-TV Device")
    }
    
    //檢查TV硬體的某些功能是否存在
    // Check if android.hardware.touchscreen feature is available.
    if (packageManager.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) {
        Log.d("HardwareFeatureTest", "Device has a touch screen.")
    }

3) 按鍵
TV中的介面事件主要包括:

   BUTTON_B、BACK	返回
   BUTTON_SELECT、BUTTON_A、ENTER、DPAD_CENTER、KEYCODE_NUMPAD_ENTER	選擇
   DPAD_UP、DPAD_DOWN、DPAD_LEFT、DPAD_RIGHT	導航

按鍵配置包括:

   nextFocusDown	定義當使用者向下導航時下一個獲得焦點的檢視。
   nextFocusLeft	定義當使用者向左導航時下一個獲得焦點的檢視。
   nextFocusRight	定義當使用者向右導航時下一個獲得焦點的檢視。
   nextFocusUp	  定義當使用者向上導航時下一個獲得焦點的檢視。
   
   <TextView android:id="@+id/Category1"
             android:nextFocusDown="@+id/Category2"\>
    

4)測試
同樣,TV端APP的測試可以直接通過TV模擬器測試,在AVD Manager裡面建立新的TV 模擬機即可。

官方文件

Wear OS by Google

Google的手錶系統,同樣是使用Android開發。國內好像沒有基於Wear OS 的手錶,而且據我所知,國外的WearOS裝置也很少了,被WatchOS全面打敗,連Google旗下的App Nest都不支援WearOS了。所以這部分我們瞭解下就行,有興趣的可以去看看官方Demo

官方文件

Jetpack-架構元件

這個模組的元件就是專門為MVVM框架服務的,但是每個庫都是可以單獨使用的,也是jetpack中比較重要的一大模組。
簡單說下MVVM,Model—View—ViewModel。

  • Model層主要指資料,比如伺服器資料,本地資料庫資料,所以網路操作和資料庫讀取就是這一層,只儲存資料。
  • View層主要指UI相關,比如xml佈局檔案,Activity介面顯示
  • ViewModel層是MVVM的核心,連線view和model,需要將model的資料展示到view上,以及view上的運算元據反映轉化到model層,所以就相當於一個雙向繫結。

所以就需要,databinding進行資料的繫結,單向或者雙向。viewmodel進行資料管理,繫結view和資料。lifecycle進行生命週期管理。LiveData進行資料的及時反饋。
迫不及待了吧,跟隨我一起看看每個庫的神奇之處。

資料繫結

資料繫結庫是一種支援庫,藉助該庫,您可以使用宣告性格式(而非程式化地)將佈局中的介面元件繫結到應用中的資料來源。

主要指的就是資料繫結庫DataBinding,下面從六個方面具體介紹下

配置應用使用資料繫結:

   android {
        ...
        dataBinding {
            enabled = true
        }
    }
    

1)佈局和繫結表示式
通過資料繫結,我們可以讓xml佈局檔案中的view與資料物件進行繫結和賦值,並且可以藉助表示式語言編寫表示式來處理檢視分派的事件。舉個?:

    //佈局 activity_main.xml
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
       <data>
           <variable name="user" type="com.example.User"/>
       </data>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.name}"/>
    </layout>
    
    //實體類User
    data class User(val name: String)
    
    
    //Activity賦值
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding: ActivityMainBinding = DataBindingUtil.setContentView(
                this, R.layout.activity_main)
        binding.user = User("Bob")
    }
    

通過@{} 符號,可以在佈局中使用資料物件,並且可以通過DataBindingUtil獲取賦值物件。並且@{} 裡面的表示式語言支援多種運算子,包括算術運算子,邏輯運算子等等。

2)可觀察的資料物件
可觀察性是指一個物件將其資料變化告知其他物件的能力。通過資料繫結庫,您可以讓物件、欄位或集合變為可觀察。

比如上文剛說到的User類,我們將name屬性改成可觀察物件,

   data class User(val name: ObservableField<String>)
   
   val userName = ObservableField<String>()
   userName.set("Bob")

   val binding: ActivityMainBinding = DataBindingUtil.setContentView(
                this, R.layout.activity_main)
   binding.user = User(userName)   

然後繫結到佈局中,這時候這個User的name屬性就是被觀察物件了,如果userName改變,佈局裡面的TextView顯示資料也會跟著改變,這就是可觀察資料物件。

3)生成的繫結類

剛才我們獲取繫結佈局是通過DataBindingUtil.setContentView方法生成ActivityMainBinding物件並繫結佈局。那麼ActivityMainBinding類是怎麼生成的呢?只要你的佈局用layout屬性包圍,編譯後就會自動生成繫結類,類名稱基於佈局檔案的名稱,它會轉換為 Pascal 大小寫形式並在末尾新增 Binding 字尾。

正常建立繫結物件是通過如下寫法:

    //Activity
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: MyLayoutBinding = MyLayoutBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
    
    
    //Fragment
    @Nullable
    fun onCreateView( inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        mDataBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_layout, container, false)
        return mDataBinding.getRoot()
    }

4)繫結介面卡

介面卡這裡指的是佈局中的屬性設定,android:text="@{user.name}" 表示式為例,庫會查詢接受user.getName()所返回型別的 setText(arg) 方法。
重要的是,我們可以自定義這個介面卡了,也就是佈局裡面的屬性我們可以隨便定義它的名字和作用。來個?

    @BindingAdapter("imageUrl")
    fun loadImage(view: ImageView, url: String) {
        Picasso.get().load(url).into(view)
    }
    
    <ImageView app:imageUrl="@{venue.imageUrl}" />

在類中定義一個外部可以訪問的方法loadImage,註釋@BindingAdapter裡面的屬性為你需要定義的屬性名稱,這裡設定的是imageUrl。所以在佈局中就可以使用app:imageUrl,並傳值為String型別,系統就會找到這個介面卡方法並執行。

5)將佈局檢視繫結到架構元件
這一塊就是實際應用了,和jetpack其他元件相結合使用,形成完整的MVVM分層架構。

        // Obtain the ViewModel component.
        val userModel: UserViewModel by viewModels()

        // Inflate view and obtain an instance of the binding class.
        val binding: ActivityDatabindingMvvmBinding =
            DataBindingUtil.setContentView(this, R.layout.activity_databinding_mvvm)

        // Assign the component to a property in the binding class.
        binding.viewmodel = userModel
        
    <data>
        <variable
            name="viewmodel"
            type="com.panda.jetpackdemo.dataBinding.UserViewModel" />
    </data>
    
    class UserViewModel : ViewModel() {
    val currentName: MutableLiveData<String> by lazy {
        MutableLiveData<String>()
    }

    init {
        currentName.value="zzz"
    }
}

6)雙向資料繫結

剛才我們介紹的都是單向繫結,也就是佈局中view繫結了資料物件,那麼如何讓資料物件也對view產生繫結呢?也就是view改變的時候資料物件也能接收到訊息,形成雙向繫結

很簡單,比如一個EditText,需求是EditText改變的時候,user物件name資料也會跟著改變,只需要把之前的"@{}"改成"@={}"


    //佈局 activity_main.xml
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
       <data>
           <variable name="user" type="com.example.User"/>
       </data>
       <EditText android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@={user.name}"/>
    </layout>

很簡單吧,同樣,這個雙向繫結功能也是支援自定義的。來個?

object SwipeRefreshLayoutBinding {

    //方法1,資料繫結到view
    @JvmStatic
    @BindingAdapter("app:bind_refreshing")
    fun setSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout,newValue: Boolean) {
        if (swipeRefreshLayout.isRefreshing != newValue)
            swipeRefreshLayout.isRefreshing = newValue
    }

    //方法1,view改變會通知bind_refreshingChanged,並且從該方法獲取view的資料
    @JvmStatic
    @InverseBindingAdapter(attribute = "app:bind_refreshing",event = "app:bind_refreshingChanged")
    fun isSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout): Boolean =swipeRefreshLayout.isRefreshing
            
    //方法3,view如何改變來影響資料內容  
    @JvmStatic
    @BindingAdapter("app:bind_refreshingChanged",requireAll = false)
    fun setOnRefreshListener(swipeRefreshLayout: SwipeRefreshLayout,bindingListener: InverseBindingListener?) {
        if (bindingListener != null)
            swipeRefreshLayout.setOnRefreshListener {
                bindingListener.onChange()
            }
    }
}


<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:bind_refreshing="@={viewModel.refreshing }">
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

簡單說明下,首先通過bind_refreshing屬性,將資料viewModel.refreshing繫結到view上,這樣資料變化,view也會跟著變化。然後view變化的時候,通過InverseBindingAdapter註釋,會呼叫bind_refreshingChanged事件,而bind_refreshingChanged事件告訴了我們view什麼時候會進行資料的修改,在這個案例中也就是swipeRefreshLayout下滑的時候會導致資料進行改變,於是資料物件會從isSwipeRefreshLayoutRefreshing方法獲取到最新的數值,也就是從view更新過來的資料。

這裡要注意的一個點是,雙向繫結要考慮到死迴圈問題,當View被改變,資料物件對應發生更新,同時,這個更新又回通知View層去重新整理UI,然後view被改變又會導致資料物件更新,無限迴圈下去了。所以防止死迴圈的做法就是判斷view的資料狀態,當發生改變的時候才去更新view。

官方文件
Demo程式碼地址

Lifecycles

生命週期感知型元件可執行操作來響應另一個元件(如 Activity 和 Fragment)的生命週期狀態的變化。這些元件有助於您寫出更有條理且往往更精簡的程式碼,這樣的程式碼更易於維護。

Lifecycles,稱為生命週期感知型元件,可以感知和響應另一個元件(如 Activity 和 Fragment)的生命週期狀態的變化。

可能有人會疑惑了,生命週期就那幾個,我為啥還要匯入一個庫呢?有了庫難道就不用寫生命週期了嗎,有什麼好處呢?
舉個?,讓你感受下。

首先匯入庫,可以根據實際專案情況匯入

        // ViewModel
        implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
        // LiveData
        implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
        // Lifecycles only (without ViewModel or LiveData)
        implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
        //.......

現在有一個定位監聽器,需要在Activity啟動的時候開啟,銷燬的時候關閉。正常程式碼如下:

class BindingActivity : AppCompatActivity() {

    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        myLocationListener = MyLocationListener(this) { location ->
            // update UI
        }
    }
    public override fun onStart() {
        super.onStart()
        myLocationListener.start()       
    }
    public override fun onStop() {
        super.onStop()
        myLocationListener.stop()
    }

    internal class MyLocationListener(
            private val context: Context,
            private val callback: (Location) -> Unit
    ) {
        fun start() {
            // connect to system location service
        }
        fun stop() {
            // disconnect from system location service
        }
    }
    
}

乍一看也沒什麼問題是吧,但是如果需要管理生命週期的類一多,是不是就不好管理了。所有的類都要在Activity裡面管理,還容易漏掉。
所以解決辦法就是實現解耦,讓需要管理生命週期的類自己管理,這樣Activity也不會遺漏和臃腫了。上程式碼:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        myLocationListener = MyLocationListener(this) { location ->
            // update UI
        }
       lifecycle.addObserver(myLocationListener)
    }



    internal class MyLocationListener (
            private val context: Context,
            private val callback: (Location) -> Unit
    ): LifecycleObserver {

        @OnLifecycleEvent(Lifecycle.Event.ON_START)
        fun start() {

        }

        @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
        fun stop() {
            // disconnect if connected
        }
    }

很簡單吧,只要實現LifecycleObserver介面,就可以用註釋的方式執行每個生命週期要執行的方法。然後在Activity裡面addObserver繫結即可。

同樣的,Lifecycle也支援自定義生命週期,只要繼承LifecycleOwner即可,然後通過markState方法設定自己類的生命週期,舉個?

class BindingActivity : AppCompatActivity(), LifecycleOwner {

    private lateinit var lifecycleRegistry: LifecycleRegistry

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleRegistry = LifecycleRegistry(this)
        lifecycleRegistry.markState(Lifecycle.State.CREATED)
    }

    public override fun onStart() {
        super.onStart()
        lifecycleRegistry.markState(Lifecycle.State.STARTED)
    }
}    

官方文件
Demo程式碼地址

LiveData

LiveData 是一種可觀察的資料儲存器類。與常規的可觀察類不同,LiveData 具有生命週期感知能力,意指它遵循其他應用元件(如 Activity、Fragment 或 Service)的生命週期。這種感知能力可確保 LiveData 僅更新處於活躍生命週期狀態的應用元件觀察者。

LiveData 是一種可觀察的資料儲存器類。
等等,這個介紹好像似曾相識?對,前面說資料繫結的時候就有一個可觀察的資料物件ObservableField。那兩者有什麼區別呢?

1) LiveData 具有生命週期感知能力,可以感知到Activity等的生命週期。這樣有什麼好處呢?很常見的一點就是可以減少記憶體洩漏和崩潰情況了呀,想想以前你的專案中針對網路介面返回資料的時候都要判斷當前介面是否銷燬,現在LiveData就幫你解決了這個問題。

具體為什麼能解決崩潰和洩漏問題呢?

  • 不會發生記憶體洩漏
    觀察者會繫結到 Lifecycle 物件,並在其關聯的生命週期遭到銷燬後進行自我清理。
  • 不會因 Activity 停止而導致崩潰
    如果觀察者的生命週期處於非活躍狀態(如返回棧中的 Activity),則它不會接收任何 LiveData 事件。
  • 自動判斷生命週期並回撥方法
    如果觀察者的生命週期處於 STARTED 或 RESUMED狀態,則 LiveData 會認為該觀察者處於活躍狀態,就會呼叫onActive方法,否則,如果 LiveData 物件沒有任何活躍觀察者時,會呼叫 onInactive()方法。

2) LiveData更新資料更靈活,不一定是改變資料,而是呼叫方法(postValue或者setValue)的方式進行UI更新或者其他操作。

好了。還是舉個?更直觀的看看吧:


    //匯入庫:
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"

    class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
        private val stockManager = StockManager(symbol)

        private val listener = { price: BigDecimal ->
            value = price
        }

        override fun onActive() {
            stockManager.requestPriceUpdates(listener)
        }

        override fun onInactive() {
            stockManager.removeUpdates(listener)
        }
    }
    
    public class MyFragment : Fragment() {
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            val myPriceListener: LiveData<BigDecimal> = StockLiveData("")
            myPriceListener.observe(this, Observer<BigDecimal> { price: BigDecimal? ->
                // 監聽livedata的資料變化,如果呼叫了setValue或者postValue會呼叫該onChanged方法
                //更新UI資料或者其他處理
            })
        }
    }
        

這是一個股票資料物件,StockManager為股票管理器,如果該物件有活躍觀察者時,就去監聽股票市場的情況,如果沒有活躍觀察者時,就可以斷開監聽。
當監聽到股票資訊變化,該股票資料物件就會通過setValue方法進行資料更新,反應到觀察者的onChanged方法。這裡要注意的是setValue方法只能在主執行緒呼叫,而postValue則是在其他執行緒呼叫。
Fragment這個觀察者生命週期發生變化時,LiveData就會移除這個觀察者,不再傳送訊息,所以也就避免崩潰問題。

官方文件
Demo程式碼地址

導航
Navigation 元件旨在用於具有一個主 Activity 和多個 Fragment 目的地的應用。主 Activity 與導航圖相關聯,且包含一個負責根據需要交換目的地的 NavHostFragment。在具有多個 Activity 目的地的應用中,每個 Activity 均擁有其自己的導航圖。

所以說白了,Navigation就是一個Fragment的管理框架。
怎麼實現?建立Activity,Fragment,進行連線。

1)匯入庫

  def nav_version = "2.3.0"
  implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
  implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

2)建立3個Fragment和一個Activity

3)建立res/navigation/my_nav.xml 檔案

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    app:startDestination="@id/myFragment1"
    tools:ignore="UnusedNavigation">

    <fragment
        android:id="@+id/myFragment1"
        android:name="com.example.studynote.blog.jetpack.navigation.MyFragment1"
        android:label="fragment_blank"
        tools:layout="@layout/fragmetn_my_1" >
        <action
            android:id="@+id/action_blankFragment_to_blankFragment2"
            app:destination="@id/myFragment2" />
    </fragment>

    <fragment
        android:id="@+id/myFragment2"
        android:name="com.example.studynote.blog.jetpack.navigation.MyFragment1"
        android:label="fragment_blank"
        tools:layout="@layout/fragmetn_my_1" >
        <action
            android:id="@+id/action_blankFragment_to_blankFragment2"
            app:destination="@id/myFragment3" />
    </fragment>

    <fragment
        android:id="@+id/myFragment3"
        android:name="com.example.studynote.blog.jetpack.navigation.MyFragment1"
        android:label="fragment_blank"
        tools:layout="@layout/fragmetn_my_1" >
    </fragment>
</navigation>

在res資料夾下新建navigation目錄,並新建my_nav.xml 檔案。配置好每個Fragment,其中:

  • app:startDestination 屬性代表一開始顯示的fragment
  • android:name 屬性代表對應的Fragment路徑
  • action 代表該Fragment存在的跳轉事件,比如myFragment1可以跳轉myFragment2。
  1. 修改Activity的佈局檔案:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">

<fragment
    android:id="@+id/nav_host_fragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:defaultNavHost="true"
    app:navGraph="@navigation/my_nav" />

</androidx.constraintlayout.widget.ConstraintLayout>

可以看到,Activity的佈局檔案就是一個fragment控制元件,name為NavHostFragment,navGraph為剛才新建的mynavigation檔案。

5)配置完了之後,就可以設定具體的跳轉邏輯了。

    override fun onClick(v: View) {
    //不帶引數
 v.findNavController().navigate(R.id.action_blankFragment_to_blankFragment2)
   //帶引數
    var bundle = bundleOf("amount" to amount)
    v.findNavController().navigate(R.id.confirmationAction, bundle)
 
    }
    
    //接收資料
    tv.text = arguments?.getString("amount")
    

需要注意的是,跳轉這塊官方建議用Safe Args 的Gradle 外掛,該外掛可以生成簡單的 object 和 builder 類,以便以型別安全的方式瀏覽和訪問任何關聯的引數。這裡就不細說了,感興趣的可以去官網看看

官方文件
Demo程式碼地址

Room

Room 永續性庫在 SQLite 的基礎上提供了一個抽象層,讓使用者能夠在充分利用 SQLite 的強大功能的同時,獲享更強健的資料庫訪問機制。

所以Room就是一個資料庫框架。問題來了,市面上那麼多資料庫元件,比如ormLite,greendao等等,為什麼google還要出一個room,有什麼優勢呢?

  • 效能優勢,一次資料庫操作主要包括:構造sql語句—編譯語句—傳入引數—執行操作。ORMLite主要在獲取引數屬性值的時候,是通過反射獲取的,所以速度較慢。GreenDao在構造sql語句的時候是通過程式碼拼接,所以較慢。Room是通過介面方法的註解生成sql語句,也就是編譯成位元組碼的時候就生成了sql語句,所以執行起來較快。
  • 支援jetpack其他元件(比如LiveData,Paging)以及RxJava,這就好比藉助了當前所在的優勢環境,就能給你帶來一些得天獨厚的優勢。當然實際使用起來也確實要方便很多,比如liveData結合,就能在資料查詢後進行自動UI更新。

既然Room這麼優秀,那就用起來吧。
Room的接入主要有三大點:DataBase、Entity、Dao。分別對應資料庫,表和資料訪問。

1)首先匯入庫:

    apply plugin: 'kotlin-kapt'

    dependencies {
      def room_version = "2.2.5"

      implementation "androidx.room:room-runtime:$room_version"
      kapt "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor

      // optional - Kotlin Extensions and Coroutines support for Room
      implementation "androidx.room:room-ktx:$room_version"

      // optional - RxJava support for Room
      implementation "androidx.room:room-rxjava2:$room_version"
    }
    

2)建立資料庫類,宣告資料庫表成員,資料庫名稱,資料庫版本,單例等等

@Database(entities = arrayOf(User::class), version = 1)
abstract class UserDb : RoomDatabase() {

    abstract fun userDao(): UserDao

    companion object {
        private var instance: UserDb? = null

        @Synchronized
        fun get(context: Context): UserDb {
            if (instance == null) {
                instance = Room.databaseBuilder(context.applicationContext,
                    UserDb::class.java, "StudentDatabase").build()
            }
            return instance!!
        }
    }
}

3)建表,可以設定主鍵,外來鍵,索引,自增等等

@Entity
data class User(@PrimaryKey(autoGenerate = true) val id: Int,
                val name: String)

4)Dao,資料操作

@Dao
interface UserDao {

    @Query("SELECT * FROM User")
    fun getAllUser(): DataSource.Factory<Int, User>

    @Query("SELECT * FROM User")
    fun getAllUser2(): LiveData<List<User>>

    @Query("SELECT * from user")
    fun getAllUser3(): Flowable<List<User>>

    @Insert
    fun insert(users: List<User>)
}

然後就可以進行資料庫操作了,很簡單吧。
官方文件
Demo程式碼地址

Paging

分頁庫可幫助您一次載入和顯示一小塊資料。按需載入部分資料會減少網路頻寬和系統資源的使用量。

所以Paging就是一個分頁庫,主要用於Recycleview列表展示。下面我就結合Room說說Paging的用法。
使用Paging主要注意兩個類:PagedList和PagedListAdapter
1)PagedList
用於載入應用資料塊,繫結資料列表,設定資料頁等。結合上述Room的Demo我繼續寫了一個UserModel進行資料管理:

class UserModel(app: Application) : AndroidViewModel(app) {
    val dao = UserDb.get(app).userDao()
    var idNum = 1

    companion object {
        private const val PAGE_SIZE = 10
    }

    //初始化PagedList
    val users = LivePagedListBuilder(
        dao.getAllUser(), PagedList.Config.Builder()
            .setPageSize(PAGE_SIZE)
            .setEnablePlaceholders(true)
            .build()
    ).build()

    //插入使用者
    fun insert() = ioThread {
        dao.insert(newTenUser())
    }

    //獲取新的10個使用者
    fun newTenUser(): ArrayList<User> {
        var newUsers = ArrayList<User>()
        for (index in 1..10) {
            newUsers.add(User(0, "bob${++idNum}"))
        }
        return newUsers
    }

}

2)PagedListAdapter
使用Recycleview必要要用到adatper,所以這裡需要繫結一個繼承自PagedListAdapter的adapter:

class UserAdapter : PagedListAdapter<User, UserAdapter.UserViewHolder>(diffCallback) {
    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        holder.bindTo(getItem(position))
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder =
        UserViewHolder(parent)

    companion object {

        private val diffCallback = object : DiffUtil.ItemCallback<User>() {
            override fun areItemsTheSame(oldItem: User, newItem: User): Boolean =
                oldItem.id == newItem.id

            override fun areContentsTheSame(oldItem: User, newItem: User): Boolean =
                oldItem == newItem
        }
    }

    class UserViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
        LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false)) {

        private val tv1 = itemView.findViewById<TextView>(R.id.name)
        var user: User? = null

        fun bindTo(user: User?) {
            this.user = user
            tv1.text = user?.name
        }
    }
}

這裡還用到了 DiffUtil.ItemCallback 類,用於比較資料,進行資料更新用。

ok,資料來源,adapter都設定好了,接下來就是監聽資料,重新整理資料就可以了

        // 監聽users資料,資料改變呼叫submitList方法
        viewModel.users.observe(this, Observer(adapter::submitList))

對,就是這麼一句,監聽PagedList,並且在它改變的時候呼叫PagedListAdapter的submitList方法。
這分層夠爽吧,其實這也就是paging或者說jetpack給我們專案帶來的優勢,層層解耦,adapter都不用維護list資料來源了。

官方文件
Demo程式碼地址

ViewModel

ViewModel 類旨在以注重生命週期的方式儲存和管理介面相關的資料。ViewModel 類讓資料可在發生螢幕旋轉等配置更改後繼續留存。

終於說到ViewModel了,其實之前的demo都用了好多遍了,ViewModel主要是從介面控制器邏輯中分離出檢視資料,為什麼要這麼做呢?主要為了解決兩大問題:

  • 以前Activity中如果被系統銷燬或者需要重新建立的時候,頁面臨時性資料都會丟失,需要通過onSaveInstanceState() 方法儲存,onCreate方法中讀取。而且資料量一大就更加不方便了。
  • 在Activity中,難免有些非同步呼叫,所以就會容易導致介面銷燬時候,這些呼叫還存在。那就會發生記憶體洩漏或者直接崩潰。

所以ViewModel誕生了,還是解耦,我把資料單獨拿出來管理,還加上生命週期,那不就可以解決這些問題了嗎。而且當所有者 Activity 完全銷燬之後,ViewModel會呼叫其onCleared() 方法,以便清理資源。

接下來舉個?,看看ViewModel具體是怎麼使用的:


def lifecycle_version = "2.2.0"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"


class SharedViewModel : ViewModel() {
    var userData = MutableLiveData<User>()

    fun select(item: User) {
        userData.value = item
    }

    override fun onCleared() {
        super.onCleared()
    }
}

class MyFragment1 : Fragment() {
    private lateinit var btn: Button

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val model=activity?.let { ViewModelProvider(it).get(SharedViewModel::class.java) }
        btn.setOnClickListener{
            model?.select(User(0,"bob"))
        }
    }
}

class MyFragment2 : Fragment() {
    private lateinit var btn: Button

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val model=activity?.let { ViewModelProvider(it).get(SharedViewModel::class.java) }
        model?.userData?.observe(viewLifecycleOwner, Observer<User> { item ->
            // Update the UI
        })
    }
}
    

Fragment中,獲取到viewmodel的例項,然後進行資料監聽等操作。等等,你能發現什麼不?
對了,資料通訊。不同的 Fragment 可以使用其父Activity共享ViewModel 來進行資料的通訊,厲害吧。還有很多其他的用法,去專案中慢慢發現吧!

官方文件
Demo程式碼地址

WorkManager

使用 WorkManager API 可以輕鬆地排程即使在應用退出或裝置重啟時仍應執行的可延遲非同步任務。

聽聽這個介紹就很神奇了,應用退出和裝置重啟都能自動執行?通過廣播?那資料又是怎麼儲存的呢?聽說還可以執行週期性非同步任務,順序鏈式呼叫哦!接下來一一解密

  • 關於應用退出和裝置重啟
    如果APP正在執行,WorkManager會在APP程式中起一個新執行緒來執行任務;如果APP沒有執行,WorkManager會選擇一個合適的方式來排程後臺任務--根據系統級別和APP狀態,WorkManager可能會使用JobScheduler,FireBase JobDispatcher或者AlarmManager
  • 關於資料儲存
    WorkManager建立的任務資料都會儲存到資料庫,用的是Room框架。然後重啟等時間段都會去資料庫尋找需要安排執行的任務,然後判斷約束條件,滿足即可執行。

一般這個API應用到什麼場景呢?想想,可靠執行,還可以週期非同步。
對了,傳送日誌。可以通過WorkManager設定週期任務,每天執行一次傳送日誌的任務。而且能夠保證你的任務可靠執行,一定可以上傳到,當然也是支援監聽任務結果等。?:

1)匯入庫

    dependencies {
      def work_version = "2.3.4"
        // Kotlin + coroutines
        implementation "androidx.work:work-runtime-ktx:$work_version"

        // optional - RxJava2 support
        implementation "androidx.work:work-rxjava2:$work_version"

        // optional - GCMNetworkManager support
        implementation "androidx.work:work-gcm:$work_version"
      }
    

2) 新建任務類,繼承Worker,重寫doWork方法,返回任務結果。

class UploadLogcatWork(appContext: Context, workerParams: WorkerParameters) :
    Worker(appContext, workerParams) {

    override fun doWork(): Result {

        if (isUploadLogcatSuc()) {
            return Result.success()
        } else if (isNeedRetry()){
            return Result.retry()
        }

        return Result.failure()
    }

    fun isUploadLogcatSuc(): Boolean {
        var isSuc: Boolean = false
        return isSuc
    }

    fun isNeedRetry(): Boolean {
        var isSuc: Boolean = false
        return isSuc
    }
}

3)最後就是設定約束(是否需要網路,是否支援低電量,是否支援充電執行,延遲等等),執行任務(單次任務或者迴圈週期任務)

        //設定約束
        val constraints =
            Constraints.Builder()
                //網路連結的時候使用
                .setRequiredNetworkType(NetworkType.CONNECTED)
                //是否在裝置空閒的時候執行
                .setRequiresDeviceIdle(false)
                //是否在低電量的時候執行
                .setRequiresBatteryNotLow(true)
                //是否在記憶體不足的時候執行
                .setRequiresStorageNotLow(true)
                //是否時充電的時候執行
                .setRequiresCharging(true)
                //延遲執行
                .setTriggerContentMaxDelay(1000 * 1, TimeUnit.MILLISECONDS)
                .build()

        //設定迴圈任務
        val uploadRequest =
            PeriodicWorkRequestBuilder<UploadLogcatWork>(1, TimeUnit.HOURS)
                .setConstraints(constraints)
                .addTag("uploadTag")
                .build()

        //執行
        WorkManager.getInstance(applicationContext).enqueue(uploadRequest)


        //監聽執行結果
        WorkManager.getInstance(this)
//            .getWorkInfosByTagLiveData("uploadTag") //通過tag拿到work
            .getWorkInfoByIdLiveData(uploadRequest.id) //通過id拿到work
            .observe(this, Observer {
                it?.apply {
                    when (this.state) {
                        WorkInfo.State.BLOCKED -> println("BLOCKED")
                        WorkInfo.State.CANCELLED -> println("CANCELLED")
                        WorkInfo.State.RUNNING -> println("RUNNING")
                        WorkInfo.State.ENQUEUED -> println("ENQUEUED")
                        WorkInfo.State.FAILED -> println("FAILED")
                        WorkInfo.State.SUCCEEDED -> println("SUCCEEDED")
                        else -> println("else status ${this.state}")
                    }
                }

            })

4)另外還支援任務取消,任務鏈式順序呼叫等

    //取消
    fun cancelWork(){
  WorkManager.getInstance(applicationContext).cancelAllWorkByTag("uploadTag")
    }

    fun startLineWork(){
        //圖片濾鏡1
        val filter1 = OneTimeWorkRequestBuilder<UploadLogcatWork>()
            .build()
        //圖片濾鏡2
        val filter2 = OneTimeWorkRequestBuilder<UploadLogcatWork>()
            .build()
        //圖片壓縮
        val compress = OneTimeWorkRequestBuilder<UploadLogcatWork>()
            .build()
        //圖片上傳
        val upload = OneTimeWorkRequestBuilder<UploadLogcatWork>()
            .build()

        WorkManager.getInstance(applicationContext)
            .beginWith(listOf(filter1, filter2))
            .then(compress)
            .then(upload)
            .enqueue()
    }

官方文件
Demo程式碼地址

Jetpack-行為元件

CameraX

CameraX 是一個 Jetpack 支援庫,旨在幫助您簡化相機應用的開發工作。它提供一致且易於使用的 API Surface,適用於大多數 Android 裝置,並可向後相容至 Android 5.0(API 級別 21)。
雖然它利用的是 camera2 的功能,但使用的是更為簡單且基於用例的方法,該方法具有生命週期感知能力。它還解決了裝置相容性問題,因此您無需在程式碼庫中新增裝置專屬程式碼。這些功能減少了將相機功能新增到應用時需要編寫的程式碼量。

想必大家都瞭解過Camera APICamera2 API,總結就是兩個字,不好用。哈哈,自我感覺,在我印象中,我要照相拍一張照片,不是應該直接呼叫一句程式碼可以完成嗎。但是用之前的API,我需要去管理相機例項,設定SufraceView相關的各種東西,還有預覽尺寸和影像尺寸,處理設定各種監聽等等,頭已暈。

可能是官方聽到了我的抱怨,於是CameraX來了,CameraX是基於camera2進行了封裝,給我們提供了更簡單的解決方案來解決我們之前的困境。?來了

    // CameraX core library using the camera2 implementation
    def camerax_version = "1.0.0-beta06"
    // The following line is optional, as the core library is included indirectly by camera-camera2
    implementation "androidx.camera:camera-core:${camerax_version}"
    implementation "androidx.camera:camera-camera2:${camerax_version}"
    // If you want to additionally use the CameraX Lifecycle library
    implementation "androidx.camera:camera-lifecycle:${camerax_version}"
    // If you want to additionally use the CameraX View class
    implementation "androidx.camera:camera-view:1.0.0-alpha13"
    // If you want to additionally use the CameraX Extensions library
    implementation "androidx.camera:camera-extensions:1.0.0-alpha13"
    
    
    <uses-permission android:name="android.permission.CAMERA" />
    
    //初始化相機
    private fun initCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener(Runnable {
            try {
                val cameraProvider = cameraProviderFuture.get()
                val preview = Preview.Builder().build()


                //圖片拍攝用例
                mImageCapture = ImageCapture.Builder()
                    .setFlashMode(ImageCapture.FLASH_MODE_AUTO)
                    .build()

                //配置引數(後置攝像頭等)
                // Choose the camera by requiring a lens facing
                val cameraSelector =
                    CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_FRONT)
                        .build()

                //指定要與相機關聯的生命週期,該生命週期會告知 CameraX 何時配置相機拍攝會話並確保相機狀態隨生命週期的轉換相應地更改。
                val camera: Camera = cameraProvider.bindToLifecycle(
                    this,
                    cameraSelector,
                    preview,
                    mImageCapture
                )

                //相機預覽
                preview.setSurfaceProvider(view_finder.createSurfaceProvider())

            } catch (e: java.lang.Exception) {
                e.printStackTrace()
            }
        }, ContextCompat.getMainExecutor(this))
    }

    //拍照並儲存
    fun takePhoto(view: View?) {
        if (mImageCapture != null) {
            val outputFileOptions: OutputFileOptions = OutputFileOptions.Builder(cretaeFile()).build()

            //拍照
            mImageCapture?.takePicture(
                outputFileOptions,
                ContextCompat.getMainExecutor(this),
                object : ImageCapture.OnImageSavedCallback {
                    override fun onImageSaved(@NonNull outputFileResults: OutputFileResults) {
                        //儲存成功
                        Log.e(TAG, "success")
                    }

                    override fun onError(@NonNull exception: ImageCaptureException) {
                        //儲存失敗
                        Log.e(TAG, "fail")
                    }
                })
        }
    }    

使用起來挺方便吧,而且可以繫結當前activity的生命週期,這就涉及到另外一個元件Lifecycle了,通過一次繫結事件,就可以使相機狀態隨生命週期的轉換相應地更改。
另外要注意的是先獲取相機許可權哦。

官方文件
Demo程式碼地址

下載管理器

DownloadManager下載管理器是一個處理長時間執行的HTTP下載的系統服務。客戶端可以請求將URI下載到特定的目標檔案。下載管理器將在後臺執行下載,負責HTTP互動,並在失敗或跨連線更改和系統重啟後重試下載。

DownloadManager,大家應該都很熟悉吧,android2.3就開通提供的API,很方便就可以下載檔案,包括可以設定是否通知顯示,下載資料夾名,檔名,下載進度狀態查詢等等。?來

class DownloadActivity : AppCompatActivity() {

    private var mDownId: Long = 0
    private var mDownloadManager: DownloadManager? = null
    private val observer: DownloadContentObserver = DownloadContentObserver()


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    //配置下載引數,enqueue開始下載
    fun download(url: String) {
        mDownloadManager =
            this.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
        val request = DownloadManager.Request(Uri.parse(url))
        // 設定資料夾檔名
        request.setDestinationInExternalPublicDir("lz_download", "test.apk")
        // 設定允許的網路型別
        request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI)
        // 檔案型別
        request.setMimeType("application/zip")
        // 設定通知是否顯示
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
        //設定通知欄標題
        request.setTitle("apk download")
        //設定通知欄內容
        request.setDescription("*** apk")

        mDownId = mDownloadManager!!.enqueue(request)

 contentResolver.registerContentObserver(mDownloadManager!!.getUriForDownloadedFile(mDownId), true, observer)
    }

    //通過ContentProvider查詢下載情況
    fun queryDownloadStatus(){
        val query = DownloadManager.Query()
        //通過下載的id查詢
        //通過下載的id查詢
        query.setFilterById(mDownId)
        val cursor: Cursor = mDownloadManager!!.query(query)
        if (cursor.moveToFirst()) {
            // 已下載位元組數
            val downloadBytes = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
            // 總位元組數
            val allBytes= cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
            // 狀態
            when (cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))) {
                DownloadManager.STATUS_PAUSED -> {
                }
                DownloadManager.STATUS_PENDING -> {
                }
                DownloadManager.STATUS_RUNNING -> {
                }
                DownloadManager.STATUS_SUCCESSFUL -> {
                    cursor.close()
                }
                DownloadManager.STATUS_FAILED -> {
                    cursor.close()
                }
            }

        }
    }

    //取消下載,刪除檔案
    fun unDownLoad(view: View?) {
        mDownloadManager!!.remove(mDownId)
    }


    override fun onDestroy() {
        super.onDestroy()
        contentResolver.unregisterContentObserver(observer)
    }


    //監聽下載情況
    inner class DownloadContentObserver : ContentObserver(Handler(Looper.getMainLooper())) {
        override fun onChange(selfChange: Boolean) {
            queryDownloadStatus()
        }
    }

}

demo應該寫的很清楚了,要注意的就是儲存下載id,後續取消下載,查詢下載進度狀態都是通過這個id來查詢。監聽下載進度主要是通過觀察getUriForDownloadedFile方法返回的uri,觀察這個uri指向的資料庫變化來獲取進度。

官方文件
Demo程式碼地址

媒體和播放

Android 多媒體框架支援播放各種常見媒體型別,以便您輕鬆地將音訊、視訊和圖片整合到應用中。

這裡媒體和播放指的是音訊視訊相關內容,主要涉及到兩個相關類:

  • MediaPlayer
  • ExoPlayer

MediaPlayer不用說了,應該所有人都用過吧,待會就順便提一嘴。
ExoPlayer是一個單獨的庫,也是google開源的媒體播放器專案,聽說是Youtube APP所使用的播放器,所以他的功能也是要比MediaPlayer強大,支援各種自定義,可以與IJKPlayer媲美,只是使用起來比較複雜。

1)MediaPlayer

        //播放本地檔案
        var mediaPlayer: MediaPlayer? = MediaPlayer.create(this, R.raw.test_media)
        mediaPlayer?.start()

        //設定播放不息屏 配合許可權WAKE_LOCK使用
        mediaPlayer?.setScreenOnWhilePlaying(true)


        //播放本地本地可用的 URI
        val myUri: Uri = Uri.EMPTY
        val mediaPlayer2: MediaPlayer? = MediaPlayer().apply {
            setAudioStreamType(AudioManager.STREAM_MUSIC)
            setDataSource(applicationContext, myUri)
            prepare()
            start()
        }

        //播放網路檔案
        val url = "http://........"
        val mediaPlayer3: MediaPlayer? = MediaPlayer().apply {
            setAudioStreamType(AudioManager.STREAM_MUSIC)
            setDataSource(url)
            prepare()
            start()
        }


        //釋放
        mediaPlayer?.release()
        mediaPlayer = null
    

2)ExoPlayer

   compile 'com.google.android.exoplayer:exoplayer:r2.X.X'
   
    var player: SimpleExoPlayer ?= null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_exoplayer)

        //初始化
        player = SimpleExoPlayer.Builder(this).build()
        video_view.player = player
        player?.playWhenReady = true

        //設定播放資源
        val dataSourceFactory: DataSource.Factory = DefaultDataSourceFactory(
            this,
            Util.getUserAgent(this, "yourApplicationName")
        )
        val uri: Uri = Uri.EMPTY
        val videoSource: MediaSource = ProgressiveMediaSource.Factory(dataSourceFactory)
            .createMediaSource(uri)
        player?.prepare(videoSource)
    }

    private fun releasePlayer() {
        //釋放
        player?.release()
        player = null
    }

好像也不復雜?哈哈,更強大的功能需要你去發現。

官方文件
Demo程式碼地址

通知

通知是指 Android 在應用的介面之外顯示的訊息,旨在向使用者提供提醒、來自他人的通訊資訊或應用中的其他實時資訊。使用者可以點按通知來開啟應用,也可以直接在通知中執行某項操作。

這個應該都瞭解,直接上個?

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val name = "mychannel"
            val descriptionText = "for test"
            val importance = NotificationManager.IMPORTANCE_DEFAULT
            val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
                description = descriptionText
            }
            // Register the channel with the system
            val notificationManager: NotificationManager =
                getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            notificationManager.createNotificationChannel(channel)
        }
    }

    private fun showNotification(){
        val intent = Intent(this, SettingActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }
        val pendingIntent: PendingIntent = PendingIntent.getActivity(this, 0, intent, 0)

        val builder = NotificationCompat.Builder(this, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setContentTitle("My notification")
            .setContentText("Hello World!")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            // Set the intent that will fire when the user taps the notification
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)

        with(NotificationManagerCompat.from(this)) {
            notify(1, builder.build())
        }

    }

官方文件

許可權

許可權的作用是保護 Android 使用者的隱私。Android 應用必須請求許可權才能訪問敏感的使用者資料(例如聯絡人和簡訊)以及某些系統功能(例如相機和網際網路)。系統可能會自動授予許可權,也可能會提示使用者批准請求,具體取決於訪問的功能。

許可權大家應該也都很熟悉了。

  • 危險許可權。6.0以後使用危險許可權需要申請,推薦RxPermissions庫
  • 可選硬體功能的許可權。 對於使用硬體的應用,比如使用了相機,如果你想讓 Google Play允許將你的應用安裝在沒有該功能的裝置上,就要配置硬體功能的許可權為不必須的:
  • 自定義許可權。這個可能有些同學沒接觸過,我們知道,如果我們設定Activity的exported屬性為true,別人就能通過包名和Activity名訪問我們的Activty,那如果我們又不想讓所有人都能訪問我這個Activty呢?可以通過自定義許可權實現。?來
//應用A
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.test.myapp" >
    
    <permission
      android:name="com.test.myapp.permission.DEADLY_ACTIVITY"
      android:permissionGroup="android.permission-group.COST_MONEY"
      android:protectionLevel="dangerous" />
    
     <activity
            android:name="MainActivity"
            android:exported="true" 
            android:permission="com.test.myapp.permission.DEADLY_ACTIVITY">
       </activity>
</manifest>

//應用B
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.test.otherapp" >
    
    <uses-permission android:name="com.test.myapp.permission.DEADLY_ACTIVITY" />
</manifest>

官方文件

偏好設定

建議使用 AndroidX Preference Library 將使用者可配置設定整合至您的應用中。此庫管理介面,並與儲存空間互動,因此您只需定義使用者可以配置的單獨設定。此庫自帶 Material 主題,可在不同的裝置和作業系統版本之間提供一致的使用者體驗。

開始看到這個標題我是懵逼的,設定?我的設定頁官方都可以幫我寫了?然後我就去研究了Preference庫,嘿,還真是,如果你的App本身就是Material風格,就可以直接用這個了。但是也正是由於風格固定,在實際多樣的APP中應用比較少。
來個?


   implementation 'androidx.preference:preference:1.1.0-alpha04'
   
   //res-xml-setting.xml
   <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <PreferenceCategory
        app:key="notifications_category"
        app:title="Notifications">
        <SwitchPreferenceCompat
            app:key="notifications"
            app:title="Enable message notifications" />
    </PreferenceCategory>

    <PreferenceCategory
        app:key="help_category"
        app:title="Help">
        <Preference
            app:key="feedback"
            app:summary="Report technical issues or suggest new features"
            app:title="Send feedback" />

        <Preference
            app:key="webpage"
            app:title="View webpage">
            <intent
                android:action="android.intent.action.VIEW"
                android:data="http://www.baidu.com" />
        </Preference>
    </PreferenceCategory>
</PreferenceScreen>


class SettingFragment : PreferenceFragmentCompat() {
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.setting, rootKey)
        val feedbackPreference: Preference? = findPreference("feedback")

        feedbackPreference?.setOnPreferenceClickListener {
            Toast.makeText(context,"hello Setting",Toast.LENGTH_SHORT).show()
            true
        }
    }
}


class SettingActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_setting)

        supportFragmentManager
            .beginTransaction()
            .replace(R.id.settings_container, SettingFragment())
            .commit()
    }
    
}
   

首先新建xml檔案,也就相當於設定頁的佈局了,包括那些分類,那些選項,以及選項的功能。
然後新建fragment繼承自PreferenceFragmentCompat,這裡就可以繫結xml檔案,並且可以設定點選事件。
最後將fragment加到Activity即可。✌️

來張效果圖看看
jetpack-setting.jpg

官方文件
Demo程式碼地址

共享

Android 應用的一大優點是它們能夠互相通訊和整合。如果某一功能並非應用的核心,而且已存在於另一個應用中,為何要重新開發它?

這裡的共享主要指的是應用間的共享,比如發郵件功能,開啟網頁功能,這些我們都可以直接呼叫系統應用或者其他三方應用來幫助我們完成這些功能,這也就是共享的意義。

    //傳送方
    val sendIntent: Intent = Intent().apply {
        action = Intent.ACTION_SEND
        putExtra(Intent.EXTRA_TEXT, "This is my text to send.")
        type = "text/plain"
    }

    val shareIntent = Intent.createChooser(sendIntent, null)
    startActivity(shareIntent)
    
    //接收方
    <activity android:name=".ui.MyActivity" >
        <intent-filter>
            <action android:name="android.intent.action.SEND" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:mimeType="text/plain" />
        </intent-filter>
    </activity>
        

官方文件

切片

切片是介面模板,可以在 Google 搜尋應用中以及 Google 助理中等其他位置顯示您應用中的豐富而動態的互動內容。切片支援全屏應用體驗之外的互動,可以幫助使用者更快地執行任務。您可以將切片構建成為應用操作的增強功能。

這個介紹確實有點模糊,但是說到Slice你會不會有點印象?2018年Google I/0宣佈推出新的介面操作Action & Slice。而這個Slice就是這裡說的切片。他能做什麼呢?可以讓使用者能快速使用到 app 裡的某個特定功能。只要開發者匯入 Slice 功能,使用者在使用搜尋、Google Play 商店、Google Assitant或其他內建功能時都會出現 Slice 的操作建議。

說白了就是你的應用一些功能可以在其他的應用顯示和操作。

所以,如果你的應用釋出在GooglePlay的話,還是可以瞭解學習下Slice相關內容,畢竟是Google為了應用輕便性做出的又一步實驗。

怎麼開發這個功能呢?很簡單,只需要一步,右鍵New—other—Slice Provider就可以了。
slice庫,provider和SliceProvider類都配置好了,方便吧。貼下程式碼:

     <provider
          android:name=".slice.MySliceProvider"
          android:authorities="com.panda.jetpackdemo.slice"
          android:exported="true">
          <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.app.slice.category.SLICE" />
                <data
                    android:host="panda.com"
                    android:pathPrefix="/"
                    android:scheme="http" />
            </intent-filter>
        </provider>
        
        
class MySliceProvider : SliceProvider() {
    /**
     * Construct the Slice and bind data if available.
     * 切片匹配
     */
    override fun onBindSlice(sliceUri: Uri): Slice? {
        val context = context ?: return null
        val activityAction = createActivityAction() ?: return null
        return if (sliceUri.path.equals("/hello") ) {
            Log.e("lz6","222")
            ListBuilder(context, sliceUri, ListBuilder.INFINITY)
                .addRow(
                    ListBuilder.RowBuilder()
                        .setTitle("Hello World")
                        .setPrimaryAction(activityAction)
                )
                .build()
        } else {
            // Error: Path not found.
            ListBuilder(context, sliceUri, ListBuilder.INFINITY)
                .addRow(
                    ListBuilder.RowBuilder()
                        .setTitle("URI not found.")
                        .setPrimaryAction(activityAction)
                )
                .build()
        }
    }

    //切片點選事件
    private fun createActivityAction(): SliceAction? {
        return SliceAction.create(
            PendingIntent.getActivity(
                context, 0, Intent(context, SettingActivity::class.java), 0
            ),
            IconCompat.createWithResource(context, R.drawable.ic_launcher_foreground),
            ListBuilder.ICON_IMAGE,
            "Open App"
        )
    }

}
        

如上就是切片的重要程式碼,其中onBindSlice是用來匹配uri的,比如上述如果uri為/hello就顯示一個ListBuilder。createActivityAction方法則是響應切片點選事件的。
可以看到在AndroidManifest.xml中是通過provider配置的,所以這個切片的原理就是通過ContentProvider形式,讓外部可以訪問這個provider,然後響應相關事件或者顯示相關的view。

好了,接下來就是測試切片使用了,完整的切片URI是slice-content://{authorities}/{action},所以這裡對應的就是slice-content://com.panda.jetpackdemo.slice/hello

又在哪裡可以使用呢?官方提供了一個可供測試的app—slice-viewer
下載下來後,配置好URI,就會提示要訪問某某應用的切片許可權提示,點選確定就可以看到切片內容了(注意最好使用模擬器測試,真機有可能無法彈出切片許可權彈窗)。如下圖,點選hello就可以跳轉到我們之前createActivityAction方法裡面設定的Activity了。

slice.jpg

官方文件
Demo程式碼地址

Jetpack-介面元件

動畫和過渡

當介面因響應使用者操作而發生變化時,您應為佈局過渡新增動畫。這些動畫可向使用者提供有關其操作的反饋,並有助於讓使用者始終關注介面。

動畫也是老生常談的內容了。說到動畫,我們都會想到幀動畫,屬性動畫,補間動畫等等。今天我們從不一樣的角度歸類一些那些你熟悉又不熟悉的動畫。

1)為點陣圖新增動畫

  • AnimationDrawable。接連載入一系列可繪製資源以建立動畫。即屬性動畫,通過設定每幀的影像,形成動畫。
  • AnimatedVectorDrawable。為向量可繪製物件的屬性新增動畫效果,例如旋轉或更改路徑資料以將其變為其他圖片。

其中主要講下AnimatedVectorDrawable,VectorDrawable是為了支援SVG而生,SVG 是可縮放向量圖形,用xml程式碼描繪影像。下面舉個?

//res-drawable-vectordrawable.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="64dp"
    android:width="64dp"
    android:viewportHeight="600"
    android:viewportWidth="600">
    <group
        android:name="rotationGroup"
        android:pivotX="300.0"
        android:pivotY="300.0"
        android:rotation="45.0" >
        <path
            android:name="v"
            android:fillColor="#000000"
            android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
    </group>
</vector>

//res-animator-path_morph.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <objectAnimator
        android:duration="3000"
        android:propertyName="pathData"
        android:valueFrom="M300,70 l 0,-70 70,70 0,0   -70,70z"
        android:valueTo="M300,70 l 0,-70 70,0  0,140 -70,0 z"
        android:valueType="pathType" />
</set>

//res-animator-rotation.xml
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="6000"
    android:propertyName="rotation"
    android:valueFrom="0"
    android:valueTo="360" />


//利用上面兩個動畫檔案和一個SVG影像,生成animated-vector可執行動畫
//res-drawable-animatiorvectordrawable.xml
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/vectordrawable" >
    <target
        android:name="rotationGroup"
        android:animation="@animator/rotation" />
    <target
        android:name="v"
        android:animation="@animator/path_morph" />
</animated-vector>


//佈局檔案activity_vector.xml
    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        app:srcCompat="@drawable/animatorvectordrawable"
        app:layout_constraintTop_toTopOf="parent"
        />
        
//activity
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_vector)
        imageView.setOnClickListener {
            (imageView.drawable as Animatable).start()
        }
    }

ok,執行後,點選影像,就會發現一個繞圈的同時又會自變的動畫了,感覺有點像地球自轉和公轉,感興趣的同學可以自己實現下。

2)為介面可見性和動作新增動畫
這一部分主要就是屬性動畫。屬性動畫的原理就是在一段時間內更新 View 物件的屬性,並隨著屬性的變化不斷地重新繪製檢視。也就是ValueAnimator,以及在此技術上衍生的ViewPropertyAnimatorObjectAnimator。主要運用到控制元件本身的基礎動畫以及自定義view動畫。

3)基於物理特性的動作
這部分可以讓動畫應儘可能運用現實世界的物理定律,以使其看起來更自然。比如彈簧動畫和投擲動畫。這裡舉個彈簧動畫的?

    def dynamicanimation_version = "1.0.0"
    implementation "androidx.dynamicanimation:dynamicanimation:$dynamicanimation_version"

        val springForce = SpringForce(0.0f)
            .setDampingRatio(0f)  //設定阻尼
            .setStiffness(0.5f)  //設定剛度

        imageView2.setOnClickListener {
            SpringAnimation(imageView2, DynamicAnimation.TRANSLATION_Y).apply {
                spring = springForce
                setStartVelocity(500f) //設定速度
                start()
            }
        }

4)為佈局更改新增動畫
藉助 Android 的過渡框架,您只需提供起始佈局和結束佈局,即可為介面中的各種運動新增動畫效果。也就是說我們只需要提供兩個場景,代表動畫前後,然後就可以自動生成動畫了。要注意的是,兩個場景其實在一個頁面中。

//兩個場景的佈局
    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/scene_root">

        <include layout="@layout/a_scene" />
    </FrameLayout>
    
//場景一
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scene_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="26sp"
        android:id="@+id/text_view1"
        android:text="Text Line 1" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="26sp"
        android:id="@+id/text_view2"
        android:text="Text Line 2" />
</LinearLayout>

//場景二
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scene_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/text_view2"
        android:textSize="22sp"
        android:text="Text Line 2" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="22sp"
        android:id="@+id/text_view1"
        android:text="Text Line 1" />
</LinearLayout>

//獲取場景,開始場景間的動畫,從場景一變化為場景二

        val sceneRoot: ViewGroup = findViewById(R.id.scene_root)
        val aScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.a_scene, this)
        val anotherScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.another_scene, this)

        titletv.setOnClickListener {
            TransitionManager.go(anotherScene)
        }

5)Activity 之間新增動畫
剛才是同一頁面不同場景之間的動畫,如果是不同頁面呢?也就是不同的Activity之間的動畫呢?更簡單了哈哈,可以在style中設定具體的動畫,也可以直接設定過渡動畫,還可以設定共享控制元件完成過渡動畫。

//樣式中定義動畫
      <item name="android:windowEnterTransition">@transition/explode</item>
      <item name="android:windowExitTransition">@transition/explode</item>
    

//設定過渡動畫,可以在兩個佈局中設定共享控制元件,android:transitionName="robot"
        val intent = Intent(this, Activity2::class.java)
        // create the transition animation - the images in the layouts
        // of both activities are defined with android:transitionName="robot"
        val options = ActivityOptions
                .makeSceneTransitionAnimation(this, androidRobotView, "robot")
        // start the new activity
        startActivity(intent, options.toBundle())

官方文件
Demo程式碼地址

表情符號

EmojiCompat 支援庫旨在讓 Android 裝置及時相容最新的表情符號。它可防止您的應用以 ☐ 的形式顯示缺少的表情符號字元,該符號表示您的裝置沒有用於顯示文字的相應字型。通過使用 EmojiCompat 支援庫,您的應用使用者無需等到 Android OS 更新即可獲取最新的表情符號。

這一模組就是為了相容性提供的一個庫:EmojiCompat,通過CharSequence文字中的 emoji 對應的unicode 編碼來識別 emoji 表情,將他們替換成EmojiSpans,最後呈現 emoji 表情符號。

emoji.png

//匯入庫
implementation "com.android.support:support-emoji:28.0.0"

//初始化
EmojiCompat.Config config = new BundledEmojiCompatConfig(this);
EmojiCompat.init(config);
       
//替換元件
<android.support.text.emoji.widget.EmojiTextView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>  

官方文件

Fragment

Fragment 表示 FragmentActivity 中的行為或介面的一部分。您可以在一個 Activity 中組合多個片段,從而構建多窗格介面,並在多個 Activity 中重複使用某個片段。您可以將片段視為 Activity 的模組化組成部分,它具有自己的生命週期,能接收自己的輸入事件,並且您可以在 Activity 執行時新增或移除片段(這有點像可以在不同 Activity 中重複使用的“子 Activity”)。
片段必須始終託管在 Activity 中,其生命週期直接受宿主 Activity 生命週期的影響。

我確實沒想到fragment也被歸入到jetpack了,哈哈,這裡我就貼一篇我覺得寫得好的文章,雖然文章比較老了,但是可以幫你更深理解Fragment
當然官方也釋出了Fragment的管理框架——Navigation,感興趣的在本文搜尋下即可。

官方文件

佈局

佈局可定義應用中的介面結構(例如 Activity 的介面結構)。佈局中的所有元素均使用 View 和 ViewGroup 物件的層次結構進行構建。View 通常繪製使用者可檢視並進行互動的內容。然而,ViewGroup 是不可見容器,用於定義 View 和其他 ViewGroup 物件的佈局結構

佈區域性分主要注意下比較新的兩個佈局ConstraintLayoutMotionLayout

  • ConstraintLayout現在用的已經很多了,確實很好用,特別是複雜的大型佈局,與RelativeLayout屬關係佈局,但是更加靈活,也可以配合Android Studio的佈局編輯器使用,具體用法還是比較多的,貼上官網連結
  • MotionLayout 是一種佈局型別,可幫助您管理應用中的運動和微件動畫。MotionLayout是 ConstraintLayout 的子類,在其豐富的佈局功能基礎之上構建而成。

所以MotionLayout就是帶動畫的ConstraintLayout唄,這裡舉個?看看效果:


   implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta8'

<androidx.constraintlayout.motion.widget.MotionLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/scene_01"
    tools:showPaths="true">

    <View
        android:id="@+id/button"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:background="@color/colorAccent"
        android:text="Button" />

</androidx.constraintlayout.motion.widget.MotionLayout>


//scene_01.xml
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">

    <Transition
        motion:constraintSetStart="@+id/start"
        motion:constraintSetEnd="@+id/end"
        motion:duration="1000">
        <OnSwipe
            motion:touchAnchorId="@+id/button"
            motion:touchAnchorSide="right"
            motion:dragDirection="dragRight" />
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/button"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="8dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent" >

            <CustomAttribute
                motion:attributeName="backgroundColor"
                motion:customColorValue="#D81B60" />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/button"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginEnd="8dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintTop_toTopOf="parent" >

            <CustomAttribute
                motion:attributeName="backgroundColor"
                motion:customColorValue="#9999FF" />
        </Constraint>
    </ConstraintSet>

</MotionScene>

執行效果如下:
motionlayout.gif

主要是通過app:layoutDescription="@xml/scene_01"設定動畫場景,然後在scene_01場景中就可以設定起始和結束位置,動畫屬性,就可以完成對動畫的設定了。是不是有點自定義view那味了,關鍵這個只需要佈局一個xml檔案就可以了!還不試試?

官方文件
Demo程式碼地址

調色盤

出色的視覺設計是應用成功的關鍵所在,而配色方案是設計的主要組成部分。調色盤庫是一個支援庫,用於從圖片中提取突出顏色,幫助您建立具有視覺吸引力的應用。

沒想到吧,Android還有官方的調色盤庫—Palette。那到底這個調色盤能做什麼呢?主要用來分析圖片中的色彩特性。比如圖片中的暗色,亮色,鮮豔顏色,柔和色,文字顏色,主色調,等等。

   implementation 'com.android.support:palette-v7:28.0.0'

    //同步分析圖片並獲取例項
    fun createPaletteSync(bitmap: Bitmap): Palette = Palette.from(bitmap).generate()

   //非同步分析圖片並獲取例項
    fun createPaletteAsync(bitmap: Bitmap) {
        Palette.from(bitmap).generate { palette ->
            // Use generated instance
        val mutedColor = palette!!.getMutedColor(Color.BLUE)
        //主色調
        val rgb: Int? = palette?.vibrantSwatch?.rgb
        //文字顏色
        val bodyTextColor: Int? = palette?.vibrantSwatch?.bodyTextColor
        //標題的顏色
        val titleTextColor: Int? = palette?.vibrantSwatch?.titleTextColor 
        }
    }
    

官方文件
Demo程式碼地址

總結

終於告一段落了,大家吃?應該吃飽了吧哈哈。
希望這篇文章能讓不怎麼熟悉Jetpack的同學多瞭解瞭解。
當然,這還遠遠不夠,在我看來,本文更像是一個科普文,只是告訴了大家jetpack大家庭有哪些成員,有什麼用處。實際專案中,我們還需要建立MVVM的思想,深刻了解每個元件的設計意義,靈活運用元件。如果大家感興趣,後面我會完整做一個MVVM的專案,並通過文章的形式記錄整個過程。(附件也有一個專案是官方的Jetpack實踐專案
最後希望大家都能通過jetpack構建高質量,簡易並優質的專案架構,從而解放生產力,成為效率達人

附件:

Jetpack實踐官方Demo—Sunflower
文章相關所有Demo

我的公眾號:碼上積木,每天三問面試題,詳細剖析,助你成為offer收割機。


你的一個?,就是我分享的動力❤️。

相關文章