Implement standby behaviour (#1980)
* Implement standby behaviour * Refine --------- Co-authored-by: Dude so hot <djohn@fbi.gov>
This commit is contained in:
1 parent
2b803d4193
commit
aff627c0cd
1 file changed
+64
-3
@@ -1,32 +1,46 @@
|
|||||||
package com.hippo.ehviewer.ui.tools
|
package com.hippo.ehviewer.ui.tools
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.VectorConverter
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.MutatorMutex
|
||||||
import androidx.compose.foundation.gestures.draggable2D
|
import androidx.compose.foundation.gestures.draggable2D
|
||||||
import androidx.compose.foundation.gestures.rememberDraggable2DState
|
import androidx.compose.foundation.gestures.rememberDraggable2DState
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.safeContent
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.FilledTonalIconButton
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.platform.LocalWindowInfo
|
import androidx.compose.ui.platform.LocalWindowInfo
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.round
|
import androidx.compose.ui.unit.round
|
||||||
|
import androidx.compose.ui.unit.toSize
|
||||||
import androidx.compose.ui.util.lerp
|
import androidx.compose.ui.util.lerp
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import moe.tarsin.kt.unreachable
|
||||||
|
|
||||||
fun lerp(start: IntOffset, stop: IntOffset, fraction: Float) = IntOffset(
|
fun lerp(start: IntOffset, stop: IntOffset, fraction: Float) = IntOffset(
|
||||||
lerp(start.x, stop.x, fraction),
|
lerp(start.x, stop.x, fraction),
|
||||||
@@ -47,6 +61,8 @@ fun BoxScope.PausableAlertDialog(
|
|||||||
text: @Composable () -> Unit,
|
text: @Composable () -> Unit,
|
||||||
idleIcon: ImageVector,
|
idleIcon: ImageVector,
|
||||||
) {
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val mutex = remember { MutatorMutex() }
|
||||||
var showDialog by remember { mutableStateOf(true) }
|
var showDialog by remember { mutableStateOf(true) }
|
||||||
if (showDialog) {
|
if (showDialog) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
@@ -58,16 +74,61 @@ fun BoxScope.PausableAlertDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
val fraction by animateFloatAsState(if (showDialog) 0f else 1f)
|
val fraction by animateFloatAsState(if (showDialog) 0f else 1f)
|
||||||
val info = LocalWindowInfo.current.containerSize
|
val (viewportW, viewportH) = LocalWindowInfo.current.containerSize.toSize()
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val (buttonW, buttonH) = with(density) { IconButtonDefaults.smallContainerSize().toSize() }
|
||||||
|
val safeContent = WindowInsets.safeContent
|
||||||
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
|
val safe = Rect(
|
||||||
|
left = (buttonW - viewportW) / 2 + safeContent.getLeft(density, layoutDirection),
|
||||||
|
top = (buttonH - viewportH) / 2 + safeContent.getTop(density),
|
||||||
|
right = (viewportW - buttonW) / 2 - safeContent.getRight(density, layoutDirection),
|
||||||
|
bottom = (viewportH - buttonH) / 2 - safeContent.getBottom(density),
|
||||||
|
)
|
||||||
|
|
||||||
// Disappear at screen center
|
// Disappear at screen center
|
||||||
val disappear = IntOffset(info.width / 2, 0)
|
val disappear = safe.center.round()
|
||||||
var idle by rememberMutableStateInDataStore("DialogBubble", OffsetSerializer) { Offset.Zero }
|
var idle by rememberMutableStateInDataStore("DialogBubble", OffsetSerializer) { Offset.Zero }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
idle = idle.safeOffset(safe)
|
||||||
|
}
|
||||||
val state = rememberDraggable2DState { delta -> idle += delta }
|
val state = rememberDraggable2DState { delta -> idle += delta }
|
||||||
FilledTonalIconButton(
|
FilledTonalIconButton(
|
||||||
onClick = { showDialog = true },
|
onClick = { showDialog = true },
|
||||||
modifier = Modifier.offset { lerp(disappear, idle.round(), fraction) }.graphicsLayer { alpha = fraction }.align(Alignment.CenterStart).draggable2D(state = state),
|
modifier = Modifier.offset {
|
||||||
|
lerp(disappear, idle.round(), fraction)
|
||||||
|
}.graphicsLayer {
|
||||||
|
alpha = fraction
|
||||||
|
}.align(Alignment.Center).draggable2D(
|
||||||
|
state = state,
|
||||||
|
onDragStopped = { v ->
|
||||||
|
val target = idle.safeOffset(safe)
|
||||||
|
scope.launch {
|
||||||
|
mutex.mutate {
|
||||||
|
Animatable(idle, Offset.VectorConverter).animateTo(target, initialVelocity = Offset(v.x, v.y)) {
|
||||||
|
idle = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = idleIcon, contentDescription = null)
|
Icon(imageVector = idleIcon, contentDescription = null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Offset.safeOffset(edge: Rect): Offset {
|
||||||
|
val (x, y) = this
|
||||||
|
val (l, t, r, b) = edge
|
||||||
|
val dl = x - l
|
||||||
|
val dr = r - x
|
||||||
|
val dt = y - t
|
||||||
|
val db = b - y
|
||||||
|
return when (minOf(dl, dr, dt, db)) {
|
||||||
|
dl -> Offset(l, y)
|
||||||
|
dr -> Offset(r, y)
|
||||||
|
dt -> Offset(x, t)
|
||||||
|
db -> Offset(x, b)
|
||||||
|
else -> unreachable()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user