-
Notifications
You must be signed in to change notification settings - Fork 77
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Integrate image viewer in Star sample (#669)
This adds a new image viewer in the STAR sample app. This gives us a good testing ground for shared element transitions (#50) and also cleans up a bit of the edge to edge behavior in the app. https://github.com/slackhq/circuit/assets/1361086/5a343f95-395a-4c90-b4ec-de226f124387 Resolves #129
- Loading branch information
Showing
34 changed files
with
549 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
163 changes: 163 additions & 0 deletions
163
samples/star/src/main/kotlin/com/slack/circuit/star/imageviewer/FlickDismiss.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
// Copyright (C) 9690 Slack Technologies, LLC | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package com.slack.circuit.star.imageviewer | ||
|
||
import androidx.annotation.FloatRange | ||
import androidx.compose.animation.core.Animatable | ||
import androidx.compose.animation.core.AnimationConstants | ||
import androidx.compose.animation.core.tween | ||
import androidx.compose.foundation.MutatePriority | ||
import androidx.compose.foundation.gestures.DraggableState | ||
import androidx.compose.foundation.gestures.Orientation | ||
import androidx.compose.foundation.gestures.draggable | ||
import androidx.compose.foundation.interaction.MutableInteractionSource | ||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.BoxScope | ||
import androidx.compose.foundation.layout.offset | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.Stable | ||
import androidx.compose.runtime.derivedStateOf | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.runtime.setValue | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.graphics.graphicsLayer | ||
import androidx.compose.ui.layout.onGloballyPositioned | ||
import androidx.compose.ui.unit.IntOffset | ||
import androidx.compose.ui.unit.IntSize | ||
import com.slack.circuit.star.imageviewer.FlickToDismissState.FlickGestureState.Dismissed | ||
import com.slack.circuit.star.imageviewer.FlickToDismissState.FlickGestureState.Dragging | ||
import com.slack.circuit.star.imageviewer.FlickToDismissState.FlickGestureState.Idle | ||
import kotlin.math.abs | ||
|
||
@Composable | ||
fun FlickToDismiss( | ||
modifier: Modifier = Modifier, | ||
state: FlickToDismissState = rememberFlickToDismissState(), | ||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, | ||
content: @Composable BoxScope.() -> Unit | ||
) { | ||
val dragStartedOnLeftSide = remember { mutableStateOf(false) } | ||
|
||
@Suppress("MagicNumber") | ||
Box( | ||
modifier = | ||
modifier | ||
.offset { IntOffset(x = 0, y = state.offset.toInt()) } | ||
.graphicsLayer { | ||
rotationZ = state.offsetRatio * if (dragStartedOnLeftSide.value) -20F else 20F | ||
} | ||
.draggable( | ||
enabled = state.enabled, | ||
state = state.draggableState, | ||
orientation = Orientation.Vertical, | ||
interactionSource = interactionSource, | ||
startDragImmediately = state.isResettingOnRelease, | ||
onDragStarted = { startedPosition -> | ||
@Suppress("UnsafeCallOnNullableType") | ||
dragStartedOnLeftSide.value = startedPosition.x < (state.contentSize.value!!.width / 2f) | ||
}, | ||
onDragStopped = { | ||
if (state.willDismissOnRelease) { | ||
state.animateDismissal() | ||
state.gestureState = Dismissed | ||
} else { | ||
state.resetOffset() | ||
} | ||
} | ||
) | ||
.onGloballyPositioned { coordinates -> state.contentSize.value = coordinates.size }, | ||
content = content | ||
) | ||
} | ||
|
||
@Composable | ||
fun rememberFlickToDismissState(): FlickToDismissState { | ||
return remember { FlickToDismissState() } | ||
} | ||
|
||
/** | ||
* @param dismissThresholdRatio Minimum distance the user's finger should move as a ratio to the | ||
* content's dimensions after which it can be dismissed. | ||
*/ | ||
@Stable | ||
data class FlickToDismissState( | ||
val enabled: Boolean = true, | ||
val dismissThresholdRatio: Float = 0.15f, | ||
val rotateOnDrag: Boolean = true, | ||
) { | ||
val offset: Float | ||
get() = offsetState.value | ||
|
||
val offsetState = mutableStateOf(0f) | ||
|
||
/** Distance dragged as a ratio of the content's height. */ | ||
@get:FloatRange(from = -1.0, to = 1.0) | ||
val offsetRatio: Float by derivedStateOf { | ||
val contentHeight = contentSize.value?.height | ||
if (contentHeight == null) { | ||
0f | ||
} else { | ||
offset / contentHeight.toFloat() | ||
} | ||
} | ||
|
||
var isResettingOnRelease: Boolean by mutableStateOf(false) | ||
private set | ||
|
||
var gestureState: FlickGestureState by mutableStateOf(Idle) | ||
internal set | ||
|
||
val willDismissOnRelease: Boolean by derivedStateOf { | ||
when (gestureState) { | ||
is Dismissed -> true | ||
is Dragging, | ||
is Idle -> abs(offsetRatio) > dismissThresholdRatio | ||
} | ||
} | ||
|
||
internal var contentSize = mutableStateOf(null as IntSize?) | ||
|
||
internal val draggableState = DraggableState { dy -> | ||
offsetState.value += dy | ||
|
||
gestureState = | ||
when { | ||
gestureState is Dismissed -> gestureState | ||
offset == 0f -> Idle | ||
else -> Dragging | ||
} | ||
} | ||
|
||
internal suspend fun resetOffset() { | ||
draggableState.drag(MutatePriority.PreventUserInput) { | ||
isResettingOnRelease = true | ||
try { | ||
Animatable(offset).animateTo(targetValue = 0f) { dragBy(value - offset) } | ||
} finally { | ||
isResettingOnRelease = false | ||
} | ||
} | ||
} | ||
|
||
internal suspend fun animateDismissal() { | ||
draggableState.drag(MutatePriority.PreventUserInput) { | ||
@Suppress("UnsafeCallOnNullableType") | ||
Animatable(offset).animateTo( | ||
targetValue = contentSize.value!!.height * if (offset > 0f) 1f else -1f, | ||
animationSpec = tween(AnimationConstants.DefaultDurationMillis) | ||
) { | ||
dragBy(value - offset) | ||
} | ||
} | ||
} | ||
|
||
sealed interface FlickGestureState { | ||
object Idle : FlickGestureState | ||
|
||
object Dragging : FlickGestureState | ||
|
||
object Dismissed : FlickGestureState | ||
} | ||
} |
Oops, something went wrong.