Skip to content

Fix StickyHeaderContainer header transitions (scroll-driven push)#4851

Merged
shai-almog merged 5 commits into
masterfrom
fix/sticky-header-scroll-driven-transitions
May 2, 2026
Merged

Fix StickyHeaderContainer header transitions (scroll-driven push)#4851
shai-almog merged 5 commits into
masterfrom
fix/sticky-header-scroll-driven-transitions

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

Fixes #4849.

Summary

  • Replaces the time-based after-the-fact header transition in StickyHeaderContainer with a scroll-driven push so the pinned header reacts in lockstep with the user's finger instead of running a 250ms animation after the swap.
  • Eliminates the "header B slides under A and then suddenly reappears below A" jump and the boundary jitter on slow scrolls.
  • Reworks all three transition styles to behave as the issue describes:
    • TRANSITION_SLIDE — the rising header pushes the pinned one up by the overlap amount; both move at scroll speed and the swap at the boundary is visually invisible.
    • TRANSITION_NONE — the pinned host is hidden during the overlap so the rising header (already in the scroller) naturally covers the slot.
    • TRANSITION_FADE — the pinned header's opacity drops linearly from 255 to 0 as the next section closes the gap, revealing it underneath.

Implementation notes

  • New pushOffset derived every scroll event from activeHeader.height - next.relY drives all three styles.
  • Removed the time-based machinery: outgoing snapshot, transitionStartMs, animate() registration, and the paintGlass overlay draw. These were the source of the jitter (per-paint setY perturbations to unwind a timeline that was disconnected from scroll).
  • setTransitionDurationMillis / getTransitionDurationMillis retained as no-ops for API compatibility; isTransitionInProgress and getTransitionProgress now reflect scroll-driven progress within the push window.

Test plan

  • StickyHeaderContainerTest rewritten for the new model — 17 tests covering activation, deactivation, slide push offset, NONE host hiding, FADE alpha ramp, and progress-tracks-scroll. All pass against mvn -f maven/core-unittests test -Dtest=StickyHeaderContainerTest.
  • Slow-scroll screenshot tests (StickyHeaderSlideTransitionScreenshotTest, StickyHeaderFadeTransitionScreenshotTest) updated to step the scroll position through the push window across the 6 grid frames instead of jumping the scroll once and stepping AnimationTime.
  • Manual verification on the simulator using the snippet from the class Javadoc to confirm the slow-scroll behaviour matches the issue's expected description.

🤖 Generated with Claude Code

Replaces the time-based after-the-fact transition with scroll-driven
push behavior so the pinned header reacts in lockstep with the user's
finger:

- TRANSITION_SLIDE: as the next section's header rises into the slot
  it pushes the pinned header up by the overlap amount, replacing it
  seamlessly when the rising header reaches the top.
- TRANSITION_NONE: the pinned host is hidden during the overlap so
  the rising header (in the scroller) covers the slot naturally.
- TRANSITION_FADE: the pinned header's opacity drops from 255 to 0
  as the next section closes the gap, revealing it underneath.

Removes the time-based machinery (snapshot, animator, paintGlass
overlay) that caused the boundary jitter and the "B slides under A
then suddenly reappears below A" jump. transitionDurationMillis is
retained for API compatibility but no longer affects visuals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 2, 2026

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 2, 2026

Compared 86 screenshots: 86 matched.

Native Android coverage

  • 📊 Line coverage: 9.87% (5364/54363 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 7.75% (26316/339620), branch 3.59% (1169/32596), complexity 4.55% (1419/31207), method 7.97% (1161/14574), class 13.02% (254/1951)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 9.87% (5364/54363 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 7.75% (26316/339620), branch 3.59% (1169/32596), complexity 4.55% (1419/31207), method 7.97% (1161/14574), class 13.02% (254/1951)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 801.000 ms
Base64 CN1 encode 208.000 ms
Base64 encode ratio (CN1/native) 0.260x (74.0% faster)
Base64 native decode 725.000 ms
Base64 CN1 decode 356.000 ms
Base64 decode ratio (CN1/native) 0.491x (50.9% faster)
Image encode benchmark status skipped (SIMD unsupported)

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 2, 2026

Compared 86 screenshots: 86 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 366 seconds

Build and Run Timing

Metric Duration
Simulator Boot 81000 ms
Simulator Boot (Run) 3000 ms
App Install 24000 ms
App Launch 28000 ms
Test Execution 293000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1565.000 ms
Base64 CN1 encode 1996.000 ms
Base64 encode ratio (CN1/native) 1.275x (27.5% slower)
Base64 native decode 1052.000 ms
Base64 CN1 decode 1520.000 ms
Base64 decode ratio (CN1/native) 1.445x (44.5% slower)
Base64 SIMD encode 563.000 ms
Base64 encode ratio (SIMD/native) 0.360x (64.0% faster)
Base64 encode ratio (SIMD/CN1) 0.282x (71.8% faster)
Base64 SIMD decode 584.000 ms
Base64 decode ratio (SIMD/native) 0.555x (44.5% faster)
Base64 decode ratio (SIMD/CN1) 0.384x (61.6% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 76.000 ms
Image createMask (SIMD on) 19.000 ms
Image createMask ratio (SIMD on/off) 0.250x (75.0% faster)
Image applyMask (SIMD off) 238.000 ms
Image applyMask (SIMD on) 127.000 ms
Image applyMask ratio (SIMD on/off) 0.534x (46.6% faster)
Image modifyAlpha (SIMD off) 178.000 ms
Image modifyAlpha (SIMD on) 97.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.545x (45.5% faster)
Image modifyAlpha removeColor (SIMD off) 208.000 ms
Image modifyAlpha removeColor (SIMD on) 130.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.625x (37.5% faster)
Image PNG encode (SIMD off) 1706.000 ms
Image PNG encode (SIMD on) 1330.000 ms
Image PNG encode ratio (SIMD on/off) 0.780x (22.0% faster)
Image JPEG encode 1034.000 ms

shai-almog and others added 4 commits May 2, 2026 08:59
iOS and Android emulator captures of StickyHeaderScreenshotTest,
StickyHeaderSlideTransitionScreenshotTest and (Android only)
StickyHeaderFadeTransitionScreenshotTest now reflect the new
scroll-driven push behavior.

The iOS Fade test PNG was not present in the artifact this run
(only the smaller preview chunks were emitted via the device log,
not the full-size PNG chunks) so its golden is left untouched and
will be refreshed once the next iOS run captures it cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous CI run dropped the full PNG chunks for this screenshot,
emitting only the preview, so it was skipped in the prior golden
update. The latest run captured the full PNG cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous retry path silently fell back to `monkey ... >/dev/null
2>&1 || true` whenever `cmd package resolve-activity --brief` returned
output that didn't fit the expected `pkg/.Activity` shape — which is
common on newer Android (the brief output sometimes prepends a
`priority=...` line and adds leading whitespace). When that happened
the retry waited the full 600s with zero diagnostic signal and
failed the build.

Replaces the resolve+monkey dance with `am start -W -a MAIN -c LAUNCHER
-p $PACKAGE_NAME`, which launches the app by intent filter and prints
a `Status=…` line we can parse. The output is logged verbatim, the
process is verified with `pidof` before committing to the 10-minute
wait, and the retry skips the wait entirely if no PID is detected so
CI fails fast instead of burning ten minutes on a no-op.

Also uploads `connectedAndroidTest*.log` as an artifact so the next
decode flake — if there is one — is reproducible from CI.

Comments in both files spell out the right mechanics and explicitly
warn against re-introducing output redirects, which were the root
cause of the silent failure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The new diagnostics surfaced the actual root cause that was hidden
behind the previous silent-monkey fallback: Gradle's
connectedAndroidTest task uninstalls BOTH APKs at teardown
(`DeviceConnector ... uninstalling com.codenameone.examples.hellocodenameone`
appears in the gradle log next to the .test uninstall). By the time
the retry block ran, the package was gone and `am start` returned
"Activity not started, unable to resolve Intent ... pkg=...".

Fix: before relaunching, check `pm list packages` (with `grep -x` to
avoid `<pkg>.test` substring matches) and `adb install -r` the main
APK from `$GRADLE_PROJECT_DIR/app/build/outputs/apk/debug/app-debug.apk`
when it's missing. We don't need the .test APK — the main APK
contains Cn1ssDeviceRunner, which re-runs the entire suite when the
launcher activity starts.

Both branches of the install logic log their action and any failure
output so the next debugger can tell at a glance whether the install
succeeded, the APK was missing, or something further down (like
`am start`) is the real issue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog shai-almog merged commit 72e4758 into master May 2, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StickyHeaderContainer header transitions are not working correctly

1 participant