天天看點

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

前言

Android Jetpack想必大家都耳熟能詳了,

Android KTX

LiveData

Room

等等一系列庫都是出自

Jetpack

。那麼

Jetpack

到底是什麼?又包含哪些你還沒用過的東西?

Google

推出這個的原因又是什麼?今天我們就一起來完善一下我們腦中的

Jetpack構圖

。(篇幅較長,建議點贊關注Mark哦🐶 )

介紹

2018年谷歌I/O,

Jetpack

橫空出世,官方介紹如下:

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

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

Jetpack

到底是什麼?

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

    官方推薦

    做法。

google

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

  • 規範開發者更快更好的開發出優質應用。一直以來,

    Android開發

    都充斥了大量的不規範的操作和重複代碼,比如生命周期的管理,開發過程的重複,項目架構的選擇等等。是以

    Google

    為了規範開發行為,就推出這套指南,旨在讓開發者們能夠

    更好,更快,更規範

    地開發出優質應用。

當然,這兩年的實踐也确實證明了

Jetpack

做到了它介紹的那樣,便捷,快速,優質。是以我們作為開發者還是應該早點應用到這些工具,提高自己的

開發效率

,也規範我們自己的開發行為。下面我們就一起了解下

Jetpack

的所有工具指南。GOGOGO!

先來一張官網的總攬圖:

(

溫馨提示❤️

本文嚴格按照下圖順序對元件進行分析,有需要的可以從目錄進入或者直接搜尋檢視)

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

這個子產品是在單元測試的基礎上可以提供更多性能測試的功能,比如執行時間等。但是實際使用的話好像大家都用的比較少?以後會多嘗試看看,如果有懂的老鐵也可以評論區科普下😁。

多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()
    
           

測試

測試應用在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。

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)
    }
}    
           

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

就會移除這個觀察者,不再發送消息,是以也就避免崩潰問題。

Navigation

導航

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

類,以便以類型安全的方式浏覽和通路任何關聯的參數。這裡就不細說了,感興趣的可以去官網看看

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>)
}
           

然後就可以進行資料庫操作了,很簡單吧。

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資料源了。

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

來進行資料的通信,厲害吧。還有很多其他的用法,去項目中慢慢發現吧!

WorkManager

使用 WorkManager API 可以輕松地排程即使在應用退出或裝置重新開機時仍應運作的可延遲異步任務。

聽聽這個介紹就很神奇了,應用退出和裝置重新開機都能自動運作?通過廣播?那資料又是怎麼儲存的呢?聽說還可以執行周期性異步任務,順序鍊式調用哦!接下來一一解密

  • 關于應用退出和裝置重新開機

    如果APP正在運作,

    WorkManager

    會在APP程序中起一個新線程來運作任務;如果APP沒有運作,

    WorkManager

    會選擇一個合适的方式來排程背景任務--根據系統級别和APP狀态,WorkManager可能會使用

    JobScheduler,FireBase JobDispatcher

    或者

    AlarmManager

  • 關于資料儲存

    WorkManager

    建立的任務資料都會儲存到資料庫,用的是

    Room

    架構。然後重新開機等時間段都會去資料庫尋找需要安排執行的任務,然後判斷

    限制條件

    ,滿足即可執行。

一般這個API應用到什麼場景呢?想想,可靠運作,還可以周期異步。

對了,發送日志。可以通過

WorkManager

設定周期任務,每天執行一次發送日志的任務。而且能夠保證你的任務可靠運作,一定可以上傳到,當然也是支援監聽任務結果等。🌰:

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()
    }

           

Jetpack-行為元件

CameraX

CameraX 是一個 Jetpack 支援庫,旨在幫助您簡化相機應用的開發工作。它提供一緻且易于使用的 API Surface,适用于大多數 Android 裝置,并可向後相容至 Android 5.0(API 級别 21)。

雖然它利用的是 camera2 的功能,但使用的是更為簡單且基于用例的方法,該方法具有生命周期感覺能力。它還解決了裝置相容性問題,是以您無需在代碼庫中添加裝置專屬代碼。這些功能減少了将相機功能添加到應用時需要編寫的代碼量。

想必大家都了解過

Camera API

Camera2 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

了,通過一次綁定事件,就可以使相機狀态随生命周期的轉換相應地更改。

另外要注意的是先擷取相機權限哦。

下載下傳管理器

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指向的資料庫變化來擷取進度。

官方文檔

媒體和播放

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
    }
           

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

通知

通知是指 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即可。✌️

來張效果圖看看

共享

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了。

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

,以及在此技術上衍生的

ViewPropertyAnimator

ObjectAnimator

。主要運用到控件本身的基礎動畫以及自定義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())
           

表情符号

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

這一子產品就是為了相容性提供的一個庫:

EmojiCompat

,通過CharSequence文本中的 emoji 對應的

unicode 編碼

來識别 emoji 表情,将他們替換成EmojiSpans,最後呈現 emoji 表情符号。

//導入庫
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 對象的布局結構

布局部分主要注意下比較新的兩個布局

ConstraintLayout

MotionLayout

  • 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>

           

運作效果如下:

主要是通過

app:layoutDescription="@xml/scene_01"

設定動畫場景,然後在scene_01場景中就可以設定起始和結束位置,動畫屬性,就可以完成對動畫的設定了。是不是有點

自定義view

那味了,關鍵這個隻需要布局一個xml檔案就可以了!還不試試?

調色闆

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

沒想到吧,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 
        }
    }
    

           

總結

終于告一段落了,大家吃🌰應該吃飽了吧哈哈。

希望這篇文章能讓不怎麼熟悉

Jetpack

的同學多了解了解。

當然,這還遠遠不夠,在我看來,本文更像是一個

科普文

,隻是告訴了大家jetpack大家庭有哪些成員,有什麼用處。實際項目中,我們還需要建立

MVVM

的思想,深刻了解每個元件的設計意義,靈活運用元件。如果大家感興趣,後面我會完整做一個MVVM的項目,并通過文章的形式記錄整個過程。(附件也有一個項目是官方的

Jetpack實踐項目

最後希望大家都能通過

jetpack

建構高品質,簡易并優質的項目架構,進而解放生産力,成為

效率達人

附件:

Jetpack實踐官方Demo—Sunflower

文章相關所有Demo

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

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