Skip to content

Commit 3a0aee9

Browse files
committed
feat(android,iOS): integrate TabBar dynamic API with badge and red dot support
1 parent 569bc43 commit 3a0aee9

13 files changed

Lines changed: 1071 additions & 40 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package com.didi.dimina.api.ui
2+
3+
import com.didi.dimina.api.APIResult
4+
import com.didi.dimina.api.AsyncResult
5+
import com.didi.dimina.api.BaseApiHandler
6+
import com.didi.dimina.ui.container.DiminaActivity
7+
import org.json.JSONObject
8+
9+
/**
10+
* TabBar dynamic APIs.
11+
*/
12+
class TabBarApi : BaseApiHandler() {
13+
private companion object {
14+
const val SET_TAB_BAR_STYLE = "setTabBarStyle"
15+
const val SET_TAB_BAR_ITEM = "setTabBarItem"
16+
const val SHOW_TAB_BAR = "showTabBar"
17+
const val HIDE_TAB_BAR = "hideTabBar"
18+
const val SET_TAB_BAR_BADGE = "setTabBarBadge"
19+
const val REMOVE_TAB_BAR_BADGE = "removeTabBarBadge"
20+
const val SHOW_TAB_BAR_RED_DOT = "showTabBarRedDot"
21+
const val HIDE_TAB_BAR_RED_DOT = "hideTabBarRedDot"
22+
}
23+
24+
override val apiNames = setOf(
25+
SET_TAB_BAR_STYLE,
26+
SET_TAB_BAR_ITEM,
27+
SHOW_TAB_BAR,
28+
HIDE_TAB_BAR,
29+
SET_TAB_BAR_BADGE,
30+
REMOVE_TAB_BAR_BADGE,
31+
SHOW_TAB_BAR_RED_DOT,
32+
HIDE_TAB_BAR_RED_DOT,
33+
)
34+
35+
override fun handleAction(
36+
activity: DiminaActivity,
37+
appId: String,
38+
apiName: String,
39+
params: JSONObject,
40+
responseCallback: (String) -> Unit,
41+
): APIResult {
42+
return when (apiName) {
43+
SET_TAB_BAR_STYLE -> {
44+
if (activity.getTabBarItemCount() == 0) {
45+
return fail(SET_TAB_BAR_STYLE, "tabBar not configured")
46+
}
47+
48+
activity.setTabBarStyle(
49+
color = params.optNullableString("color"),
50+
selectedColor = params.optNullableString("selectedColor"),
51+
backgroundColor = params.optNullableString("backgroundColor"),
52+
borderStyle = params.optNullableString("borderStyle"),
53+
)
54+
ok(SET_TAB_BAR_STYLE)
55+
}
56+
57+
SET_TAB_BAR_ITEM -> {
58+
val index = params.optInt("index", -1)
59+
validateIndex(activity, SET_TAB_BAR_ITEM, index)?.let { return it }
60+
61+
activity.setTabBarItem(
62+
index = index,
63+
text = params.optNullableString("text"),
64+
iconPath = params.optNullableString("iconPath"),
65+
selectedIconPath = params.optNullableString("selectedIconPath"),
66+
)
67+
ok(SET_TAB_BAR_ITEM)
68+
}
69+
70+
SHOW_TAB_BAR -> {
71+
activity.showTabBar()
72+
ok(SHOW_TAB_BAR)
73+
}
74+
75+
HIDE_TAB_BAR -> {
76+
activity.hideTabBar()
77+
ok(HIDE_TAB_BAR)
78+
}
79+
80+
SET_TAB_BAR_BADGE -> {
81+
val index = params.optInt("index", -1)
82+
validateIndex(activity, SET_TAB_BAR_BADGE, index)?.let { return it }
83+
84+
activity.setTabBarBadge(index, params.optString("text", ""))
85+
ok(SET_TAB_BAR_BADGE)
86+
}
87+
88+
REMOVE_TAB_BAR_BADGE -> {
89+
val index = params.optInt("index", -1)
90+
validateIndex(activity, REMOVE_TAB_BAR_BADGE, index)?.let { return it }
91+
92+
activity.removeTabBarBadge(index)
93+
ok(REMOVE_TAB_BAR_BADGE)
94+
}
95+
96+
SHOW_TAB_BAR_RED_DOT -> {
97+
val index = params.optInt("index", -1)
98+
validateIndex(activity, SHOW_TAB_BAR_RED_DOT, index)?.let { return it }
99+
100+
activity.showTabBarRedDot(index)
101+
ok(SHOW_TAB_BAR_RED_DOT)
102+
}
103+
104+
HIDE_TAB_BAR_RED_DOT -> {
105+
val index = params.optInt("index", -1)
106+
validateIndex(activity, HIDE_TAB_BAR_RED_DOT, index)?.let { return it }
107+
108+
activity.hideTabBarRedDot(index)
109+
ok(HIDE_TAB_BAR_RED_DOT)
110+
}
111+
112+
else -> super.handleAction(activity, appId, apiName, params, responseCallback)
113+
}
114+
}
115+
116+
private fun validateIndex(
117+
activity: DiminaActivity,
118+
apiName: String,
119+
index: Int,
120+
): AsyncResult? {
121+
val listLength = activity.getTabBarItemCount()
122+
if (listLength == 0) {
123+
return fail(apiName, "tabBar not configured")
124+
}
125+
if (index < 0 || index >= listLength) {
126+
return fail(apiName, "invalid index $index")
127+
}
128+
return null
129+
}
130+
131+
private fun ok(apiName: String): AsyncResult {
132+
return AsyncResult(JSONObject().apply {
133+
put("errMsg", "$apiName:ok")
134+
})
135+
}
136+
137+
private fun fail(apiName: String, message: String): AsyncResult {
138+
return AsyncResult(JSONObject().apply {
139+
put("errMsg", "$apiName:fail $message")
140+
})
141+
}
142+
143+
private fun JSONObject.optNullableString(name: String): String? {
144+
return if (has(name) && !isNull(name)) {
145+
optString(name)
146+
} else {
147+
null
148+
}
149+
}
150+
}

android/dimina/src/main/kotlin/com/didi/dimina/core/MiniApp.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import com.didi.dimina.api.ui.MenuApi
2727
import com.didi.dimina.api.ui.NavigationBarApi
2828
import com.didi.dimina.api.ui.NativeComponentApi
2929
import com.didi.dimina.api.ui.ScrollApi
30+
import com.didi.dimina.api.ui.TabBarApi
3031
import com.didi.dimina.bean.MiniProgram
3132
import com.didi.dimina.common.ApiUtils
3233
import com.didi.dimina.common.LogUtils
@@ -197,6 +198,7 @@ class MiniApp private constructor() {
197198
InteractionApi().registerWith(apiRegistry)
198199
NavigationBarApi().registerWith(apiRegistry)
199200
ScrollApi().registerWith(apiRegistry)
201+
TabBarApi().registerWith(apiRegistry)
200202
MenuApi().registerWith(apiRegistry)
201203
NativeComponentApi().registerWith(apiRegistry)
202204

android/dimina/src/main/kotlin/com/didi/dimina/ui/container/DiminaActivity.kt

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import com.didi.dimina.bean.MergedPageConfig
8989
import com.didi.dimina.bean.MiniProgram
9090
import com.didi.dimina.bean.PathInfo
9191
import com.didi.dimina.bean.TabBarConfig
92+
import com.didi.dimina.bean.TabBarItem
9293
import com.didi.dimina.common.LogUtils
9394
import com.didi.dimina.common.PathUtils
9495
import com.didi.dimina.common.Utils
@@ -130,6 +131,9 @@ class DiminaActivity : ComponentActivity() {
130131
private val backgroundColor = mutableStateOf("#FFFFFF")
131132
private val tabBarConfigState = mutableStateOf<TabBarConfig?>(null)
132133
private val selectedTabIndex = mutableIntStateOf(-1)
134+
private val tabBarVisible = mutableStateOf(true)
135+
private val tabBarBadges = mutableStateOf<List<String>>(emptyList())
136+
private val tabBarRedDots = mutableStateOf<List<Boolean>>(emptyList())
133137
private val currentPagePath = mutableStateOf("")
134138
private val useTabBarContainer = mutableStateOf(false)
135139
private val loadedTabIndices = mutableStateOf<Set<Int>>(emptySet())
@@ -477,6 +481,7 @@ class DiminaActivity : ComponentActivity() {
477481
withContext(Dispatchers.Main) {
478482
// 4.设置标题栏以及状态栏颜色模式
479483
tabBarConfigState.value = appConfig.app.tabBar
484+
resetTabBarDynamicState(appConfig.app.tabBar)
480485
val initialTabIndex = getTabBarIndex(pathInfo.pagePath)
481486
useTabBarContainer.value = miniProgram.root && initialTabIndex >= 0
482487
syncTabBarState(pathInfo.pagePath)
@@ -747,6 +752,136 @@ class DiminaActivity : ComponentActivity() {
747752
} ?: -1
748753
}
749754

755+
private fun resetTabBarDynamicState(config: TabBarConfig?) {
756+
val listLength = config?.list?.size ?: 0
757+
tabBarVisible.value = true
758+
tabBarBadges.value = List(listLength) { "" }
759+
tabBarRedDots.value = List(listLength) { false }
760+
}
761+
762+
fun getTabBarItemCount(): Int {
763+
val stateCount = tabBarConfigState.value?.list?.size ?: 0
764+
if (stateCount > 0) {
765+
return stateCount
766+
}
767+
if (!::appConfig.isInitialized) {
768+
return 0
769+
}
770+
return appConfig.app.tabBar?.list?.size ?: 0
771+
}
772+
773+
fun setTabBarStyle(
774+
color: String?,
775+
selectedColor: String?,
776+
backgroundColor: String?,
777+
borderStyle: String?,
778+
) {
779+
runOnUiThread {
780+
val config = tabBarConfigState.value ?: return@runOnUiThread
781+
val safeBorderStyle = if (borderStyle == "black" || borderStyle == "white") {
782+
borderStyle
783+
} else {
784+
null
785+
}
786+
tabBarConfigState.value = config.copy(
787+
color = color ?: config.color,
788+
selectedColor = selectedColor ?: config.selectedColor,
789+
backgroundColor = backgroundColor ?: config.backgroundColor,
790+
borderStyle = safeBorderStyle ?: config.borderStyle,
791+
)
792+
}
793+
}
794+
795+
fun setTabBarItem(
796+
index: Int,
797+
text: String?,
798+
iconPath: String?,
799+
selectedIconPath: String?,
800+
) {
801+
runOnUiThread {
802+
val config = tabBarConfigState.value ?: return@runOnUiThread
803+
val oldItem = config.list.getOrNull(index) ?: return@runOnUiThread
804+
val newList = config.list.toMutableList()
805+
newList[index] = TabBarItem(
806+
pagePath = oldItem.pagePath,
807+
iconPath = iconPath ?: oldItem.iconPath,
808+
selectedIconPath = selectedIconPath ?: oldItem.selectedIconPath,
809+
text = text ?: oldItem.text,
810+
)
811+
tabBarConfigState.value = config.copy(list = newList)
812+
}
813+
}
814+
815+
fun showTabBar() {
816+
runOnUiThread {
817+
tabBarVisible.value = true
818+
}
819+
}
820+
821+
fun hideTabBar() {
822+
runOnUiThread {
823+
tabBarVisible.value = false
824+
}
825+
}
826+
827+
fun setTabBarBadge(index: Int, text: String) {
828+
runOnUiThread {
829+
val listLength = getTabBarItemCount()
830+
val badges = normalizedBadgeList(listLength).toMutableList()
831+
val redDots = normalizedRedDotList(listLength).toMutableList()
832+
if (index in badges.indices) {
833+
badges[index] = text
834+
redDots[index] = false
835+
tabBarBadges.value = badges
836+
tabBarRedDots.value = redDots
837+
}
838+
}
839+
}
840+
841+
fun removeTabBarBadge(index: Int) {
842+
runOnUiThread {
843+
val listLength = getTabBarItemCount()
844+
val badges = normalizedBadgeList(listLength).toMutableList()
845+
if (index in badges.indices) {
846+
badges[index] = ""
847+
tabBarBadges.value = badges
848+
}
849+
}
850+
}
851+
852+
fun showTabBarRedDot(index: Int) {
853+
runOnUiThread {
854+
val listLength = getTabBarItemCount()
855+
val badges = normalizedBadgeList(listLength).toMutableList()
856+
val redDots = normalizedRedDotList(listLength).toMutableList()
857+
if (index in redDots.indices) {
858+
redDots[index] = true
859+
badges[index] = ""
860+
tabBarBadges.value = badges
861+
tabBarRedDots.value = redDots
862+
}
863+
}
864+
}
865+
866+
fun hideTabBarRedDot(index: Int) {
867+
runOnUiThread {
868+
val listLength = getTabBarItemCount()
869+
val redDots = normalizedRedDotList(listLength).toMutableList()
870+
if (index in redDots.indices) {
871+
redDots[index] = false
872+
tabBarRedDots.value = redDots
873+
}
874+
}
875+
}
876+
877+
private fun normalizedBadgeList(listLength: Int): List<String> {
878+
return List(listLength) { index -> tabBarBadges.value.getOrElse(index) { "" } }
879+
}
880+
881+
private fun normalizedRedDotList(listLength: Int): List<Boolean> {
882+
return List(listLength) { index -> tabBarRedDots.value.getOrElse(index) { false } }
883+
}
884+
750885
private fun tabWebViewIdentifier(index: Int): String {
751886
val pagePath = appConfig.app.tabBar?.list?.getOrNull(index)?.pagePath ?: "unknown"
752887
return "tab_${miniProgram.appId}_${index}_${pagePath}"
@@ -1181,7 +1316,10 @@ class DiminaActivity : ComponentActivity() {
11811316
val navBarBgColor = parseCssColor(navigationBarBackgroundColor.value)
11821317
val isCustomNavigation = !showNavigationBar.value
11831318
val tabBarConfig = tabBarConfigState.value
1184-
val shouldShowTabBar = !isLoading.value && tabBarConfig != null && getTabBarIndex(currentPagePath.value) >= 0
1319+
val shouldShowTabBar = !isLoading.value &&
1320+
tabBarConfig != null &&
1321+
tabBarVisible.value &&
1322+
getTabBarIndex(currentPagePath.value) >= 0
11851323
val statusBarHeight = ComposeWindowInsets.statusBars.asPaddingValues().calculateTopPadding()
11861324

11871325
Box(modifier = modifier.fillMaxSize()) {
@@ -1274,6 +1412,8 @@ class DiminaActivity : ComponentActivity() {
12741412
selectedIndex = selectedTabIndex.intValue,
12751413
appId = miniProgram.appId,
12761414
filesDir = filesDir,
1415+
badges = tabBarBadges.value,
1416+
redDots = tabBarRedDots.value,
12771417
onSelected = { index ->
12781418
visibleTabBarConfig.list.getOrNull(index)?.let { item ->
12791419
switchTab(item.pagePath)

0 commit comments

Comments
 (0)