Skip to content

Commit

Permalink
Merge pull request #87 from TimPushkin/pin-pointer
Browse files Browse the repository at this point in the history
Pin pointer
  • Loading branch information
TimPushkin authored Jun 8, 2024
2 parents df41b45 + c761b2e commit 6686c48
Show file tree
Hide file tree
Showing 9 changed files with 416 additions and 8 deletions.
5 changes: 4 additions & 1 deletion app/src/main/java/ru/spbu/depnav/ui/component/FloorSwitch.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
Expand All @@ -41,6 +42,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import ru.spbu.depnav.R
import ru.spbu.depnav.ui.theme.DepNavTheme
import ru.spbu.depnav.ui.theme.ON_MAP_SURFACE_ALPHA

private const val MIN_FLOOR = 1

Expand All @@ -54,7 +56,8 @@ fun FloorSwitch(
) {
Surface(
modifier = modifier,
shape = CircleShape
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = ON_MAP_SURFACE_ALPHA)
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
IconButton(
Expand Down
13 changes: 12 additions & 1 deletion app/src/main/java/ru/spbu/depnav/ui/component/MapSearchBar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ import androidx.compose.material.icons.rounded.Menu
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
Expand All @@ -55,6 +57,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import ru.spbu.depnav.R
import ru.spbu.depnav.ui.theme.DEFAULT_PADDING
import ru.spbu.depnav.ui.theme.ON_MAP_SURFACE_ALPHA
import ru.spbu.depnav.ui.viewmodel.SearchResults

// These are basically copied from SearchBar implementation
Expand Down Expand Up @@ -107,6 +110,9 @@ fun MapSearchBar(

val focusManager = LocalFocusManager.current

val containerColorAlpha =
ON_MAP_SURFACE_ALPHA + (1 - ON_MAP_SURFACE_ALPHA) * activationAnimationProgress

SearchBar(
query = query,
onQueryChange = onQueryChange,
Expand Down Expand Up @@ -141,7 +147,12 @@ fun MapSearchBar(
onClearClick = { onQueryChange("") },
modifier = Modifier.padding(end = innerEndPadding)
)
}
},
colors = SearchBarDefaults.colors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(
alpha = containerColorAlpha
)
)
) {
val keyboard = LocalSoftwareKeyboardController.current

Expand Down
6 changes: 2 additions & 4 deletions app/src/main/java/ru/spbu/depnav/ui/component/Pin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

package ru.spbu.depnav.ui.component

import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
Expand All @@ -29,7 +28,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import ru.spbu.depnav.R

private val SIZE = 30.dp
val PIN_SIZE = 30.dp

/** Pin for highlighting map markers. */
@Composable
Expand All @@ -38,8 +37,7 @@ fun Pin(modifier: Modifier = Modifier) {
painter = painterResource(R.drawable.pin),
contentDescription = stringResource(R.string.label_selected_place),
modifier = Modifier
.size(SIZE)
.offset(y = -SIZE / 2)
.size(PIN_SIZE)
.then(modifier),
tint = MaterialTheme.colorScheme.primary
)
Expand Down
177 changes: 177 additions & 0 deletions app/src/main/java/ru/spbu/depnav/ui/component/PinPointer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/**
* DepNav -- department navigator.
* Copyright (C) 2024 Timofei Pushkin
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package ru.spbu.depnav.ui.component

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.draw.rotate
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.flow.Flow
import ovh.plrapps.mapcompose.api.VisibleArea
import ovh.plrapps.mapcompose.api.fullSize
import ovh.plrapps.mapcompose.api.getLayoutSizeFlow
import ovh.plrapps.mapcompose.ui.state.MapState
import ovh.plrapps.mapcompose.utils.AngleDegree
import ovh.plrapps.mapcompose.utils.Point
import ru.spbu.depnav.data.model.Marker
import ru.spbu.depnav.utils.map.LineSegment
import ru.spbu.depnav.utils.map.bottom
import ru.spbu.depnav.utils.map.centroid
import ru.spbu.depnav.utils.map.contains
import ru.spbu.depnav.utils.map.left
import ru.spbu.depnav.utils.map.rectangularVisibleArea
import ru.spbu.depnav.utils.map.right
import ru.spbu.depnav.utils.map.rotation
import ru.spbu.depnav.utils.map.top

/**
* When the pin is outside of the map area visible on the screen, shows a pointer towards the pin.
*
* It is intended to be placed exactly over the map's composable.
*/
@Composable
fun PinPointer(mapState: MapState, pin: Marker?) {
var mapLayoutSizeFlow by remember { mutableStateOf<Flow<IntSize>?>(null) }
LaunchedEffect(mapState) { mapLayoutSizeFlow = mapState.getLayoutSizeFlow() }
val mapLayoutSize by mapLayoutSizeFlow?.collectAsStateWithLifecycle(IntSize.Zero) ?: return
if (mapLayoutSize == IntSize.Zero) {
return
}

val pinSize = with(LocalDensity.current) { PIN_SIZE.roundToPx() }

val pinPoint = pin?.run { Point(x * mapState.fullSize.width, y * mapState.fullSize.height) }
val visibleArea = mapState.rectangularVisibleArea(mapLayoutSize) // Area visible on the screen

val currentPointerPose =
if (
pinPoint != null &&
!mapState.rectangularVisibleArea( // Area on which the pin is visible on the screen
mapLayoutSize,
leftPadding = pinSize / 2,
rightPadding = pinSize / 2,
bottomPadding = pinSize
).contains(pinPoint)
) {
calculatePointerPose(visibleArea, pinPoint)
} else {
null // There is no pin or it is visible on the screen
}

// Have to remember the latest non-null pointer pose to continue showing it while the exit
// animation is still in progress
var lastPointerPose by remember { mutableStateOf(PinPointerPose.Empty) }
if (currentPointerPose != null) {
lastPointerPose = currentPointerPose
}

AnimatedVisibility(
visible = currentPointerPose != null,
modifier = Modifier.absoluteOffset { lastPointerPose.coordinates(mapLayoutSize, pinSize) },
enter = fadeIn() + slideIn { lastPointerPose.slideAnimationOffset(it) } + scaleIn(),
exit = fadeOut() + slideOut { lastPointerPose.slideAnimationOffset(it) } + scaleOut()
) {
// Cannot use mapState.rotation since it has a different pivot
Pin(modifier = Modifier.rotate(lastPointerPose.direction - visibleArea.rotation()))
}
}

private data class PinPointerPose(
val side: Side,
val sideFraction: Float,
val direction: AngleDegree
) {
enum class Side { LEFT, RIGHT, TOP, BOTTOM }

companion object {
val Empty = PinPointerPose(Side.TOP, 0f, 0f)
}

fun coordinates(boxSize: IntSize, pinSize: Int): IntOffset {
return when (side) {
Side.LEFT -> IntOffset(
x = 0,
y = (boxSize.height * sideFraction - pinSize / 2f)
.toInt()
.coerceIn(0, boxSize.height - pinSize)
)
Side.RIGHT -> IntOffset(
x = (boxSize.width - pinSize).coerceAtLeast(0),
y = (boxSize.height * sideFraction - pinSize / 2f)
.toInt()
.coerceIn(0, boxSize.height - pinSize)
)
Side.TOP -> IntOffset(
x = (boxSize.width * sideFraction - pinSize / 2f)
.toInt()
.coerceIn(0, boxSize.width - pinSize),
y = 0
)
Side.BOTTOM -> IntOffset(
x = (boxSize.width * sideFraction - pinSize / 2f)
.toInt()
.coerceIn(0, boxSize.width - pinSize),
y = (boxSize.height - pinSize).coerceAtLeast(0)
)
}
}

fun slideAnimationOffset(pinSize: IntSize) = when (side) {
Side.LEFT -> IntOffset(x = -pinSize.width, y = 0)
Side.RIGHT -> IntOffset(x = pinSize.width, y = 0)
Side.TOP -> IntOffset(x = 0, y = -pinSize.height)
Side.BOTTOM -> IntOffset(x = 0, y = pinSize.height)
}
}

private fun calculatePointerPose(visibleArea: VisibleArea, pin: Point): PinPointerPose {
val centroidPinSegment = LineSegment(visibleArea.centroid(), pin)
val direction = centroidPinSegment.slope() - 90

visibleArea.top().fractionOfIntersectionWith(centroidPinSegment)?.let { fraction ->
return PinPointerPose(PinPointerPose.Side.TOP, fraction, direction)
}
visibleArea.right().fractionOfIntersectionWith(centroidPinSegment)?.let { fraction ->
return PinPointerPose(PinPointerPose.Side.RIGHT, fraction, direction)
}
visibleArea.bottom().fractionOfIntersectionWith(centroidPinSegment)?.let { fraction ->
return PinPointerPose(PinPointerPose.Side.BOTTOM, fraction, direction)
}
visibleArea.left().fractionOfIntersectionWith(centroidPinSegment)?.let { fraction ->
return PinPointerPose(PinPointerPose.Side.LEFT, fraction, direction)
}

throw IllegalArgumentException("Pin lies inside the visible area")
}
7 changes: 6 additions & 1 deletion app/src/main/java/ru/spbu/depnav/ui/screen/MapScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,12 @@ import ru.spbu.depnav.ui.component.MainMenuSheet
import ru.spbu.depnav.ui.component.MapSearchBar
import ru.spbu.depnav.ui.component.MarkerInfoLines
import ru.spbu.depnav.ui.component.MarkerView
import ru.spbu.depnav.ui.component.PinPointer
import ru.spbu.depnav.ui.component.ZoomInHint
import ru.spbu.depnav.ui.dialog.MapLegendDialog
import ru.spbu.depnav.ui.dialog.SettingsDialog
import ru.spbu.depnav.ui.theme.DEFAULT_PADDING
import ru.spbu.depnav.ui.theme.ON_MAP_SURFACE_ALPHA
import ru.spbu.depnav.ui.viewmodel.MapUiState
import ru.spbu.depnav.ui.viewmodel.MapViewModel
import ru.spbu.depnav.ui.viewmodel.SearchResults
Expand Down Expand Up @@ -178,6 +180,8 @@ private fun OnMapUi(
) {
CompositionLocalProvider(LocalAbsoluteTonalElevation provides 4.dp) {
Box(modifier = Modifier.fillMaxSize()) {
PinPointer(mapUiState.mapState, mapUiState.pinnedMarker?.marker)

AnimatedSearchBar(
visible = mapUiState.showOnMapUi,
mapTitle = mapUiState.mapTitle,
Expand Down Expand Up @@ -298,7 +302,8 @@ private fun BoxScope.AnimatedBottom(pinnedMarker: MarkerWithText?, showZoomInHin
shape = MaterialTheme.shapes.large.copy(
bottomStart = CornerSize(0),
bottomEnd = CornerSize(0)
)
),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = ON_MAP_SURFACE_ALPHA)
) {
// Have to remember the latest pinned marker to continue showing it while the exit
// animation is still in progress
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/ru/spbu/depnav/ui/theme/Theme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ val DEFAULT_PADDING = 16.dp
/** Alpha value applied to disabled elements. */
const val DISABLED_ALPHA = 0.38f

/** Alpha value applied to surfaces comprising on-map UI. */
const val ON_MAP_SURFACE_ALPHA = 0.9f

/** Theme of the application. */
@Composable
fun DepNavTheme(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,6 @@ class MapViewModel @Inject constructor(
y = marker.y,
zIndex = 1f,
clickable = false,
relativeOffset = Offset(-0.5f, -0.5f),
clipShape = null
) { Pin() }
}
Expand Down
Loading

0 comments on commit 6686c48

Please sign in to comment.