From a523684ffb5f5643a8933cfe586e3dd1e519de53 Mon Sep 17 00:00:00 2001 From: Arty Bishop Date: Fri, 6 Dec 2024 14:45:56 +0000 Subject: [PATCH] Updated dependencies and gradle plugin, minor fixes --- app/build.gradle.kts | 1 + .../look4sat/presentation/MainScreen.kt | 24 +-- .../presentation/components/Common.kt | 1 - .../components/PullRefreshIndicator.kt | 187 ------------------ .../components/PullRefreshModifier.kt | 128 ------------ .../components/PullRefreshState.kt | 184 ----------------- .../presentation/passes/PassesScreen.kt | 43 ++-- gradle/libs.versions.toml | 17 +- settings.gradle.kts | 6 +- 9 files changed, 54 insertions(+), 537 deletions(-) delete mode 100644 app/src/main/java/com/rtbishop/look4sat/presentation/components/PullRefreshIndicator.kt delete mode 100644 app/src/main/java/com/rtbishop/look4sat/presentation/components/PullRefreshModifier.kt delete mode 100644 app/src/main/java/com/rtbishop/look4sat/presentation/components/PullRefreshState.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9bdee973..ba7daedd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -46,6 +46,7 @@ dependencies { implementation(libs.androidx.room.runtime) ksp(libs.androidx.room.compiler) + implementation(platform(libs.compose.bom)) implementation(libs.bundles.composeAll) implementation(libs.other.okhttp) diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/MainScreen.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/MainScreen.kt index d726c48f..8a850fef 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/MainScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/MainScreen.kt @@ -29,7 +29,7 @@ import com.rtbishop.look4sat.presentation.satellites.SatellitesScreen import com.rtbishop.look4sat.presentation.satellites.SatellitesViewModel import com.rtbishop.look4sat.presentation.settings.SettingsScreen -sealed class Screen(var title: String, var icon: Int, var route: String) { +private sealed class Screen(var title: String, var icon: Int, var route: String) { data object Main : Screen("Main", R.drawable.ic_sputnik, "main") data object Radar : Screen("Radar", R.drawable.ic_sputnik, "radar") data object Satellites : Screen("Satellites", R.drawable.ic_sputnik, "satellites") @@ -61,17 +61,19 @@ private fun NavBarScreen(navToRadar: (Int, Long) -> Unit) { val navToPasses = { innerNavController.navigate(Screen.Passes.route) } Scaffold(bottomBar = { MainNavBar(navController = innerNavController) }) { innerPadding -> Box(modifier = Modifier.padding(innerPadding)) { - NavHost(innerNavController, startDestination = Screen.Passes.route) { + NavHost(navController = innerNavController, startDestination = Screen.Passes.route) { composable(Screen.Satellites.route) { val viewModel = viewModel( - SatellitesViewModel::class.java, factory = SatellitesViewModel.Factory + modelClass = SatellitesViewModel::class.java, + factory = SatellitesViewModel.Factory ) val uiState = viewModel.uiState.collectAsStateWithLifecycle().value SatellitesScreen(uiState, navToPasses) } composable(Screen.Passes.route) { val viewModel = viewModel( - PassesViewModel::class.java, factory = PassesViewModel.Factory + modelClass = PassesViewModel::class.java, + factory = PassesViewModel.Factory ) val uiState = viewModel.uiState.value PassesScreen(uiState, navToRadar) @@ -91,18 +93,18 @@ private fun MainNavBar(navController: NavController) { val currentRoute = currentBackStackEntry.value?.destination?.route NavigationBar { items.forEach { item -> - NavigationBarItem(selected = currentRoute?.contains(item.route) ?: false, + NavigationBarItem( + icon = { Icon(painterResource(item.icon), item.title) }, + label = { Text(item.title) }, + selected = currentRoute?.contains(item.route) ?: false, onClick = { navController.navigate(item.route) { - navController.graph.startDestinationRoute?.let { - popUpTo(it) { saveState = false } - } + popUpTo(navController.graph.startDestinationId) { saveState = false } launchSingleTop = true restoreState = false } - }, - icon = { Icon(painterResource(id = item.icon), contentDescription = item.title) }, - label = { Text(item.title) }) + } + ) } } } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/components/Common.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/components/Common.kt index 53e25a51..760da81a 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/components/Common.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/components/Common.kt @@ -34,7 +34,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/components/PullRefreshIndicator.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/components/PullRefreshIndicator.kt deleted file mode 100644 index caec9a09..00000000 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/components/PullRefreshIndicator.kt +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.rtbishop.look4sat.presentation.components - -import androidx.compose.animation.Crossfade -import androidx.compose.animation.core.tween -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.contentColorFor -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.geometry.center -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.PathFillType -import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.graphics.drawscope.rotate -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.unit.dp -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min -import kotlin.math.pow - -/** - * The default indicator for Compose pull-to-refresh, based on Android's SwipeRefreshLayout. - * @param refreshing A boolean representing whether a refresh is occurring. - * @param state The [PullRefreshState] which controls where and how the indicator will be drawn. - * @param modifier Modifiers for the indicator. - * @param backgroundColor The color of the indicator's background. - * @param contentColor The color of the indicator's arc and arrow. - * @param scale A boolean controlling whether the indicator's size scales with pull progress or not. - */ -@Composable -fun PullRefreshIndicator( - refreshing: Boolean, - state: PullRefreshState, - modifier: Modifier = Modifier, - backgroundColor: Color = MaterialTheme.colorScheme.surface, - contentColor: Color = contentColorFor(backgroundColor), - scale: Boolean = false -) { - val showElevation by remember(refreshing, state) { - derivedStateOf { refreshing || state.position > 0.5f } - } - Surface( - modifier = modifier - .size(indicatorSize) - .pullRefreshIndicatorTransform(state, scale), - shape = spinnerShape, - color = backgroundColor, - shadowElevation = if (showElevation) elevation else 0.dp, - ) { - Crossfade( - targetState = refreshing, - animationSpec = tween(durationMillis = CROSSFADE_DURATION_MS), - label = "pullToRefresh" - ) { refreshing -> - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - val spinnerSize = (arcRadius + strokeWidth).times(2) - if (refreshing) { - CircularProgressIndicator( - color = contentColor, - strokeWidth = strokeWidth, - modifier = Modifier.size(spinnerSize), - ) - } else { - CircularArrowIndicator(state, contentColor, Modifier.size(spinnerSize)) - } - } - } - } -} - -/** - * Modifier.size MUST be specified. - */ -@Composable -private fun CircularArrowIndicator(state: PullRefreshState, color: Color, modifier: Modifier) { - val path = remember { Path().apply { fillType = PathFillType.EvenOdd } } - Canvas(modifier.semantics { contentDescription = "Refreshing" }) { - val values = ArrowValues(state.progress) - rotate(degrees = values.rotation) { - val arcRadius = arcRadius.toPx() + strokeWidth.toPx() / 2f - val arcBounds = Rect( - size.center.x - arcRadius, - size.center.y - arcRadius, - size.center.x + arcRadius, - size.center.y + arcRadius - ) - drawArc( - color = color, - alpha = values.alpha, - startAngle = values.startAngle, - sweepAngle = values.endAngle - values.startAngle, - useCenter = false, - topLeft = arcBounds.topLeft, - size = arcBounds.size, - style = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Square) - ) - drawArrow(path, arcBounds, color, values) - } - } -} - -@Immutable -private class ArrowValues( - val alpha: Float, val rotation: Float, val startAngle: Float, val endAngle: Float, val scale: Float -) - -private fun ArrowValues(progress: Float): ArrowValues { - // Discard first 40% of progress. Scale remaining progress to full range between 0 and 100%. - val adjustedPercent = max(min(1f, progress) - 0.4f, 0f) * 5 / 3 - // How far beyond the threshold pull has gone, as a percentage of the threshold. - val overshootPercent = abs(progress) - 1.0f - // Limit the overshoot to 200%. Linear between 0 and 200. - val linearTension = overshootPercent.coerceIn(0f, 2f) - // Non-linear tension. Increases with linearTension, but at a decreasing rate. - val tensionPercent = linearTension - linearTension.pow(2) / 4 - - // Calculations based on SwipeRefreshLayout specification. - val alpha = progress.coerceIn(0f, 1f) - val endTrim = adjustedPercent * MAX_PROGRESS_ARC - val rotation = (-0.25f + 0.4f * adjustedPercent + tensionPercent) * 0.5f - val startAngle = rotation * 360 - val endAngle = (rotation + endTrim) * 360 - val scale = min(1f, adjustedPercent) - - return ArrowValues(alpha, rotation, startAngle, endAngle, scale) -} - -private fun DrawScope.drawArrow(arrow: Path, bounds: Rect, color: Color, values: ArrowValues) { - arrow.reset() - arrow.moveTo(0f, 0f) // Move to left corner - arrow.lineTo(x = arrowWidth.toPx() * values.scale, y = 0f) // Line to right corner - - // Line to tip of arrow - arrow.lineTo(x = arrowWidth.toPx() * values.scale / 2, y = arrowHeight.toPx() * values.scale) - - val radius = min(bounds.width, bounds.height) / 2f - val inset = arrowWidth.toPx() * values.scale / 2f - arrow.translate(Offset(x = radius + bounds.center.x - inset, y = bounds.center.y + strokeWidth.toPx() / 2f)) - arrow.close() - rotate(degrees = values.endAngle) { - drawPath(path = arrow, color = color, alpha = values.alpha) - } -} - -private const val CROSSFADE_DURATION_MS = 100 -private const val MAX_PROGRESS_ARC = 0.8f -private val indicatorSize = 40.dp -private val spinnerShape = CircleShape -private val arcRadius = 7.5.dp -private val strokeWidth = 2.5.dp -private val arrowWidth = 10.dp -private val arrowHeight = 5.dp -private val elevation = 6.dp diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/components/PullRefreshModifier.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/components/PullRefreshModifier.kt deleted file mode 100644 index 64410a86..00000000 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/components/PullRefreshModifier.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.rtbishop.look4sat.presentation.components - -import androidx.compose.animation.core.LinearOutSlowInEasing -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.Drag -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.debugInspectorInfo -import androidx.compose.ui.platform.inspectable -import androidx.compose.ui.unit.Velocity - -/** - * A modifier for translating the position and scaling the size of a pull-to-refresh indicator - * based on the given [PullRefreshState]. - * @param state The [PullRefreshState] which determines the position of the indicator. - * @param scale A boolean controlling whether the indicator's size scales with pull progress or not. - */ -fun Modifier.pullRefreshIndicatorTransform( - state: PullRefreshState, - scale: Boolean = false, -) = composed(inspectorInfo = debugInspectorInfo { - name = "pullRefreshIndicatorTransform" - properties["state"] = state - properties["scale"] = scale -}) { - var height by remember { mutableIntStateOf(0) } - Modifier - .onSizeChanged { height = it.height } - .graphicsLayer { - translationY = state.position - height - if (scale && !state.refreshing) { - val scaleFraction = LinearOutSlowInEasing - .transform(state.position / state.threshold) - .coerceIn(0f, 1f) - scaleX = scaleFraction - scaleY = scaleFraction - } - } -} - -/** - * PullRefresh modifier to be used in conjunction with [PullRefreshState]. Provides a connection - * to the nested scroll system. Based on Android's SwipeRefreshLayout. - * @param state The [PullRefreshState] associated with this pull-to-refresh component. - * The state will be updated by this modifier. - * @param enabled If not enabled, all scroll delta and fling velocity will be ignored. - */ -fun Modifier.pullRefresh( - state: PullRefreshState, enabled: Boolean = true -) = inspectable(inspectorInfo = debugInspectorInfo { - name = "pullRefresh" - properties["state"] = state - properties["enabled"] = enabled -}) { - Modifier.pullRefresh(state::onPull, { state.onRelease() }, enabled) -} - -/** - * A modifier for building pull-to-refresh components. Provides a connection to the nested scroll - * system. - * @param onPull Callback for dispatching vertical scroll delta, takes float pullDelta as argument. - * Positive delta (pulling down) is dispatched only if the child does not consume it (i.e. pulling - * down despite being at the top of a scrollable component), whereas negative delta (swiping up) is - * dispatched first (in case it is needed to push the indicator back up), and then whatever is not - * consumed is passed on to the child. - * @param onRelease Callback for when drag is released, takes float flingVelocity as argument. - * @param enabled If not enabled, all scroll delta and fling velocity will be ignored and neither - * [onPull] nor [onRelease] will be invoked. - */ -fun Modifier.pullRefresh( - onPull: (pullDelta: Float) -> Float, onRelease: suspend (flingVelocity: Float) -> Unit, enabled: Boolean = true -) = inspectable(inspectorInfo = debugInspectorInfo { - name = "pullRefresh" - properties["onPull"] = onPull - properties["onRelease"] = onRelease - properties["enabled"] = enabled -}) { - Modifier.nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled)) -} - -private class PullRefreshNestedScrollConnection( - private val onPull: (pullDelta: Float) -> Float, - private val onRelease: suspend (flingVelocity: Float) -> Unit, - private val enabled: Boolean -) : NestedScrollConnection { - - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = when { - !enabled -> Offset.Zero - source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up - else -> Offset.Zero - } - - override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset = when { - !enabled -> Offset.Zero - source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down - else -> Offset.Zero - } - - override suspend fun onPreFling(available: Velocity): Velocity { - onRelease(available.y) - return Velocity.Zero - } -} diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/components/PullRefreshState.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/components/PullRefreshState.kt deleted file mode 100644 index 16df54da..00000000 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/components/PullRefreshState.kt +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.rtbishop.look4sat.presentation.components - -import androidx.compose.animation.core.animate -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import kotlin.math.abs -import kotlin.math.pow -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -/** - * Creates a [PullRefreshState] that is remembered across compositions. - * Changes to [refreshing] will result in [PullRefreshState] being updated. - * @param refreshing A boolean representing whether a refresh is currently occurring. - * @param onRefresh The function to be called to trigger a refresh. - * @param refreshThreshold The threshold below which, if a release - * occurs, [onRefresh] will be called. - * @param refreshingOffset The offset at which the indicator will be drawn while refreshing. This - * offset corresponds to the position of the bottom of the indicator. - */ -@Composable -fun rememberPullRefreshState( - refreshing: Boolean, - onRefresh: () -> Unit, - refreshThreshold: Dp = PullRefreshDefaults.RefreshThreshold, - refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset, -): PullRefreshState { - require(refreshThreshold > 0.dp) { "The refresh trigger must be greater than zero!" } - val scope = rememberCoroutineScope() - val onRefreshState = rememberUpdatedState(onRefresh) - val thresholdPx: Float - val refreshingOffsetPx: Float - with(LocalDensity.current) { - thresholdPx = refreshThreshold.toPx() - refreshingOffsetPx = refreshingOffset.toPx() - } - // refreshThreshold and refreshingOffset should not be changed after instantiation, so any - // changes to these values are ignored. - val state = remember(scope) { PullRefreshState(scope, onRefreshState, refreshingOffsetPx, thresholdPx) } - SideEffect { state.setRefreshing(refreshing) } - return state -} - -/** - * A state object that can be used in conjunction with [pullRefresh] to add pull-to-refresh - * behaviour to a scroll component. Based on Android's SwipeRefreshLayout. - * - * Provides [progress], a float representing how far the user has pulled as a percentage of the - * refreshThreshold. Values of one or less indicate that the user has not yet pulled past the - * threshold. Values greater than one indicate how far past the threshold the user has pulled. - * - * Can be used in conjunction with [pullRefreshIndicatorTransform] to implement Android-like - * pull-to-refresh behaviour with a custom indicator. - * - * Should be created using [rememberPullRefreshState]. - */ -class PullRefreshState internal constructor( - private val animationScope: CoroutineScope, - private val onRefreshState: State<() -> Unit>, - private val refreshingOffset: Float, - internal val threshold: Float -) { - /** - * A float representing how far the user has pulled as a percentage of the refreshThreshold. - * - * If the component has not been pulled at all, progress is zero. If the pull has reached - * halfway to the threshold, progress is 0.5f. A value greater than 1 indicates that pull has - * gone beyond the refreshThreshold - e.g. a value of 2f indicates that the user has pulled to - * two times the refreshThreshold. - */ - val progress get() = adjustedDistancePulled / threshold - - internal val refreshing get() = _refreshing - internal val position get() = _position - - private val adjustedDistancePulled by derivedStateOf { distancePulled * DRAG_MULTIPLIER } - - private var _refreshing by mutableStateOf(false) - private var _position by mutableFloatStateOf(0f) - private var distancePulled by mutableFloatStateOf(0f) - - internal fun onPull(pullDelta: Float): Float { - if (this._refreshing) return 0f // Already refreshing, do nothing. - - val newOffset = (distancePulled + pullDelta).coerceAtLeast(0f) - val dragConsumed = newOffset - distancePulled - distancePulled = newOffset - _position = calculateIndicatorPosition() - return dragConsumed - } - - internal fun onRelease() { - if (!this._refreshing) { - if (adjustedDistancePulled > threshold) { - onRefreshState.value() - } else { - animateIndicatorTo(0f) - } - } - distancePulled = 0f - } - - internal fun setRefreshing(refreshing: Boolean) { - if (this._refreshing != refreshing) { - this._refreshing = refreshing - this.distancePulled = 0f - animateIndicatorTo(if (refreshing) refreshingOffset else 0f) - } - } - - private fun animateIndicatorTo(offset: Float) = animationScope.launch { - animate(initialValue = _position, targetValue = offset) { value, _ -> - _position = value - } - } - - private fun calculateIndicatorPosition(): Float = when { - // If drag hasn't gone past the threshold, the position is the adjustedDistancePulled. - adjustedDistancePulled <= threshold -> adjustedDistancePulled - else -> { - // How far beyond the threshold pull has gone, as a percentage of the threshold. - val overshootPercent = abs(progress) - 1.0f - // Limit the overshoot to 200%. Linear between 0 and 200. - val linearTension = overshootPercent.coerceIn(0f, 2f) - // Non-linear tension. Increases with linearTension, but at a decreasing rate. - val tensionPercent = linearTension - linearTension.pow(2) / 4 - // The additional offset beyond the threshold. - val extraOffset = threshold * tensionPercent - threshold + extraOffset - } - } -} - -/** - * Default parameter values for [rememberPullRefreshState]. - */ -object PullRefreshDefaults { - /** - * If the indicator is below this threshold offset when it is released, a refresh - * will be triggered. - */ - val RefreshThreshold = 80.dp - - /** - * The offset at which the indicator should be rendered whilst a refresh is occurring. - */ - val RefreshingOffset = 56.dp -} - -/** - * The distance pulled is multiplied by this value to give us the adjusted distance pulled, which - * is used in calculating the indicator position (when the adjusted distance pulled is less than - * the refresh threshold, it is the indicator position, otherwise the indicator position is - * derived from the progress). - */ -private const val DRAG_MULTIPLIER = 0.5f diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/passes/PassesScreen.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/passes/PassesScreen.kt index 9eb0da64..8c84dbf0 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/passes/PassesScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/passes/PassesScreen.kt @@ -3,7 +3,6 @@ package com.rtbishop.look4sat.presentation.passes 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.Row import androidx.compose.foundation.layout.Spacer @@ -15,11 +14,15 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -38,12 +41,8 @@ import com.rtbishop.look4sat.domain.predict.OrbitalPass import com.rtbishop.look4sat.presentation.MainTheme import com.rtbishop.look4sat.presentation.components.CardIcon import com.rtbishop.look4sat.presentation.components.NextPassRow -import com.rtbishop.look4sat.presentation.components.PullRefreshIndicator -import com.rtbishop.look4sat.presentation.components.PullRefreshState import com.rtbishop.look4sat.presentation.components.TimerBar import com.rtbishop.look4sat.presentation.components.TimerRow -import com.rtbishop.look4sat.presentation.components.pullRefresh -import com.rtbishop.look4sat.presentation.components.rememberPullRefreshState import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -54,7 +53,6 @@ private val sdfTime = SimpleDateFormat("HH:mm:ss", Locale.ENGLISH) @Composable fun PassesScreen(uiState: PassesState, navToRadar: (Int, Long) -> Unit) { val refreshPasses = { uiState.takeAction(PassesAction.RefreshPasses) } - val refreshState = rememberPullRefreshState(refreshing = uiState.isRefreshing, onRefresh = refreshPasses) val showPassesDialog = { uiState.takeAction(PassesAction.TogglePassesDialog) } val showRadiosDialog = { uiState.takeAction(PassesAction.ToggleRadiosDialog) } if (uiState.isPassesDialogShown) { @@ -74,31 +72,47 @@ fun PassesScreen(uiState: PassesState, navToRadar: (Int, Long) -> Unit) { CardIcon(onClick = { showRadiosDialog() }, iconId = R.drawable.ic_satellite) } NextPassRow(pass = uiState.nextPass) - PassesList(refreshState, uiState.isRefreshing, uiState.itemsList, navToRadar) + PassesList(uiState.isRefreshing, uiState.itemsList, navToRadar, refreshPasses) } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun PassesList( - refreshState: PullRefreshState, isRefreshing: Boolean, passes: List, - navToRadar: (Int, Long) -> Unit + navToRadar: (Int, Long) -> Unit, + refreshPasses: () -> Unit ) { - val backgroundColor = MaterialTheme.colorScheme.primary + val refreshState = rememberPullToRefreshState() ElevatedCard(modifier = Modifier.fillMaxSize()) { - Box(modifier = Modifier.pullRefresh(refreshState), contentAlignment = Alignment.TopCenter) { + PullToRefreshBox( + isRefreshing = isRefreshing, + state = refreshState, + onRefresh = refreshPasses, + indicator = { + PullToRefreshDefaults.Indicator( + state = refreshState, + isRefreshing = isRefreshing, + color = MaterialTheme.colorScheme.background, + containerColor = MaterialTheme.colorScheme.primary, + modifier = Modifier.align(Alignment.TopCenter), + ) + } + ) { LazyColumn(modifier = Modifier.fillMaxSize()) { items(items = passes, key = { item -> item.catNum + item.aosTime }) { pass -> - PassItem(pass = pass, navToRadar = navToRadar, modifier = Modifier.animateItem()) + PassItem( + pass = pass, + navToRadar = navToRadar, + modifier = Modifier.animateItem() + ) } } - PullRefreshIndicator(refreshing = isRefreshing, state = refreshState, backgroundColor = backgroundColor) } } } - @Preview(showBackground = true) @Composable private fun DeepSpacePassPreview() { @@ -230,6 +244,7 @@ private fun PassItem(pass: OrbitalPass, navToRadar: (Int, Long) -> Unit, modifie ) LinearProgressIndicator( progress = { if (pass.isDeepSpace) 100f else pass.progress }, + drawStopIndicator = {}, modifier = modifier.fillMaxWidth(0.75f) ) Text( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6ee70ef0..4ece4007 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,16 @@ [versions] -android-gradle-plugin = "8.7.2" -google-ksp = "2.0.20-1.0.25" +android-gradle-plugin = "8.7.3" +google-ksp = "2.0.21-1.0.26" kotlin = "2.0.21" androidx-core-ktx = "1.15.0" androidx-core-splashscreen = "1.0.1" androidx-room = "2.6.1" -composeBom = "2024.10.01" +compose-bom = "2024.11.00" compose-activity = "1.9.3" compose-lifecycle = "2.8.7" -compose-material3 = "1.3.1" -compose-navigation = "2.8.3" +compose-navigation = "2.8.4" other-coroutines = "1.9.0" other-json = "20240303" @@ -31,14 +30,14 @@ androidx-room = { module = "androidx.room:room-ktx", version.ref = "androidx-roo androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidx-room" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" } -compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } compose-animation = { group = "androidx.compose.animation", name = "animation" } +compose-material3 = { group = "androidx.compose.material3", name = "material3" } compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } compose-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } compose-activity = { module = "androidx.activity:activity-compose", version.ref = "compose-activity" } compose-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle" } -compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" } compose-navigation = { module = "androidx.navigation:navigation-compose", version.ref = "compose-navigation" } compose-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "compose-lifecycle" } @@ -67,9 +66,9 @@ kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } [bundles] composeAll = [ - "compose-bom", "compose-animation", "compose-runtime", "compose-tooling", "compose-activity", + "compose-animation", "compose-runtime", "compose-tooling", "compose-activity", "compose-lifecycle", "compose-material3", "compose-navigation", "compose-viewmodel" ] -composeDebug = ["compose-bom", "compose-debug-manifest", "compose-debug-tooling"] +composeDebug = ["compose-debug-manifest", "compose-debug-tooling"] unitTest = ["test-coroutines", "test-junit4", "test-mockk"] androidTest = ["androidTest-junit", "androidTest-espresso"] diff --git a/settings.gradle.kts b/settings.gradle.kts index 3828d680..82aa8a86 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,3 @@ -@file:Suppress("UnstableApiUsage") - pluginManagement { repositories { google() @@ -7,6 +5,9 @@ pluginManagement { gradlePluginPortal() } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version ("0.8.0") +} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { @@ -14,6 +15,5 @@ dependencyResolutionManagement { mavenCentral() } } - rootProject.name = "Look4Sat" include(":app", ":data", ":domain")