fold_transition
Fold transitions for Android foldables. Android is the only supported platform. The package picks a driver at runtime:
- Tracked — animation follows a continuous hinge angle (Pixel Fold, Surface Duo).
- Triggered — a settled posture change plays a fixed-duration animation
(Samsung Galaxy Z Fold).
TYPE_HINGE_ANGLEthere only reports 0 / 90 / 180. The finer Samsung vendor sensor needs a signature-level permission that third-party apps cannot obtain.
Both modes expose the same Animation<double> (0 at rest, 1 fully transitioned).
Effect code does not branch on mode.
Mode is detected empirically from readings. Advertised sensor precision is not trusted — a device can claim fine resolution and still emit only three values.
Install
In the consumer app pubspec.yaml (not this plugin's):
dependencies:
fold_transition: ^0.1.1
Then:
flutter pub get
Until a hosted version is available, depend on git:
dependencies:
fold_transition:
git:
url: https://github.com/techlapse/fold_transition.git
ref: main
Required: Android manifest
A plugin cannot inject this into the consumer's manifest. Without it, Android recreates the Activity on unfold and there is no continuous state left to animate.
In android/app/src/main/AndroidManifest.xml on the launcher activity:
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|uiMode|density|locale|fontScale|layoutDirection"
android:resizeableActivity="true"
... >
The example app already has this. Copy it into every real host app.
Minimum Android for live hinge angle: API 30 (Sensor.TYPE_HINGE_ANGLE).
The plugin still loads on older APIs; HingeCapability.hasAngleSensor is false
and you drive the effect with debugTrigger() or MockHingeSource.
Usage
class _InboxState extends State<Inbox> with SingleTickerProviderStateMixin {
late final FoldAnimator _animator = FoldAnimator(vsync: this);
@override
void dispose() {
_animator.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FoldTransition(
animator: _animator,
effect: const BlurDissolveEffect(),
child: InboxBody(folder: widget.folder),
);
}
}
If the host State ever creates more than one AnimationController (for
example recreating FoldAnimator when the user toggles mode), use
TickerProviderStateMixin instead of SingleTickerProviderStateMixin.
Force a driver
FoldAnimator(
vsync: this,
preference: FoldModePreference.forceTriggered, // or forceTracked
);
auto (default) starts triggered and promotes to tracked after three
intermediate hinge readings in the same session. It never demotes.
Tests and debugTrigger
Widget tests and animator.debugTrigger() do not need a hinge. Live angle
and posture still require an Android device with TYPE_HINGE_ANGLE (API 30+).
Effects
| Effect | What it does |
|---|---|
BlurDissolveEffect |
Outgoing snapshot scales up, blurs, and fades. |
BendEffect |
Crease-bend via shaders/bend.frag. |
AppleDuoEffect |
Cover-to-inner glass handoff. |
Pass the effect into FoldTransition. Effects consume animation.value as
transition amount: 0 at rest, 1 fully transitioned.
Example app
cd example
flutter run -d RZCY61H3BLP # or any attached Android foldable
The example shows live mode, intermediate count, posture, degrees, sensor
capability, animation value, and whether a snapshot is held. Segmented
Auto / Tracked / Triggered rebuilds the animator so you can compare
drivers on one device. Trigger calls debugTrigger().
On a Galaxy Z Fold, force Tracked once: the animation snaps between 0 / 90 / 180. That is the public sensor, and why triggered is the default.
Stay on the inner display. Folding to the cover stops the inner Activity
(WindowStopped); Dart is paused and posture changes in that window are missed.
Device support
| Device family | Public hinge angle | Resulting mode |
|---|---|---|
| Samsung Galaxy Z Fold / Flip | Discrete 0 / 90 / 180 | Triggered |
| Pixel Fold | Continuous | Tracked (after 3 intermediate readings) |
| Surface Duo | Continuous (historically 0–360°) | Tracked |
| Android emulator (no hinge) | None | Triggered — use Trigger or MockHingeSource |
iOS, web, Windows, macOS, and Linux are not supported. This plugin registers on Android only.
Logging
Disabled by default (foldLogEnabled = false). The example sets it true in
main().
adb logcat -s flutter | grep 'fold_transition\.'
Do not grep fold_transition alone — that matches the Android package name and
floods WindowManager noise.
License
MIT. See LICENSE.
Publishing to pub.dev
This section is the complete setup for maintainers shipping
fold_transition to pub.dev. Publishing is permanent:
you cannot delete a version. You can retract within 7 days (it still exists
and still occupies that version number).
Official docs: Publishing packages.
Publish from the plugin root, never from example/
example/ is a demo app. It has publish_to: 'none', no LICENSE, no
version, and a path dependency on this plugin. That is correct. Do not
remove publish_to, and do not run publish there.
If you see A private package cannot be published, missing a "version" field, LICENSE, or Don't depend on "fold_transition" from the path source, you are in example/.
pwd
# .../Fold_Transition ← correct
# .../Fold_Transition/example ← wrong
cd /path/to/Fold_Transition
ls LICENSE pubspec.yaml lib/fold_transition.dart
dart pub publish --dry-run
1. Accounts and access
You need:
- Flutter SDK on PATH (
flutter doctorclean enough to build Android). This package: Dart^3.5.0, Flutter>=3.24.0. - A Google Account (Gmail or any Google-backed email). pub.dev uses it for upload permission.
- The legal right to ship everything in the tarball (MIT code, shaders, screenshots).
- Write access to https://github.com/techlapse/fold_transition (the
repository/issue_trackerURLs inpubspec.yaml).
Invite at least one other org member as a pub.dev uploader or verified-publisher admin so the package is not locked to a single person.
2. Verified publisher (recommended)
Do this once for the org (for example techlapse.com / techlapse.dev):
- Open pub.dev and sign in with Google.
- User menu → Create Publisher.
- Enter the domain you control.
- Complete Google Search Console verification (often a DNS TXT record). DNS can take hours.
- Invite the rest of the team as publisher members.
First version of a brand-new package cannot be published directly onto a publisher. Workaround from Dart:
dart pub publishwhile signed in as a personal Google Account.- Open
https://pub.dev/packages/fold_transition/admin. - Transfer to publisher. This cannot be reversed.
Later versions can be published by any publisher member (or by GitHub Actions once automated publishing is enabled).
3. What must exist in the repo
pub.dev builds the package page from these files. Missing any of them costs pub points or fails validation.
| File | Role |
|---|---|
pubspec.yaml |
Name, version, description (≤ 180 chars), SDK, repository, homepage, topics, screenshots, plugin platforms |
README.md |
Package page body (this file) |
CHANGELOG.md |
Scores tab + version history. Heading must match the version you publish (## 0.1.0) |
LICENSE |
Required. This repo is MIT |
lib/fold_transition.dart |
Public API barrel. Every public member needs dartdoc (public_member_api_docs) |
example/ |
Runnable app. example/pubspec.yaml keeps publish_to: 'none' — only the plugin is published, the example is bundled as a demo |
test/ |
Tests. Not required to upload, required for pub points |
screenshots/blur_dissolve.png |
Listed in pubspec.yaml screenshots: — file must exist or publish fails |
android/ |
FoldTransitionPlugin (com.techlapse.fold_transition) |
shaders/bend.frag |
Bundled with the plugin |
Do not add publish_to: 'none' on the root pubspec.yaml. That blocks
pub.dev. Only the example app uses publish_to: 'none'.
Root pubspec.yaml fields that affect the listing:
name: fold_transition
version: 0.1.1
homepage: https://github.com/techlapse/fold_transition
repository: https://github.com/techlapse/fold_transition
issue_tracker: https://github.com/techlapse/fold_transition/issues
topics:
- foldable
- animation
- hinge
- android
Description must stay ≤ 180 characters. It is already at the limit.
Never depend on git / path / non-pub hosted packages in the root
pubspec.yaml. pub.dev only allows hosted pub.dev deps and sdk: flutter.
4. GitHub repository
If origin is empty:
- Create a public GitHub repo
techlapse/fold_transition(private packages cannot be fetched by typical consumers). git remote add origin git@github.com:techlapse/fold_transition.git- Push
main. - Confirm
pubspec.yamlrepository:matches the real URL.
.gitignore already excludes .dart_tool/, /build/, /pubspec.lock (correct
for a library). If dart pub publish --dry-run warns about checked-in files
that are also gitignored, either stop ignoring them or stop tracking them.
Use .pubignore when you want different publish vs git rules:
https://dart.dev/go/pubignore
5. Versioning
| Change | Bump | Example |
|---|---|---|
| Bug fix, docs | PATCH | 0.1.0 → 0.1.1 |
| New API, compatible | MINOR | 0.1.1 → 0.2.0 |
| Breaking API | MAJOR | 0.2.0 → 1.0.0 (or 0.3.0 while still 0.x) |
| Prerelease | suffix | 0.2.0-dev.1 — hidden from normal search, does not replace README |
Every bump:
version:in rootpubspec.yaml- Matching
## x.y.zsection at the top ofCHANGELOG.md - Git tag after a successful publish:
git tag v0.1.0 && git push origin v0.1.0
You cannot reuse a version number. Yanking (retract) does not free it.
6. Pre-publish gates
Run from the package root (Fold_Transition/). Paste real output into the
PR. Zero issues, including infos.
dart format --set-exit-if-changed .
dart analyze
flutter test
cd example && flutter test
cd example && flutter build apk --debug
Package-specific checks for this plugin:
grep -rn "\.resolution" lib/src/fold_mode_detector.dart # must be empty
grep -rn "Future.delayed" test/ # must be empty
grep -rn "SSENSOR\|folding_angle" lib/ android/ example/lib example/android example/test
grep -n "controller.value =" lib/src/fold_animator.dart # only debugSetValue
Optional local pub-points preview (installs pana):
dart pub global activate pana
dart pub global run pana --no-warning .
Aim for 160/160. Typical deductions:
- Missing / thin README, CHANGELOG, or LICENSE
- Analysis warnings or infos
- Public API without dartdoc
- Missing example
- Tight SDK constraints
- Missing
repository/homepage - Platforms not detected (this plugin declares Android only)
7. Dry run (always)
dart pub publish --dry-run
Read:
- File list — cancel if you see secrets,
key.properties, keystores,.env,local.properties, hugebuild/trees, or personal screenshots. - Warnings — dirty git, gitignore mismatches, missing docs.
- Archive size — keep gzip well under 100 MB (this package is ~0.5 MB).
Do not publish until git is clean (git status) so the uploaded tree matches
the tag.
8. First publish (human)
# From a clean main, version already bumped + CHANGELOG written
dart pub publish
- Browser opens → sign in with the Google Account that should own the package.
- Confirm the file list.
- Wait a few minutes → https://pub.dev/packages/fold_transition
- Transfer to the verified publisher (Admin tab) if you created one.
- Add extra uploaders on the Admin tab if you are not using a publisher.
- Tag the commit:
git tag v0.1.0 && git push origin v0.1.0
Consumers then:
dependencies:
fold_transition: ^0.1.1
Why search does not show the package yet
The package page is live as soon as publish succeeds:
https://pub.dev/packages/fold_transition
Search is a separate index. It can take 15–30 minutes to pick up a new
package. Until then, searching fold_transition returns unrelated hits
(foldable_list, scaffolds, icons) and will not list this one. Google Search
is slower (days); new packages are delayed on purpose as anti-spam.
This package is not unlisted and not discontinued. After indexing, it
still ranks near the bottom of generic queries (fold, animation) because
ranking uses likes, downloads, and pub points. Right after upload those are
all zero until pana finishes.
There is no extra “enable search” switch. Do this:
- Open the direct URL above. If that 404s, publish did not succeed.
- Wait for the Scores tab to show real pub points (often 30–60 minutes).
- Search
"fold_transition"orpackage:fold_transitionon pub.dev. - After topics index, try
topic:foldable. - Transfer to a verified publisher (Admin tab). That is the org trust badge; it does not instantly boost rank.
- Do not mark the package Unlisted or Discontinued.
- Do not leave a prerelease (
0.1.0-dev.1) as the only version — those are hidden from normal search.
Ranking over the following weeks comes from pub points (aim 160), likes, and
apps that depend on fold_transition: ^0.1.0. The word “fold” will still
prefer older popular packages.
9. Later publishes
- Land the change on
main. - Bump
version+CHANGELOG.md. - Gates in section 6 +
dart pub publish --dry-run. dart pub publish(any publisher member / uploader).- Push tag
vX.Y.Z.
10. Automated publishing (after the first version)
https://dart.dev/tools/pub/automated-publishing
On https://pub.dev/packages/fold_transition/admin enable Publishing from
GitHub Actions. Restrict to this repo and a tag pattern such as
v[0-9]+.[0-9]+.[0-9]+*.
Then add .github/workflows/publish.yml:
name: Publish to pub.dev
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*'
jobs:
publish:
permissions:
id-token: write
uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1
Do not store a pub.dev token in GitHub Secrets. Trusted publishing uses
OIDC (id-token: write).
Suggested CI on every PR (.github/workflows/ci.yml):
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- run: dart format --set-exit-if-changed .
- run: dart analyze
- run: flutter test
11. Screenshot for the pub.dev listing
pubspec.yaml already declares:
screenshots:
- description: Blur-dissolve fold transition driven by a live hinge angle.
path: screenshots/blur_dissolve.png
The PNG must exist at that path (keep it small; this is not the example APK). Capture from the example on a foldable or from Chrome + Trigger. Do not commit unrelated device photos.
12. Retract, restore, discontinue
- Retract (Admin tab): within 7 days of publish. Stops new apps from
resolving that version. Existing
pubspec.lockpins still work. Restore within 7 days of retraction. - Prefer a new patch over retract for ordinary bugs.
- Discontinue: package stays up, badge shows, drops out of search. Optional replacement package name.
13. Release checklist
Copy this into the release PR:
CHANGELOG.mdhas##matchingpubspec.yamlversionREADME install snippet uses that versiondart format --set-exit-if-changed .dart analyze— no issuesflutter testandcd example && flutter testExample still has the ActivityconfigChanges+resizeableActivityscreenshots/blur_dissolve.pngpresentNoSSENSOR/folding_angleinlib/,android/, example sourcesdart pub publish --dry-run— expected files only, warnings understoodGit working tree cleandart pub publishConfirm https://pub.dev/packages/fold_transition/versionsTransfer to verified publisher (first release only)git tag vX.Y.Z && git push origin vX.Y.Z
14. What a correct Samsung log looks like
Inner display, one close:
fold_transition.boot: logging enabled
fold_transition.effect: prepare type=BlurDissolveEffect success=true
fold_transition.sensor: capability hasAngleSensor=true maxDegrees=180.0 ...
fold_transition.posture: from=unknown to=flat degrees=180.0
fold_transition.posture: from=flat to=halfOpen degrees=90.0
fold_transition.posture: from=halfOpen to=closed degrees=0.0
fold_transition.trigger: reason=posture mode=triggered duration=500ms
fold_transition.anim: status dismissed → forward
fold_transition.capture: taken width=… height=… attempt=1
fold_transition.anim: status forward → completed
fold_transition.capture: snapshot disposed
No mode promotion line — 0 / 90 / 180 are never intermediate.
Libraries
- fold_transition
- Fold transitions for Android foldables, tracked or triggered at runtime.