Skip to content

Instantly share code, notes, and snippets.

@tonyawino
Created February 26, 2023 11:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tonyawino/2fd8ca0bcbf65177dc098d26e3476803 to your computer and use it in GitHub Desktop.
Save tonyawino/2fd8ca0bcbf65177dc098d26e3476803 to your computer and use it in GitHub Desktop.
Popup with Custom Animation in Jetpack Compose
package com.example.popupsample
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
import androidx.compose.ui.window.PopupProperties
import com.example.popupsample.ui.theme.PopupSampleTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PopupSampleTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween
) {
val bottomStartState = remember { PopupState(false) }
val bottomCenterState = remember { PopupState(false) }
val bottomEndState = remember { PopupState(false) }
val topStartState = remember { PopupState(false) }
val topCenterState = remember { PopupState(false) }
val topEndState = remember { PopupState(false) }
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
IconWithCustomPopup(
bottomStartState,
"Bottom Start"
)
IconWithCustomPopup(
bottomCenterState,
"Bottom Center is long to make it center"
)
IconWithCustomPopup(
bottomEndState,
"Bottom End"
)
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
IconWithCustomPopup(
topStartState,
"Top Start"
)
IconWithCustomPopup(
topCenterState,
"Top Center is long to make it center"
)
IconWithCustomPopup(
topEndState,
"Top End"
)
}
}
}
}
}
}
@Composable
private fun IconWithCustomPopup(
popupState: PopupState = remember {
PopupState(false)
},
text: String
) {
Box {
CustomPopup(
popupState = popupState,
onDismissRequest = {
popupState.isVisible = false
}
) {
Text(
text = text,
modifier = Modifier.background(Color.Yellow)
)
}
Icon(
imageVector = Icons.Default.Info,
contentDescription = "Info Icon",
modifier = Modifier.clickable {
popupState.isVisible = !popupState.isVisible
}
)
}
}
}
/**
* Class that defines the state of a popup.
*
* @property isVisible Boolean that defines whether the popup is currently visible or not.
*/
@Stable
class PopupState(
isVisible: Boolean = false
) {
/**
* Horizontal alignment from which the popup will expand from and shrink to.
*/
var horizontalAlignment: Alignment.Horizontal by mutableStateOf(Alignment.CenterHorizontally)
/**
* Boolean that defines whether the popup is displayed above or below the anchor.
*/
var isTop: Boolean by mutableStateOf(false)
/**
* Boolean that defines whether the popup is currently visible or not.
*/
var isVisible: Boolean by mutableStateOf(isVisible)
}
/**
* Composable for displaying a custom popup.
*
* @param popupState the [PopupState] used to track whether the popup is visible
* @param onDismissRequest callback to be invoked when the popup is dismissed
* @param modifier [Modifier] used to modify the composable's layout and behavior
* @param offset the [DpOffset] to apply to the popup's position
* @param properties the [PopupProperties] used to configure the popup
* @param content the [ColumnScope] composable that defines the popup's content
*/
@Composable
fun CustomPopup(
popupState: PopupState,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(0.dp, 0.dp),
properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit
) {
// Create a transition state to track whether the popup is expanded.
val expandedStates = remember { MutableTransitionState(false) }
expandedStates.targetState = popupState.isVisible
// Only show the popup if it's visible.
if (expandedStates.currentState || expandedStates.targetState) {
val density = LocalDensity.current
// Instantiate a CustomPopupPositionProvider with the given offset.
val popupPositionProvider = CustomPopupPositionProvider(
contentOffset = offset,
density = density
) { alignment, isTop ->
// Update the PopupState's alignment and direction.
popupState.horizontalAlignment = alignment
popupState.isTop = !isTop
}
// Display the popup using the Popup composable.
Popup(
onDismissRequest = onDismissRequest,
popupPositionProvider = popupPositionProvider,
properties = properties
) {
// Display the popup's content using the CustomPopupContent composable.
CustomPopupContent(
expandedStates = expandedStates,
transformOrigin = TransformOrigin(
pivotFractionX = when (popupState.horizontalAlignment) {
Alignment.Start -> 0f
Alignment.CenterHorizontally -> 0.5f
else -> 1f
},
pivotFractionY = if (popupState.isTop) 1f else 0f
),
modifier = modifier,
content = content
)
}
}
}
/**
* A custom implementation of PopupPositionProvider.
* This calculates the position of the popup relative to the anchor
*
* @property contentOffset The offset of the popup content from the anchor position.
* @property density The density of the display.
* @property onPopupPositionFound A callback that is called once the popup position is found.
*/
@Immutable
private data class CustomPopupPositionProvider(
val contentOffset: DpOffset,
val density: Density,
val onPopupPositionFound: (Alignment.Horizontal, Boolean) -> Unit
) : PopupPositionProvider {
override fun calculatePosition(
anchorBounds: IntRect,
windowSize: IntSize,
layoutDirection: LayoutDirection,
popupContentSize: IntSize
): IntOffset {
// The content offset specified using the dropdown offset parameter.
val contentOffsetX = with(density) { contentOffset.x.roundToPx() }
// The content offset specified using the dropdown offset parameter.
val contentOffsetY = with(density) { contentOffset.y.roundToPx() }
val isFitEnd = (anchorBounds.left + contentOffsetX + popupContentSize.width) < windowSize.width
val isFitStart = (anchorBounds.left - contentOffsetX - popupContentSize.width) > 0
val popupHalfWidth = popupContentSize.width / 2
val halfAnchor = (anchorBounds.right - anchorBounds.left) / 2
val isFitCenter =
((anchorBounds.left + halfAnchor + popupHalfWidth) < windowSize.width) &&
((anchorBounds.left + halfAnchor - popupHalfWidth) > 0)
val endPlacementOffset = anchorBounds.left - contentOffsetX
val centerPlacementOffset = anchorBounds.left - popupHalfWidth + contentOffsetX
val startPlacementOffset = anchorBounds.right + contentOffsetX - popupContentSize.width
val bottomCoordinatesY = anchorBounds.bottom + popupContentSize.height
val isFitBottom = bottomCoordinatesY <= windowSize.height
val topCoordinatesY = anchorBounds.top - popupContentSize.height
val isFitTop = topCoordinatesY > 0 || anchorBounds.top > windowSize.height
// Compute vertical position.
val toBottom = anchorBounds.bottom + contentOffsetY
val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height
val toCenter = anchorBounds.top - popupContentSize.height / 2
val toDisplayBottom = windowSize.height - popupContentSize.height
val yOffset = sequenceOf(
if (isFitTop) toBottom else toTop,
toCenter,
toDisplayBottom
).firstOrNull {
it + popupContentSize.height <= windowSize.height
} ?: toTop
val horizontalAndOffset = getHorizontalOffset(
isFitsStart = isFitStart,
isFitsEnd = isFitEnd,
isFitsCenter = isFitCenter,
endPlacementOffset = endPlacementOffset,
startPlacementOffset = startPlacementOffset,
centerPlacementOffset = centerPlacementOffset
)
onPopupPositionFound(horizontalAndOffset.first, isFitTop)
return IntOffset(horizontalAndOffset.second, yOffset)
}
}
@Suppress("ModifierParameter")
/**
* Composable that defines custom animations for a popup and applies them to a [Surface].
*
* @param expandedStates [MutableTransitionState] that determines whether the popup is expanding or shrinking.
* @param transformOrigin [TransformOrigin] determining from which position the popup (dis)appears from.
* @param modifier [Modifier] for this composable.
* @param content content that will be displayed within the popup.
*/
@Composable
private fun CustomPopupContent(
expandedStates: MutableTransitionState<Boolean>,
transformOrigin: TransformOrigin,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) {
// Menu open/close animation.
val transition = updateTransition(expandedStates, "Popup")
// Scale animation.
val scale by transition.animateFloat(
transitionSpec = {
if (false isTransitioningTo true) {
// Dismissed to expanded.
tween(durationMillis = 200)
} else {
// Expanded to dismissed.
tween(durationMillis = 200)
}
},
label = "Popup Scale"
) {
if (it) {
// Popup is expanded.
1f
} else {
// Popup is dismissed.
0f
}
}
// Alpha animation.
val alpha by transition.animateFloat(
transitionSpec = {
if (false isTransitioningTo true) {
// Dismissed to expanded.
tween(durationMillis = 200)
} else {
// Expanded to dismissed.
tween(durationMillis = 200)
}
},
label = "Popup Alpha"
) {
if (it) {
// Popup is expanded.
1f
} else {
// Popup is dismissed.
0f
}
}
// Helper function for applying animations to graphics layer.
fun GraphicsLayerScope.graphicsLayerAnim() {
scaleX = scale
scaleY = scale
this.alpha = alpha
this.transformOrigin = transformOrigin
}
Surface(
modifier = Modifier
.graphicsLayer {
graphicsLayerAnim()
}
) {
Column(
modifier = modifier
.width(IntrinsicSize.Max)
.verticalScroll(rememberScrollState()),
content = content
)
}
}
/**
* Get the horizontal offset of the popup based on fitting and placement.
*
* @param isFitsStart Whether the popup fits when placed at the start.
* @param isFitsEnd Whether the popup fits when placed at the end.
* @param isFitsCenter Whether the popup fits when placed at the center.
* @param endPlacementOffset The offset when the popup is placed at the end.
* @param startPlacementOffset The offset when the popup is placed at the start.
* @param centerPlacementOffset The offset when the popup is placed at the center.
* @return A pair consisting of the horizontal alignment and the horizontal offset.
*/
private fun getHorizontalOffset(
isFitsStart: Boolean,
isFitsEnd: Boolean,
isFitsCenter: Boolean,
endPlacementOffset: Int,
startPlacementOffset: Int,
centerPlacementOffset: Int
): Pair<Alignment.Horizontal, Int> {
// Check which alignment fits the best.
val alignments = listOf(
Alignment.Start,
Alignment.CenterHorizontally,
Alignment.End
)
// Check the corresponding offsets.
val offsets = listOf(
endPlacementOffset,
centerPlacementOffset,
startPlacementOffset
)
// Check which alignment and offset fits the best.
val fallbacks = mutableListOf<Pair<Alignment.Horizontal, Int>>()
for (index in 0..2) {
if (listOf(isFitsEnd, isFitsCenter, isFitsStart)[index]) {
fallbacks.add(Pair(alignments[index], offsets[index]))
}
}
// If there is a fallback, choose it as the alignment and offset.
val fallback = fallbacks.firstOrNull()
if (fallback != null) {
return fallback
}
// If there is no fallback, calculate the horizontal offset.
val finalHorizontalFallback = if (isFitsStart) {
0
} else if (isFitsEnd) {
2
} else {
1
}
val fallbackHorizontalOffset = offsets[finalHorizontalFallback]
return Pair(alignments[finalHorizontalFallback], fallbackHorizontalOffset)
}
package com.example.popupsample
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
import androidx.compose.ui.window.PopupProperties
import com.example.popupsample.ui.theme.PopupSampleTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PopupSampleTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween
) {
val bottomStartState = remember { PopupState(false) }
val bottomCenterState = remember { PopupState(false) }
val bottomEndState = remember { PopupState(false) }
val topStartState = remember { PopupState(false) }
val topCenterState = remember { PopupState(false) }
val topEndState = remember { PopupState(false) }
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
IconWithCustomPopup(
bottomStartState,
"Bottom Start"
)
IconWithCustomPopup(
bottomCenterState,
"Bottom Center is long to make it center"
)
IconWithCustomPopup(
bottomEndState,
"Bottom End"
)
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
IconWithCustomPopup(
topStartState,
"Top Start"
)
IconWithCustomPopup(
topCenterState,
"Top Center is long to make it center"
)
IconWithCustomPopup(
topEndState,
"Top End"
)
}
}
}
}
}
}
@Composable
private fun IconWithCustomPopup(
popupState: PopupState = remember {
PopupState(false)
},
text: String
) {
Box {
CustomPopup(
popupState = popupState,
onDismissRequest = {
popupState.isVisible = false
}
) {
Text(
text = text,
modifier = Modifier.background(Color.Yellow)
)
}
Icon(
imageVector = Icons.Default.Info,
contentDescription = "Info Icon",
modifier = Modifier.clickable {
popupState.isVisible = !popupState.isVisible
}
)
}
}
}
/**
* Class that defines the state of a popup.
*
* @property isVisible Boolean that defines whether the popup is currently visible or not.
*/
@Stable
class PopupState(
isVisible: Boolean = false
) {
/**
* Horizontal alignment from which the popup will expand from and shrink to.
*/
var horizontalAlignment: Alignment.Horizontal by mutableStateOf(Alignment.CenterHorizontally)
/**
* Boolean that defines whether the popup is displayed above or below the anchor.
*/
var isTop: Boolean by mutableStateOf(false)
/**
* Boolean that defines whether the popup is currently visible or not.
*/
var isVisible: Boolean by mutableStateOf(isVisible)
}
/**
* Composable for displaying a custom popup.
*
* @param popupState the [PopupState] used to track whether the popup is visible
* @param onDismissRequest callback to be invoked when the popup is dismissed
* @param modifier [Modifier] used to modify the composable's layout and behavior
* @param offset the [DpOffset] to apply to the popup's position
* @param properties the [PopupProperties] used to configure the popup
* @param content the [ColumnScope] composable that defines the popup's content
*/
@Composable
fun CustomPopup(
popupState: PopupState,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(0.dp, 0.dp),
properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit
) {
// Create a transition state to track whether the popup is expanded.
val expandedStates = remember { MutableTransitionState(false) }
expandedStates.targetState = popupState.isVisible
// Only show the popup if it's visible.
if (expandedStates.currentState || expandedStates.targetState) {
val density = LocalDensity.current
// Instantiate a CustomPopupPositionProvider with the given offset.
val popupPositionProvider = CustomPopupPositionProvider(
contentOffset = offset,
density = density
) { alignment, isTop ->
// Update the PopupState's alignment and direction.
popupState.horizontalAlignment = alignment
popupState.isTop = !isTop
}
// Display the popup using the Popup composable.
Popup(
onDismissRequest = onDismissRequest,
popupPositionProvider = popupPositionProvider,
properties = properties
) {
// Display the popup's content using the CustomPopupContent composable.
CustomPopupContent(
expandedStates = expandedStates,
transformOrigin = TransformOrigin(
pivotFractionX = when (popupState.horizontalAlignment) {
Alignment.Start -> 0f
Alignment.CenterHorizontally -> 0.5f
else -> 1f
},
pivotFractionY = if (popupState.isTop) 1f else 0f
),
modifier = modifier,
content = content
)
}
}
}
/**
* A custom implementation of PopupPositionProvider.
* This calculates the position of the popup relative to the anchor
*
* @property contentOffset The offset of the popup content from the anchor position.
* @property density The density of the display.
* @property onPopupPositionFound A callback that is called once the popup position is found.
*/
@Immutable
private data class CustomPopupPositionProvider(
val contentOffset: DpOffset,
val density: Density,
val onPopupPositionFound: (Alignment.Horizontal, Boolean) -> Unit
) : PopupPositionProvider {
override fun calculatePosition(
anchorBounds: IntRect,
windowSize: IntSize,
layoutDirection: LayoutDirection,
popupContentSize: IntSize
): IntOffset {
// The content offset specified using the dropdown offset parameter.
val contentOffsetX = with(density) { contentOffset.x.roundToPx() }
// The content offset specified using the dropdown offset parameter.
val contentOffsetY = with(density) { contentOffset.y.roundToPx() }
val isFitEnd = (anchorBounds.left + contentOffsetX + popupContentSize.width) < windowSize.width
val isFitStart = (anchorBounds.left - contentOffsetX - popupContentSize.width) > 0
val popupHalfWidth = popupContentSize.width / 2
val halfAnchor = (anchorBounds.right - anchorBounds.left) / 2
val isFitCenter =
((anchorBounds.left + halfAnchor + popupHalfWidth) < windowSize.width) &&
((anchorBounds.left + halfAnchor - popupHalfWidth) > 0)
val endPlacementOffset = anchorBounds.left - contentOffsetX
val centerPlacementOffset = anchorBounds.left - popupHalfWidth + contentOffsetX
val startPlacementOffset = anchorBounds.right + contentOffsetX - popupContentSize.width
val bottomCoordinatesY = anchorBounds.bottom + popupContentSize.height
val isFitBottom = bottomCoordinatesY <= windowSize.height
val topCoordinatesY = anchorBounds.top - popupContentSize.height
val isFitTop = topCoordinatesY > 0 || anchorBounds.top > windowSize.height
// Compute vertical position.
val toBottom = anchorBounds.bottom + contentOffsetY
val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height
val toCenter = anchorBounds.top - popupContentSize.height / 2
val toDisplayBottom = windowSize.height - popupContentSize.height
val yOffset = sequenceOf(
if (isFitTop) toBottom else toTop,
toCenter,
toDisplayBottom
).firstOrNull {
it + popupContentSize.height <= windowSize.height
} ?: toTop
val horizontalAndOffset = getHorizontalOffset(
isFitsStart = isFitStart,
isFitsEnd = isFitEnd,
isFitsCenter = isFitCenter,
endPlacementOffset = endPlacementOffset,
startPlacementOffset = startPlacementOffset,
centerPlacementOffset = centerPlacementOffset
)
onPopupPositionFound(horizontalAndOffset.first, isFitTop)
return IntOffset(horizontalAndOffset.second, yOffset)
}
}
@Suppress("ModifierParameter")
/**
* Composable that defines custom animations for a popup and applies them to a [Surface].
*
* @param expandedStates [MutableTransitionState] that determines whether the popup is expanding or shrinking.
* @param transformOrigin [TransformOrigin] determining from which position the popup (dis)appears from.
* @param modifier [Modifier] for this composable.
* @param content content that will be displayed within the popup.
*/
@Composable
private fun CustomPopupContent(
expandedStates: MutableTransitionState<Boolean>,
transformOrigin: TransformOrigin,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) {
// Menu open/close animation.
val transition = updateTransition(expandedStates, "Popup")
// Scale animation.
val scale by transition.animateFloat(
transitionSpec = {
if (false isTransitioningTo true) {
// Dismissed to expanded.
tween(durationMillis = 200)
} else {
// Expanded to dismissed.
tween(durationMillis = 200)
}
},
label = "Popup Scale"
) {
if (it) {
// Popup is expanded.
1f
} else {
// Popup is dismissed.
0f
}
}
// Alpha animation.
val alpha by transition.animateFloat(
transitionSpec = {
if (false isTransitioningTo true) {
// Dismissed to expanded.
tween(durationMillis = 200)
} else {
// Expanded to dismissed.
tween(durationMillis = 200)
}
},
label = "Popup Alpha"
) {
if (it) {
// Popup is expanded.
1f
} else {
// Popup is dismissed.
0f
}
}
// Helper function for applying animations to graphics layer.
fun GraphicsLayerScope.graphicsLayerAnim() {
scaleX = scale
scaleY = scale
this.alpha = alpha
this.transformOrigin = transformOrigin
}
Surface(
modifier = Modifier
.graphicsLayer {
graphicsLayerAnim()
}
) {
Column(
modifier = modifier
.width(IntrinsicSize.Max)
.verticalScroll(rememberScrollState()),
content = content
)
}
}
/**
* Get the horizontal offset of the popup based on fitting and placement.
*
* @param isFitsStart Whether the popup fits when placed at the start.
* @param isFitsEnd Whether the popup fits when placed at the end.
* @param isFitsCenter Whether the popup fits when placed at the center.
* @param endPlacementOffset The offset when the popup is placed at the end.
* @param startPlacementOffset The offset when the popup is placed at the start.
* @param centerPlacementOffset The offset when the popup is placed at the center.
* @return A pair consisting of the horizontal alignment and the horizontal offset.
*/
private fun getHorizontalOffset(
isFitsStart: Boolean,
isFitsEnd: Boolean,
isFitsCenter: Boolean,
endPlacementOffset: Int,
startPlacementOffset: Int,
centerPlacementOffset: Int
): Pair<Alignment.Horizontal, Int> {
// Check which alignment fits the best.
val alignments = listOf(
Alignment.Start,
Alignment.CenterHorizontally,
Alignment.End
)
// Check the corresponding offsets.
val offsets = listOf(
endPlacementOffset,
centerPlacementOffset,
startPlacementOffset
)
// Check which alignment and offset fits the best.
val fallbacks = mutableListOf<Pair<Alignment.Horizontal, Int>>()
for (index in 0..2) {
if (listOf(isFitsEnd, isFitsCenter, isFitsStart)[index]) {
fallbacks.add(Pair(alignments[index], offsets[index]))
}
}
// If there is a fallback, choose it as the alignment and offset.
val fallback = fallbacks.firstOrNull()
if (fallback != null) {
return fallback
}
// If there is no fallback, calculate the horizontal offset.
val finalHorizontalFallback = if (isFitsStart) {
0
} else if (isFitsEnd) {
2
} else {
1
}
val fallbackHorizontalOffset = offsets[finalHorizontalFallback]
return Pair(alignments[finalHorizontalFallback], fallbackHorizontalOffset)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment