Skip to content

Commit 7476b3b

Browse files
committed
#1846 feat: replace tap target on actions tab with pulsing animation
1 parent 914d96c commit 7476b3b

File tree

10 files changed

+87
-181
lines changed

10 files changed

+87
-181
lines changed

base/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ dependencies {
9393
implementation(libs.google.flexbox)
9494
implementation(libs.squareup.okhttp)
9595
coreLibraryDesugaring(libs.desugar.jdk.libs)
96-
implementation(libs.canopas.introshowcaseview)
9796
implementation(libs.dagger.hilt.android)
9897
ksp(libs.dagger.hilt.android.compiler)
9998
implementation(libs.bundles.splitties)

base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import io.github.sds100.keymapper.base.R
77
import io.github.sds100.keymapper.base.actions.keyevent.FixKeyEventActionDelegate
88
import io.github.sds100.keymapper.base.keymaps.KeyMap
99
import io.github.sds100.keymapper.base.keymaps.ShortcutModel
10+
import io.github.sds100.keymapper.base.onboarding.OnboardingTapTarget
1011
import io.github.sds100.keymapper.base.onboarding.OnboardingTipDelegate
12+
import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase
1113
import io.github.sds100.keymapper.base.onboarding.SetupAccessibilityServiceDelegate
1214
import io.github.sds100.keymapper.base.utils.getFullMessage
1315
import io.github.sds100.keymapper.base.utils.isFixable
@@ -47,6 +49,7 @@ class ConfigActionsViewModel @Inject constructor(
4749
private val createAction: CreateActionUseCase,
4850
private val testAction: TestActionUseCase,
4951
private val config: ConfigActionsUseCase,
52+
private val onboardingUseCase: OnboardingUseCase,
5053
setupAccessibilityServiceDelegate: SetupAccessibilityServiceDelegate,
5154
fixKeyEventActionDelegate: FixKeyEventActionDelegate,
5255
onboardingTipDelegate: OnboardingTipDelegate,
@@ -153,6 +156,9 @@ class ConfigActionsViewModel @Inject constructor(
153156
val actionData = navigate("add_action", NavDestination.ChooseAction) ?: return@launch
154157

155158
config.addAction(actionData)
159+
160+
// Never show the tap target to add an action again.
161+
onboardingUseCase.completedTapTarget(OnboardingTapTarget.CHOOSE_ACTION)
156162
}
157163
}
158164

base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapScreen.kt

Lines changed: 66 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package io.github.sds100.keymapper.base.keymaps
22

33
import androidx.activity.compose.BackHandler
4+
import androidx.compose.animation.Animatable
5+
import androidx.compose.animation.core.tween
6+
import androidx.compose.foundation.background
47
import androidx.compose.foundation.layout.Arrangement
58
import androidx.compose.foundation.layout.BoxWithConstraints
69
import androidx.compose.foundation.layout.Column
@@ -15,6 +18,7 @@ import androidx.compose.foundation.layout.padding
1518
import androidx.compose.foundation.layout.width
1619
import androidx.compose.foundation.pager.HorizontalPager
1720
import androidx.compose.foundation.pager.rememberPagerState
21+
import androidx.compose.foundation.shape.RoundedCornerShape
1822
import androidx.compose.material.icons.Icons
1923
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
2024
import androidx.compose.material.icons.automirrored.rounded.HelpOutline
@@ -36,10 +40,12 @@ import androidx.compose.material3.Switch
3640
import androidx.compose.material3.Tab
3741
import androidx.compose.material3.Text
3842
import androidx.compose.runtime.Composable
43+
import androidx.compose.runtime.LaunchedEffect
3944
import androidx.compose.runtime.getValue
4045
import androidx.compose.runtime.mutableStateOf
4146
import androidx.compose.runtime.remember
4247
import androidx.compose.runtime.rememberCoroutineScope
48+
import androidx.compose.runtime.saveable.rememberSaveable
4349
import androidx.compose.runtime.setValue
4450
import androidx.compose.ui.Alignment
4551
import androidx.compose.ui.Modifier
@@ -50,12 +56,8 @@ import androidx.compose.ui.tooling.preview.Devices
5056
import androidx.compose.ui.tooling.preview.Preview
5157
import androidx.compose.ui.unit.Dp
5258
import androidx.compose.ui.unit.dp
53-
import com.canopas.lib.showcase.IntroShowcase
5459
import io.github.sds100.keymapper.base.R
5560
import io.github.sds100.keymapper.base.compose.KeyMapperTheme
56-
import io.github.sds100.keymapper.base.onboarding.OnboardingTapTarget
57-
import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperTapTarget
58-
import io.github.sds100.keymapper.base.utils.ui.compose.keyMapperShowcaseStyle
5961
import io.github.sds100.keymapper.base.utils.ui.compose.openUriSafe
6062
import kotlinx.coroutines.launch
6163

@@ -72,9 +74,7 @@ fun BaseConfigKeyMapScreen(
7274
onBackClick: () -> Unit = {},
7375
onDoneClick: () -> Unit = {},
7476
snackbarHostState: SnackbarHostState = SnackbarHostState(),
75-
showActionTapTarget: Boolean = false,
76-
onActionTapTargetCompleted: () -> Unit = {},
77-
onSkipTutorialClick: () -> Unit = {},
77+
showActionPulse: Boolean = false,
7878
) {
7979
val scope = rememberCoroutineScope()
8080
val triggerHelpUrl = stringResource(R.string.url_trigger_guide)
@@ -128,48 +128,72 @@ fun BaseConfigKeyMapScreen(
128128
@Composable
129129
fun Tabs() {
130130
for ((index, tab) in tabs.withIndex()) {
131-
val tapTarget: OnboardingTapTarget? = when {
132-
showActionTapTarget && tab == ConfigKeyMapTab.ACTIONS -> OnboardingTapTarget.CHOOSE_ACTION
133-
else -> null
134-
}
131+
val tabModifier = if (tab == ConfigKeyMapTab.ACTIONS) {
132+
133+
val defaultBackgroundColor = MaterialTheme.colorScheme.surface
134+
val pulseBackgroundColor =
135+
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)
136+
val animatedBackgroundColor =
137+
remember { Animatable(defaultBackgroundColor) }
138+
139+
var finishedAnimation by rememberSaveable { mutableStateOf(false) }
140+
141+
LaunchedEffect(showActionPulse) {
142+
var startedAnimation = false
143+
144+
repeat(10) {
145+
// Check at the start of each repeat so that it is
146+
// a smooth animation to the old position when it stops.
147+
val isActionsTabSelected =
148+
pagerState.targetPage == index && tab == ConfigKeyMapTab.ACTIONS
149+
150+
if (!showActionPulse || finishedAnimation || isActionsTabSelected) {
151+
return@repeat
152+
}
153+
154+
startedAnimation = true
155+
156+
animatedBackgroundColor.animateTo(
157+
pulseBackgroundColor,
158+
tween(700)
159+
)
135160

136-
IntroShowcase(
137-
showIntroShowCase = tapTarget != null,
138-
onShowCaseCompleted = onActionTapTargetCompleted,
139-
dismissOnClickOutside = true,
140-
) {
141-
var tabModifier: Modifier = Modifier
142-
143-
if (tapTarget != null) {
144-
tabModifier = tabModifier.introShowCaseTarget(
145-
index = 0,
146-
style = keyMapperShowcaseStyle(),
147-
) {
148-
KeyMapperTapTarget(
149-
tapTarget = tapTarget,
150-
onSkipClick = onSkipTutorialClick,
161+
animatedBackgroundColor.animateTo(
162+
defaultBackgroundColor,
163+
tween(700)
151164
)
152165
}
166+
167+
if (startedAnimation) {
168+
finishedAnimation = true
169+
}
153170
}
154171

155-
Tab(
156-
modifier = tabModifier,
157-
selected = pagerState.targetPage == index,
158-
text = {
159-
Text(
160-
text = getTabTitle(tab),
161-
maxLines = 1,
162-
)
163-
},
164-
onClick = {
165-
scope.launch {
166-
pagerState.animateScrollToPage(
167-
tabs.indexOf(tab),
168-
)
169-
}
170-
},
172+
Modifier.background(
173+
color = animatedBackgroundColor.value,
174+
shape = RoundedCornerShape(8.dp)
171175
)
176+
} else {
177+
Modifier
172178
}
179+
180+
Tab(
181+
modifier = tabModifier,
182+
selected = pagerState.targetPage == index,
183+
text = {
184+
Text(
185+
text = getTabTitle(tab),
186+
maxLines = 1,
187+
)
188+
},
189+
onClick = {
190+
scope.launch {
191+
pagerState.animateScrollToPage(
192+
tabs.indexOf(tab),
193+
)
194+
}
195+
}
196+
)
173197
}
174198
}
175199

base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapScreen.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ fun ConfigKeyMapScreen(
2121
optionsScreen: @Composable () -> Unit,
2222
) {
2323
val isKeyMapEnabled by keyMapViewModel.isEnabled.collectAsStateWithLifecycle()
24-
val showActionTapTarget by keyMapViewModel.showActionsTapTarget.collectAsStateWithLifecycle()
24+
val showActionPulse by keyMapViewModel.showActionsTapTarget.collectAsStateWithLifecycle()
2525
var showBackDialog by rememberSaveable { mutableStateOf(false) }
2626

2727
if (showBackDialog) {
@@ -51,8 +51,6 @@ fun ConfigKeyMapScreen(
5151
},
5252
onDoneClick = keyMapViewModel::onDoneClick,
5353
snackbarHostState = snackbarHostState,
54-
showActionTapTarget = showActionTapTarget,
55-
onActionTapTargetCompleted = keyMapViewModel::onActionTapTargetCompleted,
56-
onSkipTutorialClick = keyMapViewModel::onSkipTutorialClick,
54+
showActionPulse = showActionPulse,
5755
)
5856
}

base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapViewModel.kt

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase
88
import io.github.sds100.keymapper.base.trigger.ConfigTriggerUseCase
99
import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider
1010
import io.github.sds100.keymapper.base.utils.ui.DialogProvider
11+
import io.github.sds100.keymapper.common.utils.State
1112
import io.github.sds100.keymapper.common.utils.dataOrNull
1213
import kotlinx.coroutines.flow.SharingStarted
1314
import kotlinx.coroutines.flow.StateFlow
1415
import kotlinx.coroutines.flow.combine
16+
import kotlinx.coroutines.flow.filterIsInstance
1517
import kotlinx.coroutines.flow.map
1618
import kotlinx.coroutines.flow.stateIn
1719
import kotlinx.coroutines.launch
@@ -38,10 +40,13 @@ class ConfigKeyMapViewModel @Inject constructor(
3840
val showActionsTapTarget: StateFlow<Boolean> =
3941
combine(
4042
onboarding.showTapTarget(OnboardingTapTarget.CHOOSE_ACTION),
41-
configKeyMapState.keyMap,
43+
configKeyMapState.keyMap.filterIsInstance<State.Data<KeyMap>>(),
4244
) { showTapTarget, keyMapState ->
43-
// Show the choose action tap target if they have recorded a key.
44-
showTapTarget && keyMapState.dataOrNull()?.trigger?.keys?.isNotEmpty() ?: false
45+
// Show the choose action tap target if they have recorded a key and
46+
// have no actions.
47+
showTapTarget &&
48+
keyMapState.data.trigger.keys.isNotEmpty() &&
49+
keyMapState.data.actionList.isEmpty()
4550
}.stateIn(viewModelScope, SharingStarted.Lazily, false)
4651

4752
fun onDoneClick() {
@@ -73,14 +78,6 @@ class ConfigKeyMapViewModel @Inject constructor(
7378
}
7479
}
7580

76-
fun onActionTapTargetCompleted() {
77-
onboarding.completedTapTarget(OnboardingTapTarget.CHOOSE_ACTION)
78-
}
79-
80-
fun onSkipTutorialClick() {
81-
onboarding.skipTapTargetOnboarding()
82-
}
83-
8481
fun onEnabledChanged(enabled: Boolean) {
8582
configTrigger.setEnabled(enabled)
8683
}
Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
package io.github.sds100.keymapper.base.onboarding
22

3-
import androidx.annotation.StringRes
4-
import io.github.sds100.keymapper.base.R
5-
63
enum class OnboardingTapTarget(
7-
@StringRes val titleRes: Int,
8-
@StringRes val messageRes: Int,
94
) {
10-
CHOOSE_ACTION(
11-
titleRes = R.string.tap_target_choose_action_title,
12-
messageRes = R.string.tap_target_choose_action_message,
13-
),
5+
CHOOSE_ACTION,
146
}

base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ package io.github.sds100.keymapper.base.onboarding
33
import androidx.datastore.preferences.core.Preferences
44
import io.github.sds100.keymapper.base.utils.VersionHelper
55
import io.github.sds100.keymapper.common.BuildConfigProvider
6-
import io.github.sds100.keymapper.common.utils.State
76
import io.github.sds100.keymapper.data.Keys
8-
import io.github.sds100.keymapper.data.entities.KeyMapEntity
97
import io.github.sds100.keymapper.data.repositories.KeyMapRepository
108
import io.github.sds100.keymapper.data.repositories.PreferenceRepository
119
import io.github.sds100.keymapper.data.utils.PrefDelegate
@@ -16,7 +14,6 @@ import io.github.sds100.keymapper.system.permissions.PermissionAdapter
1614
import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter
1715
import kotlinx.coroutines.flow.Flow
1816
import kotlinx.coroutines.flow.combine
19-
import kotlinx.coroutines.flow.filterIsInstance
2017
import kotlinx.coroutines.flow.map
2118
import javax.inject.Inject
2219
import javax.inject.Singleton
@@ -94,13 +91,7 @@ class OnboardingUseCaseImpl @Inject constructor(
9491
override fun showTapTarget(tapTarget: OnboardingTapTarget): Flow<Boolean> {
9592
val shownKey = getTapTargetKey(tapTarget)
9693

97-
return combine(
98-
settingsRepository.get(shownKey).map { it ?: false },
99-
settingsRepository.get(Keys.skipTapTargetTutorial).map { it ?: false },
100-
keyMapRepository.keyMapList.filterIsInstance<State.Data<List<KeyMapEntity>>>(),
101-
) { isShown, skipTapTarget, keyMapList ->
102-
showTutorialTapTarget(tapTarget, isShown, skipTapTarget, keyMapList.data)
103-
}
94+
return settingsRepository.get(shownKey).map { isShown -> !(isShown ?: false) }
10495
}
10596

10697
override fun completedTapTarget(tapTarget: OnboardingTapTarget) {
@@ -109,40 +100,10 @@ class OnboardingUseCaseImpl @Inject constructor(
109100
}
110101

111102
private fun getTapTargetKey(tapTarget: OnboardingTapTarget): Preferences.Key<Boolean> {
112-
val key = when (tapTarget) {
113-
OnboardingTapTarget.CHOOSE_ACTION -> Keys.shownTapTargetChooseAction
114-
}
115-
return key
116-
}
117-
118-
/**
119-
* Whether to show a tutorial tap target. This will try to determine whether the user
120-
* has interacted with each feature before by checking the key maps they've created (if any).
121-
* E.g if they have no key maps with actions then show a tap target highlighting the action tab
122-
* when they create a key map.
123-
*/
124-
private fun showTutorialTapTarget(
125-
tapTarget: OnboardingTapTarget,
126-
isShown: Boolean,
127-
skipTutorial: Boolean,
128-
keyMapList: List<KeyMapEntity>,
129-
): Boolean {
130-
if (isShown) {
131-
return false
132-
}
133-
134-
if (skipTutorial) {
135-
return false
136-
}
137-
138103
return when (tapTarget) {
139-
OnboardingTapTarget.CHOOSE_ACTION -> keyMapList.all { it.actionList.isEmpty() }
104+
OnboardingTapTarget.CHOOSE_ACTION -> Keys.shownTapTargetChooseAction
140105
}
141106
}
142-
143-
override fun skipTapTargetOnboarding() {
144-
settingsRepository.set(Keys.skipTapTargetTutorial, true)
145-
}
146107
}
147108

148109
interface OnboardingUseCase {
@@ -167,5 +128,4 @@ interface OnboardingUseCase {
167128

168129
fun showTapTarget(tapTarget: OnboardingTapTarget): Flow<Boolean>
169130
fun completedTapTarget(tapTarget: OnboardingTapTarget)
170-
fun skipTapTargetOnboarding()
171131
}

0 commit comments

Comments
 (0)