前言
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這個模塊是在單元測驗的基礎上可以提供更多性能測驗的功能,比如執行時間等,但是實際使用的話好像大家都用的比較少?以后會多嘗試看看,如果有懂的老鐵也可以評論區科普下??,
官方檔案
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="https://www.cnblogs.com/jimuzz/p/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 = https://www.cnblogs.com/jimuzz/p/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 = StockLiveData("")
myPriceListener.observe(this, Observer<BigDecimal> { price: BigDecimal? ->
// 監聽livedata的資料變化,如果呼叫了setValue或者postValue會呼叫該onChanged方法
//更新UI資料或者其他處理
})
}
}
這是一個股票資料物件,StockManager為股票管理器,如果該物件有活躍觀察者時,就去監聽股票市場的情況,如果沒有活躍觀察者時,就可以斷開監聽,
當監聽到股票資訊變化,該股票資料物件就會通過setValue方法進行資料更新,反應到觀察者的onChanged方法,這里要注意的是setValue方法只能在主執行緒呼叫,而postValue則是在其他執行緒呼叫,
當Fragment這個觀察者生命周期發生變化時,LiveData就會移除這個觀察者,不再發送訊息,所以也就避免崩潰問題,
官方檔案
Demo代碼地址
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屬性代表一開始顯示的fragmentandroid:name屬性代表對應的Fragment路徑action代表該Fragment存在的跳轉事件,比如myFragment1可以跳轉myFragment2,
- 修改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 = https://www.cnblogs.com/jimuzz/p/MutableLiveData()
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 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 one rror(@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 多媒體框架支持播放各種常見媒體型別,以便您輕松地將音頻、視頻和圖片集成到應用中,
這里媒體和播放指的是音頻視頻相關內容,主要涉及到兩個相關類:
MediaPlayerExoPlayer
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即可,??
來張效果圖看看

官方檔案
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了,

官方檔案
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="https://www.cnblogs.com/jimuzz/p/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())
官方檔案
Demo代碼地址
表情符號
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="https://www.cnblogs.com/jimuzz/p/#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="https://www.cnblogs.com/jimuzz/p/#9999FF" />
</Constraint>
</ConstraintSet>
</MotionScene>
運行效果如下:

主要是通過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收割機,
你的一個??,就是我分享的動力??,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/172688.html
標籤:Android

