mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
maps UI tweaks
This commit is contained in:
1 parent
3994c9bd2e
commit
a2d9ed7008
4 files changed
+107
-16
No files matched your search
@@ -5,6 +5,11 @@ import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
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.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -125,6 +130,30 @@ private fun SurfaceHost(
|
||||
if (pan != Offset.Zero) connection.surfaceScroll(-pan.x, -pan.y)
|
||||
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 ->
|
||||
SurfaceView(context).apply {
|
||||
@@ -188,3 +217,6 @@ private fun ChromeBar(
|
||||
|
||||
/** Height of [ChromeBar]; the template's own chrome starts below it. */
|
||||
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 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.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -246,7 +248,15 @@ private fun SearchContent(template: SearchTemplate, modifier: Modifier) {
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = androidx.compose.foundation.shape.CircleShape,
|
||||
modifier = modifier.fillMaxWidth().height(ICON_BUTTON_SIZE),
|
||||
) {
|
||||
Text(
|
||||
query.ifEmpty { hint.ifEmpty { "Search" } },
|
||||
style = ChromeType.body,
|
||||
color = if (query.isEmpty()) MaterialTheme.colorScheme.onSurfaceVariant
|
||||
else MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 8.dp),
|
||||
)
|
||||
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(
|
||||
query.ifEmpty { hint.ifEmpty { "Search" } },
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = if (query.isEmpty()) muted else MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
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
|
||||
|
||||
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()
|
||||
if (error.isNotEmpty()) {
|
||||
Text(
|
||||
|
||||
@@ -680,5 +680,5 @@ private fun androidx.compose.ui.graphics.ImageBitmap.isPlainWhite(): Boolean {
|
||||
return seen > 0
|
||||
}
|
||||
|
||||
private val ICON_BUTTON_SIZE = 54.dp
|
||||
internal val ICON_BUTTON_SIZE = 54.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? {
|
||||
val icon = this?.icon ?: return null
|
||||
val key = icon.describe()
|
||||
val key = icon.cacheKey(context)
|
||||
iconCache.get(key)?.let { return it }
|
||||
val drawable: Drawable = runCatching { icon.loadDrawable(context) }
|
||||
.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
|
||||
}
|
||||
|
||||
/**
|
||||
* [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. */
|
||||
private fun androidx.core.graphics.drawable.IconCompat.describe(): String = when (type) {
|
||||
androidx.core.graphics.drawable.IconCompat.TYPE_URI,
|
||||
|
||||
Reference in new issue
Block a user