mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 08:12:26 +01:00
maps UI tweaks
This commit is contained in:
1 parent
3994c9bd2e
commit
a2d9ed7008
4 files changed
+102
-11
No files matched your search
@@ -5,6 +5,11 @@ import android.view.SurfaceHolder
|
|||||||
import android.view.SurfaceView
|
import android.view.SurfaceView
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.gestures.detectTransformGestures
|
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||||
|
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
|
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||||
|
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
||||||
|
import kotlin.math.hypot
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -125,6 +130,30 @@ private fun SurfaceHost(
|
|||||||
if (pan != Offset.Zero) connection.surfaceScroll(-pan.x, -pan.y)
|
if (pan != Offset.Zero) connection.surfaceScroll(-pan.x, -pan.y)
|
||||||
if (zoom != 1f) connection.surfaceScale(centroid.x, centroid.y, zoom)
|
if (zoom != 1f) connection.surfaceScale(centroid.x, centroid.y, zoom)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.pointerInput(connection) {
|
||||||
|
// detectTransformGestures has no velocity, so a fling is measured here alongside
|
||||||
|
// it: one finger lifting while still moving lets the map glide on, as on a car.
|
||||||
|
// Velocity is the finger's own, which is GestureDetector's onFling convention.
|
||||||
|
awaitEachGesture {
|
||||||
|
val down = awaitFirstDown(requireUnconsumed = false)
|
||||||
|
val tracker = VelocityTracker()
|
||||||
|
tracker.addPosition(down.uptimeMillis, down.position)
|
||||||
|
var pinched = false
|
||||||
|
while (true) {
|
||||||
|
val event = awaitPointerEvent(PointerEventPass.Final)
|
||||||
|
if (event.changes.size > 1) pinched = true
|
||||||
|
val change = event.changes.firstOrNull { it.id == down.id } ?: break
|
||||||
|
tracker.addPosition(change.uptimeMillis, change.position)
|
||||||
|
if (!change.pressed) {
|
||||||
|
val v = tracker.calculateVelocity()
|
||||||
|
if (!pinched && hypot(v.x, v.y) >= FLING_MIN_PX_S) {
|
||||||
|
connection.surfaceFling(v.x, v.y)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
factory = { context ->
|
factory = { context ->
|
||||||
SurfaceView(context).apply {
|
SurfaceView(context).apply {
|
||||||
@@ -188,3 +217,6 @@ private fun ChromeBar(
|
|||||||
|
|
||||||
/** Height of [ChromeBar]; the template's own chrome starts below it. */
|
/** Height of [ChromeBar]; the template's own chrome starts below it. */
|
||||||
private val CHROME_BAR = 44.dp
|
private val CHROME_BAR = 44.dp
|
||||||
|
|
||||||
|
/** Slower than this when the finger lifts and it was a drag that stopped, not a fling. */
|
||||||
|
private const val FLING_MIN_PX_S = 400f
|
||||||
@@ -2,6 +2,8 @@ package app.seb3thehacker.gearslip.car
|
|||||||
|
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@@ -246,7 +248,15 @@ private fun SearchContent(template: SearchTemplate, modifier: Modifier) {
|
|||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
template.headerAction?.let { ActionButton(it) }
|
template.headerAction?.let { ActionButton(it) }
|
||||||
SearchField(query, template.searchHint.orEmpty(), Modifier.weight(1f))
|
SearchField(
|
||||||
|
query,
|
||||||
|
template.searchHint.orEmpty(),
|
||||||
|
Modifier.weight(1f),
|
||||||
|
onClear = {
|
||||||
|
query = ""
|
||||||
|
delegate.textChanged("")
|
||||||
|
},
|
||||||
|
)
|
||||||
ActionRow(template.actionStrip?.actions.orEmpty())
|
ActionRow(template.actionStrip?.actions.orEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,22 +276,59 @@ private fun SearchContent(template: SearchTemplate, modifier: Modifier) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The field the on-screen keyboard types into. It is the same height and shape as the round
|
||||||
|
* buttons beside it, so the header reads as one row of controls rather than a thin strip squeezed
|
||||||
|
* between two big buttons, and it carries a magnifier so it reads as search at a glance.
|
||||||
|
* [onClear] adds a clear button once something is typed; [searchIcon] is off for sign-in fields.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun SearchField(query: String, hint: String, modifier: Modifier = Modifier) {
|
private fun SearchField(
|
||||||
|
query: String,
|
||||||
|
hint: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
searchIcon: Boolean = true,
|
||||||
|
onClear: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
val muted = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
Surface(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||||
shape = RoundedCornerShape(10.dp),
|
shape = androidx.compose.foundation.shape.CircleShape,
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth().height(ICON_BUTTON_SIZE),
|
||||||
) {
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier.padding(start = if (searchIcon) 16.dp else 22.dp, end = 6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
if (searchIcon) {
|
||||||
|
androidx.compose.material3.Icon(
|
||||||
|
androidx.compose.material.icons.Icons.Filled.Search,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = muted,
|
||||||
|
modifier = Modifier.size(26.dp),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
query.ifEmpty { hint.ifEmpty { "Search" } },
|
query.ifEmpty { hint.ifEmpty { "Search" } },
|
||||||
style = ChromeType.body,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = if (query.isEmpty()) MaterialTheme.colorScheme.onSurfaceVariant
|
color = if (query.isEmpty()) muted else MaterialTheme.colorScheme.onSurface,
|
||||||
else MaterialTheme.colorScheme.onSurface,
|
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 8.dp),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
|
if (onClear != null && query.isNotEmpty()) {
|
||||||
|
GsIconButton(
|
||||||
|
androidx.compose.material.icons.Icons.Filled.Close,
|
||||||
|
"Clear",
|
||||||
|
onClear,
|
||||||
|
colors = GsColors(androidx.compose.ui.graphics.Color.Transparent, muted),
|
||||||
|
size = 42.dp,
|
||||||
|
iconSize = 22.dp,
|
||||||
|
shape = androidx.compose.foundation.shape.CircleShape,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +391,7 @@ private fun InputSignIn(method: InputSignInMethod, modifier: Modifier) {
|
|||||||
val masked = method.inputType == InputSignInMethod.INPUT_TYPE_PASSWORD
|
val masked = method.inputType == InputSignInMethod.INPUT_TYPE_PASSWORD
|
||||||
|
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
SearchField(if (masked) "•".repeat(value.length) else value, method.hint.text())
|
SearchField(if (masked) "•".repeat(value.length) else value, method.hint.text(), searchIcon = false)
|
||||||
val error = method.errorMessage.text()
|
val error = method.errorMessage.text()
|
||||||
if (error.isNotEmpty()) {
|
if (error.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -680,5 +680,5 @@ private fun androidx.compose.ui.graphics.ImageBitmap.isPlainWhite(): Boolean {
|
|||||||
return seen > 0
|
return seen > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private val ICON_BUTTON_SIZE = 54.dp
|
internal val ICON_BUTTON_SIZE = 54.dp
|
||||||
private val ICON_BUTTON_GLYPH = 30.dp
|
private val ICON_BUTTON_GLYPH = 30.dp
|
||||||
@@ -52,7 +52,7 @@ private val iconCache = android.util.LruCache<String, ImageBitmap>(200)
|
|||||||
*/
|
*/
|
||||||
fun CarIcon?.image(context: Context): ImageBitmap? {
|
fun CarIcon?.image(context: Context): ImageBitmap? {
|
||||||
val icon = this?.icon ?: return null
|
val icon = this?.icon ?: return null
|
||||||
val key = icon.describe()
|
val key = icon.cacheKey(context)
|
||||||
iconCache.get(key)?.let { return it }
|
iconCache.get(key)?.let { return it }
|
||||||
val drawable: Drawable = runCatching { icon.loadDrawable(context) }
|
val drawable: Drawable = runCatching { icon.loadDrawable(context) }
|
||||||
.onFailure { GearslipLog.w("icon: loadDrawable threw for ${icon.describe()}: ${it.javaClass.simpleName}: ${it.message}") }
|
.onFailure { GearslipLog.w("icon: loadDrawable threw for ${icon.describe()}: ${it.javaClass.simpleName}: ${it.message}") }
|
||||||
@@ -77,6 +77,18 @@ fun CarIcon?.image(context: Context): ImageBitmap? {
|
|||||||
return bitmap
|
return bitmap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [describe], plus when the app was last installed for a resource icon. Resource ids are only
|
||||||
|
* numbers inside one build of the app: an update that adds a drawable renumbers the ones after it,
|
||||||
|
* so a cache keyed on the bare id kept drawing the old build's picture on the wrong button (Vela's
|
||||||
|
* new Saved button came up as its search glyph).
|
||||||
|
*/
|
||||||
|
private fun androidx.core.graphics.drawable.IconCompat.cacheKey(context: Context): String {
|
||||||
|
if (type != androidx.core.graphics.drawable.IconCompat.TYPE_RESOURCE) return describe()
|
||||||
|
val installed = runCatching { context.packageManager.getPackageInfo(resPackage, 0).lastUpdateTime }.getOrDefault(0L)
|
||||||
|
return "${describe()}@$installed"
|
||||||
|
}
|
||||||
|
|
||||||
/** Enough of an [androidx.core.graphics.drawable.IconCompat] to tell log lines apart without dumping the whole object. */
|
/** Enough of an [androidx.core.graphics.drawable.IconCompat] to tell log lines apart without dumping the whole object. */
|
||||||
private fun androidx.core.graphics.drawable.IconCompat.describe(): String = when (type) {
|
private fun androidx.core.graphics.drawable.IconCompat.describe(): String = when (type) {
|
||||||
androidx.core.graphics.drawable.IconCompat.TYPE_URI,
|
androidx.core.graphics.drawable.IconCompat.TYPE_URI,
|
||||||
|
|||||||
Reference in new issue
Block a user