mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +01:00
home: the player floats over the map as a card, with theme shadows on the card, cover and nav bar
The map keeps its full size underneath, so docking the player only re-centres it. Tactile and Keycap cast shadows, Flat stays flat. The card slides in and a new cover fades in.
This commit is contained in:
1 parent
a1a0546b74
commit
8a2dae4173
6 files changed
+133
-25
No files matched your search
@@ -11,6 +11,8 @@ import androidx.compose.ui.input.pointer.PointerEventPass
|
|||||||
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
||||||
import kotlin.math.hypot
|
import kotlin.math.hypot
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -73,10 +75,28 @@ fun CarAppStage(
|
|||||||
if (status.surfaceRequested) {
|
if (status.surfaceRequested) {
|
||||||
AppSurface(connection, frame, template, Modifier.fillMaxSize())
|
AppSurface(connection, frame, template, Modifier.fillMaxSize())
|
||||||
}
|
}
|
||||||
|
// The docked player floats over one side of the map: the chrome keeps clear of it,
|
||||||
|
// and the app hears that side is covered, so its map centres in the rest.
|
||||||
|
val panel = LocalSidePanelInset.current
|
||||||
|
val panelAtStart = LocalCardsAtEnd.current
|
||||||
TemplateChrome(
|
TemplateChrome(
|
||||||
template,
|
template,
|
||||||
Modifier.fillMaxSize().padding(top = if (embedded) 0.dp else CHROME_BAR, bottom = navBar),
|
Modifier.fillMaxSize().padding(
|
||||||
|
top = if (embedded) 0.dp else CHROME_BAR,
|
||||||
|
bottom = navBar,
|
||||||
|
start = if (panelAtStart) panel else 0.dp,
|
||||||
|
end = if (panelAtStart) 0.dp else panel,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
if (panel > 0.dp) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.align(if (panelAtStart) Alignment.CenterStart else Alignment.CenterEnd)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.width(panel)
|
||||||
|
.covers("side-panel", if (panelAtStart) CoverEdge.START else CoverEdge.END),
|
||||||
|
)
|
||||||
|
}
|
||||||
BottomCover(navBar, Modifier.align(Alignment.BottomCenter))
|
BottomCover(navBar, Modifier.align(Alignment.BottomCenter))
|
||||||
}
|
}
|
||||||
if (!embedded) ChromeBar(status, onDisconnect, Modifier.align(Alignment.TopCenter))
|
if (!embedded) ChromeBar(status, onDisconnect, Modifier.align(Alignment.TopCenter))
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import app.seb3thehacker.gearslip.car.theme.gsPanelShadow
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
@@ -64,9 +66,9 @@ import app.seb3thehacker.gearslip.media.NowPlaying
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The home screen: the map, with the docked player beside it when asked for. With the player on
|
* The home screen: the map, with the docked player floating over one side of it when asked for.
|
||||||
* the left, the map app's cards move to the map's far edge, so the player, the map and the cards
|
* With the player on the left, the map app's cards move to the map's far edge, so the player, the
|
||||||
* each keep their own space.
|
* map and the cards each keep their own space.
|
||||||
*
|
*
|
||||||
* The map is whatever navigation app was used last, brought back on its own. The player sits in
|
* The map is whatever navigation app was used last, brought back on its own. The player sits in
|
||||||
* the nav bar's pill until its dock button moves it here; the column closes itself back to a
|
* the nav bar's pill until its dock button moves it here; the column closes itself back to a
|
||||||
@@ -96,28 +98,52 @@ fun CarHome() {
|
|||||||
}
|
}
|
||||||
val splitScreen = phase == CarMedia.Phase.READY && now.isActive && panel != SidePanel.NONE
|
val splitScreen = phase == CarMedia.Phase.READY && now.isActive && panel != SidePanel.NONE
|
||||||
val playerLeft by CarSettings.playerLeft.collectAsStateWithLifecycle()
|
val playerLeft by CarSettings.playerLeft.collectAsStateWithLifecycle()
|
||||||
|
// What the card showed last, so it still has something to show while it slides away.
|
||||||
|
val shown = remember { androidx.compose.runtime.mutableStateOf(SidePanel.CONTROLS) }
|
||||||
|
if (panel != SidePanel.NONE) shown.value = panel
|
||||||
|
|
||||||
// No padding anywhere, split screen or not: the player sits flush against the map.
|
// The map runs edge to edge underneath; the player floats over one side of it as a card, with
|
||||||
Row(Modifier.fillMaxSize()) {
|
// a gap all round. The map's surface keeps its size when the card comes and goes, so the app
|
||||||
val side = Modifier.weight(1f).fillMaxHeight().padding(bottom = LocalNavBarInset.current)
|
// only re-centres its map instead of drawing onto a new surface.
|
||||||
if (splitScreen && playerLeft) {
|
BoxWithConstraints(Modifier.fillMaxSize()) {
|
||||||
if (panel == SidePanel.LYRICS) SideLyrics(now, side) else DockedPlayer(now, side)
|
val column = maxWidth / PLAYER_SHARE
|
||||||
}
|
androidx.compose.runtime.CompositionLocalProvider(
|
||||||
Box(
|
LocalCardsAtEnd provides (splitScreen && playerLeft),
|
||||||
Modifier
|
LocalSidePanelInset provides if (splitScreen) column else 0.dp,
|
||||||
.weight(if (splitScreen) 2.1f else 1f)
|
|
||||||
.fillMaxHeight(),
|
|
||||||
) {
|
) {
|
||||||
androidx.compose.runtime.CompositionLocalProvider(LocalCardsAtEnd provides (splitScreen && playerLeft)) {
|
|
||||||
MapPane(navStatus, frame)
|
MapPane(navStatus, frame)
|
||||||
}
|
}
|
||||||
}
|
androidx.compose.animation.AnimatedVisibility(
|
||||||
if (splitScreen && !playerLeft) {
|
splitScreen,
|
||||||
if (panel == SidePanel.LYRICS) SideLyrics(now, side) else DockedPlayer(now, side)
|
Modifier.align(if (playerLeft) Alignment.CenterStart else Alignment.CenterEnd),
|
||||||
|
enter = androidx.compose.animation.slideInHorizontally(tween(220)) { if (playerLeft) -it / 3 else it / 3 } +
|
||||||
|
androidx.compose.animation.fadeIn(tween(180)),
|
||||||
|
exit = androidx.compose.animation.slideOutHorizontally(tween(180)) { if (playerLeft) -it / 3 else it / 3 } +
|
||||||
|
androidx.compose.animation.fadeOut(tween(140)),
|
||||||
|
) {
|
||||||
|
val card = Modifier
|
||||||
|
.width(column)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(
|
||||||
|
start = if (playerLeft) CARD_GAP else 0.dp,
|
||||||
|
end = if (playerLeft) 0.dp else CARD_GAP,
|
||||||
|
top = CARD_GAP,
|
||||||
|
bottom = LocalNavBarInset.current + CARD_GAP,
|
||||||
|
)
|
||||||
|
if (shown.value == SidePanel.LYRICS) SideLyrics(now, card) else DockedPlayer(now, card)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The player takes one part in this many of the screen's width, as it did beside the map. */
|
||||||
|
private const val PLAYER_SHARE = 3.1f
|
||||||
|
|
||||||
|
/** Between the player card and the screen's edges, and the map's own cards beside it. */
|
||||||
|
private val CARD_GAP = 12.dp
|
||||||
|
|
||||||
|
/** The card's corners: rounder than a button's, as the map app's own cards are. */
|
||||||
|
private val CARD_SHAPE = RoundedCornerShape(24.dp)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MapPane(status: CarAppConnection.Status, frame: CarEnvironment.Frame) {
|
private fun MapPane(status: CarAppConnection.Status, frame: CarEnvironment.Frame) {
|
||||||
val navigator = LocalCarNavigator.current
|
val navigator = LocalCarNavigator.current
|
||||||
@@ -127,7 +153,10 @@ private fun MapPane(status: CarAppConnection.Status, frame: CarEnvironment.Frame
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
Modifier.fillMaxSize().padding(24.dp),
|
Modifier.fillMaxSize().padding(
|
||||||
|
start = if (LocalCardsAtEnd.current) LocalSidePanelInset.current else 0.dp,
|
||||||
|
end = if (LocalCardsAtEnd.current) 0.dp else LocalSidePanelInset.current,
|
||||||
|
).padding(24.dp),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
@@ -181,7 +210,7 @@ private fun DockedPlayer(now: NowPlaying, modifier: Modifier) {
|
|||||||
|
|
||||||
// The theme's surface rather than the art's colour: a slab of album colour beside the map
|
// The theme's surface rather than the art's colour: a slab of album colour beside the map
|
||||||
// fought it for attention, and the art says the same thing in less space.
|
// fought it for attention, and the art says the same thing in less space.
|
||||||
Surface(modifier = modifier, color = scheme.surfaceContainer) {
|
Surface(modifier = modifier.gsPanelShadow(CARD_SHAPE), shape = CARD_SHAPE, color = scheme.surfaceContainer) {
|
||||||
BoxWithConstraints(Modifier.fillMaxSize().padding(12.dp)) {
|
BoxWithConstraints(Modifier.fillMaxSize().padding(12.dp)) {
|
||||||
// Whatever height the controls below leave over, so the art grows on a taller
|
// Whatever height the controls below leave over, so the art grows on a taller
|
||||||
// screen and shrinks rather than pushing the buttons off a short one.
|
// screen and shrinks rather than pushing the buttons off a short one.
|
||||||
@@ -189,15 +218,20 @@ private fun DockedPlayer(now: NowPlaying, modifier: Modifier) {
|
|||||||
Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Box(Modifier.fillMaxWidth()) {
|
Box(Modifier.fillMaxWidth()) {
|
||||||
// Art opens the full media screen, same as on the pill.
|
// Art opens the full media screen, same as on the pill.
|
||||||
|
val coverShape = RoundedCornerShape(16.dp)
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.TopCenter)
|
.align(Alignment.TopCenter)
|
||||||
.size(artSize)
|
.size(artSize)
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.gsPanelShadow(coverShape)
|
||||||
|
.clip(coverShape)
|
||||||
.background(scheme.surface)
|
.background(scheme.surface)
|
||||||
.clickable { navigator.media() },
|
.clickable { navigator.media() },
|
||||||
) {
|
) {
|
||||||
art?.let { Image(it.asImageBitmap(), null, Modifier.fillMaxSize(), contentScale = ContentScale.Crop) }
|
// A new song's cover fades in over the old one instead of cutting to it.
|
||||||
|
androidx.compose.animation.Crossfade(art, animationSpec = tween(350), label = "cover") { shown ->
|
||||||
|
shown?.let { Image(it.asImageBitmap(), null, Modifier.fillMaxSize(), contentScale = ContentScale.Crop) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
Icons.Filled.Close, "Back to the nav bar", navigator::closeSidePanel,
|
Icons.Filled.Close, "Back to the nav bar", navigator::closeSidePanel,
|
||||||
@@ -242,7 +276,7 @@ private fun SideLyrics(now: NowPlaying, modifier: Modifier) {
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
val art by produceState(now.art, now.art, now.artUri) { value = now.art ?: MediaArt.load(context, now.artUri) }
|
val art by produceState(now.art, now.art, now.artUri) { value = now.art ?: MediaArt.load(context, now.artUri) }
|
||||||
Surface(modifier = modifier, color = scheme.surfaceContainer) {
|
Surface(modifier = modifier.gsPanelShadow(CARD_SHAPE), shape = CARD_SHAPE, color = scheme.surfaceContainer) {
|
||||||
Column(Modifier.fillMaxSize().padding(start = 18.dp, end = 12.dp, top = 12.dp, bottom = 8.dp)) {
|
Column(Modifier.fillMaxSize().padding(start = 18.dp, end = 12.dp, top = 12.dp, bottom = 8.dp)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Box(Modifier.size(36.dp).clip(RoundedCornerShape(8.dp)).background(scheme.surface)) {
|
Box(Modifier.size(36.dp).clip(RoundedCornerShape(8.dp)).background(scheme.surface)) {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ import app.seb3thehacker.gearslip.car.theme.GsColors
|
|||||||
import app.seb3thehacker.gearslip.car.theme.withTintedSurfaces
|
import app.seb3thehacker.gearslip.car.theme.withTintedSurfaces
|
||||||
import app.seb3thehacker.gearslip.car.theme.GsIconBox
|
import app.seb3thehacker.gearslip.car.theme.GsIconBox
|
||||||
import app.seb3thehacker.gearslip.car.theme.GsIconButton
|
import app.seb3thehacker.gearslip.car.theme.GsIconButton
|
||||||
|
import app.seb3thehacker.gearslip.car.theme.gsBarShadow
|
||||||
import app.seb3thehacker.gearslip.car.theme.GsButton
|
import app.seb3thehacker.gearslip.car.theme.GsButton
|
||||||
import app.seb3thehacker.gearslip.car.theme.GsTone
|
import app.seb3thehacker.gearslip.car.theme.GsTone
|
||||||
import app.seb3thehacker.gearslip.car.theme.gsColors
|
import app.seb3thehacker.gearslip.car.theme.gsColors
|
||||||
@@ -164,7 +165,8 @@ internal fun CarNavBar(navigator: CarNavigator) {
|
|||||||
val frame by CarEnvironment.frame.collectAsState()
|
val frame by CarEnvironment.frame.collectAsState()
|
||||||
val running = rememberRunningApps()
|
val running = rememberRunningApps()
|
||||||
|
|
||||||
Surface(color = MaterialTheme.colorScheme.surfaceContainer) {
|
// Casts the theme's panel shadow up onto the map, so the bar sits above it rather than beside it.
|
||||||
|
Surface(Modifier.gsBarShadow(), color = MaterialTheme.colorScheme.surfaceContainer) {
|
||||||
BoxWithConstraints(Modifier.fillMaxWidth().height(NAV_BAR_HEIGHT).padding(horizontal = 10.dp)) {
|
BoxWithConstraints(Modifier.fillMaxWidth().height(NAV_BAR_HEIGHT).padding(horizontal = 10.dp)) {
|
||||||
Row(Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
// Home and Apps are two different places now, not a toggle on one button - the
|
// Home and Apps are two different places now, not a toggle on one button - the
|
||||||
|
|||||||
@@ -160,6 +160,13 @@ internal val LocalChromeCover = compositionLocalOf<ChromeCover?> { null }
|
|||||||
*/
|
*/
|
||||||
internal val LocalCardsAtEnd = compositionLocalOf { false }
|
internal val LocalCardsAtEnd = compositionLocalOf { false }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How much of the map's width the docked player floats over, on the side opposite the cards. The
|
||||||
|
* map itself runs underneath at full size; the app is told this part is covered, and the chrome
|
||||||
|
* keeps clear of it.
|
||||||
|
*/
|
||||||
|
internal val LocalSidePanelInset = compositionLocalOf { 0.dp }
|
||||||
|
|
||||||
/** The edge the app's cards cover, and the one its map buttons do. */
|
/** The edge the app's cards cover, and the one its map buttons do. */
|
||||||
internal val cardEdge @Composable get() = if (LocalCardsAtEnd.current) CoverEdge.END else CoverEdge.START
|
internal val cardEdge @Composable get() = if (LocalCardsAtEnd.current) CoverEdge.END else CoverEdge.START
|
||||||
internal val buttonEdge @Composable get() = if (LocalCardsAtEnd.current) CoverEdge.START else CoverEdge.END
|
internal val buttonEdge @Composable get() = if (LocalCardsAtEnd.current) CoverEdge.START else CoverEdge.END
|
||||||
|
|||||||
@@ -172,6 +172,37 @@ fun Modifier.gsSurface(
|
|||||||
* outlines: cheap, works on every API level, and already soft enough that the car's video
|
* outlines: cheap, works on every API level, and already soft enough that the car's video
|
||||||
* compression has nothing crisp to smear.
|
* compression has nothing crisp to smear.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* The current theme's shadow under a panel floating over the map, such as the docked player.
|
||||||
|
* Draws nothing in a look without one.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun Modifier.gsPanelShadow(shape: Shape): Modifier {
|
||||||
|
val look = gsLook()
|
||||||
|
if (look.panelShadow <= 0.dp || look.panelShadowAlpha <= 0f) return this
|
||||||
|
return drawBehind { drawSoftShadow(shape, look.panelShadow.toPx(), look.panelShadowAlpha, 1f) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The same shadow cast upwards from the top edge of a bar along the bottom of the screen. */
|
||||||
|
@Composable
|
||||||
|
fun Modifier.gsBarShadow(): Modifier {
|
||||||
|
val look = gsLook()
|
||||||
|
if (look.panelShadow <= 0.dp || look.panelShadowAlpha <= 0f) return this
|
||||||
|
return drawBehind {
|
||||||
|
val reach = look.panelShadow.toPx() * 1.5f
|
||||||
|
drawRect(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
0f to Color.Transparent,
|
||||||
|
1f to Color.Black.copy(alpha = look.panelShadowAlpha * 0.6f),
|
||||||
|
startY = -reach,
|
||||||
|
endY = 0f,
|
||||||
|
),
|
||||||
|
topLeft = androidx.compose.ui.geometry.Offset(0f, -reach),
|
||||||
|
size = Size(size.width, reach),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun DrawScope.drawSoftShadow(shape: Shape, elevation: Float, alpha: Float, lift: Float) {
|
private fun DrawScope.drawSoftShadow(shape: Shape, elevation: Float, alpha: Float, lift: Float) {
|
||||||
if (alpha <= 0f) return
|
if (alpha <= 0f) return
|
||||||
val blur = elevation * 2f
|
val blur = elevation * 2f
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ data class ControlLook(
|
|||||||
* from; a flat look takes the dot.
|
* from; a flat look takes the dot.
|
||||||
*/
|
*/
|
||||||
val holdMarker: HoldMarkerStyle = HoldMarkerStyle.PEEL,
|
val holdMarker: HoldMarkerStyle = HoldMarkerStyle.PEEL,
|
||||||
|
/**
|
||||||
|
* The shadow a panel floating over the map casts onto it (the docked player, the lyrics), and
|
||||||
|
* the nav bar casts up. 0 for none, so a flat look stays flat.
|
||||||
|
*/
|
||||||
|
val panelShadow: Dp = 0.dp,
|
||||||
|
val panelShadowAlpha: Float = 0f,
|
||||||
/** Replaces this look when the UI is light, for effects that only work on one background. */
|
/** Replaces this look when the UI is light, for effects that only work on one background. */
|
||||||
val light: ControlLook? = null,
|
val light: ControlLook? = null,
|
||||||
)
|
)
|
||||||
@@ -72,7 +78,11 @@ object GsThemes {
|
|||||||
val Keycap = GsTheme(
|
val Keycap = GsTheme(
|
||||||
id = "keycap",
|
id = "keycap",
|
||||||
name = "Keycap",
|
name = "Keycap",
|
||||||
control = ControlLook(lip = 4.dp, pressedScale = 1f, holdMarker = HoldMarkerStyle.PEEL),
|
// Short and dark, like the keys' own solid base.
|
||||||
|
control = ControlLook(
|
||||||
|
lip = 4.dp, pressedScale = 1f, holdMarker = HoldMarkerStyle.PEEL,
|
||||||
|
panelShadow = 4.dp, panelShadowAlpha = 0.45f,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
val Tactile = GsTheme(
|
val Tactile = GsTheme(
|
||||||
@@ -89,6 +99,8 @@ object GsThemes {
|
|||||||
pressedDim = 0.12f,
|
pressedDim = 0.12f,
|
||||||
pressedScale = 1f,
|
pressedScale = 1f,
|
||||||
holdMarker = HoldMarkerStyle.PEEL,
|
holdMarker = HoldMarkerStyle.PEEL,
|
||||||
|
panelShadow = 12.dp,
|
||||||
|
panelShadowAlpha = 0.42f,
|
||||||
// A wide soft shadow bands once the car's video stream compresses it on a light
|
// A wide soft shadow bands once the car's video stream compresses it on a light
|
||||||
// background, so light mode gets a tight one and leans on the bevel instead.
|
// background, so light mode gets a tight one and leans on the bevel instead.
|
||||||
light = ControlLook(
|
light = ControlLook(
|
||||||
@@ -102,6 +114,8 @@ object GsThemes {
|
|||||||
pressedDim = 0.12f,
|
pressedDim = 0.12f,
|
||||||
pressedScale = 1f,
|
pressedScale = 1f,
|
||||||
holdMarker = HoldMarkerStyle.PEEL,
|
holdMarker = HoldMarkerStyle.PEEL,
|
||||||
|
panelShadow = 4.dp,
|
||||||
|
panelShadowAlpha = 0.22f,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user