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