mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
Merge pull request #836 from permissionlesstech/codex/restore-globe-picker-ux
Restore globe picker UX and teleport selection
This commit is contained in:
commit
13f15ca1ef
@ -15,9 +15,6 @@ import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
@ -37,8 +34,6 @@ import com.bitchat.android.geohash.Geohash
|
||||
import com.bitchat.android.geohash.GeohashChannelLevel
|
||||
import com.bitchat.android.geohash.LocationChannelManager
|
||||
import com.bitchat.android.ui.globe.GlobeColors
|
||||
import com.bitchat.android.ui.globe.GlobeRenderQuality
|
||||
import com.bitchat.android.ui.globe.GlobeRenderQualityPreference
|
||||
import com.bitchat.android.ui.globe.GlobeState
|
||||
import com.bitchat.android.ui.globe.GlobeView
|
||||
import com.bitchat.android.ui.globe.LandData
|
||||
@ -95,18 +90,13 @@ class GeohashPickerActivity : OrientationAwareActivity() {
|
||||
BitchatTheme {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val initialRenderQuality = remember(context) {
|
||||
GlobeRenderQualityPreference.load(context)
|
||||
}
|
||||
var renderQuality by remember { mutableStateOf(initialRenderQuality) }
|
||||
|
||||
val globeState = remember {
|
||||
GlobeState(
|
||||
targetLat = targetLat,
|
||||
targetLon = targetLon,
|
||||
initialPrecision = initialPrecision,
|
||||
startZoomedOut = true,
|
||||
initialRenderQuality = initialRenderQuality
|
||||
startZoomedOut = true
|
||||
).apply {
|
||||
introTarget = Triple(targetLat, targetLon, initialPrecision)
|
||||
}
|
||||
@ -193,62 +183,15 @@ class GeohashPickerActivity : OrientationAwareActivity() {
|
||||
tonalElevation = 3.dp,
|
||||
shadowElevation = 6.dp
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
Text(
|
||||
text = stringResource(R.string.pan_zoom_instruction),
|
||||
fontSize = 12.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
fontFamily = BitchatFontFamily,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.pan_zoom_instruction),
|
||||
fontSize = 12.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
fontFamily = BitchatFontFamily,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
val qualities = GlobeRenderQuality.entries
|
||||
SingleChoiceSegmentedButtonRow(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
qualities.forEachIndexed { index, quality ->
|
||||
val selected = renderQuality == quality
|
||||
SegmentedButton(
|
||||
selected = selected,
|
||||
onClick = {
|
||||
globeState.setRenderQuality(quality)
|
||||
renderQuality = quality
|
||||
GlobeRenderQualityPreference.save(context, quality)
|
||||
},
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = index,
|
||||
count = qualities.size
|
||||
),
|
||||
icon = {},
|
||||
label = {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (selected) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.size(18.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(quality.labelResource),
|
||||
fontSize = 11.sp,
|
||||
fontFamily = BitchatFontFamily
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Floating bottom controls
|
||||
@ -347,13 +290,6 @@ class GeohashPickerActivity : OrientationAwareActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private val GlobeRenderQuality.labelResource: Int
|
||||
get() = when (this) {
|
||||
GlobeRenderQuality.FAST -> R.string.globe_render_quality_fast
|
||||
GlobeRenderQuality.MEDIUM -> R.string.globe_render_quality_medium
|
||||
GlobeRenderQuality.HIGH -> R.string.globe_render_quality_high
|
||||
}
|
||||
|
||||
private fun levelForLength(length: Int): GeohashChannelLevel {
|
||||
return when (length) {
|
||||
in 0..2 -> GeohashChannelLevel.REGION
|
||||
|
||||
@ -137,6 +137,18 @@ fun LocationChannelsSheet(
|
||||
var customGeohash by remember { mutableStateOf("") }
|
||||
var customError by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val teleportToGeohash: (String) -> Unit = { value ->
|
||||
val channel = channelForManualGeohash(value)
|
||||
if (channel != null) {
|
||||
customError = null
|
||||
locationManager.selectManual(channel)
|
||||
onDismiss()
|
||||
} else {
|
||||
customGeohash = value.trim().lowercase().replace("#", "")
|
||||
customError = context.getString(R.string.invalid_geohash)
|
||||
}
|
||||
}
|
||||
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
@ -158,8 +170,7 @@ fun LocationChannelsSheet(
|
||||
if (result.resultCode == android.app.Activity.RESULT_OK) {
|
||||
val gh = result.data?.getStringExtra(GeohashPickerActivity.EXTRA_RESULT_GEOHASH)
|
||||
if (!gh.isNullOrBlank()) {
|
||||
customGeohash = gh
|
||||
customError = null
|
||||
teleportToGeohash(gh)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -503,15 +514,7 @@ fun LocationChannelsSheet(
|
||||
mapPickerLauncher.launch(intent)
|
||||
},
|
||||
onTeleport = {
|
||||
val normalized = customGeohash.trim().lowercase().replace("#", "")
|
||||
if (validateGeohash(normalized)) {
|
||||
val level = levelForLength(normalized.length)
|
||||
val channel = GeohashChannel(level = level, geohash = normalized)
|
||||
locationManager.selectManual(channel)
|
||||
onDismiss()
|
||||
} else {
|
||||
customError = context.getString(R.string.invalid_geohash)
|
||||
}
|
||||
teleportToGeohash(customGeohash)
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -1173,6 +1176,15 @@ private fun validateGeohash(geohash: String): Boolean {
|
||||
return geohash.all { it in allowed }
|
||||
}
|
||||
|
||||
internal fun channelForManualGeohash(value: String): GeohashChannel? {
|
||||
val normalized = value.trim().lowercase().replace("#", "")
|
||||
if (!validateGeohash(normalized)) return null
|
||||
return GeohashChannel(
|
||||
level = levelForLength(normalized.length),
|
||||
geohash = normalized
|
||||
)
|
||||
}
|
||||
|
||||
private fun levelForLength(length: Int): GeohashChannelLevel {
|
||||
return when (length) {
|
||||
in 0..2 -> GeohashChannelLevel.REGION
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
package com.bitchat.android.ui.globe
|
||||
|
||||
import android.content.Context
|
||||
|
||||
enum class GlobeRenderQuality {
|
||||
FAST,
|
||||
MEDIUM,
|
||||
HIGH;
|
||||
|
||||
companion object {
|
||||
fun fromStoredValue(value: String?): GlobeRenderQuality =
|
||||
entries.firstOrNull { it.name == value } ?: MEDIUM
|
||||
}
|
||||
}
|
||||
|
||||
object GlobeRenderQualityPreference {
|
||||
private const val PREFERENCES_NAME = "bitchat_settings"
|
||||
private const val KEY_RENDER_QUALITY = "geohash_globe_render_quality"
|
||||
|
||||
fun load(context: Context): GlobeRenderQuality {
|
||||
val preferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||
return GlobeRenderQuality.fromStoredValue(
|
||||
preferences.getString(KEY_RENDER_QUALITY, GlobeRenderQuality.MEDIUM.name)
|
||||
)
|
||||
}
|
||||
|
||||
fun save(context: Context, quality: GlobeRenderQuality) {
|
||||
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putString(KEY_RENDER_QUALITY, quality.name)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
@ -28,8 +28,7 @@ class GlobeState(
|
||||
targetLat: Double,
|
||||
targetLon: Double,
|
||||
initialPrecision: Int,
|
||||
startZoomedOut: Boolean,
|
||||
initialRenderQuality: GlobeRenderQuality = GlobeRenderQuality.MEDIUM
|
||||
startZoomedOut: Boolean
|
||||
) {
|
||||
var centerLat by mutableFloatStateOf(if (startZoomedOut) (targetLat * 0.4).toFloat() else targetLat.toFloat())
|
||||
private set
|
||||
@ -48,8 +47,6 @@ class GlobeState(
|
||||
|
||||
internal var baseRadiusPx by mutableFloatStateOf(0f)
|
||||
internal var screenMinPx by mutableFloatStateOf(0f)
|
||||
internal var renderQuality = initialRenderQuality
|
||||
private set
|
||||
|
||||
private var scope: CoroutineScope? = null
|
||||
private var animJob: Job? = null
|
||||
@ -71,14 +68,6 @@ class GlobeState(
|
||||
this.scope = scope
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendering quality only affects moving frames. This is deliberately not snapshot state:
|
||||
* changing it must not invalidate the expensive stationary globe beneath the selector.
|
||||
*/
|
||||
fun setRenderQuality(quality: GlobeRenderQuality) {
|
||||
renderQuality = quality
|
||||
}
|
||||
|
||||
fun setViewport(baseRadiusPx: Float, screenMinPx: Float) {
|
||||
if (baseRadiusPx <= 0f || screenMinPx <= 0f) return
|
||||
this.baseRadiusPx = baseRadiusPx
|
||||
|
||||
@ -18,8 +18,11 @@ import androidx.compose.foundation.gestures.calculateZoom
|
||||
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.runtime.snapshotFlow
|
||||
import androidx.compose.runtime.withFrameNanos
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
@ -39,10 +42,7 @@ import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.bitchat.android.geohash.Geohash
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.min
|
||||
@ -76,12 +76,15 @@ internal data class GlobeFrameDetail(
|
||||
val showNeighborCells: Boolean
|
||||
)
|
||||
|
||||
internal fun globeFrameDetail(
|
||||
quality: GlobeRenderQuality,
|
||||
isMoving: Boolean
|
||||
): GlobeFrameDetail {
|
||||
if (!isMoving || quality == GlobeRenderQuality.HIGH) {
|
||||
return GlobeFrameDetail(
|
||||
internal enum class GlobeMotionDetail {
|
||||
FULL,
|
||||
BALANCED,
|
||||
FAST
|
||||
}
|
||||
|
||||
internal fun globeFrameDetail(motionDetail: GlobeMotionDetail): GlobeFrameDetail =
|
||||
when (motionDetail) {
|
||||
GlobeMotionDetail.FULL -> GlobeFrameDetail(
|
||||
graticuleStepDegrees = 4.0,
|
||||
landPointStride = 1,
|
||||
showBorders = true,
|
||||
@ -90,18 +93,7 @@ internal fun globeFrameDetail(
|
||||
showGeohashGrid = true,
|
||||
showNeighborCells = true
|
||||
)
|
||||
}
|
||||
return when (quality) {
|
||||
GlobeRenderQuality.FAST -> GlobeFrameDetail(
|
||||
graticuleStepDegrees = 10.0,
|
||||
landPointStride = 2,
|
||||
showBorders = false,
|
||||
cityMaxRank = -1,
|
||||
showCityLabels = false,
|
||||
showGeohashGrid = false,
|
||||
showNeighborCells = false
|
||||
)
|
||||
GlobeRenderQuality.MEDIUM -> GlobeFrameDetail(
|
||||
GlobeMotionDetail.BALANCED -> GlobeFrameDetail(
|
||||
graticuleStepDegrees = 8.0,
|
||||
landPointStride = 2,
|
||||
showBorders = true,
|
||||
@ -110,7 +102,44 @@ internal fun globeFrameDetail(
|
||||
showGeohashGrid = true,
|
||||
showNeighborCells = false
|
||||
)
|
||||
GlobeRenderQuality.HIGH -> error("Handled above")
|
||||
GlobeMotionDetail.FAST -> GlobeFrameDetail(
|
||||
graticuleStepDegrees = 10.0,
|
||||
landPointStride = 2,
|
||||
showBorders = false,
|
||||
cityMaxRank = -1,
|
||||
showCityLabels = false,
|
||||
showGeohashGrid = false,
|
||||
showNeighborCells = false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts moving-frame detail from measured frame cadence. Hysteresis keeps the renderer
|
||||
* from oscillating between levels when timings sit near a boundary.
|
||||
*/
|
||||
internal fun nextGlobeMotionDetail(
|
||||
current: GlobeMotionDetail,
|
||||
averageFrameMillis: Float
|
||||
): GlobeMotionDetail {
|
||||
if (!averageFrameMillis.isFinite()) return GlobeMotionDetail.FAST
|
||||
return when (current) {
|
||||
GlobeMotionDetail.FULL -> when {
|
||||
averageFrameMillis > SEVERELY_SLOW_FRAME_MILLIS -> GlobeMotionDetail.FAST
|
||||
averageFrameMillis > SLOW_FRAME_MILLIS -> GlobeMotionDetail.BALANCED
|
||||
else -> GlobeMotionDetail.FULL
|
||||
}
|
||||
GlobeMotionDetail.BALANCED -> when {
|
||||
averageFrameMillis > VERY_SLOW_FRAME_MILLIS -> GlobeMotionDetail.FAST
|
||||
averageFrameMillis < SMOOTH_FRAME_MILLIS -> GlobeMotionDetail.FULL
|
||||
else -> GlobeMotionDetail.BALANCED
|
||||
}
|
||||
GlobeMotionDetail.FAST -> {
|
||||
if (averageFrameMillis < RECOVERING_FRAME_MILLIS) {
|
||||
GlobeMotionDetail.BALANCED
|
||||
} else {
|
||||
GlobeMotionDetail.FAST
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,19 +198,48 @@ fun GlobeView(
|
||||
}
|
||||
|
||||
LaunchedEffect(state) {
|
||||
snapshotFlow { state.selectedGeohash to state.isInMotion }
|
||||
.distinctUntilChanged()
|
||||
snapshotFlow { state.selectedGeohash }
|
||||
.drop(1)
|
||||
.collectLatest { (geohash, inMotion) ->
|
||||
if (geohash.isNotEmpty() && !inMotion) {
|
||||
delay(SETTLED_HAPTIC_DELAY_MS)
|
||||
if (!state.isInMotion && state.selectedGeohash == geohash) {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
}
|
||||
}
|
||||
.collect {
|
||||
@Suppress("DEPRECATION")
|
||||
view.performHapticFeedback(
|
||||
HapticFeedbackConstants.KEYBOARD_TAP,
|
||||
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var motionDetail by remember(state) { mutableStateOf(GlobeMotionDetail.BALANCED) }
|
||||
LaunchedEffect(state, state.isInMotion) {
|
||||
if (!state.isInMotion) {
|
||||
motionDetail = GlobeMotionDetail.FULL
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
// Balanced is a safe first frame; measured cadence then moves detail up or down.
|
||||
motionDetail = GlobeMotionDetail.BALANCED
|
||||
var previousFrameNanos = withFrameNanos { it }
|
||||
var sampledFrameMillis = 0f
|
||||
var sampledFrameCount = 0
|
||||
while (state.isInMotion) {
|
||||
val frameNanos = withFrameNanos { it }
|
||||
val frameMillis = ((frameNanos - previousFrameNanos) / 1_000_000f)
|
||||
.coerceIn(1f, MAX_SAMPLED_FRAME_MILLIS)
|
||||
previousFrameNanos = frameNanos
|
||||
sampledFrameMillis += frameMillis
|
||||
sampledFrameCount++
|
||||
|
||||
if (sampledFrameCount >= FRAME_SAMPLE_COUNT) {
|
||||
motionDetail = nextGlobeMotionDetail(
|
||||
current = motionDetail,
|
||||
averageFrameMillis = sampledFrameMillis / sampledFrameCount
|
||||
)
|
||||
sampledFrameMillis = 0f
|
||||
sampledFrameCount = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val labelTextSize = with(density) { 12.5.sp.toPx() }
|
||||
val labelTextSizeSmall = with(density) { 10.sp.toPx() }
|
||||
|
||||
@ -325,7 +383,9 @@ fun GlobeView(
|
||||
|
||||
val clip = ClipRect(-size.width, -size.height, size.width * 2f, size.height * 2f)
|
||||
|
||||
val frameDetail = globeFrameDetail(state.renderQuality, state.isInMotion)
|
||||
val frameDetail = globeFrameDetail(
|
||||
if (state.isInMotion) motionDetail else GlobeMotionDetail.FULL
|
||||
)
|
||||
|
||||
// Graticule
|
||||
drawGraticule(
|
||||
@ -433,7 +493,13 @@ fun GlobeView(
|
||||
}
|
||||
}
|
||||
|
||||
private const val SETTLED_HAPTIC_DELAY_MS = 80L
|
||||
private const val FRAME_SAMPLE_COUNT = 8
|
||||
private const val MAX_SAMPLED_FRAME_MILLIS = 50f
|
||||
private const val SMOOTH_FRAME_MILLIS = 18.5f
|
||||
private const val SLOW_FRAME_MILLIS = 22f
|
||||
private const val RECOVERING_FRAME_MILLIS = 22f
|
||||
private const val VERY_SLOW_FRAME_MILLIS = 29f
|
||||
private const val SEVERELY_SLOW_FRAME_MILLIS = 34f
|
||||
|
||||
private fun DrawScope.drawGraticule(
|
||||
cx: Float,
|
||||
|
||||
@ -540,9 +540,6 @@
|
||||
<string name="nobody_around">Nobody around…</string>
|
||||
<string name="you_suffix"> (you)</string>
|
||||
<string name="pan_zoom_instruction">Drag to spin · Pinch to zoom · Tap to focus</string>
|
||||
<string name="globe_render_quality_fast">Fast</string>
|
||||
<string name="globe_render_quality_medium">Medium</string>
|
||||
<string name="globe_render_quality_high">High</string>
|
||||
<string name="select">Select</string>
|
||||
<string name="type_a_message_placeholder">Type a message…</string>
|
||||
<string name="mention_suggestion_at">@%1$s</string>
|
||||
|
||||
@ -42,6 +42,22 @@ class LocationChannelsSheetTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `globe result resolves to a manual teleport channel`() {
|
||||
assertEquals(
|
||||
GeohashChannel(
|
||||
level = GeohashChannelLevel.CITY,
|
||||
geohash = "u33dc"
|
||||
),
|
||||
channelForManualGeohash(" #U33DC ")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid globe result cannot create a teleport channel`() {
|
||||
assertNull(channelForManualGeohash("not-a-geohash"))
|
||||
}
|
||||
|
||||
private fun channel(geohash: String) = GeohashChannel(
|
||||
level = GeohashChannelLevel.CITY,
|
||||
geohash = geohash
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
package com.bitchat.android.ui.globe
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class GlobeInteractionPolicyTest {
|
||||
|
||||
@Test
|
||||
fun fullDetail_preservesAllGlobeFeatures() {
|
||||
val detail = globeFrameDetail(GlobeMotionDetail.FULL)
|
||||
|
||||
assertEquals(1, detail.landPointStride)
|
||||
assertTrue(detail.showBorders)
|
||||
assertNull(detail.cityMaxRank)
|
||||
assertTrue(detail.showCityLabels)
|
||||
assertTrue(detail.showGeohashGrid)
|
||||
assertTrue(detail.showNeighborCells)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun balancedDetail_preservesOrientationAndSelection() {
|
||||
val detail = globeFrameDetail(GlobeMotionDetail.BALANCED)
|
||||
|
||||
assertEquals(2, detail.landPointStride)
|
||||
assertTrue(detail.showBorders)
|
||||
assertEquals(1, detail.cityMaxRank)
|
||||
assertFalse(detail.showCityLabels)
|
||||
assertTrue(detail.showGeohashGrid)
|
||||
assertFalse(detail.showNeighborCells)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fastDetail_usesMinimumMovingFrameWork() {
|
||||
val detail = globeFrameDetail(GlobeMotionDetail.FAST)
|
||||
|
||||
assertEquals(2, detail.landPointStride)
|
||||
assertFalse(detail.showBorders)
|
||||
assertEquals(-1, detail.cityMaxRank)
|
||||
assertFalse(detail.showCityLabels)
|
||||
assertFalse(detail.showGeohashGrid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun adaptiveDetail_degradesOnSlowFramesAndRecoversWithHysteresis() {
|
||||
assertEquals(
|
||||
GlobeMotionDetail.BALANCED,
|
||||
nextGlobeMotionDetail(GlobeMotionDetail.FULL, averageFrameMillis = 24f)
|
||||
)
|
||||
assertEquals(
|
||||
GlobeMotionDetail.FAST,
|
||||
nextGlobeMotionDetail(GlobeMotionDetail.BALANCED, averageFrameMillis = 32f)
|
||||
)
|
||||
assertEquals(
|
||||
GlobeMotionDetail.BALANCED,
|
||||
nextGlobeMotionDetail(GlobeMotionDetail.FAST, averageFrameMillis = 17f)
|
||||
)
|
||||
assertEquals(
|
||||
GlobeMotionDetail.FULL,
|
||||
nextGlobeMotionDetail(GlobeMotionDetail.BALANCED, averageFrameMillis = 17f)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,78 +0,0 @@
|
||||
package com.bitchat.android.ui.globe
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class GlobeRenderQualityTest {
|
||||
|
||||
@Test
|
||||
fun invalidStoredValue_defaultsToMedium() {
|
||||
assertEquals(GlobeRenderQuality.MEDIUM, GlobeRenderQuality.fromStoredValue(null))
|
||||
assertEquals(GlobeRenderQuality.MEDIUM, GlobeRenderQuality.fromStoredValue("UNKNOWN"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun globeState_usesAndUpdatesRenderQualityWithoutReplacingState() {
|
||||
val state = GlobeState(
|
||||
targetLat = 0.0,
|
||||
targetLon = 0.0,
|
||||
initialPrecision = 2,
|
||||
startZoomedOut = false,
|
||||
initialRenderQuality = GlobeRenderQuality.FAST
|
||||
)
|
||||
|
||||
assertEquals(GlobeRenderQuality.FAST, state.renderQuality)
|
||||
state.setRenderQuality(GlobeRenderQuality.HIGH)
|
||||
assertEquals(GlobeRenderQuality.HIGH, state.renderQuality)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stationaryFrame_alwaysUsesFullDetail() {
|
||||
GlobeRenderQuality.entries.forEach { quality ->
|
||||
val detail = globeFrameDetail(quality, isMoving = false)
|
||||
|
||||
assertEquals(1, detail.landPointStride)
|
||||
assertTrue(detail.showBorders)
|
||||
assertNull(detail.cityMaxRank)
|
||||
assertTrue(detail.showCityLabels)
|
||||
assertTrue(detail.showGeohashGrid)
|
||||
assertTrue(detail.showNeighborCells)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun movingFastFrame_usesMinimumDetail() {
|
||||
val detail = globeFrameDetail(GlobeRenderQuality.FAST, isMoving = true)
|
||||
|
||||
assertEquals(2, detail.landPointStride)
|
||||
assertFalse(detail.showBorders)
|
||||
assertEquals(-1, detail.cityMaxRank)
|
||||
assertFalse(detail.showGeohashGrid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun movingMediumFrame_preservesOrientationAndSelection() {
|
||||
val detail = globeFrameDetail(GlobeRenderQuality.MEDIUM, isMoving = true)
|
||||
|
||||
assertEquals(2, detail.landPointStride)
|
||||
assertTrue(detail.showBorders)
|
||||
assertEquals(1, detail.cityMaxRank)
|
||||
assertFalse(detail.showCityLabels)
|
||||
assertTrue(detail.showGeohashGrid)
|
||||
assertFalse(detail.showNeighborCells)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun movingHighFrame_usesFullDetail() {
|
||||
val detail = globeFrameDetail(GlobeRenderQuality.HIGH, isMoving = true)
|
||||
|
||||
assertEquals(1, detail.landPointStride)
|
||||
assertTrue(detail.showBorders)
|
||||
assertNull(detail.cityMaxRank)
|
||||
assertTrue(detail.showCityLabels)
|
||||
assertTrue(detail.showNeighborCells)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user