Skip to content

Commit 0fbbe37

Browse files
[Drop-In UI] Moved CameraModeButtonComponent outside Drop-In UI (#6202)
* NavigationCamera component + CameraMode component. Unit tests for new components. Re-gen libnavui-maps Metalava API file. * Regen libnavui-base Metalava API. * CHAGELOG entry * Static analysis fix.
1 parent 0835fb1 commit 0fbbe37

20 files changed

Lines changed: 888 additions & 236 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ Mapbox welcomes participation and contributions from everyone.
44

55
## Unreleased
66
#### Features
7+
- Added `ComponentInstaller` for the `NavigationCameraComponent` that offers simplified integration of the `NavigationCamera`. [#6202](https://github.com/mapbox/mapbox-navigation-android/pull/6202)
8+
- Added `ComponentInstaller` for the `CameraModeButtonComponent` that offers simplified integration of the `NavigationCamera` mode button. [#6202](https://github.com/mapbox/mapbox-navigation-android/pull/6202)
9+
710
#### Bug fixes and improvements
811

912
## Mapbox Navigation SDK 2.8.0-alpha.3 - 17 August, 2022

libnavui-base/api/current.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ package com.mapbox.navigation.ui.base.installer {
1212
@com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public sealed interface ComponentInstaller {
1313
method public default com.mapbox.navigation.ui.base.installer.Installation component(com.mapbox.navigation.ui.base.lifecycle.UIComponent component);
1414
method public com.mapbox.navigation.ui.base.installer.Installation components(com.mapbox.navigation.ui.base.lifecycle.UIComponent... components);
15+
method public <T> T? findComponent(kotlin.jvm.functions.Function1<java.lang.Object,java.lang.Boolean> predicate);
1516
}
1617

1718
public final class ComponentInstallerKt {
19+
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public static inline <reified T> T! findComponent(com.mapbox.navigation.ui.base.installer.ComponentInstaller);
1820
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public static void installComponents(com.mapbox.navigation.core.lifecycle.MapboxNavigationApp, androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function1<? super com.mapbox.navigation.ui.base.installer.ComponentInstaller,kotlin.Unit> config);
1921
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public static void installComponents(com.mapbox.navigation.core.MapboxNavigation, androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function1<? super com.mapbox.navigation.ui.base.installer.ComponentInstaller,kotlin.Unit> config);
2022
}

libnavui-base/src/main/java/com/mapbox/navigation/ui/base/installer/ComponentInstaller.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ sealed interface ComponentInstaller {
2424
* Install multiple UIComponents and manage their lifecycle alongside MapboxNavigation lifecycle.
2525
*/
2626
fun components(vararg components: UIComponent): Installation
27+
28+
/**
29+
* Find installed component that matches given [predicate].
30+
*/
31+
fun <T> findComponent(predicate: (Any) -> Boolean): T?
2732
}
2833

2934
/**
@@ -126,4 +131,18 @@ internal class NavigationComponents(
126131
componentsChain.addAll(*components)
127132
return Installation { componentsChain.removeAndDetach(*components) }
128133
}
134+
135+
override fun <T> findComponent(predicate: (Any) -> Boolean): T? {
136+
return componentsChain.toList().firstOrNull(predicate) as? T
137+
}
138+
}
139+
140+
/**
141+
* Find installed component of type T.
142+
*
143+
* Shorthand for `findComponent { it is T } as? T`
144+
*/
145+
@ExperimentalPreviewMapboxNavigationAPI
146+
inline fun <reified T> ComponentInstaller.findComponent(): T? {
147+
return findComponent { it is T } as? T
129148
}

libnavui-dropin/src/main/java/com/mapbox/navigation/dropin/binder/ActionButtonBinder.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver
1313
import com.mapbox.navigation.dropin.ActionButtonDescription
1414
import com.mapbox.navigation.dropin.NavigationViewContext
1515
import com.mapbox.navigation.dropin.R
16-
import com.mapbox.navigation.dropin.component.cameramode.CameraModeButtonComponent
16+
import com.mapbox.navigation.dropin.component.cameramode.CameraModeButtonComponentContractImpl
1717
import com.mapbox.navigation.dropin.component.recenter.RecenterButtonComponentContractImpl
1818
import com.mapbox.navigation.dropin.databinding.MapboxActionButtonsLayoutBinding
1919
import com.mapbox.navigation.dropin.internal.extensions.reloadOnChange
2020
import com.mapbox.navigation.ui.app.internal.Store
2121
import com.mapbox.navigation.ui.app.internal.audioguidance.AudioAction
2222
import com.mapbox.navigation.ui.app.internal.navigation.NavigationState
2323
import com.mapbox.navigation.ui.base.lifecycle.UIBinder
24+
import com.mapbox.navigation.ui.maps.internal.ui.CameraModeButtonComponent
2425
import com.mapbox.navigation.ui.maps.internal.ui.RecenterButtonComponent
2526
import com.mapbox.navigation.ui.voice.internal.ui.AudioComponentContract
2627
import com.mapbox.navigation.ui.voice.internal.ui.AudioGuidanceButtonComponent
@@ -43,11 +44,7 @@ internal class ActionButtonBinder(
4344
audioGuidanceButtonComponent(binding, style, store)
4445
},
4546
reloadOnChange(context.styles.cameraModeButtonStyle) { style ->
46-
CameraModeButtonComponent(
47-
store = store,
48-
cameraModeButton = binding.cameraModeButton,
49-
cameraModeStyle = style
50-
)
47+
cameraModeButtonComponent(binding, style, store)
5148
},
5249
reloadOnChange(context.styles.recenterButtonStyle) { style ->
5350
recenterButtonComponent(binding, style, store)
@@ -91,6 +88,16 @@ internal class ActionButtonBinder(
9188
})
9289
}
9390

91+
private fun cameraModeButtonComponent(
92+
binding: MapboxActionButtonsLayoutBinding,
93+
style: Int,
94+
store: Store
95+
): CameraModeButtonComponent {
96+
return CameraModeButtonComponent(binding.cameraModeButton, contractProvider = {
97+
CameraModeButtonComponentContractImpl(context.viewModel.viewModelScope, store)
98+
}, style)
99+
}
100+
94101
private fun recenterButtonComponent(
95102
binding: MapboxActionButtonsLayoutBinding,
96103
style: Int,

libnavui-dropin/src/main/java/com/mapbox/navigation/dropin/component/cameramode/CameraModeButtonComponent.kt

Lines changed: 0 additions & 60 deletions
This file was deleted.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.mapbox.navigation.dropin.component.cameramode
2+
3+
import android.view.View
4+
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
5+
import com.mapbox.navigation.ui.app.internal.Store
6+
import com.mapbox.navigation.ui.app.internal.camera.CameraAction
7+
import com.mapbox.navigation.ui.app.internal.camera.TargetCameraMode
8+
import com.mapbox.navigation.ui.app.internal.camera.toNavigationCameraState
9+
import com.mapbox.navigation.ui.app.internal.navigation.NavigationState
10+
import com.mapbox.navigation.ui.maps.camera.state.NavigationCameraState
11+
import com.mapbox.navigation.ui.maps.internal.ui.CameraModeButtonComponentContract
12+
import kotlinx.coroutines.CoroutineScope
13+
import kotlinx.coroutines.flow.StateFlow
14+
15+
@ExperimentalPreviewMapboxNavigationAPI
16+
internal class CameraModeButtonComponentContractImpl(
17+
coroutineScope: CoroutineScope,
18+
private val store: Store
19+
) : CameraModeButtonComponentContract {
20+
21+
override val buttonState: StateFlow<NavigationCameraState> =
22+
store.slice(coroutineScope) { state ->
23+
val cameraState = state.camera
24+
if (cameraState.cameraMode == TargetCameraMode.Idle) {
25+
cameraState.savedCameraMode.toNavigationCameraState()
26+
} else {
27+
cameraState.cameraMode.toNavigationCameraState()
28+
}
29+
}
30+
31+
override val isVisible: StateFlow<Boolean> =
32+
store.slice(coroutineScope) { it.navigation != NavigationState.RoutePreview }
33+
34+
override fun onClick(view: View) {
35+
val cameraMode = store.state.value.camera.cameraMode.let {
36+
if (it != TargetCameraMode.Idle) it
37+
else store.state.value.camera.savedCameraMode
38+
}
39+
when (cameraMode) {
40+
TargetCameraMode.Following ->
41+
store.dispatch(CameraAction.SetCameraMode(TargetCameraMode.Overview))
42+
else ->
43+
store.dispatch(CameraAction.SetCameraMode(TargetCameraMode.Following))
44+
}
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package com.mapbox.navigation.dropin.component.cameramode
2+
3+
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
4+
import com.mapbox.navigation.core.MapboxNavigation
5+
import com.mapbox.navigation.dropin.util.TestStore
6+
import com.mapbox.navigation.testing.MainCoroutineRule
7+
import com.mapbox.navigation.ui.app.internal.camera.CameraAction
8+
import com.mapbox.navigation.ui.app.internal.camera.TargetCameraMode
9+
import com.mapbox.navigation.ui.app.internal.navigation.NavigationState
10+
import com.mapbox.navigation.ui.maps.camera.state.NavigationCameraState
11+
import com.mapbox.navigation.ui.maps.internal.ui.CameraModeButtonComponentContract
12+
import io.mockk.mockk
13+
import io.mockk.spyk
14+
import io.mockk.verify
15+
import kotlinx.coroutines.ExperimentalCoroutinesApi
16+
import kotlinx.coroutines.flow.take
17+
import kotlinx.coroutines.flow.toList
18+
import kotlinx.coroutines.launch
19+
import kotlinx.coroutines.test.runBlockingTest
20+
import kotlinx.coroutines.yield
21+
import org.junit.Assert.assertEquals
22+
import org.junit.Before
23+
import org.junit.Rule
24+
import org.junit.Test
25+
26+
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class, ExperimentalCoroutinesApi::class)
27+
class CameraModeButtonComponentContractImplTest {
28+
29+
@get:Rule
30+
var coroutineRule = MainCoroutineRule()
31+
32+
private lateinit var mockMapboxNavigation: MapboxNavigation
33+
private lateinit var testStore: TestStore
34+
35+
private lateinit var sut: CameraModeButtonComponentContract
36+
37+
@Before
38+
fun setUp() {
39+
testStore = spyk(TestStore())
40+
mockMapboxNavigation = mockk(relaxed = true)
41+
sut = CameraModeButtonComponentContractImpl(
42+
coroutineRule.coroutineScope,
43+
testStore,
44+
)
45+
}
46+
47+
@Test
48+
fun `buttonState should map TargetCameraMode to NavigationCameraState`() = runBlockingTest {
49+
testStore.updateState {
50+
it.copy(camera = it.camera.copy(cameraMode = TargetCameraMode.Following))
51+
}
52+
val buttonStates = mutableListOf<NavigationCameraState>()
53+
val job = launch {
54+
sut.buttonState.take(2).toList(buttonStates)
55+
yield()
56+
}
57+
testStore.updateState {
58+
it.copy(camera = it.camera.copy(cameraMode = TargetCameraMode.Overview))
59+
}
60+
61+
job.join()
62+
assertEquals(
63+
listOf(NavigationCameraState.FOLLOWING, NavigationCameraState.OVERVIEW),
64+
buttonStates
65+
)
66+
}
67+
68+
@Test
69+
fun `buttonState when TargetCameraMode is Idle should return saved state`() = runBlockingTest {
70+
testStore.updateState {
71+
it.copy(
72+
camera = it.camera.copy(
73+
cameraMode = TargetCameraMode.Idle,
74+
savedCameraMode = TargetCameraMode.Following,
75+
)
76+
)
77+
}
78+
79+
val navCamState = sut.buttonState.take(1).toList().first()
80+
81+
assertEquals(NavigationCameraState.FOLLOWING, navCamState)
82+
}
83+
84+
@Test
85+
fun `isVisible use NavigationState to determine visibility`() = runBlockingTest {
86+
testStore.updateState {
87+
it.copy(navigation = NavigationState.RoutePreview)
88+
}
89+
val visibility = mutableListOf<Boolean>()
90+
val job = launch {
91+
sut.isVisible.take(2).toList(visibility)
92+
yield()
93+
}
94+
testStore.updateState {
95+
it.copy(navigation = NavigationState.ActiveNavigation)
96+
}
97+
98+
job.join()
99+
assertEquals(listOf(false, true), visibility)
100+
}
101+
102+
@Test
103+
fun `onClick should request Overview mode current is Following`() = runBlockingTest {
104+
testStore.updateState {
105+
it.copy(
106+
camera = it.camera.copy(
107+
cameraMode = TargetCameraMode.Following
108+
)
109+
)
110+
}
111+
112+
sut.onClick(mockk())
113+
114+
verify { testStore.dispatch(CameraAction.SetCameraMode(TargetCameraMode.Overview)) }
115+
}
116+
117+
@Test
118+
fun `onClick should request Following mode current is Overview`() = runBlockingTest {
119+
testStore.updateState {
120+
it.copy(
121+
camera = it.camera.copy(
122+
cameraMode = TargetCameraMode.Overview
123+
)
124+
)
125+
}
126+
127+
sut.onClick(mockk())
128+
129+
verify { testStore.dispatch(CameraAction.SetCameraMode(TargetCameraMode.Following)) }
130+
}
131+
132+
@Test
133+
fun `onClick should use savedCameraMode mode value when current is Idle`() = runBlockingTest {
134+
testStore.updateState {
135+
it.copy(
136+
camera = it.camera.copy(
137+
cameraMode = TargetCameraMode.Idle,
138+
savedCameraMode = TargetCameraMode.Overview,
139+
)
140+
)
141+
}
142+
143+
sut.onClick(mockk())
144+
145+
verify { testStore.dispatch(CameraAction.SetCameraMode(TargetCameraMode.Following)) }
146+
}
147+
}

0 commit comments

Comments
 (0)