Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f102835c46 | ||
|
|
bdcf2b3823 |
@@ -83,8 +83,10 @@ fun calendarSlideTransition(
|
||||
initialContentExit =
|
||||
slideOutHorizontally(spec) { w -> -dir * w / SLIDE_TRAVEL_DIVISOR } + fadeOut(fadeSpec),
|
||||
// AnimatedContent clips to the animating container by default, which
|
||||
// shears the pages against the viewport edge as they pass. There is no
|
||||
// size change here to contain — both pages are the same grid.
|
||||
// shears the pages against the viewport edge as they pass. Left off even
|
||||
// where the two pages differ in height — the split grid stands as many
|
||||
// rows as its month spans (#162) — since a page sliding out over the row
|
||||
// below it reads as travel, and the shear reads as a fault.
|
||||
sizeTransform = SizeTransform(clip = false),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -197,18 +196,26 @@ fun rememberTimelinePinchZoom(
|
||||
* names, jumping the whole column as a pinch drifts across each half pixel.
|
||||
* Pinning the hour to whole pixels keeps every part of the timeline on one grid.
|
||||
*
|
||||
* The bounds themselves are pulled onto that grid too, each in the direction
|
||||
* that keeps its own promise — up for the fill floor, so no dead space opens
|
||||
* under midnight, down for the ceiling. A fractional bound would be a height the
|
||||
* pinch can be held against but never actually land on, and the difference feeds
|
||||
* the focal anchor a scroll correction on every frame the fingers sit still.
|
||||
* The ceiling is pulled onto that grid too, downwards, so it stays a height the
|
||||
* pinch can actually land on.
|
||||
*
|
||||
* [fillPx] is deliberately *not* rounded (#290). It is the one height the whole
|
||||
* day exactly fills the viewport at, and it is the same value
|
||||
* [TimelineScale.FitDay] resolves to — rounding it up by the fraction of a pixel
|
||||
* that 24 hours don't divide the viewport into leaves the timeline a pixel per
|
||||
* hour taller than its own viewport, so a pinched-all-the-way-out day still
|
||||
* scrolls a hair and bounces off Android's overscroll stretch, while the
|
||||
* identical FitDay preset sits still. Being the clamp result rather than a bound
|
||||
* the gesture is merely held against, it is a height the pinch does land on: the
|
||||
* next frame reads it back unchanged and the focal anchor is handed nothing to
|
||||
* correct.
|
||||
*/
|
||||
internal fun pinchedHourHeightPx(target: Float, fillPx: Float, maxPx: Float): Float =
|
||||
// Filling the viewport wins over the ceiling: on a screen tall enough for
|
||||
// the two to disagree, dead space is the worse of the two failures.
|
||||
target.roundToInt().toFloat()
|
||||
.coerceAtMost(floor(maxPx))
|
||||
.coerceAtLeast(ceil(fillPx))
|
||||
.coerceAtLeast(fillPx)
|
||||
|
||||
/**
|
||||
* The scroll offset that keeps the moment under [centroidY] under it after the
|
||||
|
||||
@@ -1141,10 +1141,10 @@ private val MONTH_EXPAND_THRESHOLD = 48.dp
|
||||
* lists whatever day is selected — and a downward drag trades the pane away for
|
||||
* the full paged grid, an upward one brings it back (#53).
|
||||
*
|
||||
* The grid slides between months like the paged style, which it can only do
|
||||
* because it always reserves [SPLIT_GRID_ROWS] rows. Sized to its own month it
|
||||
* stood 4–6 rows tall, so every swipe shunted the pane up or down by a row on
|
||||
* top of swapping the grid — the pane now holds still and only the grid moves.
|
||||
* The grid slides between months like the paged style, and stands only as many
|
||||
* rows tall as its own month spans (#162). A swipe between a five-row month and
|
||||
* a six-row one therefore moves the pane by a row as well as swapping the grid;
|
||||
* the row it hands back is worth more to the pane than a still edge is.
|
||||
*
|
||||
* Expansion is deliberately **not** a stored preference. It is a way to look at
|
||||
* the month you are on, not a fourth style; persisted, someone would expand it
|
||||
@@ -1441,13 +1441,6 @@ private val SPLIT_DOT_SIZE = 5.dp
|
||||
// the paged grid's lanes, so the two caps have to be the same number or a dot
|
||||
// would have no bar to become (#53).
|
||||
|
||||
/**
|
||||
* Rows the split grid always reserves — the most any month needs. A month that
|
||||
* fits in fewer pads the remainder with blank rows rather than shrinking, which
|
||||
* is what lets the pane below hold still from month to month.
|
||||
*/
|
||||
private const val SPLIT_GRID_ROWS = 6
|
||||
|
||||
/**
|
||||
* The expand handle: M3's drag-handle pill (32×4dp), in a row tall enough to be a
|
||||
* comfortable tap target on its own.
|
||||
@@ -1460,6 +1453,11 @@ private val SPLIT_HANDLE_ROW_HEIGHT = 24.dp
|
||||
* The split style's grid (#53): the month compressed to day numbers and event
|
||||
* dots, with the selected day listed underneath by [SplitDayPane].
|
||||
*
|
||||
* Only the rows the month actually spans. It used to pad every month out to six
|
||||
* so the pane below held still from page to page, but a row is a sixth of the
|
||||
* grid and a third of what the pane gets to show — too much to leave blank on
|
||||
* the months that don't need it (#162).
|
||||
*
|
||||
* Tapping selects rather than drilling into the Day view — the pane is the
|
||||
* answer to "what's on this day", so opening a whole screen for it would defeat
|
||||
* the layout. The full Day view stays one tap away on the pane's date header.
|
||||
@@ -1518,12 +1516,6 @@ internal fun SplitMonthGrid(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Hold the grid at a constant height whatever shape the month is, so the
|
||||
// pane beneath it doesn't move as you page and one month can slide over
|
||||
// another without a height change under it.
|
||||
repeat(SPLIT_GRID_ROWS - state.weeks.size) {
|
||||
Spacer(Modifier.fillMaxWidth().height(SPLIT_ROW_HEIGHT))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.jeanlucmakiola.calendula.ui.common
|
||||
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -75,17 +76,16 @@ class TimelineZoomTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a pinch held against a fractional bound stays put`() {
|
||||
// A bound that is not a whole pixel is a height the pinch can be pushed
|
||||
// against but never land on, so every frame of a held gesture would look
|
||||
// like a scale change and hand the focal anchor a scroll correction.
|
||||
fun `a pinch held against either bound stays put`() {
|
||||
val fillPx = 62.083f
|
||||
val maxPx = 616.5f
|
||||
|
||||
val floor = pinchedHourHeightPx(target = 1f, fillPx, maxPx)
|
||||
val ceiling = pinchedHourHeightPx(target = 9_000f, fillPx, maxPx)
|
||||
|
||||
assertThat(floor).isEqualTo(63f)
|
||||
// The ceiling is pulled onto the pixel grid so it stays landable; the
|
||||
// fill floor is landable as it is, being the clamp result itself.
|
||||
assertThat(floor).isEqualTo(fillPx)
|
||||
assertThat(ceiling).isEqualTo(616f)
|
||||
// Landing there and being pushed further must not move them again.
|
||||
assertThat(pinchedHourHeightPx(floor * 0.9f, fillPx, maxPx)).isEqualTo(floor)
|
||||
@@ -100,6 +100,35 @@ class TimelineZoomTest {
|
||||
assertThat(floor * 24).isAtLeast(viewport)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pinching all the way out leaves nothing to scroll`() {
|
||||
// #290: rounding the fill floor up to a whole pixel made the day one
|
||||
// pixel per hour taller than the viewport it was supposed to fill, so a
|
||||
// fully zoomed-out timeline still scrolled a hair and bounced off
|
||||
// Android's overscroll stretch -- while FitDay, at the same zoom, sat
|
||||
// still. 1490 is deliberately not divisible by 24.
|
||||
val viewport = 1490f
|
||||
val floor = pinchedHourHeightPx(target = 1f, fillPx = viewport / 24f, maxPx = 616f)
|
||||
assertThat(floor * 24).isWithin(0.01f).of(viewport)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the pinch floor is the height FitDay resolves to`() {
|
||||
// The inconsistency the issue is about: the two ways to reach "the whole
|
||||
// day on one screen" have to arrive at the same height.
|
||||
val density = Density(2.5f)
|
||||
val viewport = 596.dp
|
||||
with(density) {
|
||||
val fitDay = TimelineScale.FitDay.hourHeight(viewport).toPx()
|
||||
val pinched = pinchedHourHeightPx(
|
||||
target = 1f,
|
||||
fillPx = fillHourHeight(viewport).toPx(),
|
||||
maxPx = MAX_PINCH_HOUR_HEIGHT.toPx(),
|
||||
)
|
||||
assertThat(pinched).isWithin(0.01f).of(fitDay)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a settled pinch is what gets persisted`() {
|
||||
var persisted: TimelineScale? = null
|
||||
|
||||
@@ -11,7 +11,7 @@ composeBom = "2026.06.01"
|
||||
# Material 3 Expressive APIs currently live only in the 1.5 alpha line.
|
||||
# Pin explicitly to override the BOM (which ships stable 1.4.0).
|
||||
# Re-evaluate when 1.5.0 stable lands.
|
||||
material3 = "1.5.0-alpha28"
|
||||
material3 = "1.5.0-alpha26"
|
||||
datastore = "1.2.1"
|
||||
junit = "6.1.3"
|
||||
junitPlatform = "6.1.3"
|
||||
|
||||
Reference in New Issue
Block a user