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