fold_transition 0.1.1
fold_transition: ^0.1.1 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.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:fold_transition/fold_transition.dart';
void main() {
foldLogEnabled = true;
foldLog('boot', 'logging enabled');
runApp(const FoldTransitionExampleApp());
}
/// Demo gallery that plays a fold effect on posture change or Trigger.
class FoldTransitionExampleApp extends StatelessWidget {
/// Creates the example app.
const FoldTransitionExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(),
home: const FoldTransitionExamplePage(),
);
}
}
/// Hosts a [FoldAnimator] and a tile grid wrapped in [FoldTransition].
class FoldTransitionExamplePage extends StatefulWidget {
/// Creates the example page.
const FoldTransitionExamplePage({super.key});
@override
State<FoldTransitionExamplePage> createState() =>
_FoldTransitionExamplePageState();
}
class _FoldTransitionExamplePageState extends State<FoldTransitionExamplePage>
with TickerProviderStateMixin {
late FoldAnimator _animator;
var _preference = FoldModePreference.auto;
final _blur = const BlurDissolveEffect();
final _bend = BendEffect();
var _useBend = false;
var _hasSnapshot = false;
static const _palette = [
Color(0xFFE57373),
Color(0xFF64B5F6),
Color(0xFF81C784),
Color(0xFFFFB74D),
Color(0xFFBA68C8),
Color(0xFF4DB6AC),
Color(0xFFFF8A65),
Color(0xFF90A4AE),
];
@override
void initState() {
super.initState();
_animator = _createAnimator();
}
FoldAnimator _createAnimator() {
final animator = FoldAnimator(
vsync: this,
source: SensorHingeSource(),
preference: _preference,
);
animator.addListener(_rebuild);
return animator;
}
void _setPreference(FoldModePreference preference) {
if (preference == _preference) return;
final old = _animator;
old.removeListener(_rebuild);
_preference = preference;
final next = _createAnimator();
setState(() {
_hasSnapshot = false;
_animator = next;
});
old.dispose();
}
@override
void dispose() {
_animator.removeListener(_rebuild);
_animator.dispose();
super.dispose();
}
void _rebuild() {
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
final capability = _animator.capability;
return Scaffold(
backgroundColor: const Color(0xFF111111),
body: FoldTransition(
animator: _animator,
effect: _useBend ? _bend : _blur,
onSnapshotChanged: (held) {
if (mounted) setState(() => _hasSnapshot = held);
},
child: GridView.builder(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 120),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: 24,
itemBuilder: (context, index) {
return Container(
color: _palette[index % _palette.length],
alignment: Alignment.center,
child: Text(
'Tile ${index + 1}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
);
},
),
),
bottomNavigationBar: Material(
color: const Color(0xFF1C1C1C),
child: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_animator.mode.name.toUpperCase(),
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: 1.2,
),
),
const SizedBox(height: 4),
Text(
'nInt=${_animator.intermediateCount} · '
'${_animator.posture.name} · '
'${_animator.degrees.toStringAsFixed(1)}° · '
'sensor=${capability.hasAngleSensor} · '
'max=${capability.maxDegrees} · '
'v=${_animator.animation.value.toStringAsFixed(2)} · '
'snap=${_hasSnapshot ? 'held' : 'none'}',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.55),
fontSize: 11,
fontFamily: 'monospace',
),
),
const SizedBox(height: 8),
SegmentedButton<FoldModePreference>(
segments: const [
ButtonSegment(
value: FoldModePreference.auto,
label: Text('Auto'),
),
ButtonSegment(
value: FoldModePreference.forceTracked,
label: Text('Tracked'),
),
ButtonSegment(
value: FoldModePreference.forceTriggered,
label: Text('Triggered'),
),
],
selected: {_preference},
onSelectionChanged: (value) => _setPreference(value.first),
),
const SizedBox(height: 8),
Row(
children: [
FilledButton(
onPressed: _animator.debugTrigger,
child: const Text('Trigger'),
),
const SizedBox(width: 12),
Expanded(
child: SegmentedButton<bool>(
segments: const [
ButtonSegment(value: false, label: Text('Blur')),
ButtonSegment(value: true, label: Text('Bend')),
],
selected: {_useBend},
onSelectionChanged: (value) {
setState(() => _useBend = value.first);
},
),
),
],
),
],
),
),
),
),
);
}
}