Bug 1949514 - Update IconButton and LongPressIconButton with haptics support r=android-reviewers,tchoh
Overcome missing support from Compose upstream. Differential Revision: https://phabricator.services.mozilla.com/D239359
This commit is contained in:
@@ -0,0 +1,128 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.compose.base.button
|
||||||
|
|
||||||
|
import android.view.SoundEffectConstants
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.Interaction
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.material.ContentAlpha
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.LocalContentAlpha
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.minimumInteractiveComponentSize
|
||||||
|
import androidx.compose.material.ripple
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import mozilla.components.compose.base.annotation.LightDarkPreview
|
||||||
|
import mozilla.components.compose.base.theme.AcornTheme
|
||||||
|
import mozilla.components.ui.icons.R as iconsR
|
||||||
|
|
||||||
|
// Temporary workaround to Compose buttons not having click sounds
|
||||||
|
// see https://issuetracker.google.com/issues/218064821
|
||||||
|
|
||||||
|
private val RippleRadius = 24.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Button with the following functionalities:
|
||||||
|
* - it has a minimum touch target size of 48dp
|
||||||
|
* - it will play a sound effect for clicks
|
||||||
|
* - it will use the [AcornTheme] ripple color.
|
||||||
|
*
|
||||||
|
* @param onClick Callback for when this button is clicked.
|
||||||
|
* @param contentDescription Text used by accessibility services to describe what this button does.
|
||||||
|
* @param modifier Optional modifier for further customisation of this button.
|
||||||
|
* @param onClickLabel Semantic / accessibility label for the [onClick] action.
|
||||||
|
* Will be read as "Double tap to [onClickLabel]".
|
||||||
|
* @param enabled Whether or not this button will handle input events and appear enabled
|
||||||
|
* for semantics purposes. `true` by default.
|
||||||
|
* @param interactionSource An optional hoisted [MutableInteractionSource] for observing and
|
||||||
|
* emitting [Interaction]s for this button. You can use this to change the button's appearance
|
||||||
|
* or preview the button in different states. Note that if `null` is provided interactions will
|
||||||
|
* still happen internally.
|
||||||
|
* @param content The content to be shown inside this button.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun IconButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
contentDescription: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClickLabel: String? = null,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.semantics { this.contentDescription = contentDescription }
|
||||||
|
.minimumInteractiveComponentSize()
|
||||||
|
.clickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = ripple(
|
||||||
|
bounded = false,
|
||||||
|
radius = RippleRadius,
|
||||||
|
color = AcornTheme.colors.ripple,
|
||||||
|
),
|
||||||
|
enabled = enabled,
|
||||||
|
onClickLabel = onClickLabel,
|
||||||
|
role = Role.Button,
|
||||||
|
onClick = {
|
||||||
|
view.playSoundEffect(SoundEffectConstants.CLICK)
|
||||||
|
onClick()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
val contentAlpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled
|
||||||
|
CompositionLocalProvider(LocalContentAlpha provides contentAlpha, content = content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@LightDarkPreview
|
||||||
|
@Composable
|
||||||
|
private fun IconButtonPreview() {
|
||||||
|
AcornTheme {
|
||||||
|
IconButton(
|
||||||
|
onClick = {},
|
||||||
|
contentDescription = "test",
|
||||||
|
modifier = Modifier.background(AcornTheme.colors.layer1),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(iconsR.drawable.mozac_ic_bookmark_fill_24),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = AcornTheme.colors.iconButton,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@LightDarkPreview
|
||||||
|
@Composable
|
||||||
|
private fun TextButtonPreview() {
|
||||||
|
AcornTheme {
|
||||||
|
IconButton(
|
||||||
|
onClick = {},
|
||||||
|
contentDescription = "test",
|
||||||
|
modifier = Modifier.background(AcornTheme.colors.layer1),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "button",
|
||||||
|
color = AcornTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.components.compose.base.button
|
||||||
|
|
||||||
|
import android.view.SoundEffectConstants
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.interaction.Interaction
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.material.ContentAlpha
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.LocalContentAlpha
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.minimumInteractiveComponentSize
|
||||||
|
import androidx.compose.material.ripple
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import mozilla.components.compose.base.annotation.LightDarkPreview
|
||||||
|
import mozilla.components.compose.base.modifier.rightClickable
|
||||||
|
import mozilla.components.compose.base.theme.AcornTheme
|
||||||
|
import mozilla.components.ui.icons.R as iconsR
|
||||||
|
|
||||||
|
// Temporary workaround to Compose buttons not having click sounds
|
||||||
|
// see https://issuetracker.google.com/issues/219984415
|
||||||
|
|
||||||
|
private val RippleRadius = 24.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A button with the following functionalities:
|
||||||
|
* - it has a minimum touch target size of 48dp
|
||||||
|
* - it will perform a haptic feedback for long clicks or right clicks
|
||||||
|
* - it will play a sound effect for clicks
|
||||||
|
* - it will use the [AcornTheme] ripple color.
|
||||||
|
*
|
||||||
|
* @param onClick Callback for when this button is clicked.
|
||||||
|
* @param onLongClick Callback for when this button is long clicked or right click.
|
||||||
|
* @param contentDescription Text used by accessibility services to describe what this button does.
|
||||||
|
* @param modifier Optional modifier for further customisation of this button.
|
||||||
|
* @param onClickLabel Semantic / accessibility label for the [onClick] action.
|
||||||
|
* Will be read as "Double tap to [onLongClick]". Leave `null` for "activate" to be read.
|
||||||
|
* @param onLongClickLabel Semantic / accessibility label for the [onLongClick] action.
|
||||||
|
* Will be read as "Double tap and hold to [onLongClickLabel]". Leave `null` for "long press" to be read.
|
||||||
|
* @param enabled Whether or not this button will handle input events and appear enabled
|
||||||
|
* for semantics purposes. `true` by default.
|
||||||
|
* @param interactionSource An optional hoisted [MutableInteractionSource] for observing and
|
||||||
|
* emitting [Interaction]s for this button. You can use this to change the button's appearance
|
||||||
|
* or preview the button in different states. Note that if `null` is provided, interactions will
|
||||||
|
* still happen internally.
|
||||||
|
* @param content The content to be shown inside this button.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun LongPressIconButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: (() -> Unit),
|
||||||
|
contentDescription: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClickLabel: String? = null,
|
||||||
|
onLongClickLabel: String? = null,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val haptic = LocalHapticFeedback.current
|
||||||
|
val view = LocalView.current
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.semantics { this.contentDescription = contentDescription }
|
||||||
|
.minimumInteractiveComponentSize()
|
||||||
|
.combinedClickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = ripple(bounded = false, radius = RippleRadius, color = AcornTheme.colors.ripple),
|
||||||
|
enabled = enabled,
|
||||||
|
onClickLabel = onClickLabel,
|
||||||
|
role = Role.Button,
|
||||||
|
onLongClickLabel = onLongClickLabel,
|
||||||
|
onLongClick = {
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
onLongClick()
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
view.playSoundEffect(SoundEffectConstants.CLICK)
|
||||||
|
onClick()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.rightClickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = ripple(bounded = false, radius = RippleRadius, color = AcornTheme.colors.ripple),
|
||||||
|
onRightClick = {
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
onLongClick()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
val contentAlpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled
|
||||||
|
CompositionLocalProvider(LocalContentAlpha provides contentAlpha, content = content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@LightDarkPreview
|
||||||
|
@Composable
|
||||||
|
private fun LongPressIconButtonPreview() {
|
||||||
|
AcornTheme {
|
||||||
|
LongPressIconButton(
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = {},
|
||||||
|
contentDescription = "test",
|
||||||
|
modifier = Modifier.background(AcornTheme.colors.layer1),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(iconsR.drawable.mozac_ic_bookmark_fill_24),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = AcornTheme.colors.iconButton,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@LightDarkPreview
|
||||||
|
@Composable
|
||||||
|
private fun LongPressTextButtonPreview() {
|
||||||
|
AcornTheme {
|
||||||
|
LongPressIconButton(
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = {},
|
||||||
|
contentDescription = "test",
|
||||||
|
modifier = Modifier.background(AcornTheme.colors.layer1),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "button",
|
||||||
|
color = AcornTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ import org.mozilla.samples.toolbar.middleware.SearchSelectorInteractions.Setting
|
|||||||
import org.mozilla.samples.toolbar.middleware.SearchSelectorInteractions.TabsClicked
|
import org.mozilla.samples.toolbar.middleware.SearchSelectorInteractions.TabsClicked
|
||||||
import mozilla.components.ui.icons.R as iconsR
|
import mozilla.components.ui.icons.R as iconsR
|
||||||
|
|
||||||
sealed class SearchSelectorInteractions : BrowserToolbarEvent {
|
private sealed class SearchSelectorInteractions : BrowserToolbarEvent {
|
||||||
data object BookmarksClicked : SearchSelectorInteractions()
|
data object BookmarksClicked : SearchSelectorInteractions()
|
||||||
data object TabsClicked : SearchSelectorInteractions()
|
data object TabsClicked : SearchSelectorInteractions()
|
||||||
data object HistoryClicked : SearchSelectorInteractions()
|
data object HistoryClicked : SearchSelectorInteractions()
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ import mozilla.components.browser.state.selector.selectedTab
|
|||||||
import mozilla.components.browser.state.store.BrowserStore
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
import mozilla.components.compose.base.Divider
|
import mozilla.components.compose.base.Divider
|
||||||
import mozilla.components.compose.base.annotation.LightDarkPreview
|
import mozilla.components.compose.base.annotation.LightDarkPreview
|
||||||
|
import mozilla.components.compose.base.button.IconButton
|
||||||
|
import mozilla.components.compose.base.button.LongPressIconButton
|
||||||
import mozilla.components.lib.state.ext.observeAsComposableState
|
import mozilla.components.lib.state.ext.observeAsComposableState
|
||||||
import mozilla.components.lib.state.ext.observeAsState
|
import mozilla.components.lib.state.ext.observeAsState
|
||||||
import mozilla.components.ui.tabcounter.TabCounterMenu
|
import mozilla.components.ui.tabcounter.TabCounterMenu
|
||||||
@@ -49,8 +51,6 @@ import org.mozilla.fenix.components.AppStore
|
|||||||
import org.mozilla.fenix.components.appstate.AppState
|
import org.mozilla.fenix.components.appstate.AppState
|
||||||
import org.mozilla.fenix.components.components
|
import org.mozilla.fenix.components.components
|
||||||
import org.mozilla.fenix.components.toolbar.NewTabMenu
|
import org.mozilla.fenix.components.toolbar.NewTabMenu
|
||||||
import org.mozilla.fenix.compose.IconButton
|
|
||||||
import org.mozilla.fenix.compose.LongPressIconButton
|
|
||||||
import org.mozilla.fenix.compose.utils.KeyboardState
|
import org.mozilla.fenix.compose.utils.KeyboardState
|
||||||
import org.mozilla.fenix.compose.utils.keyboardAsState
|
import org.mozilla.fenix.compose.utils.keyboardAsState
|
||||||
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
|
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
|
||||||
@@ -405,6 +405,7 @@ private fun BackButton(
|
|||||||
LongPressIconButton(
|
LongPressIconButton(
|
||||||
onClick = onBackButtonClick,
|
onClick = onBackButtonClick,
|
||||||
onLongClick = onBackButtonLongPress,
|
onLongClick = onBackButtonLongPress,
|
||||||
|
contentDescription = stringResource(R.string.browser_menu_back),
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
@@ -429,6 +430,7 @@ private fun ForwardButton(
|
|||||||
LongPressIconButton(
|
LongPressIconButton(
|
||||||
onClick = onForwardButtonClick,
|
onClick = onForwardButtonClick,
|
||||||
onLongClick = onForwardButtonLongPress,
|
onLongClick = onForwardButtonLongPress,
|
||||||
|
contentDescription = stringResource(R.string.browser_menu_forward),
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
@@ -448,6 +450,7 @@ private fun SearchWebButton(
|
|||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onSearchButtonClick,
|
onClick = onSearchButtonClick,
|
||||||
|
contentDescription = stringResource(R.string.search_hint),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag(NavBarTestTags.searchButton),
|
.testTag(NavBarTestTags.searchButton),
|
||||||
) {
|
) {
|
||||||
@@ -469,6 +472,7 @@ private fun MenuButton(
|
|||||||
if (isMenuRedesignEnabled) {
|
if (isMenuRedesignEnabled) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onMenuButtonClick,
|
onClick = onMenuButtonClick,
|
||||||
|
contentDescription = stringResource(R.string.content_description_menu),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.testTag(NavBarTestTags.menuButton),
|
.testTag(NavBarTestTags.menuButton),
|
||||||
@@ -509,6 +513,7 @@ private fun OpenInBrowserButton(
|
|||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onOpenInBrowserButtonClick,
|
onClick = onOpenInBrowserButtonClick,
|
||||||
|
contentDescription = stringResource(R.string.browser_menu_open_in_fenix, stringResource(R.string.app_name)),
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag(NavBarTestTags.openInBrowserButton),
|
.testTag(NavBarTestTags.openInBrowserButton),
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.compose
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.material.ContentAlpha
|
|
||||||
import androidx.compose.material.LocalContentAlpha
|
|
||||||
import androidx.compose.material.minimumInteractiveComponentSize
|
|
||||||
import androidx.compose.material.ripple
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.semantics.Role
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import org.mozilla.fenix.theme.FirefoxTheme
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An [androidx.compose.material.IconButton] that allows for setting the indication
|
|
||||||
* to be a ripple that matches the [FirefoxTheme]
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun IconButton(
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
interactionSource: MutableInteractionSource? = null,
|
|
||||||
content: @Composable () -> Unit,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.minimumInteractiveComponentSize()
|
|
||||||
.clickable(
|
|
||||||
onClick = onClick,
|
|
||||||
enabled = enabled,
|
|
||||||
role = Role.Button,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = ripple(bounded = false, radius = RippleRadius, color = FirefoxTheme.colors.ripple),
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
val contentAlpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled
|
|
||||||
CompositionLocalProvider(LocalContentAlpha provides contentAlpha, content = content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val RippleRadius = 24.dp
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.compose
|
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.material.ContentAlpha
|
|
||||||
import androidx.compose.material.IconButton
|
|
||||||
import androidx.compose.material.LocalContentAlpha
|
|
||||||
import androidx.compose.material.minimumInteractiveComponentSize
|
|
||||||
import androidx.compose.material.ripple
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.semantics.Role
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import org.mozilla.fenix.theme.FirefoxTheme
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An [IconButton] that supports a long press gesture.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
|
||||||
fun LongPressIconButton(
|
|
||||||
onClick: () -> Unit,
|
|
||||||
onLongClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
|
||||||
content: @Composable () -> Unit,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.minimumInteractiveComponentSize()
|
|
||||||
.combinedClickable(
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick,
|
|
||||||
enabled = enabled,
|
|
||||||
role = Role.Button,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = ripple(bounded = false, radius = RippleRadius, color = FirefoxTheme.colors.ripple),
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
val contentAlpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled
|
|
||||||
CompositionLocalProvider(LocalContentAlpha provides contentAlpha, content = content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val RippleRadius = 24.dp
|
|
||||||
Reference in New Issue
Block a user