commit 2dfcf9f43a9e10711e93e6db630c374fb37ed2c8
parent 9d3174d8469d3d57838c2faa5f2bc3bbc6462b05
Author: Iván Ávalos <avalos@disroot.org>
Date: Thu, 7 Nov 2024 15:09:05 +0100
[wallet] Improve user interaction of Taler action button
Diffstat:
4 files changed, 123 insertions(+), 55 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/MainFragment.kt b/wallet/src/main/java/net/taler/wallet/MainFragment.kt
@@ -23,7 +23,7 @@ import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
@@ -60,7 +60,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
-import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
@@ -79,6 +78,7 @@ import androidx.fragment.compose.FragmentState
import androidx.fragment.compose.rememberFragmentState
import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
import net.taler.wallet.balances.BalanceState
import net.taler.wallet.balances.BalancesComposable
import net.taler.wallet.balances.ScopeInfo
@@ -96,7 +96,7 @@ class MainFragment: Fragment() {
private val model: MainViewModel by activityViewModels()
- @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
+ @OptIn(ExperimentalMaterial3Api::class)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -115,6 +115,7 @@ class MainFragment: Fragment() {
val selectedScope by model.transactionManager.selectedScope.collectAsStateLifecycleAware()
val txResult by remember(selectedScope) { model.transactionManager.transactionsFlow(selectedScope) }.collectAsStateLifecycleAware()
val selectedSpec = remember(selectedScope) { selectedScope?.let { model.balanceManager.getSpecForScopeInfo(it) } }
+ val actionButtonUsed by remember { model.getActionButtonUsed(context) }.collectAsStateLifecycleAware(true)
Scaffold(
bottomBar = {
@@ -126,47 +127,17 @@ class MainFragment: Fragment() {
onClick = { tab = Tab.BALANCES },
)
- TooltipBox(
- positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
- tooltip = { PlainTooltip { Text(stringResource(R.string.actions)) } },
- state = rememberTooltipState(),
- ) {
- var offsetY by remember { mutableFloatStateOf(0f) }
-
- DemandAttention {
- LargeFloatingActionButton(
- modifier = Modifier
- .requiredSize(86.dp)
- .padding(8.dp)
- .offset { IntOffset(0, offsetY.roundToInt() / 6) }
- .draggable(
- orientation = Orientation.Vertical,
- state = rememberDraggableState { delta ->
- if (delta < 0) { offsetY += delta }
- },
- onDragStopped = {
- offsetY = 0.0f
- onScanQr()
- },
- ),
- shape = CircleShape,
- onClick = { showSheet = true },
- ) {
- if (offsetY == 0.0f) {
- Icon(
- painterResource(R.drawable.ic_actions),
- modifier = Modifier.size(38.dp),
- contentDescription = stringResource(R.string.actions),
- )
- } else {
- Icon(
- painterResource(R.drawable.ic_scan_qr),
- contentDescription = stringResource(R.string.actions),
- )
- }
- }
- }
- }
+ TalerActionButton(
+ demandAttention = !actionButtonUsed,
+ onShowSheet = {
+ showSheet = true
+ model.saveActionButtonUsed(context)
+ },
+ onScanQr = {
+ onScanQr()
+ model.saveActionButtonUsed(context)
+ },
+ )
NavigationBarItem(
icon = { Icon(Icons.Default.Settings, contentDescription = null) },
@@ -251,6 +222,8 @@ class MainFragment: Fragment() {
}
}
+
+
override fun onStart() {
super.onStart()
model.balanceManager.loadBalances()
@@ -317,6 +290,70 @@ fun SettingsView(
)
}
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+private fun TalerActionButton(
+ demandAttention: Boolean,
+ onShowSheet: () -> Unit,
+ onScanQr: () -> Unit,
+) {
+ val tooltipState = rememberTooltipState()
+ TooltipBox(
+ positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
+ tooltip = { PlainTooltip { Text(stringResource(R.string.actions)) } },
+ state = tooltipState,
+ ) {
+ val offsetY = remember { Animatable(0f) }
+ var cancelled by remember { mutableStateOf(false) }
+
+ DemandAttention(demandAttention = demandAttention) {
+ LargeFloatingActionButton(
+ modifier = Modifier
+ .requiredSize(86.dp)
+ .padding(8.dp)
+ .offset { IntOffset(0, offsetY.value.roundToInt() / 6) }
+ .draggable(
+ orientation = Orientation.Vertical,
+ state = rememberDraggableState { delta ->
+ runBlocking { offsetY.snapTo(offsetY.value + delta) }
+ if (delta > 0) {
+ cancelled = true
+ }
+ },
+ onDragStopped = {
+ offsetY.animateTo(0.0f)
+ if (!cancelled) {
+ onScanQr()
+ }
+ cancelled = false
+ },
+ ),
+ shape = CircleShape,
+ onClick = { onShowSheet() },
+ ) {
+ if (offsetY.value == 0.0f) {
+ Icon(
+ painterResource(R.drawable.ic_actions),
+ modifier = Modifier.size(38.dp),
+ contentDescription = stringResource(R.string.actions),
+ )
+ } else {
+ Icon(
+ painterResource(R.drawable.ic_scan_qr),
+ contentDescription = stringResource(R.string.actions),
+ )
+ }
+ }
+ }
+ }
+
+ LaunchedEffect(demandAttention) {
+ if (demandAttention) {
+ tooltipState.show()
+ }
+ }
+}
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TalerActionsModal(
diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
@@ -281,9 +281,9 @@ class MainViewModel(
}
}
- fun getSelectedScope(c: Context) = c.userPreferencesDataStore.data.map { scope ->
- if (scope.hasSelectedScope()) {
- ScopeInfo.fromPrefs(scope.selectedScope)
+ fun getSelectedScope(c: Context) = c.userPreferencesDataStore.data.map { prefs ->
+ if (prefs.hasSelectedScope()) {
+ ScopeInfo.fromPrefs(prefs.selectedScope)
} else {
null
}
@@ -302,6 +302,22 @@ class MainViewModel(
}
}
}
+
+ fun getActionButtonUsed(c: Context) = c.userPreferencesDataStore.data.map { prefs ->
+ if (prefs.hasActionButtonUsed()) {
+ prefs.actionButtonUsed
+ } else {
+ false
+ }
+ }
+
+ fun saveActionButtonUsed(c: Context) = viewModelScope.launch {
+ c.userPreferencesDataStore.updateData { current ->
+ current.toBuilder()
+ .setActionButtonUsed(true)
+ .build()
+ }
+ }
}
enum class ScanQrContext {
diff --git a/wallet/src/main/java/net/taler/wallet/compose/DemandAttention.kt b/wallet/src/main/java/net/taler/wallet/compose/DemandAttention.kt
@@ -31,31 +31,45 @@ import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
@Composable
-fun DemandAttention(content: @Composable () -> Unit) {
+fun DemandAttention(
+ initialDelayMillis: Long = 400,
+ wiggleWidth: Float = 5f,
+ wiggleCount: Int = 2,
+ demandAttention: Boolean = true,
+ content: @Composable () -> Unit,
+) {
val offsetX = remember { Animatable(0f) }
- LaunchedEffect(Unit) {
- delay(400)
+ suspend fun wiggle() {
offsetX.animateTo(
- targetValue = -5f,
- animationSpec = tween(90, easing = LinearEasing),
+ targetValue = -wiggleWidth,
+ animationSpec = tween(80, easing = LinearEasing),
)
offsetX.animateTo(
- targetValue = 5f,
+ targetValue = wiggleWidth,
animationSpec = repeatable(
iterations = 5,
- animation = tween(80, easing = LinearEasing),
+ animation = tween(90, easing = LinearEasing),
repeatMode = RepeatMode.Reverse,
)
)
offsetX.animateTo(
targetValue = 0f,
- animationSpec = tween(90, easing = LinearEasing),
+ animationSpec = tween(80, easing = LinearEasing),
)
}
+ LaunchedEffect(demandAttention) {
+ if (demandAttention) {
+ delay(initialDelayMillis)
+ repeat(wiggleCount) {
+ wiggle()
+ }
+ }
+ }
+
Box(Modifier.offset(offsetX.value.dp, 0.dp)) {
content()
}
diff --git a/wallet/src/main/proto/user_prefs.proto b/wallet/src/main/proto/user_prefs.proto
@@ -17,4 +17,5 @@ message PrefsScopeInfo {
message UserPreferences {
optional PrefsScopeInfo selectedScope = 1;
+ optional bool actionButtonUsed = 2;
}
\ No newline at end of file