mirror of
https://github.com/bitwarden/android.git
synced 2026-08-26 22:04:07 -05:00
Introduce Expandable floating action button (#6)
This commit is contained in:
+2
-2
@@ -10,8 +10,8 @@ import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
@@ -81,7 +81,7 @@ fun ItemScreen(
|
||||
enter = scaleIn(),
|
||||
exit = scaleOut(),
|
||||
) {
|
||||
FloatingActionButton(
|
||||
ExtendedFloatingActionButton(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
onClick = remember(viewModel) { { onNavigateToEditItem(state.itemId) } },
|
||||
modifier = Modifier
|
||||
|
||||
+2
-1
@@ -21,9 +21,10 @@ fun NavGraphBuilder.itemListingGraph(
|
||||
) {
|
||||
itemListingDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToAddItemScreen = { /*navController.navigateToAddItem()*/ },
|
||||
onNavigateToQrCodeScanner = { /*navController.navigateToQrCodeScanner()*/ },
|
||||
onNavigateToItemScreen = { navController.navigateToItem(itemId = it) },
|
||||
onNavigateToEditItemScreen = { /*navController.navigateToEditItem(itemId = it)*/ },
|
||||
onNavigateToManualKeyEntry = { /*navController.navigateToManualKeySetup()*/ },
|
||||
)
|
||||
itemDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
|
||||
+4
-2
@@ -10,7 +10,8 @@ const val ITEM_LIST_ROUTE = "item_list"
|
||||
*/
|
||||
fun NavGraphBuilder.itemListingDestination(
|
||||
onNavigateBack: () -> Unit = { },
|
||||
onNavigateToAddItemScreen: () -> Unit = { },
|
||||
onNavigateToQrCodeScanner: () -> Unit = { },
|
||||
onNavigateToManualKeyEntry: () -> Unit = { },
|
||||
onNavigateToItemScreen: (id: String) -> Unit = { },
|
||||
onNavigateToEditItemScreen: (id: String) -> Unit = { },
|
||||
) {
|
||||
@@ -19,7 +20,8 @@ fun NavGraphBuilder.itemListingDestination(
|
||||
) {
|
||||
ItemListingScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToAddItemScreen = onNavigateToAddItemScreen,
|
||||
onNavigateToQrCodeScanner = onNavigateToQrCodeScanner,
|
||||
onNavigateToManualKeyEntry = onNavigateToManualKeyEntry,
|
||||
onNavigateToItemScreen = onNavigateToItemScreen,
|
||||
onNavigateToEditItemScreen = onNavigateToEditItemScreen
|
||||
)
|
||||
|
||||
+47
-17
@@ -9,16 +9,12 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -30,6 +26,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.authenticator.R
|
||||
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.ItemListingExpandableFabAction
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
@@ -37,6 +34,9 @@ import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.BasicDial
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.fab.ExpandableFabIcon
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.fab.ExpandableFloatingActionButton
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.model.IconResource
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
||||
|
||||
/**
|
||||
@@ -47,7 +47,8 @@ import com.x8bit.bitwarden.authenticator.ui.platform.components.scaffold.Bitward
|
||||
fun ItemListingScreen(
|
||||
viewModel: ItemListingViewModel = hiltViewModel(),
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToAddItemScreen: () -> Unit,
|
||||
onNavigateToQrCodeScanner: () -> Unit,
|
||||
onNavigateToManualKeyEntry: () -> Unit,
|
||||
onNavigateToItemScreen: (id: String) -> Unit,
|
||||
onNavigateToEditItemScreen: (id: String) -> Unit,
|
||||
) {
|
||||
@@ -60,7 +61,8 @@ fun ItemListingScreen(
|
||||
when (event) {
|
||||
ItemListingEvent.NavigateBack -> onNavigateBack()
|
||||
ItemListingEvent.DismissPullToRefresh -> pullToRefreshState.endRefresh()
|
||||
ItemListingEvent.NavigateToAddItem -> onNavigateToAddItemScreen()
|
||||
ItemListingEvent.NavigateToQrCodeScanner -> onNavigateToQrCodeScanner()
|
||||
ItemListingEvent.NavigateToManualAddItem -> onNavigateToManualKeyEntry()
|
||||
is ItemListingEvent.NavigateToItem -> onNavigateToItemScreen(event.id)
|
||||
is ItemListingEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.message(context.resources), Toast.LENGTH_LONG)
|
||||
@@ -83,20 +85,48 @@ fun ItemListingScreen(
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ItemListingAction.AddItemClick) }
|
||||
},
|
||||
ExpandableFloatingActionButton(
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "AddItemButton" }
|
||||
.padding(bottom = 16.dp),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_plus),
|
||||
contentDescription = stringResource(id = R.string.add_item),
|
||||
)
|
||||
}
|
||||
label = R.string.add_item.asText(),
|
||||
items = listOf(
|
||||
ItemListingExpandableFabAction.ScanQrCode(
|
||||
label = R.string.scan_a_qr_code.asText(),
|
||||
icon = IconResource(
|
||||
iconPainter = painterResource(id = R.drawable.ic_camera),
|
||||
contentDescription = stringResource(id = R.string.scan_a_qr_code),
|
||||
testTag = "ScanQRCodeButton"
|
||||
),
|
||||
onScanQrCodeClick = {
|
||||
viewModel.trySendAction(
|
||||
ItemListingAction.ScanQrCodeClick
|
||||
)
|
||||
}
|
||||
),
|
||||
ItemListingExpandableFabAction.EnterSetupKey(
|
||||
label = R.string.enter_a_setup_key.asText(),
|
||||
icon = IconResource(
|
||||
iconPainter = painterResource(id = R.drawable.ic_copy),
|
||||
contentDescription = stringResource(id = R.string.enter_a_setup_key),
|
||||
testTag = "EnterSetupKeyButton"
|
||||
),
|
||||
onEnterSetupKeyClick = {
|
||||
viewModel.trySendAction(
|
||||
ItemListingAction.EnterSetupKeyClick
|
||||
)
|
||||
}
|
||||
)
|
||||
),
|
||||
expandableFabIcon = ExpandableFabIcon(
|
||||
iconData = IconResource(
|
||||
iconPainter = painterResource(id = R.drawable.ic_plus),
|
||||
contentDescription = stringResource(id = R.string.add_item),
|
||||
testTag = "AddItemButton",
|
||||
),
|
||||
iconRotation = 45f,
|
||||
),
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.EndOverlay,
|
||||
) { paddingValues ->
|
||||
|
||||
+16
-6
@@ -91,9 +91,9 @@ class ItemListingViewModel @Inject constructor(
|
||||
|
||||
override fun handleAction(action: ItemListingAction) {
|
||||
when (action) {
|
||||
ItemListingAction.AddItemClick -> sendEvent(ItemListingEvent.NavigateToAddItem)
|
||||
ItemListingAction.ScanQrCodeClick -> sendEvent(ItemListingEvent.NavigateToQrCodeScanner)
|
||||
ItemListingAction.EnterSetupKeyClick -> sendEvent(ItemListingEvent.NavigateToManualAddItem)
|
||||
ItemListingAction.BackClick -> sendEvent(ItemListingEvent.NavigateBack)
|
||||
|
||||
is ItemListingAction.ItemClick -> sendEvent(ItemListingEvent.NavigateToItem(action.id))
|
||||
ItemListingAction.DialogDismiss -> handleDialogDismiss()
|
||||
is ItemListingAction.Internal.AuthenticatorDataReceive -> handleAuthenticatorDataReceive(
|
||||
@@ -276,9 +276,14 @@ sealed class ItemListingEvent {
|
||||
data object NavigateBack : ItemListingEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the Add Item screen.
|
||||
* Navigate to the QR Code Scanner screen.
|
||||
*/
|
||||
data object NavigateToAddItem : ItemListingEvent()
|
||||
data object NavigateToQrCodeScanner : ItemListingEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the Manual Add Item screen.
|
||||
*/
|
||||
data object NavigateToManualAddItem : ItemListingEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the View Item screen.
|
||||
@@ -314,9 +319,14 @@ sealed class ItemListingAction {
|
||||
data object BackClick : ItemListingAction()
|
||||
|
||||
/**
|
||||
* The user clicked the Add Item button.
|
||||
* The user clicked the Scan QR Code button.
|
||||
*/
|
||||
data object AddItemClick : ItemListingAction()
|
||||
data object ScanQrCodeClick : ItemListingAction()
|
||||
|
||||
/**
|
||||
* The user clicked the Enter Setup Key button.
|
||||
*/
|
||||
data object EnterSetupKeyClick : ItemListingAction()
|
||||
|
||||
/**
|
||||
* The user clicked a list item.
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model
|
||||
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.fab.ExpandableFabOption
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.model.IconResource
|
||||
|
||||
sealed class ItemListingExpandableFabAction(
|
||||
label: Text?,
|
||||
icon: IconResource,
|
||||
onFabOptionClick: () -> Unit,
|
||||
) : ExpandableFabOption(label, icon, onFabOptionClick) {
|
||||
|
||||
class ScanQrCode(
|
||||
label: Text?,
|
||||
icon: IconResource,
|
||||
onScanQrCodeClick: () -> Unit,
|
||||
) : ItemListingExpandableFabAction(
|
||||
label,
|
||||
icon,
|
||||
onScanQrCodeClick
|
||||
)
|
||||
|
||||
class EnterSetupKey(
|
||||
label: Text?,
|
||||
icon: IconResource,
|
||||
onEnterSetupKeyClick: () -> Unit,
|
||||
) : ItemListingExpandableFabAction(
|
||||
label,
|
||||
icon,
|
||||
onEnterSetupKeyClick
|
||||
)
|
||||
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
package com.x8bit.bitwarden.authenticator.ui.platform.components.fab
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.expandHorizontally
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkHorizontally
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.authenticator.R
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.model.IconResource
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.theme.Typography
|
||||
|
||||
@Composable
|
||||
fun <T : ExpandableFabOption> ExpandableFloatingActionButton(
|
||||
modifier: Modifier = Modifier,
|
||||
label: Text?,
|
||||
items: List<T>,
|
||||
expandableFabState: MutableState<ExpandableFabState> = rememberExpandableFabState(),
|
||||
expandableFabIcon: ExpandableFabIcon,
|
||||
onStateChange: (expandableFabState: ExpandableFabState) -> Unit = { },
|
||||
) {
|
||||
val rotation by animateFloatAsState(
|
||||
targetValue = if (expandableFabState.value == ExpandableFabState.Expanded) {
|
||||
expandableFabIcon.iconRotation ?: 0f
|
||||
} else {
|
||||
0f
|
||||
},
|
||||
label = stringResource(R.string.add_item_rotation),
|
||||
)
|
||||
Column(
|
||||
modifier = modifier.wrapContentSize(),
|
||||
horizontalAlignment = Alignment.End,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = expandableFabState.value.isExpanded(),
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.padding(bottom = 16.dp),
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
items(items) { expandableFabOption ->
|
||||
ExpandableFabOption(
|
||||
onFabOptionClick = {
|
||||
expandableFabState.value = expandableFabState.value.toggleValue()
|
||||
onStateChange(expandableFabState.value)
|
||||
expandableFabOption.onFabOptionClick()
|
||||
},
|
||||
expandableFabOption = expandableFabOption
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
expandableFabState.value = expandableFabState.value.toggleValue()
|
||||
onStateChange(expandableFabState.value)
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
) {
|
||||
|
||||
if (label != null) {
|
||||
AnimatedVisibility(
|
||||
visible = expandableFabState.value.isExpanded(),
|
||||
enter = fadeIn() + expandHorizontally(),
|
||||
exit = fadeOut() + shrinkHorizontally(),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
text = label(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.rotate(rotation)
|
||||
.semantics { expandableFabIcon.iconData.testTag },
|
||||
painter = expandableFabIcon.iconData.iconPainter,
|
||||
contentDescription = expandableFabIcon.iconData.contentDescription,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T : ExpandableFabOption> ExpandableFabOption(
|
||||
expandableFabOption: T,
|
||||
onFabOptionClick: (option: T) -> Unit,
|
||||
) {
|
||||
SmallFloatingActionButton(
|
||||
onClick = { onFabOptionClick(expandableFabOption) },
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.padding(end = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
expandableFabOption.label?.let { label ->
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(size = 8.dp))
|
||||
.padding(all = 8.dp),
|
||||
text = label(),
|
||||
style = Typography.labelSmall,
|
||||
)
|
||||
}
|
||||
|
||||
Icon(
|
||||
painter = expandableFabOption.iconData.iconPainter,
|
||||
contentDescription = expandableFabOption.iconData.contentDescription,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberExpandableFabState() =
|
||||
remember { mutableStateOf<ExpandableFabState>(ExpandableFabState.Collapsed) }
|
||||
|
||||
abstract class ExpandableFabOption(
|
||||
val label: Text?,
|
||||
val iconData: IconResource,
|
||||
val onFabOptionClick: () -> Unit,
|
||||
)
|
||||
|
||||
data class ExpandableFabIcon(
|
||||
val iconData: IconResource,
|
||||
val iconRotation: Float?,
|
||||
)
|
||||
|
||||
sealed class ExpandableFabState {
|
||||
|
||||
fun isExpanded() = this is Expanded
|
||||
|
||||
fun toggleValue() = if (isExpanded()) {
|
||||
Collapsed
|
||||
} else {
|
||||
Expanded
|
||||
}
|
||||
|
||||
data object Collapsed : ExpandableFabState()
|
||||
|
||||
data object Expanded : ExpandableFabState()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="18dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="18">
|
||||
<path
|
||||
android:pathData="M3.125,5.25C2.78,5.25 2.5,5.53 2.5,5.875C2.5,6.22 2.78,6.5 3.125,6.5H4.375C4.72,6.5 5,6.22 5,5.875C5,5.53 4.72,5.25 4.375,5.25H3.125Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M14.219,0.25H7.969C6.588,0.25 5.469,1.369 5.469,2.75H2.5C1.119,2.75 0,3.869 0,5.25V14C0,15.381 1.119,16.5 2.5,16.5H7.396C8.479,17.286 9.81,17.75 11.25,17.75C12.69,17.75 14.021,17.286 15.104,16.5H17.5C18.881,16.5 20,15.381 20,14V5.25C20,3.869 18.881,2.75 17.5,2.75H16.719C16.719,1.369 15.599,0.25 14.219,0.25ZM4.688,11.188C4.688,12.722 5.214,14.133 6.096,15.25H2.5C1.81,15.25 1.25,14.69 1.25,14V5.25C1.25,4.56 1.81,4 2.5,4H5.469C6.159,4 6.719,3.44 6.719,2.75C6.719,2.06 7.278,1.5 7.969,1.5H14.219C14.909,1.5 15.469,2.06 15.469,2.75C15.469,3.44 16.028,4 16.719,4H17.5C18.19,4 18.75,4.56 18.75,5.25V14C18.75,14.69 18.19,15.25 17.5,15.25H16.404C16.376,15.286 16.347,15.322 16.318,15.357C17.252,14.224 17.813,12.771 17.813,11.188C17.813,7.563 14.874,4.625 11.25,4.625C7.626,4.625 4.688,7.563 4.688,11.188ZM11.25,16.5C14.184,16.5 16.563,14.122 16.563,11.188C16.563,8.253 14.184,5.875 11.25,5.875C8.316,5.875 5.938,8.253 5.938,11.188C5.938,14.122 8.316,16.5 11.25,16.5Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
@@ -18,4 +18,7 @@
|
||||
<string name="view_item">View Item</string>
|
||||
<string name="bitwarden_authenticator">Bitwarden Authenticator</string>
|
||||
<string name="name">Name</string>
|
||||
<string name="add_item_rotation">Add Item Rotation</string>
|
||||
<string name="scan_a_qr_code">Scan a QR code</string>
|
||||
<string name="enter_a_setup_key">Enter a setup key</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user