Conversation
ScoreChip layered an Animatable<Float> interpolation on top of AnimatedCounter's per-digit AnimatedContent. The outer Animatable's .value was read in ScoreChip's composition scope, so the rollup ticked the entire chip ~60×/s for the duration of every score change, which in turn re-allocated 4–6 AnimatedContent slots inside AnimatedCounter on every frame. A single score event burned roughly 500 recompositions across both chips. Pass the raw value straight through. AnimatedCounter's per-digit slide already provides the motion; the outer rollup was gilding the lily. One recomp per score change instead of ~250 per chip. While here, fix the AnimatedContent slot keying — the previous comment claimed positional keying but the code matched slots by list index, so on digit-count transitions (999 → 1,000) the ones- place '9' animated into the thousands-place '1' and a digit slot morphed into the ',' separator. Wrap each AnimatedContent in key(positionFromRight) so slots stay anchored to their digit place. Also memoize formatScore() by value so a parent recomp doesn't re-format unnecessarily. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
TraySlot read `pieceAlpha` and `slotBg` via `by` delegates on
animateFloatAsState / animateColorAsState, so each frame of those
animations re-ran the whole TraySlot composition — Layout Inspector
showed 292 recomp / 0 skips over a normal play session, with three
slots affected.
Drop the `by` delegates, keep the State<Float> / State<Color>
references, and read them in draw phase instead:
- slotBg → drawBehind { drawRect(slotBgState.value) } replaces
Modifier.background(slotBg). The color tick now invalidates only
draw, not composition.
- pieceAlpha → applied via a wrapping Modifier.graphicsLayer
{ alpha = pieceAlphaState.value } around MiniPiece, instead of
being baked into the Color passed as a parameter.
Same pattern the breath/wiggle animations in this file already used
(see existing comment at line 163). After this, TraySlot drops to
36 / 0 (-87%) and BlockPiece skips 100% of its calls.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The iOS banner slot reserved 0dp until the ad reported success, but the
Google Mobile Ads SDK reads GADBannerView.bounds at .load() time and
rejects every request whose container has zero width or height — before
the request even reaches the network. Result: chicken-and-egg, no ad
ever loaded on iOS. Logs were full of
[AdCoordinator] banner failed to load:
Error Domain=com.google.admob Code=0 "Invalid ad width or height."
(Response ID: null)
with the cascading WebContent / RBS noise that comes from WebKit
spinning up an ad container, failing, and tearing down.
Fix: reserve 50dp up-front so the BannerView has valid bounds at load
time, then collapse to 0dp only after the SDK reports a real load
failure (offline / no-fill). If a later refresh succeeds the slot
expands back to 50dp. Matches the Android UX (no empty banner-sized
hole when there's no ad to show) while accommodating the iOS SDK's
stricter bounds check that Android's AdView quietly works around via
its declared AdSize.BANNER intrinsic size.
Wiring:
- IosAdBridge.makeBannerView gains a third onFailed: () -> Unit param
- AdCoordinator.makeBannerView forwards onFailed to BannerDelegate
- BannerDelegate.didFailToReceiveAdWithError invokes it
- AdBanner.ios.kt tracks isAdLoaded + hasFailed; visible iff loaded or
not-yet-failed
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
popIn used an aggressive anisotropic squash (scaleX 1.25 / scaleY 0.65, recoil 1.18) plus a per-cell (x+y) * 25L wave delay. Both were tuned for a single isolated cell and backfired on multi-cell pieces: at peak squash each cell intruded ~12% past its grid bounds into the neighbour's slot (the inter-cell gap is only 2dp ≈ 8%), and the wave delay staggered that deformation so different cells of the same piece were at clashing scales at the same instant. Testers reported "ghost cells" and a jerky placement animation. Pull the squash amplitudes way down (1.05 / 0.95 / 1.02) so the cell stays inside its grid slot throughout the animation. Switch all eases to FastOutSlowInEasing and raise the recoil spring's damping ratio to 0.8 with stiffness 450 so the settle is critically-damped rather than ringing. Drop the wave delay at the call site so all cells of a placed piece animate in lockstep. Total duration is still ~250ms; the visible collision is gone and the motion reads as a gentle settle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Polish + perf release. Five commits, all responding to specific tester
reports or Layout Inspector measurements taken during 1.4.6 testing.
User-visible fixes
Placement animation: "ghost cells" / jerky bounce on multi-cell pieces.
popIn used an aggressive anisotropic squash (scaleX 1.25 / scaleY 0.65)
plus a per-cell
(x+y) * 25Lwave delay. Both were tuned for a singleisolated cell. On a 2×2 or larger piece each cell intruded ~12% past
its grid slot into the neighbour's (gap is only 2dp ≈ 8%), and the
wave staggered the deformation so different cells of the same piece
had clashing scales at the same frame — testers reported it as
jittery, with extra cells momentarily appearing. Squash pulled down
to 1.05 / 0.95 / 1.02, eases switched to FastOutSlowInEasing, recoil
spring critically-damped (dampingRatio 0.8 / stiffness 450), wave
delay dropped so cells of a placed piece animate in lockstep.
(5da72f8)
iOS AdMob banner never loaded. The slot reserved 0dp until the ad
reported success, but the Google Mobile Ads SDK reads
GADBannerView.boundsat.load()time and rejects every requestwhose container has zero size — chicken-and-egg, no ad ever loaded.
Logs were full of
"Invalid ad width or height." (Response ID: null)with cascading WebContent / RBS noise as WebKit spun up the ad
container, failed, and tore down. Reserve 50dp up-front so the
BannerView has valid bounds at load time; collapse to 0dp only after
a real load failure (offline / no-fill), expand back when a later
refresh succeeds. Matches Android's "no ad = no slot" UX while
accommodating the iOS SDK's stricter bounds check.
(9bf6f4c)
Performance — Compose recomposition cleanup
Measurements taken with Layout Inspector during a normal play session.
Each fix is keyed to specific numbers we saw.
ScoreChip: 24 / 0 → 0 / 29 (100% skip). ScoreChip wrapped
AnimatedCounterin anAnimatable<Float>tally-rollup whose.valuewas read in the chip's composition scope. Every frame of therollup recomposed the whole chip, which cascaded into AnimatedCounter
re-allocating 4–6 AnimatedContent slots per chip per frame — ~500
recompositions per score event across both chips. AnimatedCounter
already animates each digit independently; the outer rollup was
gilding the lily. Removing it drops the per-event count to 1.
Also fixed: AnimatedContent slots weren't keyed by digit position
(the existing comment claimed they were, but
key(...)was missing),so on transitions like 999 → 1,000 the ones-place '9' animated into
the thousands-place '1' and a digit slot morphed into the ','.
(e65b6d6)
TraySlot: 292 / 0 → 36 / 0 (-87% recomp). Two State<Float|Color>
values inside TraySlot (
pieceAlpha,slotBg) were read viabydelegates in composition. Each animation tick recomposed all three
slots. Drop the
by, keep the State refs, read them in draw phasevia
Modifier.drawBehind(for the background color) and a wrappingModifier.graphicsLayer { alpha = ... }around MiniPiece (for thedim). Same pattern the file's breath/wiggle animations already used.
BlockPiece consequently went from 51 / 76 to 0 / 83 (100% skip)
because TraySlot stopped dragging it into recompositions.
(6b3e288)
Versioning
appVersionName1.4.6 → 1.4.7appVersionCode10 → 11MARKETING_VERSION1.4.6 → 1.4.7,CURRENT_PROJECT_VERSION10 → 11fastlane/.../changelogs/11.txtv1.4.7Known / not in scope
holders @stable globally) was prototyped during measurement —
it would push
GridCellfrom 83 / 99 (54% skip) to ~95% skip andinverts
GameGrid's 182 / 0 to similar. The remainingGameGrid: 182 / 0looks bad in the inspector but is harmless in practice:its children (GridCell, BlockPiece) skip, so its body only runs a
cheap for-loop with parameter compares. Deferred to keep this PR
focused on user-visible wins.
installs its own SIGABRT/SIGBUS/SIGSEGV handlers, Crashlytics warns
it may not catch crashes inside the SDK). Known SDK incompatibility;
not addressed here.
block_place.mp3,line_clear_1..4.mp3)flagged in 1.4.6 are still missing — placement and clear sounds are
silent on both platforms. The iOS lazy-load wired in 1.4.6 will pick
them up automatically once the files are added.