fold_transition 0.1.0
fold_transition: ^0.1.0 copied to clipboard
Fold transitions for Android foldables. Tracks a continuous hinge angle when the sensor allows it, and plays a fixed-duration animation on quantized hardware such as Galaxy Z Fold.
fold_transition #
Fold transitions for Android foldables. 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.0
Then:
flutter pub get
Until the first pub.dev release, 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.
Desktop, web, emulator #
There is no hinge sensor. Call animator.debugTrigger() or feed a
MockHingeSource.
BendEffect uses FragmentProgram.fromAsset. That throws on Flutter web.
FoldTransition catches the failure, logs it, and draws nothing — the app
must not crash. Blur-dissolve does not need a shader.
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 # attached Android foldable
flutter run -d chrome # Trigger button; mode stays triggered
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 |
| Emulator / desktop / web | None | Triggered — use mock or Trigger |
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.0
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 (plugin
platforms:inpubspec.yamlalready lists android, linux, macos, windows, web)
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.0
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.yamlversion - ❌ README install snippet uses that version
- ❌
dart format --set-exit-if-changed . - ❌
dart analyze— no issues - ❌
flutter testandcd example && flutter test - ❌ Example still has the Activity
configChanges+resizeableActivity - ❌
screenshots/blur_dissolve.pngpresent - ❌ No
SSENSOR/folding_angleinlib/,android/, example sources - ❌
dart pub publish --dry-run— expected files only, warnings understood - ❌ Git working tree clean
- ❌
dart pub publish - ❌ Confirm https://pub.dev/packages/fold_transition/versions
- ❌ Transfer 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.
