Skip to content

Animation Performance Fix (Android) - #134

Open
tifroz wants to merge 6 commits into
skiptools:mainfrom
tifroz:experimental_animation-performance
Open

Animation Performance Fix (Android)#134
tifroz wants to merge 6 commits into
skiptools:mainfrom
tifroz:experimental_animation-performance

Conversation

@tifroz

@tifroz tifroz commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Thank you for contributing to the Skip project! Please use this space to describe your change and add any labels (bug, enhancement, documentation, etc.) to help categorize your contribution.

Please review the contribution guide at https://skip.dev/docs/contributing/ for advice and guidance on making high-quality PRs.

Skip Pull Request Checklist:

  • REQUIRED: I have signed the Contributor Agreement
  • REQUIRED: I have tested my change locally with swift test
  • OPTIONAL: I have tested my change on an iOS simulator or device
  • OPTIONAL: I have tested my change on an Android emulator or device

  • AI was used to generate or assist with generating this PR. Please specify below how you used AI to help you, and what steps you have taken to manually verify the changes.

Codex generated the code with supervision, UI tests conducted manually to validate performance improvements


Summary

Animations on Android can become choppy in complex UI hierarchies when an animated state change causes unrelated subtrees to be reevaluated, re-bridged, or recomposed. That additional work competes with rendering animated frames.

androidEquatable, androidCompositionBoundary

This PR adds two explicit Android recomposition controls:

  • androidEquatable acts as an update gate. It skips reevaluating and re-bridging a subtree while its supplied Swift Equatable value remains unchanged.
  • androidCompositionBoundary gives a subtree its own retained Compose identity and lifecycle. Stable id and inputs values reuse its existing projection, while changed inputs update its content without replacing the retained host.

These APIs serve different purposes: use androidEquatable to avoid unnecessary evaluation of ordinary UI, and androidCompositionBoundary for independently retained content such as WebViews, maps, or video surfaces.

compositorTransform

compositorTransform applies scale and translation in a single Android graphics layer without relocating the Compose layout node. This is useful for native-backed surfaces such as WebViews, where keeping layout identity stable during animation avoids unnecessary layout and lifecycle work.

Usage Example

Use androidEquatable when a subtree can update as a unit and should only be reevaluated when its inputs change:

ComplexMetadataPanel(movie: movie)
    .androidEquatable(recomposeOverride: movie)

Use androidCompositionBoundary when a subtree such as a WebView, map, or video surface needs independently retained identity and lifecycle:

BrowserView(url: url)
    .androidCompositionBoundary(
        id: "browser",
        inputs: url.absoluteString
    )

Use compositorTransform when native-backed content such as a WebView must scale and translate in one Android compositor layer:

BrowserView(url: url)
    .compositorTransform(
        scaleX: scale,
        scaleY: scale,
        translationX: translationX,
        translationY: 0
    )

API Shape

extension View where Self: Equatable {
    func androidEquatable() -> AndroidEquatableView<Self, Self>
}

extension View {
    func androidEquatable<Value: Equatable>(
        recomposeOverride: Value
    ) -> AndroidEquatableView<Self, Value>

    func androidCompositionBoundary(
        id: String,
        inputs: String = ""
    ) -> some View

    func compositorTransform(
        scaleX: CGFloat,
        scaleY: CGFloat,
        translationX: CGFloat,
        translationY: CGFloat,
        anchor: UnitPoint = .center
    ) -> some View
}

Notes

This PR depends on skiptools/skip-ui#500

  • androidCompositionBoundary callers must keep id stable for retained identity and include every parent-driven update in inputs
  • compositorTransform changes rendered pixels without changing layout geometry; callers that need layout movement should continue using the standard layout modifiers
  • compositorTransform is intentionally distinct from chaining scaleEffect and offset: on Android those modifiers use separate graphics-layer and placement operations, while native-backed content such as WebViews needs scale and translation fused into one compositor layer so the Compose layout node remains stationary
  • this PR temporarily depends on the matching tifroz/skip-ui:experimental_animation-performance branch and should move back to a released SkipUI dependency after the paired SkipUI PR lands

@cla-bot cla-bot Bot added the cla-signed label Jul 29, 2026
@tifroz tifroz changed the title Experimental animation performance Animation Performance on Android Aug 6, 2026
@tifroz tifroz changed the title Animation Performance on Android Animation Performance Fix (Android) Aug 7, 2026
@tifroz

tifroz commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@marcprux pinging you for a code review (not urgent, but I think this may have fallen through the cracks)

tifroz added 6 commits August 22, 2026 13:01
- Add androidCompositionBoundary(id:inputs:) for retained Android composition roots
- Add androidEquatable wrappers to skip parent-driven recomposition
- Remove the graphicsLayerOffset bridge
- retain composition-boundary projections by instance and inputs
- preserve full Swift equality in androidEquatable
- add compositor and experimental first-render modifiers
- document Android recomposition controls and add regression tests
- verify replacing boundary inputs releases the previous projection
- verify the replacement remains retained until the boundary releases it
- document that bridged content inherits its environment and parent sizing
@tifroz
tifroz force-pushed the experimental_animation-performance branch from f3b3673 to 3d04364 Compare August 22, 2026 20:35
@marcprux

Copy link
Copy Markdown
Member

Apologies for the delay; I expect to be able to start getting back to reviewing the bigger PRs that have piled up.

Just checking whether this is ready for review? I ask only because you just pushed a few more commits…

@tifroz

tifroz commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Yes it is ready for review along the SkipUI companion (the last update was just adding some tests + documentation clarifications to tighten things up, no changes to the codebase)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants