Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reimplemented Tutorials in Compose #5746

Merged
merged 12 commits into from
Jul 16, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import de.westnordost.streetcomplete.screens.main.bottom_sheet.IsMapOrientationA
import de.westnordost.streetcomplete.screens.main.bottom_sheet.IsMapPositionAware
import de.westnordost.streetcomplete.screens.main.bottom_sheet.MoveNodeFragment
import de.westnordost.streetcomplete.screens.main.bottom_sheet.SplitWayFragment
import de.westnordost.streetcomplete.screens.main.controls.LocationState
import de.westnordost.streetcomplete.screens.main.controls.LocationStateButton
import de.westnordost.streetcomplete.screens.main.controls.MainMenuDialog
import de.westnordost.streetcomplete.screens.main.edithistory.EditHistoryFragment
Expand Down Expand Up @@ -668,7 +669,7 @@ class MainFragment :

@SuppressLint("MissingPermission")
private fun onLocationIsEnabled() {
binding.gpsTrackingButton.state = LocationStateButton.State.SEARCHING
binding.gpsTrackingButton.state = LocationState.SEARCHING
mapFragment!!.startPositionTracking()

setIsFollowingPosition(wasFollowingPosition ?: true)
Expand All @@ -677,8 +678,8 @@ class MainFragment :

private fun onLocationIsDisabled() {
binding.gpsTrackingButton.state = when {
requireContext().hasLocationPermission -> LocationStateButton.State.ALLOWED
else -> LocationStateButton.State.DENIED
requireContext().hasLocationPermission -> LocationState.ALLOWED
else -> LocationState.DENIED
}
binding.gpsTrackingButton.isNavigation = false
binding.locationPointerPin.visibility = View.GONE
Expand All @@ -688,7 +689,7 @@ class MainFragment :

private fun onLocationChanged(location: Location) {
viewLifecycleScope.launch {
binding.gpsTrackingButton.state = LocationStateButton.State.UPDATING
binding.gpsTrackingButton.state = LocationState.UPDATING
updateLocationPointerPin()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package de.westnordost.streetcomplete.screens.main.controls

/** State of location updates */
enum class LocationState {
/** user declined to give this app access to location */
DENIED,
/** user allowed this app to access location (but location disabled) */
ALLOWED,
/** location service is turned on (but no location request active) */
ENABLED,
/** requested location updates and waiting for first fix */
SEARCHING,
/** receiving location updates */
UPDATING;

val isEnabled: Boolean get() = ordinal >= ENABLED.ordinal
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ class LocationStateButton @JvmOverloads constructor(
defStyle: Int = 0
) : AppCompatImageButton(context, attrs, defStyle) {

var state: State
get() = _state ?: State.DENIED
var state: LocationState
get() = _state ?: LocationState.DENIED
set(value) { _state = value }

// this is necessary because state is accessed before it is initialized (in constructor of super)
private var _state: State? = null
private var _state: LocationState? = null
set(value) {
if (field != value) {
field = value
Expand All @@ -55,12 +55,12 @@ class LocationStateButton @JvmOverloads constructor(
a.recycle()
}

private fun determineStateFrom(a: TypedArray): State = when {
a.getBoolean(R.styleable.LocationStateButton_state_updating, false) -> State.UPDATING
a.getBoolean(R.styleable.LocationStateButton_state_searching, false) -> State.SEARCHING
a.getBoolean(R.styleable.LocationStateButton_state_enabled, false) -> State.ENABLED
a.getBoolean(R.styleable.LocationStateButton_state_allowed, false) -> State.ALLOWED
else -> State.DENIED
private fun determineStateFrom(a: TypedArray): LocationState = when {
a.getBoolean(R.styleable.LocationStateButton_state_updating, false) -> LocationState.UPDATING
a.getBoolean(R.styleable.LocationStateButton_state_searching, false) -> LocationState.SEARCHING
a.getBoolean(R.styleable.LocationStateButton_state_enabled, false) -> LocationState.ENABLED
a.getBoolean(R.styleable.LocationStateButton_state_allowed, false) -> LocationState.ALLOWED
else -> LocationState.DENIED
}

override fun drawableStateChanged() {
Expand Down Expand Up @@ -103,18 +103,7 @@ class LocationStateButton @JvmOverloads constructor(
}
}

enum class State {
DENIED, // user declined to give this app access to location
ALLOWED, // user allowed this app to access location (but location disabled)
ENABLED, // location service is turned on (but no location request active)
SEARCHING, // requested location updates and waiting for first fix
UPDATING;

// receiving location updates
val isEnabled: Boolean get() = ordinal >= ENABLED.ordinal
}

private val State.styleableAttributes: List<Int> get() =
private val LocationState.styleableAttributes: List<Int> get() =
listOf(
R.attr.state_allowed,
R.attr.state_enabled,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package de.westnordost.streetcomplete.screens.main.controls

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
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.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import de.westnordost.streetcomplete.R
import kotlinx.coroutines.delay

@Composable
fun LocationStateButton(
onClick: () -> Unit,
state: LocationState,
modifier: Modifier = Modifier,
isNavigationMode: Boolean = false,
isFollowing: Boolean = false,
enabled: Boolean = true
) {
var iconResource by remember(state) { mutableStateOf(getIcon(state, isNavigationMode)) }

MapButton(
onClick = onClick,
modifier = modifier,
enabled = enabled
) {
LaunchedEffect(state) {
if (state == LocationState.SEARCHING) {
while (true) {
delay(750)
iconResource = getIcon(LocationState.UPDATING, isNavigationMode)
delay(750)
iconResource = getIcon(LocationState.ENABLED, isNavigationMode)
}
}
}
Icon(
painter = painterResource(iconResource),
contentDescription = stringResource(R.string.map_btn_gps_tracking),
tint = if (isFollowing) MaterialTheme.colors.secondary else Color.Black
)
}
}

private fun getIcon(state: LocationState, isNavigationMode: Boolean) = when (state) {
LocationState.DENIED,
LocationState.ALLOWED ->
R.drawable.ic_location_disabled_24dp
LocationState.ENABLED,
LocationState.SEARCHING ->
if (isNavigationMode) R.drawable.ic_location_navigation_no_location_24dp
else R.drawable.ic_location_no_location_24dp
LocationState.UPDATING ->
if (isNavigationMode) R.drawable.ic_location_navigation_24dp
else R.drawable.ic_location_24dp
}

@Preview
@Composable
private fun PreviewLocationButton() {
Column {
for (state in LocationState.entries) {
Row {
LocationStateButton(onClick = {}, state = state)
LocationStateButton(onClick = {}, state = state, isNavigationMode = true)
LocationStateButton(onClick = {}, state = state, isFollowing = true)
LocationStateButton(onClick = {}, state = state, isNavigationMode = true, isFollowing = true)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package de.westnordost.streetcomplete.screens.main.controls

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import de.westnordost.streetcomplete.R

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MapButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
content: @Composable() (BoxScope.() -> Unit)
) {
Surface(
onClick = onClick,
modifier = modifier,
enabled = enabled,
shape = CircleShape,
color = Color.White,
elevation = 4.dp
) {
Box(Modifier.padding(16.dp), content = content)
}
}

@Preview
@Composable
private fun PreviewMapButton() {
MapButton(onClick = {}) {
Icon(painterResource(R.drawable.ic_location_24dp), null)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package de.westnordost.streetcomplete.screens.tutorial

import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.Path
import androidx.compose.ui.graphics.vector.VectorPainter
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.dp
import de.westnordost.streetcomplete.ui.theme.LeafGreen
import de.westnordost.streetcomplete.ui.util.svgPath

@Composable
fun checkmarkCirclePainter(progress: Float): VectorPainter = rememberVectorPainter(
defaultWidth = 128.dp,
defaultHeight = 128.dp,
viewportWidth = 128f,
viewportHeight = 128f,
autoMirror = false
) { _, _ ->
Path(
pathData = circlePath,
strokeLineWidth = 12f,
stroke = SolidColor(LeafGreen),
trimPathEnd = (progress * 3f/2).coerceIn(0f, 1f)
)
Path(
pathData = checkmarkPath,
strokeLineWidth = 12f,
stroke = SolidColor(LeafGreen),
trimPathEnd = ((progress - 2f/3) * 3f).coerceIn(0f, 1f)
)
}

private val circlePath = svgPath("m122,64a58,58 0,0 1,-58 58,58 58,0 0,1 -58,-58 58,58 0,0 1,58 -58,58 58,0 0,1 58,58z")
private val checkmarkPath = svgPath("m28.459,67.862c7.344,4.501 19.241,13.97 27.571,23.732 11.064,-20.587 27.756,-39.206 44.333,-55.458")
Loading