模仿Android微信小程序,實(shí)現(xiàn)小程序獨(dú)立任務(wù)視圖的效果
?今天跟大家分享一個非常有趣的技術(shù),如何在我們的App中實(shí)現(xiàn)類似于微信小程序的功能。
哈哈開個玩笑,如果我能徒手實(shí)現(xiàn)一套微信小程序系統(tǒng)的話,早就被騰訊挖過去當(dāng)架構(gòu)師了。
小程序相信現(xiàn)在所有人都使用過的對吧,很多人甚至天天都在使用。小程序特別的方便,無需下載,無需安裝,在微信當(dāng)中打開就能立刻使用。隨取隨用,隨用隨走,也不占用任何手機(jī)的存儲空間。
而Android上的微信小程序做得格外的像一個真正的應(yīng)用程序。為什么這么說呢?因為Android上的每個微信小程序甚至還能擁有自己的任務(wù)視圖,就像是一個真正的獨(dú)立應(yīng)用程序一樣。點(diǎn)擊手機(jī)任務(wù)欄鍵可以看到如下界面:
上圖中美團(tuán)外賣、微博熱搜、星巴克都是小程序。
擁有獨(dú)立的任務(wù)視圖的話,就可以更加方便地在多個小程序或微信本體之間進(jìn)行快速切換,在這點(diǎn)上Android的體驗要比iOS更好。
那么問題來了,這種依附于其他程序的小程序是如何做到擁有一個獨(dú)立的任務(wù)視圖的呢?
本篇文章我們就來一探究竟。
事實(shí)上,這是一個很基礎(chǔ)的功能。有多基礎(chǔ)呢?任何一位Android開發(fā)者在入門時都一定學(xué)過這個知識:Launch Mode。
因此,我就不在這里對Launch Mode進(jìn)行展開講解了。如果你真的從來沒有聽說過Launch Mode,建議參考《第一行代碼 第3版》第3章的內(nèi)容。
我們都知道,Android中Activity的啟動模式一共有4種:standdard、singleTop、singleTask和singleInstance。
從字面意思上來看,singleTask表示的就是要啟用一個單獨(dú)的任務(wù)來存放當(dāng)前Activity。但假如你把一個Activity聲明成了singleTask,你會發(fā)現(xiàn)并不能得到我們想要的效果,所有的Activity仍然是放在同一個任務(wù)當(dāng)中的。
這是因為,singleTask還會關(guān)聯(lián)一個叫taskAffinity的屬性,只有被聲明成singleTask的Activity,且它的taskAffinity值也是獨(dú)立的,那么這個Activity才會被放在一個單獨(dú)的任務(wù)當(dāng)中。
而默認(rèn)情況下,每個Activity的taskAffinity屬性值都是當(dāng)前應(yīng)用程序的包名,也就是說它們的值都是相同的,所以才不能得到我們想要的效果。
那么解決方法也很簡單,給每一個要啟用獨(dú)立任務(wù)視圖的Activity都賦值一個不同的taskAffinity值即可。
接下來我們就開始動手實(shí)踐一下吧。
首先創(chuàng)建一個叫MiniProgramTest的項目。
接下來創(chuàng)建3個空的Activity,分別給它們起名為FirstActivity、SecondActivity和ThirdActivity。
然后編輯項目的activity_main.xml布局文件,在里面加入3個按鈕,分別用于啟動FirstActivity、SecondActivity和ThirdActivity:
<androidx.constraintlayout.widget.ConstraintLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/first_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="啟動第一行代碼"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintBottom_toTopOf="@+id/second_btn"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/second_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="啟動第二行代碼"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintBottom_toTopOf="@+id/third_btn"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/first_btn" />
<Button
android:id="@+id/third_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="啟動第三行代碼"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/second_btn" />
</androidx.constraintlayout.widget.ConstraintLayout>
布局文件定義好了之后,接下來修改MainActivity的代碼,加入啟動邏輯:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val firstBtn = findViewById<Button>(R.id.first_btn)
val secondBtn = findViewById<Button>(R.id.second_btn)
val thirdBtn = findViewById<Button>(R.id.third_btn)
firstBtn.setOnClickListener {
val intent = Intent(this, FirstActivity::class.java)
startActivity(intent)
}
secondBtn.setOnClickListener {
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
}
thirdBtn.setOnClickListener {
val intent = Intent(this, ThirdActivity::class.java)
startActivity(intent)
}
}
}
代碼非常簡單,點(diǎn)擊哪個按鈕就去啟動相應(yīng)的Activity就可以了。
但如果僅僅是這樣,F(xiàn)irstActivity、SecondActivity和ThirdActivity一定與MainActivity是存放在同一個任務(wù)當(dāng)中的。
因此下面我們就要去編寫最核心的代碼了,修改AndroidManifest.xml文件,如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.miniprogramtest">
<application
...>
<activity
android:name=".FirstActivity"
android:exported="false"
android:label="第一行代碼"
android:launchMode="singleTask"
android:taskAffinity="com.example.miniprogramtest.first"
/>
<activity
android:name=".SecondActivity"
android:exported="false"
android:label="第二行代碼"
android:launchMode="singleTask"
android:taskAffinity="com.example.miniprogramtest.second" />
<activity
android:name=".ThirdActivity"
android:exported="false"
android:label="第三行代碼"
android:launchMode="singleTask"
android:taskAffinity="com.example.miniprogramtest.third"
/>
...
</application>
</manifest>
可以看到,這里我們將FirstActivity、SecondActivity和ThirdActivity的launchMode都設(shè)置成了singleTask,并且給它們都指定了一個不同的taskAffinity。
現(xiàn)在運(yùn)行一下程序,并分別點(diǎn)擊界面上的3個按鈕,然后按下手機(jī)任務(wù)欄鍵,我們就能看到如下效果了:
有沒有覺得很神奇?明明都是同一個App中的3個Activity,現(xiàn)在我們竟然可以讓它們在3個獨(dú)立的任務(wù)視圖中顯示,是不是感覺就好像是微信小程序一樣?
不過,雖然FirstActivity、SecondActivity和ThirdActivity都擁有獨(dú)立的任務(wù)視圖了,它們和微信小程序還有一個非常明顯的差距。
因為每個程序都有自己專屬的應(yīng)用Logo,小程序也不例外。就像我們在最開始的圖片中看到的一樣,美團(tuán)小程序有美團(tuán)的Logo,微博小程序有微博的Logo,星巴克小程序有星巴克的Logo。
而目前,F(xiàn)irstActivity、SecondActivity和ThirdActivity顯示的都是MiniProgramTest這個項目的Logo,這使得它們看上去仍然不像是一個獨(dú)立的應(yīng)用程序。
下面我們就開始著手優(yōu)化這部分問題。
首先,這里我準(zhǔn)備了3張圖片first_line.png、second_line.png、third_line.png,分別用于作為FirstActivity、SecondActivity和ThirdActivity的Logo:
接下來,編輯FirstActivity、SecondActivity和ThirdActivity的代碼,在里面加入如下邏輯:
class FirstActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_first)
setCustomTaskDescription()
}
private fun setCustomTaskDescription() {
val taskDescription = ActivityManager.TaskDescription(
"FirstActivity",
BitmapFactory.decodeResource(resources, R.drawable.first_line)
)
setTaskDescription(taskDescription)
}
}
class SecondActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
setCustomTaskDescription()
}
private fun setCustomTaskDescription() {
val taskDescription = ActivityManager.TaskDescription(
"SecondActivity",
BitmapFactory.decodeResource(resources, R.drawable.second_line)
)
setTaskDescription(taskDescription)
}
}
class ThirdActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_third)
setCustomTaskDescription()
}
private fun setCustomTaskDescription() {
val taskDescription = ActivityManager.TaskDescription(
"ThirdActivity",
BitmapFactory.decodeResource(resources, R.drawable.third_line)
)
setTaskDescription(taskDescription)
}
}
這3段代碼的邏輯基本都是相同的。
核心部分就是調(diào)用了setCustomTaskDescription()方法來給當(dāng)前Activity設(shè)置一個自定義的TaskDescription。
所謂TaskDescription就是給當(dāng)前的任務(wù)設(shè)置一個描述,描述中可以包含任務(wù)的名稱和圖標(biāo)。
那么這里我們給FirstActivity、SecondActivity和ThirdActivity分別設(shè)置了不同的TaskDescription,這樣在任務(wù)視圖當(dāng)中,就可以看到各不相同的應(yīng)用Logo了,如下圖所示:
其實(shí)到這里為止,我們就把微信小程序的外殼搭建得差不多了。剩下的部分,當(dāng)然也是最難的部分,就是在這個殼子里面添加小程序的內(nèi)容了。這部分的技術(shù)以前端為主,并不是我擅長的領(lǐng)域,我也講不了,因此就不再繼續(xù)向下延伸了。
不過或許還有些朋友會存在這樣的疑惑:目前我們的技術(shù)實(shí)現(xiàn)方案是給每個小程序定義一個單獨(dú)的Activity(FirstActivity、SecondActivity和ThirdActivity),而微信小程序卻可以有無限多個,我們顯然不可能在AndroidManifest.xml文件中注冊無限個Activity,那么微信又是如何實(shí)現(xiàn)的呢?
其實(shí)這只是一個美麗的誤會,因為微信小程序并不是可以有無限多個,只是你平時沒有注意這個小細(xì)節(jié)而已。
我們通過做個實(shí)驗來驗證一下吧,觀察下圖中的效果:
可以看到,這里我事先依次按照順序打開了嗶哩嗶哩、QQ音樂、微博熱搜、京東購物、星巴克,這5個小程序。
這個時候回到微信當(dāng)中,再打開一個順豐速運(yùn)小程序。
再次回到任務(wù)視圖列表界面,你會發(fā)現(xiàn)現(xiàn)在多了一個順豐速運(yùn)的小程序,而最早打開的嗶哩嗶哩小程序卻從任務(wù)視圖列表中消失不見了。
由此可以看出,微信其實(shí)在AndroidManifest.xml文件中也只是放置了5個占位的Activity。當(dāng)你嘗試打開第6個小程序時,最先打開的那個小程序就會被回收,將它的容器提供給第6個小程序使用。
好了,本篇文章到這里就結(jié)束了。內(nèi)容其實(shí)非常的簡單,但是已經(jīng)把在Android上如何實(shí)現(xiàn)小程序外層的架子講明白了。至于如何實(shí)現(xiàn)小程序最核心的內(nèi)容部分,那就要看各位架構(gòu)師的水準(zhǔn)了。?