11package com.what3words.components.compose.maps.buttons
22
33import android.graphics.PointF
4+ import androidx.compose.animation.AnimatedVisibility
5+ import androidx.compose.animation.core.tween
6+ import androidx.compose.animation.fadeIn
7+ import androidx.compose.animation.fadeOut
8+ import androidx.compose.animation.slideIn
9+ import androidx.compose.animation.slideOut
410import androidx.compose.foundation.background
511import androidx.compose.foundation.layout.padding
612import androidx.compose.foundation.layout.size
713import androidx.compose.foundation.shape.CircleShape
814import androidx.compose.material3.Icon
915import androidx.compose.material3.IconButton
1016import androidx.compose.runtime.Composable
17+ import androidx.compose.runtime.LaunchedEffect
1118import androidx.compose.runtime.getValue
1219import androidx.compose.runtime.mutableStateOf
1320import androidx.compose.runtime.remember
@@ -18,9 +25,10 @@ import androidx.compose.ui.draw.shadow
1825import androidx.compose.ui.geometry.Offset
1926import androidx.compose.ui.graphics.graphicsLayer
2027import androidx.compose.ui.layout.onGloballyPositioned
21- import androidx.compose.ui.layout.positionInWindow
28+ import androidx.compose.ui.layout.positionInRoot
2229import androidx.compose.ui.res.painterResource
2330import androidx.compose.ui.tooling.preview.Preview
31+ import androidx.compose.ui.unit.IntOffset
2432import androidx.compose.ui.unit.dp
2533import com.what3words.components.compose.maps.buttons.W3WMapButtonsDefault.defaultRecallButtonColor
2634import com.what3words.components.compose.maps.buttons.W3WMapButtonsDefault.defaultRecallButtonLayoutConfig
@@ -36,6 +44,9 @@ import com.what3words.map.components.compose.R
3644 * @param contentDescription The content description for the button.
3745 * @param onRecallClicked The callback when the button is clicked.
3846 * @param onRecallButtonPositionProvided The callback providing the button's position as a PointF.
47+ * @param selectedPosition The position used for calculating the offset for animation, defaults to PointF().
48+ * @param isVisible Determines whether the button should be visible.
49+ * @param isCameraMoving Indicates if the camera is currently moving, affecting button visibility.
3950 */
4051@Composable
4152internal fun RecallButton (
@@ -46,39 +57,87 @@ internal fun RecallButton(
4657 contentDescription : W3WMapButtonsDefault .ContentDescription = W3WMapButtonsDefault .defaultContentDescription(),
4758 onRecallClicked : () -> Unit ,
4859 onRecallButtonPositionProvided : (PointF ) -> Unit ,
60+ selectedPosition : PointF = PointF (),
61+ isVisible : Boolean = false,
62+ isCameraMoving : Boolean = false
4963) {
5064
51- var position: Offset by remember { mutableStateOf(Offset .Zero ) }
65+ // Track previous isVisible state to detect changes
66+ var prevIsVisible by remember { mutableStateOf(isVisible) }
67+ var shouldBeVisible by remember { mutableStateOf(isVisible) }
68+
5269 val positionCallback = rememberUpdatedState(onRecallButtonPositionProvided)
70+ var buttonPosition: Offset by remember { mutableStateOf(Offset .Unspecified ) }
5371
54- IconButton (
55- onClick = { onRecallClicked() },
56- modifier = modifier
57- .onGloballyPositioned { coordinate ->
58- // Only trigger one time when the button is initialized
59- // The coordinate is affected by the rotation
60- if (position == Offset .Zero ) {
61- position = coordinate.positionInWindow()
62- val point = PointF (position.x, position.y)
63- positionCallback.value(point)
64- }
72+ LaunchedEffect (isVisible, isCameraMoving) {
73+ // Show the button when isVisible is true,
74+ // OR prevIsVisible was true AND we're currently moving the camera (isCameraMoving is true)
75+ shouldBeVisible = isVisible || (prevIsVisible && isCameraMoving)
76+ prevIsVisible = isVisible
77+ }
78+
79+ AnimatedVisibility (
80+ visible = shouldBeVisible,
81+ enter = fadeIn(
82+ initialAlpha = 0f ,
83+ animationSpec = tween(400 )
84+ ) + slideIn(
85+ animationSpec = tween(400 ),
86+ initialOffset = { size ->
87+ IntOffset (
88+ x = (selectedPosition.x - buttonPosition.x).toInt(),
89+ y = (selectedPosition.y - buttonPosition.y).toInt()
90+ )
6591 }
66- .padding(layoutConfig.buttonPadding)
67- .graphicsLayer {
68- rotationZ = rotation
92+ ),
93+ exit = fadeOut(
94+ targetAlpha = 0f ,
95+ animationSpec = tween(durationMillis = 400 , delayMillis = 200 )
96+ ) + slideOut(
97+ animationSpec = tween(durationMillis = 400 , delayMillis = 200 ),
98+ targetOffset = { size ->
99+ IntOffset (
100+ x = (selectedPosition.x - buttonPosition.x).toInt(),
101+ y = (selectedPosition.y - buttonPosition.y).toInt()
102+ )
69103 }
70- .shadow(elevation = 3 .dp, shape = CircleShape )
71- .size(layoutConfig.buttonSize)
72- .background(recallButtonColor.recallBackgroundColor)
73- ) {
74- Icon (
75- modifier = Modifier
76- .size(layoutConfig.imageSize)
77- .padding(layoutConfig.imagePadding),
78- painter = painterResource(R .drawable.ic_arrow_back),
79- contentDescription = contentDescription.recallButtonDescription,
80- tint = recallButtonColor.recallArrowColor
81104 )
105+ ) {
106+ IconButton (
107+ onClick = { onRecallClicked() },
108+ modifier = modifier
109+ .onGloballyPositioned { coordinate ->
110+ // Get the center position of the recall button
111+ // Only trigger one time when the button is initialized
112+ // NOTE: onGloballyPositioned is called AFTER a composition,
113+ // so the first appear time the button can't perform the animation fully because lack of position info.
114+ if (! buttonPosition.isValid()) {
115+ val size = coordinate.size
116+ val position = coordinate.positionInRoot()
117+ val centerX = position.x + size.width / 2
118+ val centerY = position.y + size.height / 2
119+ val centerPoint = PointF (centerX, centerY)
120+ buttonPosition = Offset (centerX, centerY)
121+ positionCallback.value(centerPoint)
122+ }
123+ }
124+ .padding(layoutConfig.buttonPadding)
125+ .shadow(elevation = 3 .dp, shape = CircleShape )
126+ .size(layoutConfig.buttonSize)
127+ .background(recallButtonColor.recallBackgroundColor)
128+ ) {
129+ Icon (
130+ modifier = Modifier
131+ .size(layoutConfig.imageSize)
132+ .padding(layoutConfig.imagePadding)
133+ .graphicsLayer {
134+ rotationZ = rotation
135+ },
136+ painter = painterResource(R .drawable.ic_arrow_back),
137+ contentDescription = contentDescription.recallButtonDescription,
138+ tint = recallButtonColor.recallArrowColor
139+ )
140+ }
82141 }
83142}
84143
@@ -88,6 +147,8 @@ private fun A1() {
88147 RecallButton (
89148 modifier = Modifier ,
90149 rotation = 0f ,
150+ isVisible = true ,
151+ isCameraMoving = false ,
91152 onRecallClicked = {},
92153 onRecallButtonPositionProvided = {}
93154 )
@@ -99,6 +160,8 @@ private fun A2() {
99160 RecallButton (
100161 modifier = Modifier ,
101162 rotation = 45f ,
163+ isVisible = true ,
164+ isCameraMoving = false ,
102165 onRecallClicked = {},
103166 onRecallButtonPositionProvided = {}
104167 )
@@ -110,6 +173,8 @@ private fun A3() {
110173 RecallButton (
111174 modifier = Modifier ,
112175 rotation = 90f ,
176+ isVisible = true ,
177+ isCameraMoving = false ,
113178 onRecallClicked = {},
114179 onRecallButtonPositionProvided = {}
115180 )
@@ -121,6 +186,8 @@ private fun A4() {
121186 RecallButton (
122187 modifier = Modifier ,
123188 rotation = 135f ,
189+ isVisible = true ,
190+ isCameraMoving = false ,
124191 onRecallClicked = {},
125192 onRecallButtonPositionProvided = {}
126193 )
@@ -132,6 +199,8 @@ private fun A5() {
132199 RecallButton (
133200 modifier = Modifier ,
134201 rotation = 180f ,
202+ isVisible = true ,
203+ isCameraMoving = false ,
135204 onRecallClicked = {},
136205 onRecallButtonPositionProvided = {}
137206 )
0 commit comments