mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-29 07:16:08 +00:00
Merge pull request #916 from permissionlesstech/codex/fix-wear-shape-and-font-scaling
Improve Wear layouts for round screens
This commit is contained in:
commit
4f828b5643
@ -19,8 +19,8 @@ android {
|
||||
targetSdk = libs.versions.targetSdk.get().toInt()
|
||||
// Wear releases use a separate high range because Play requires every artifact in
|
||||
// one application ID to have a unique version code across all form factors.
|
||||
versionCode = 1_000_000_002
|
||||
versionName = "0.1.1"
|
||||
versionCode = 1_000_000_003
|
||||
versionName = "0.1.2"
|
||||
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
|
||||
@ -8,7 +8,6 @@ import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@ -21,13 +20,20 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.CompositingStrategy
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.changedToUp
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
|
||||
@ -44,6 +50,7 @@ import com.bitchat.watch.ui.theme.ChatVisualTokens
|
||||
import com.bitchat.watch.ui.theme.LocalBitchatPalette
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlin.math.sign
|
||||
|
||||
/**
|
||||
* The shared chat body for global chat and DM threads, following the classic messenger
|
||||
@ -82,35 +89,34 @@ fun ChatScaffold(
|
||||
previousCount = messages.size
|
||||
}
|
||||
|
||||
// One state drives both overlays: visible at the bottom or when scrolling toward it,
|
||||
// hidden when scrolling up into history. The 24px (~12dp) threshold is deliberately
|
||||
// small so the controls answer every flick immediately.
|
||||
var atNewest by remember { mutableStateOf(true) }
|
||||
// Follow intent is changed only by an actual user scroll away from the newest item or by
|
||||
// reaching the end again. A new item temporarily makes canScrollForward true before layout;
|
||||
// treating that transient range change as user intent breaks automatic following.
|
||||
var followNewest by remember { mutableStateOf(true) }
|
||||
val controlsVisible = remember { mutableStateOf(true) }
|
||||
LaunchedEffect(columnState) {
|
||||
var lastPosition = -1
|
||||
var scrollIntent = ChatScrollIntentState()
|
||||
snapshotFlow {
|
||||
val first = columnState.layoutInfo.visibleItems.firstOrNull()
|
||||
Triple(columnState.canScrollForward, first?.index ?: 0, first?.offset ?: 0)
|
||||
}.collect { (canScrollForward, index, offset) ->
|
||||
val position = index * 100_000 + offset
|
||||
atNewest = !canScrollForward
|
||||
if (!canScrollForward) {
|
||||
controlsVisible.value = true
|
||||
} else if (lastPosition >= 0) {
|
||||
when {
|
||||
position > lastPosition + 24 -> controlsVisible.value = true
|
||||
position < lastPosition - 24 -> controlsVisible.value = false
|
||||
}
|
||||
}
|
||||
lastPosition = position
|
||||
ChatScrollSnapshot(
|
||||
canScrollForward = columnState.canScrollForward,
|
||||
isScrollInProgress = columnState.isScrollInProgress,
|
||||
position = (first?.index ?: 0) * 100_000 + (first?.offset ?: 0)
|
||||
)
|
||||
}.collect { snapshot ->
|
||||
scrollIntent = updatedChatScrollIntent(
|
||||
current = scrollIntent,
|
||||
snapshot = snapshot,
|
||||
previousPosition = lastPosition
|
||||
)
|
||||
followNewest = scrollIntent.followsNewest
|
||||
controlsVisible.value = scrollIntent.controlsVisible
|
||||
lastPosition = snapshot.position
|
||||
}
|
||||
}
|
||||
|
||||
// Stick to bottom: follow new messages while resting at the newest.
|
||||
// Capture this when the message count changes, before the new layout can temporarily make
|
||||
// canScrollForward true and report that the user is browsing history.
|
||||
val followNewest = remember(messages.size) { atNewest }
|
||||
// Stick to bottom when the user has not intentionally moved into history.
|
||||
LaunchedEffect(columnState, messages.size) {
|
||||
if (messages.isNotEmpty() && followNewest) {
|
||||
val expectedSingleMessageKey = messages.singleOrNull()?.id
|
||||
@ -155,6 +161,50 @@ internal data class MeasuredChatLayout(
|
||||
val singleVisibleItemKey: Any?
|
||||
)
|
||||
|
||||
internal data class ChatScrollSnapshot(
|
||||
val canScrollForward: Boolean,
|
||||
val isScrollInProgress: Boolean,
|
||||
val position: Int
|
||||
)
|
||||
|
||||
internal data class ChatScrollIntentState(
|
||||
val followsNewest: Boolean = true,
|
||||
val controlsVisible: Boolean = true,
|
||||
val accumulatedDeltaPx: Int = 0
|
||||
)
|
||||
|
||||
internal fun updatedChatScrollIntent(
|
||||
current: ChatScrollIntentState,
|
||||
snapshot: ChatScrollSnapshot,
|
||||
previousPosition: Int
|
||||
): ChatScrollIntentState {
|
||||
if (!snapshot.canScrollForward) return ChatScrollIntentState()
|
||||
if (!snapshot.isScrollInProgress || previousPosition < 0) return current
|
||||
|
||||
val delta = snapshot.position - previousPosition
|
||||
val accumulatedDelta = when {
|
||||
delta == 0 -> current.accumulatedDeltaPx
|
||||
current.accumulatedDeltaPx == 0 ||
|
||||
current.accumulatedDeltaPx.sign == delta.sign ->
|
||||
current.accumulatedDeltaPx + delta
|
||||
else -> delta
|
||||
}
|
||||
val movedAway = accumulatedDelta <= -CHAT_SCROLL_DIRECTION_THRESHOLD_PX
|
||||
val movedTowardNewest = accumulatedDelta >= CHAT_SCROLL_DIRECTION_THRESHOLD_PX
|
||||
|
||||
return current.copy(
|
||||
followsNewest = current.followsNewest && !movedAway,
|
||||
controlsVisible = when {
|
||||
movedAway -> false
|
||||
movedTowardNewest -> true
|
||||
else -> current.controlsVisible
|
||||
},
|
||||
// Keep sub-threshold movement across discrete rotary events. Once intent is clear,
|
||||
// start a fresh accumulator so reversing direction gets the same threshold treatment.
|
||||
accumulatedDeltaPx = if (movedAway || movedTowardNewest) 0 else accumulatedDelta
|
||||
)
|
||||
}
|
||||
|
||||
internal suspend fun scrollToNewestAfterItemsMeasured(
|
||||
expectedItemCount: Int,
|
||||
expectedSingleMessageKey: Any?,
|
||||
@ -185,7 +235,7 @@ private fun ChatBody(
|
||||
val palette = LocalBitchatPalette.current
|
||||
val context = LocalContext.current
|
||||
val transformationSpec = rememberTransformationSpec()
|
||||
|
||||
val isScreenRound = LocalConfiguration.current.isScreenRound
|
||||
// Slide-to-cancel: while recording, the finger's position is tracked globally; the
|
||||
// overlay's mic button reports its bounds and becomes the cancel target when the
|
||||
// finger hovers it (with generous slack so the snap engages on approach).
|
||||
@ -256,16 +306,65 @@ private fun ChatBody(
|
||||
}
|
||||
}
|
||||
) {
|
||||
ScreenScaffold(scrollState = columnState) {
|
||||
ScreenScaffold(scrollState = columnState) { scaffoldPadding ->
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
TransformingLazyColumn(
|
||||
state = columnState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
// Keep the list full-screen and geometrically unclipped. Wear's transformation
|
||||
// spec curves rows along the round display; a soft destination-alpha mask then
|
||||
// makes them fully transparent at the physical edges instead of cutting glyphs.
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
compositingStrategy = CompositingStrategy.Offscreen
|
||||
}
|
||||
.drawWithCache {
|
||||
val edgeMask = if (isScreenRound) {
|
||||
// Fade toward the actual circular contour so glyphs become
|
||||
// transparent before the panel can crop their left or right edge.
|
||||
Brush.radialGradient(
|
||||
0f to Color.Black,
|
||||
CHAT_ROUND_EDGE_OPAQUE_STOP to Color.Black,
|
||||
1f to Color.Transparent,
|
||||
center = Offset(size.width / 2f, size.height / 2f),
|
||||
radius = size.minDimension / 2f
|
||||
)
|
||||
} else {
|
||||
val topOpaqueStop =
|
||||
(CHAT_HEADER_EDGE_FADE.toPx() / size.height).coerceIn(0f, 1f)
|
||||
val bottomOpaqueStop =
|
||||
(1f - CHAT_ACTION_BAR_EDGE_FADE.toPx() / size.height)
|
||||
.coerceIn(topOpaqueStop, 1f)
|
||||
Brush.verticalGradient(
|
||||
0f to Color.Transparent,
|
||||
topOpaqueStop to Color.Black,
|
||||
bottomOpaqueStop to Color.Black,
|
||||
1f to Color.Transparent
|
||||
)
|
||||
}
|
||||
onDrawWithContent {
|
||||
drawContent()
|
||||
drawRect(
|
||||
brush = edgeMask,
|
||||
blendMode = BlendMode.DstIn
|
||||
)
|
||||
}
|
||||
},
|
||||
// Arrangement.Bottom anchors short content to the bottom: the first message
|
||||
// starts just above the action bar and new messages push history upward.
|
||||
// The padding reserves permanent room for the floating header and action
|
||||
// bar; being constant, it never disturbs an in-flight scroll gesture.
|
||||
// The scroll range reserves resting space for the floating controls while the
|
||||
// full-screen viewport preserves autoscroll and the native transformation focal
|
||||
// point. Rows may travel behind the overlays only after they have begun the Wear
|
||||
// edge scale/fade treatment.
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
contentPadding = PaddingValues(top = 30.dp, bottom = 64.dp)
|
||||
// Keep Wear Material's responsive horizontal inset while replacing its vertical
|
||||
// inset with the overlay clearances used before the shape fix. This avoids both
|
||||
// duplicated padding and a shortened list viewport.
|
||||
contentPadding = scaffoldPadding.withVerticalClearance(
|
||||
layoutDirection = layoutDirection,
|
||||
top = CHAT_HEADER_CONTENT_CLEARANCE,
|
||||
bottom = CHAT_ACTION_BAR_CLEARANCE
|
||||
)
|
||||
) {
|
||||
if (messages.isEmpty()) {
|
||||
item {
|
||||
@ -299,7 +398,12 @@ private fun ChatBody(
|
||||
|
||||
// The header stays put and shrinks to its dense form instead of disappearing;
|
||||
// as an overlay its size animation never touches the list's scroll geometry.
|
||||
Box(modifier = Modifier.align(Alignment.TopCenter)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
header(controlsVisible)
|
||||
}
|
||||
|
||||
@ -333,6 +437,12 @@ private fun ChatBody(
|
||||
// Extra finger slack (px, ~28dp at watch density) around the cancel target so the snap
|
||||
// engages as the finger approaches, not only on exact contact.
|
||||
private const val CANCEL_HOVER_SLANT_PX = 56f
|
||||
private const val CHAT_SCROLL_DIRECTION_THRESHOLD_PX = 24
|
||||
private val CHAT_HEADER_CONTENT_CLEARANCE = 30.dp
|
||||
private val CHAT_ACTION_BAR_CLEARANCE = 64.dp
|
||||
private val CHAT_HEADER_EDGE_FADE = 36.dp
|
||||
private val CHAT_ACTION_BAR_EDGE_FADE = 72.dp
|
||||
private const val CHAT_ROUND_EDGE_OPAQUE_STOP = 0.78f
|
||||
// Magnetic zone geometry (px at watch density): the button starts reacting at
|
||||
// MAGNET_OUTER_PX from its center and fully blushes at MAGNET_INNER_PX (~the activation
|
||||
// boundary); it leans toward the finger by up to MAGNET_PULL_PX.
|
||||
|
||||
@ -67,10 +67,11 @@ fun PeopleScreen(onOpenDm: (String) -> Unit, onEditNickname: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
ScreenScaffold(scrollState = listState) {
|
||||
ScreenScaffold(scrollState = listState) { scaffoldPadding ->
|
||||
ScalingLazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = scaffoldPadding
|
||||
) {
|
||||
item {
|
||||
ListHeader {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package com.bitchat.watch.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@ -15,6 +14,7 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@ -48,11 +48,17 @@ fun UserDetailScreen(
|
||||
val listState = rememberScalingLazyListState()
|
||||
val palette = LocalBitchatPalette.current
|
||||
|
||||
ScreenScaffold(scrollState = listState) {
|
||||
ScreenScaffold(scrollState = listState) { scaffoldPadding ->
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
ScalingLazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 8.dp)
|
||||
contentPadding = scaffoldPadding
|
||||
.withAdditionalPadding(
|
||||
layoutDirection = layoutDirection,
|
||||
horizontal = 10.dp,
|
||||
vertical = 8.dp
|
||||
)
|
||||
) {
|
||||
item {
|
||||
ListHeader {
|
||||
|
||||
@ -2,7 +2,6 @@ package com.bitchat.watch.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@ -14,6 +13,7 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@ -43,11 +43,17 @@ fun VerificationCodeScreen(peerID: String) {
|
||||
val listState = rememberScalingLazyListState()
|
||||
val palette = LocalBitchatPalette.current
|
||||
|
||||
ScreenScaffold(scrollState = listState) {
|
||||
ScreenScaffold(scrollState = listState) { scaffoldPadding ->
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
ScalingLazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 8.dp)
|
||||
contentPadding = scaffoldPadding
|
||||
.withAdditionalPadding(
|
||||
layoutDirection = layoutDirection,
|
||||
horizontal = 10.dp,
|
||||
vertical = 8.dp
|
||||
)
|
||||
) {
|
||||
item {
|
||||
ListHeader {
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
package com.bitchat.watch.ui
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Preserve the responsive, shape-aware padding supplied by Wear Material while allowing a
|
||||
* screen to reserve additional room for its own content or floating controls.
|
||||
*/
|
||||
internal fun PaddingValues.withAdditionalPadding(
|
||||
layoutDirection: LayoutDirection,
|
||||
horizontal: Dp = 0.dp,
|
||||
vertical: Dp = 0.dp
|
||||
): PaddingValues {
|
||||
return PaddingValues(
|
||||
start = calculateStartPadding(layoutDirection) + horizontal,
|
||||
top = calculateTopPadding() + vertical,
|
||||
end = calculateEndPadding(layoutDirection) + horizontal,
|
||||
bottom = calculateBottomPadding() + vertical
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the responsive horizontal inset supplied by Wear Material while replacing its vertical
|
||||
* padding with screen-owned clearances. This keeps the lazy list full-screen for correct Wear
|
||||
* transformation and scroll calculations without stacking duplicate vertical insets.
|
||||
*/
|
||||
internal fun PaddingValues.withVerticalClearance(
|
||||
layoutDirection: LayoutDirection,
|
||||
top: Dp,
|
||||
bottom: Dp
|
||||
): PaddingValues {
|
||||
return PaddingValues(
|
||||
start = calculateStartPadding(layoutDirection),
|
||||
top = top,
|
||||
end = calculateEndPadding(layoutDirection),
|
||||
bottom = bottom
|
||||
)
|
||||
}
|
||||
|
||||
private fun PaddingValues.calculateStartPadding(layoutDirection: LayoutDirection): Dp =
|
||||
when (layoutDirection) {
|
||||
LayoutDirection.Ltr -> calculateLeftPadding(layoutDirection)
|
||||
LayoutDirection.Rtl -> calculateRightPadding(layoutDirection)
|
||||
}
|
||||
|
||||
private fun PaddingValues.calculateEndPadding(layoutDirection: LayoutDirection): Dp =
|
||||
when (layoutDirection) {
|
||||
LayoutDirection.Ltr -> calculateRightPadding(layoutDirection)
|
||||
LayoutDirection.Rtl -> calculateLeftPadding(layoutDirection)
|
||||
}
|
||||
@ -10,6 +10,102 @@ import org.junit.Test
|
||||
|
||||
class ChatAutoScrollTest {
|
||||
|
||||
@Test
|
||||
fun `new scroll range from appended message keeps follow intent`() {
|
||||
val updated = updatedChatScrollIntent(
|
||||
current = ChatScrollIntentState(),
|
||||
snapshot = ChatScrollSnapshot(
|
||||
canScrollForward = true,
|
||||
isScrollInProgress = false,
|
||||
position = 100
|
||||
),
|
||||
previousPosition = 100
|
||||
)
|
||||
|
||||
assertEquals(ChatScrollIntentState(), updated)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `user scroll away disables follow until list reaches newest again`() {
|
||||
val browsingHistory = updatedChatScrollIntent(
|
||||
current = ChatScrollIntentState(),
|
||||
snapshot = ChatScrollSnapshot(
|
||||
canScrollForward = true,
|
||||
isScrollInProgress = true,
|
||||
position = 60
|
||||
),
|
||||
previousPosition = 100
|
||||
)
|
||||
assertFalse(browsingHistory.followsNewest)
|
||||
assertFalse(browsingHistory.controlsVisible)
|
||||
|
||||
val dockedAgain = updatedChatScrollIntent(
|
||||
current = browsingHistory,
|
||||
snapshot = ChatScrollSnapshot(
|
||||
canScrollForward = false,
|
||||
isScrollInProgress = false,
|
||||
position = 200
|
||||
),
|
||||
previousPosition = 60
|
||||
)
|
||||
assertEquals(ChatScrollIntentState(), dockedAgain)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `slow scroll away accumulates intent across sub-threshold updates`() {
|
||||
var state = ChatScrollIntentState()
|
||||
var previousPosition = 100
|
||||
|
||||
listOf(94, 88, 82, 76).forEach { position ->
|
||||
state = updatedChatScrollIntent(
|
||||
current = state,
|
||||
snapshot = ChatScrollSnapshot(
|
||||
canScrollForward = true,
|
||||
isScrollInProgress = true,
|
||||
position = position
|
||||
),
|
||||
previousPosition = previousPosition
|
||||
)
|
||||
previousPosition = position
|
||||
state = updatedChatScrollIntent(
|
||||
current = state,
|
||||
snapshot = ChatScrollSnapshot(
|
||||
canScrollForward = true,
|
||||
isScrollInProgress = false,
|
||||
position = position
|
||||
),
|
||||
previousPosition = previousPosition
|
||||
)
|
||||
}
|
||||
|
||||
assertFalse(state.followsNewest)
|
||||
assertFalse(state.controlsVisible)
|
||||
assertEquals(0, state.accumulatedDeltaPx)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `slow scroll toward newest reveals controls without restoring follow early`() {
|
||||
var state = ChatScrollIntentState(followsNewest = false, controlsVisible = false)
|
||||
var previousPosition = 60
|
||||
|
||||
listOf(66, 72, 78, 84).forEach { position ->
|
||||
state = updatedChatScrollIntent(
|
||||
current = state,
|
||||
snapshot = ChatScrollSnapshot(
|
||||
canScrollForward = true,
|
||||
isScrollInProgress = true,
|
||||
position = position
|
||||
),
|
||||
previousPosition = previousPosition
|
||||
)
|
||||
previousPosition = position
|
||||
}
|
||||
|
||||
assertFalse(state.followsNewest)
|
||||
assertEquals(true, state.controlsVisible)
|
||||
assertEquals(0, state.accumulatedDeltaPx)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `newest scroll waits until appended message is measured`() = runTest {
|
||||
val measuredLayouts = MutableStateFlow(MeasuredChatLayout(3, null))
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
package com.bitchat.watch.ui
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class WearContentPaddingTest {
|
||||
|
||||
@Test
|
||||
fun `additional padding preserves scaffold insets`() {
|
||||
val resolved = PaddingValues(start = 10.dp, top = 18.dp, end = 12.dp, bottom = 20.dp)
|
||||
.withAdditionalPadding(
|
||||
layoutDirection = LayoutDirection.Ltr,
|
||||
horizontal = 10.dp,
|
||||
vertical = 8.dp
|
||||
)
|
||||
|
||||
assertEquals(20.dp, resolved.calculateLeftPadding(LayoutDirection.Ltr))
|
||||
assertEquals(22.dp, resolved.calculateRightPadding(LayoutDirection.Ltr))
|
||||
assertEquals(26.dp, resolved.calculateTopPadding())
|
||||
assertEquals(28.dp, resolved.calculateBottomPadding())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vertical clearance preserves responsive width and replaces scaffold height inset`() {
|
||||
val resolved = PaddingValues(start = 10.dp, top = 20.dp, end = 12.dp, bottom = 20.dp)
|
||||
.withVerticalClearance(
|
||||
layoutDirection = LayoutDirection.Ltr,
|
||||
top = 30.dp,
|
||||
bottom = 64.dp
|
||||
)
|
||||
|
||||
assertEquals(10.dp, resolved.calculateLeftPadding(LayoutDirection.Ltr))
|
||||
assertEquals(12.dp, resolved.calculateRightPadding(LayoutDirection.Ltr))
|
||||
assertEquals(30.dp, resolved.calculateTopPadding())
|
||||
assertEquals(64.dp, resolved.calculateBottomPadding())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `additional padding preserves logical start and end in rtl`() {
|
||||
val resolved = PaddingValues(start = 10.dp, top = 18.dp, end = 12.dp, bottom = 20.dp)
|
||||
.withAdditionalPadding(
|
||||
layoutDirection = LayoutDirection.Rtl,
|
||||
horizontal = 10.dp,
|
||||
vertical = 8.dp
|
||||
)
|
||||
|
||||
assertEquals(22.dp, resolved.calculateLeftPadding(LayoutDirection.Rtl))
|
||||
assertEquals(20.dp, resolved.calculateRightPadding(LayoutDirection.Rtl))
|
||||
assertEquals(26.dp, resolved.calculateTopPadding())
|
||||
assertEquals(28.dp, resolved.calculateBottomPadding())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vertical clearance preserves logical start and end in rtl`() {
|
||||
val resolved = PaddingValues(start = 10.dp, top = 20.dp, end = 12.dp, bottom = 20.dp)
|
||||
.withVerticalClearance(
|
||||
layoutDirection = LayoutDirection.Rtl,
|
||||
top = 30.dp,
|
||||
bottom = 64.dp
|
||||
)
|
||||
|
||||
assertEquals(12.dp, resolved.calculateLeftPadding(LayoutDirection.Rtl))
|
||||
assertEquals(10.dp, resolved.calculateRightPadding(LayoutDirection.Rtl))
|
||||
assertEquals(30.dp, resolved.calculateTopPadding())
|
||||
assertEquals(64.dp, resolved.calculateBottomPadding())
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user