Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Created June 20, 2023 07:52
Show Gist options
  • Save PlugFox/c50d4b8c7a056057f0b1707adbcf4704 to your computer and use it in GitHub Desktop.
Save PlugFox/c50d4b8c7a056057f0b1707adbcf4704 to your computer and use it in GitHub Desktop.
Sizer and AdaptiveWidget
import 'package:flutter/widgets.dart';
import 'sizer.dart';
class AdaptiveWidget extends StatefulWidget {
const AdaptiveWidget({
required this.compactChild,
required this.extendedChild,
this.alignment = Alignment.center,
super.key,
});
final Widget compactChild;
final Widget extendedChild;
final Alignment alignment;
@override
State<AdaptiveWidget> createState() => _AdaptiveWidgetState();
}
class _AdaptiveWidgetState extends State<AdaptiveWidget> {
static bool fits(Size size, Size constraints) =>
size.width <= constraints.width && size.height <= constraints.height;
final ValueNotifier<Size> _largeSizeNotifier = ValueNotifier(Size.zero);
bool get hasNotSizedYet => _largeSizeNotifier.value.isEmpty;
@override
void dispose() {
_largeSizeNotifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => RepaintBoundary(
child: LayoutBuilder(
builder: (context, constraints) => Stack(
children: <Positioned>[
// Compact widget
Positioned.fill(
child: ValueListenableBuilder<Size>(
valueListenable: _largeSizeNotifier,
builder: (context, size, child) {
final offstage =
hasNotSizedYet || fits(size, constraints.biggest);
return Offstage(
offstage: offstage,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 350),
opacity: offstage ? 0 : 1,
child: child,
),
);
},
child: FittedBox(
clipBehavior: Clip.hardEdge,
fit: BoxFit.scaleDown,
alignment: widget.alignment,
child: widget.compactChild,
),
),
),
// Large widget
Positioned.fill(
child: ValueListenableBuilder<Size>(
valueListenable: _largeSizeNotifier,
builder: (context, size, child) {
final offstage =
hasNotSizedYet || !fits(size, constraints.biggest);
return Offstage(
offstage: offstage,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 350),
opacity: offstage ? 0 : 1,
child: child,
),
);
},
child: FittedBox(
clipBehavior: Clip.hardEdge,
fit: BoxFit.none,
alignment: widget.alignment,
child: Sizer(
onSizeChanged: (size) => _largeSizeNotifier.value = size,
child: widget.extendedChild,
),
),
),
),
],
),
),
);
}
void main() => runApp(const App());
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) => const MaterialApp(
home: Scaffold(
body: Center(
child: Padding(
padding: EdgeInsets.all(16),
child: SizedBox(
height: 600,
width: 700,
child: Center(
child: Card(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Center(
child: AdaptiveWidget(
compactChild: SmallWidget(),
extendedChild: LargeWidget(),
),
),
),
),
),
),
),
),
),
);
}
class SmallWidget extends StatelessWidget {
const SmallWidget({super.key});
@override
Widget build(BuildContext context) => const SizedBox(
height: 100,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ColoredBox(
color: Colors.green,
child: SizedBox(
width: 100,
child: Center(
child: Text('Small'),
),
),
),
SizedBox(width: 8),
ColoredBox(
color: Colors.green,
child: SizedBox(
width: 100,
child: Center(
child: Text('Widget'),
),
),
),
],
),
);
}
class LargeWidget extends StatelessWidget {
const LargeWidget({super.key});
@override
Widget build(BuildContext context) => const SizedBox(
height: 100,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ColoredBox(
color: Colors.red,
child: SizedBox(
width: 150,
child: Center(
child: Text('VERY'),
),
),
),
SizedBox(width: 8),
ColoredBox(
color: Colors.red,
child: SizedBox(
width: 150,
child: Center(
child: Text('LARGE'),
),
),
),
SizedBox(width: 8),
ColoredBox(
color: Colors.red,
child: SizedBox(
width: 150,
child: Center(
child: Text('WIDGET'),
),
),
),
],
),
);
}
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
/// Measure and call callback after child size changed.
class Sizer extends SingleChildRenderObjectWidget {
const Sizer({
required super.child,
this.onSizeChanged,
this.dispatchNotification = false,
super.key,
});
/// Callback when child size changed and after layout rebuild.
final void Function(Size size)? onSizeChanged;
/// Send [SizeChangedLayoutNotification] notification.
final bool dispatchNotification;
@override
RenderObject createRenderObject(BuildContext context) =>
_SizerRenderObject((Size size) {
final fn = onSizeChanged;
if (fn != null) {
SchedulerBinding.instance.addPostFrameCallback((_) => fn(size));
}
if (dispatchNotification) {
SizeChangedNotification(size).dispatch(context);
}
});
}
/// Render object for [Sizer].
class _SizerRenderObject extends RenderProxyBox {
_SizerRenderObject(this.onLayoutChangedCallback);
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) =>
super.debugFillProperties(
properties..add(StringProperty('oldSize', size.toString())),
);
/// Callback when child size changed and after layout rebuild.
final void Function(Size size) onLayoutChangedCallback;
/// Old size.
Size? _oldSize;
@override
void performLayout() {
super.performLayout();
final content = child;
assert(content is RenderBox, 'Must contain content');
assert(content?.hasSize ?? false, 'Content must obtain a size');
final newSize = content?.size;
if (newSize == null || newSize == _oldSize) return;
_oldSize = newSize;
onLayoutChangedCallback(newSize);
}
}
/// Notification about size changed.
@immutable
class SizeChangedNotification extends SizeChangedLayoutNotification {
const SizeChangedNotification(this.size);
/// Current size of nested widget.
final Size size;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment