Skip to content

Instantly share code, notes, and snippets.

@pskink
Last active February 26, 2024 09:07
Show Gist options
  • Save pskink/d05d4f5124293597ad1e77f957b2e6d0 to your computer and use it in GitHub Desktop.
Save pskink/d05d4f5124293597ad1e77f957b2e6d0 to your computer and use it in GitHub Desktop.
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:quiver/cache.dart';
import 'package:quiver/collection.dart';
part 'nine_patch_aux.dart';
invalidateNinePatchCacheItem(ImageProvider key) => _lruMap.remove(key);
invalidateNinePatchCacheWhere(bool Function(ImageProvider key) test) => _lruMap.removeWhere((key, value) => test(key));
invalidateNinePatchCache() => _lruMap.clear();
set ninePatchCacheSize(int maximumSize) => _lruMap.maximumSize = maximumSize;
int get ninePatchCacheSize => _lruMap.maximumSize;
typedef NinePatchBuilder = Widget Function(ui.Image image, Rect centerSlice, EdgeInsets padding);
class NinePatch extends StatefulWidget {
const NinePatch({
super.key,
required Widget this.child,
required this.imageProvider,
this.colorFilter,
this.opacity = 1,
this.fit,
this.position = DecorationPosition.background,
this.debugLabel,
}) : builder = null;
const NinePatch.builder({
super.key,
required NinePatchBuilder this.builder,
required this.imageProvider,
this.colorFilter,
this.opacity = 1,
this.fit,
this.position = DecorationPosition.background,
this.debugLabel,
}) : child = null;
final Widget? child;
final NinePatchBuilder? builder;
final ImageProvider imageProvider;
final ColorFilter? colorFilter;
final double opacity;
final BoxFit? fit;
final DecorationPosition position;
final String? debugLabel;
@override
State<NinePatch> createState() => _NinePatchState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ImageProvider>('imageProvider', imageProvider));
properties.add(DiagnosticsProperty<ColorFilter>('colorFilter', colorFilter, defaultValue: null));
properties.add(DoubleProperty('opacity', opacity, defaultValue: 1));
properties.add(EnumProperty<BoxFit>('fit', fit, defaultValue: null));
properties.add(EnumProperty<DecorationPosition>('position', position, defaultValue: DecorationPosition.background));
properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null));
}
}
class _NinePatchState extends State<NinePatch> with _NinePatchImageProviderStateMixin {
_ImageRecord? _imageRecord;
final _notifier = ValueNotifier(Size.zero);
@override
Widget build(BuildContext context) {
if (_imageRecord == null) return const SizedBox.shrink();
final ir = _imageRecord!;
final effectiveChild = widget.builder == null? widget.child : widget.builder!(ir.image, ir.centerSlice, ir.padding);
return _buildNinePatch(
imageRecord: ir,
colorFilter: widget.colorFilter,
opacity: widget.opacity,
fit: widget.fit,
position: widget.position,
notifier: _notifier,
child: effectiveChild,
debugLabel: '${widget.debugLabel ?? '<empty debugLabel>'} ${ir.debugLabel}',
);
}
@override
ImageProvider<Object> get imageProvider => widget.imageProvider;
@override
bool didProviderChange(NinePatch widget, NinePatch oldWidget) {
return widget.imageProvider != oldWidget.imageProvider;
}
@override
void updateImage(ImageInfo imageInfo, bool synchronousCall) async {
final imageRecordFuture = _cache.get(widget.imageProvider, ifAbsent: (k) async {
debugPrint('processing ${imageInfo.debugLabel}...');
final data = await imageInfo.image.toByteData(format: ui.ImageByteFormat.rawRgba);
final rect = Offset.zero & Size(imageInfo.image.width.toDouble(), imageInfo.image.height.toDouble());
return decoder.processImage(data!, imageInfo, [rect]);
});
final oldImageProvider = widget.imageProvider;
final imageRecord = (await imageRecordFuture)![0];
if (oldImageProvider != widget.imageProvider) {
debugPrint('skipping update for ${widget.toString()}, reason: $oldImageProvider != this widget imageProvider');
return;
}
if (widget.debugLabel != null) {
debugPrint('${widget.toString()} image: ${imageInfo.debugLabel}, centerSlice: ${imageRecord.centerSlice}, padding: ${imageRecord.padding}');
}
setState(() {
// Trigger a build whenever the image changes.
_imageRecord = imageRecord;
});
}
}
typedef NinePatchAnimatedBuilder = Widget Function(ui.Image image, Rect centerSlice, EdgeInsets padding, Animation<double> animation);
class AnimatedNinePatch extends ImplicitlyAnimatedWidget {
const AnimatedNinePatch({
super.key,
required this.child,
required this.imageProvider,
this.color,
this.blendMode = BlendMode.srcIn,
this.opacity = 1,
this.fit,
this.position = DecorationPosition.background,
this.debugLabel,
super.curve,
required super.duration,
super.onEnd,
}) : builder = null;
const AnimatedNinePatch.builder({
super.key,
required NinePatchAnimatedBuilder this.builder,
required this.imageProvider,
this.color,
this.blendMode = BlendMode.srcIn,
this.opacity = 1,
this.fit,
this.position = DecorationPosition.background,
this.debugLabel,
super.curve,
required super.duration,
super.onEnd,
}) : child = null;
final Widget? child;
final NinePatchAnimatedBuilder? builder;
final ImageProvider imageProvider;
final Color? color;
final BlendMode blendMode;
final double opacity;
final BoxFit? fit;
final DecorationPosition position;
final String? debugLabel;
@override
ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState() {
return _AnimatedNinePatchState();
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ImageProvider>('imageProvider', imageProvider));
properties.add(ColorProperty('color', color, defaultValue: null));
properties.add(DiagnosticsProperty<BlendMode>('blendMode', blendMode, defaultValue: BlendMode.srcIn));
properties.add(DoubleProperty('opacity', opacity, defaultValue: 1));
properties.add(EnumProperty<BoxFit>('fit', fit, defaultValue: null));
properties.add(EnumProperty<DecorationPosition>('position', position, defaultValue: DecorationPosition.background));
properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null));
}
}
class _AnimatedNinePatchState extends AnimatedWidgetBaseState<AnimatedNinePatch>
with _NinePatchImageProviderStateMixin {
ColorTween? _color;
Tween<double>? _opacity;
_ImageRecordHolderTween? _imageRecordHolder;
final _notifier = ValueNotifier(Size.zero);
@override
Widget build(BuildContext context) {
final holder = _imageRecordHolder?.evaluate(animation);
if (holder == null) return const SizedBox.shrink();
final ir = holder.imageRecord!;
final color = _color?.evaluate(animation);
final colorFilter = color != null? ColorFilter.mode(color, widget.blendMode) : null;
final opacity = _opacity?.evaluate(animation) ?? 1;
final effectiveChild = widget.builder == null? widget.child : widget.builder!(ir.image, ir.centerSlice, ir.padding, animation);
return _buildNinePatch(
imageRecord: ir,
colorFilter: colorFilter,
opacity: opacity,
fit: widget.fit,
position: widget.position,
notifier: _notifier,
child: effectiveChild,
debugLabel: '${widget.debugLabel ?? '<empty debugLabel>'} ${ir.debugLabel}',
);
}
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_color = visitor(_color, widget.color, (dynamic value) => ColorTween(begin: value as Color)) as ColorTween?;
_opacity = visitor(_opacity, widget.opacity, (dynamic value) => Tween<double>(begin: value as double)) as Tween<double>?;
_imageRecordHolder = visitor(_imageRecordHolder, _ImageRecordHolder(), (dynamic value) => _ImageRecordHolderTween(begin: value as _ImageRecordHolder)) as _ImageRecordHolderTween?;
}
@override
ImageProvider<Object> get imageProvider => widget.imageProvider;
@override
bool didProviderChange(AnimatedNinePatch widget, AnimatedNinePatch oldWidget) {
return widget.imageProvider != oldWidget.imageProvider;
}
@override
void updateImage(ImageInfo imageInfo, bool synchronousCall) async {
final imageRecordFuture = _cache.get(widget.imageProvider, ifAbsent: (k) async {
debugPrint('processing ${imageInfo.debugLabel}...');
final data = await imageInfo.image.toByteData(format: ui.ImageByteFormat.rawRgba);
final rect = Offset.zero & Size(imageInfo.image.width.toDouble(), imageInfo.image.height.toDouble());
return decoder.processImage(data!, imageInfo, [rect]);
});
/*
TODO do we need this check (the same as in NinePatch)?
final oldImageProvider = widget.imageProvider;
final imageRecord = (await imageRecordFuture)!;
if (oldImageProvider != widget.imageProvider) {
debugPrint('skipping update for ${widget.toString()}, reason: $oldImageProvider != this widget imageProvider');
return;
}
*/
final imageRecord = (await imageRecordFuture)![0];
if (widget.debugLabel != null) {
debugPrint('${widget.toString()} image: ${imageInfo.debugLabel}, centerSlice: ${imageRecord.centerSlice}, padding: ${imageRecord.padding}');
}
setState(() {
// Trigger a build whenever the image changes.
// _imageRecord = imageRecord;
_imageRecordHolder!.end!.imageRecord = imageRecord;
});
}
}
class _ImageRecordHolder {
_ImageRecordHolder({
this.imageRecord,
});
_ImageRecord? imageRecord;
}
class _ImageRecordHolderTween extends Tween<_ImageRecordHolder?> {
_ImageRecordHolderTween({ super.begin, super.end });
@override
_ImageRecordHolder? transform(double t) {
return switch ((begin?.imageRecord, end?.imageRecord)) {
(null, null) => null,
(_, null) => begin,
(_, _) => _ImageRecordHolder(
imageRecord: (
image: t < 0.5? begin!.imageRecord!.image : end!.imageRecord!.image,
centerSlice: Rect.lerp(begin!.imageRecord!.centerSlice, end!.imageRecord!.centerSlice, t)!,
padding: EdgeInsets.lerp(begin!.imageRecord!.padding, end!.imageRecord!.padding, t)!,
debugLabel: t < 0.5? begin!.imageRecord!.debugLabel : end!.imageRecord!.debugLabel,
),
),
};
}
}
typedef FrameBuilder = (int frameCount, Rect Function(int frameNumber) rectBuilder);
FrameBuilder horizontalFixedSizeFrameBuilder(int frameCount, Size size) {
return (frameCount, (int i) => Rect.fromLTWH(i * size.width, 0, size.width, size.height));
}
FrameBuilder verticalFixedSizeFrameBuilder(int frameCount, Size size) {
return (frameCount, (int i) => Rect.fromLTWH(0, i * size.height, size.width, size.height));
}
FrameBuilder gridFixedSizeFrameBuilder(int frameCount, int numColumns, Size size) {
return (frameCount, (int i) {
final col = i % numColumns;
final row = i ~/ numColumns;
return Rect.fromLTWH(col * size.width, row * size.height, size.width, size.height);
});
}
class AnimatedMultiNinePatch extends ImplicitlyAnimatedWidget {
const AnimatedMultiNinePatch({
super.key,
required this.child,
required this.imageProvider,
required this.frameBuilder,
this.phase,
this.index,
this.color,
this.blendMode = BlendMode.srcIn,
this.opacity = 1,
this.fit,
this.position = DecorationPosition.background,
this.debugLabel,
super.curve,
required super.duration,
super.onEnd,
}) : assert(phase == null || index == null, 'cannot use both phase and index');
final Widget child;
final ImageProvider imageProvider;
final FrameBuilder frameBuilder;
final double? phase;
final int? index;
final Color? color;
final BlendMode blendMode;
final double opacity;
final BoxFit? fit;
final DecorationPosition position;
final String? debugLabel;
@override
ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState() {
return _AnimatedMultiNinePatchState();
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ImageProvider>('imageProvider', imageProvider));
properties.add(DoubleProperty('phase', phase));
properties.add(IntProperty('index', index));
properties.add(ColorProperty('color', color, defaultValue: null));
properties.add(DiagnosticsProperty<BlendMode>('blendMode', blendMode, defaultValue: BlendMode.srcIn));
properties.add(DoubleProperty('opacity', opacity, defaultValue: 1));
properties.add(EnumProperty<BoxFit>('fit', fit, defaultValue: null));
properties.add(EnumProperty<DecorationPosition>('position', position, defaultValue: DecorationPosition.background));
properties.add(StringProperty('debugLabel', debugLabel, defaultValue: null));
}
}
class _AnimatedMultiNinePatchState extends AnimatedWidgetBaseState<AnimatedMultiNinePatch>
with _NinePatchImageProviderStateMixin {
ColorTween? _color;
Tween<double>? _opacity;
Tween<double>? _phase;
IntTween? _index;
List<_ImageRecord>? _imageRecords;
final _notifier = ValueNotifier(Size.zero);
@override
Widget build(BuildContext context) {
if (_imageRecords == null) return const SizedBox.shrink();
final color = _color?.evaluate(animation);
final colorFilter = color != null? ColorFilter.mode(color, widget.blendMode) : null;
final opacity = _opacity?.evaluate(animation) ?? 1;
assert(widget.phase == null || 0 <= widget.phase! && widget.phase! <= 1);
assert(widget.index == null || 0 <= widget.index! && widget.index! < widget.frameBuilder.$1);
final phase = _phase?.evaluate(animation);
final index = _index?.evaluate(animation);
final idx = index ?? ((phase ?? 0) * (_imageRecords!.length - 1)).round();
// TODO add lerps of _ImageRecord.centerSlice and _ImageRecord.padding ???
return _buildNinePatch(
imageRecord: _imageRecords![idx],
colorFilter: colorFilter,
opacity: opacity,
fit: widget.fit,
position: widget.position,
notifier: _notifier,
debugLabel: widget.debugLabel != null? '${widget.debugLabel}[$idx]' : null,
child: widget.child,
);
}
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_color = visitor(_color, widget.color, (dynamic value) => ColorTween(begin: value as Color)) as ColorTween?;
_opacity = visitor(_opacity, widget.opacity, (dynamic value) => Tween<double>(begin: value as double)) as Tween<double>?;
_phase = visitor(_phase, widget.phase, (dynamic value) => Tween<double>(begin: value as double)) as Tween<double>?;
_index = visitor(_index, widget.index, (dynamic value) => IntTween(begin: value as int)) as IntTween?;
}
@override
ImageProvider<Object> get imageProvider => widget.imageProvider;
@override
bool didProviderChange(AnimatedMultiNinePatch widget, AnimatedMultiNinePatch oldWidget) {
return widget.imageProvider != oldWidget.imageProvider;
}
@override
void updateImage(ImageInfo imageInfo, bool synchronousCall) async {
final imageRecordFuture = _cache.get(widget.imageProvider, ifAbsent: (k) async {
debugPrint('processing ${imageInfo.debugLabel}...');
final data = await imageInfo.image.toByteData(format: ui.ImageByteFormat.rawRgba);
final (frameCount, rectBuilder) = widget.frameBuilder;
final rects = List.generate(frameCount, rectBuilder);
return decoder.processImage(data!, imageInfo, rects);
});
final oldImageProvider = widget.imageProvider;
final imageRecords = (await imageRecordFuture)!;
if (oldImageProvider != widget.imageProvider) {
debugPrint('skipping update for ${widget.toString()}, reason: $oldImageProvider != this widget imageProvider');
return;
}
setState(() {
// Trigger a build whenever the image changes.
_imageRecords = imageRecords;
});
}
}
part of 'nine_patch.dart';
typedef _Range = ({int start, int length});
typedef _ImageRecord = ({ui.Image image, Rect centerSlice, EdgeInsets padding, String? debugLabel});
final _lruMap = LruMap<ImageProvider, List<_ImageRecord>>(maximumSize: 32);
final _cache = MapCache<ImageProvider, List<_ImageRecord>>(map: _lruMap);
Widget _buildNinePatch({
required _ImageRecord imageRecord,
required ColorFilter? colorFilter,
required double opacity,
required BoxFit? fit,
required DecorationPosition position,
required ValueNotifier<Size> notifier,
required Widget? child,
required String? debugLabel,
}) {
final painter = _NinePatchPainter(
image: imageRecord.image,
centerSlice: imageRecord.centerSlice,
colorFilter: colorFilter,
opacity: opacity,
fit: fit,
notifier: notifier,
debugLabel: debugLabel,
);
final customPaint = CustomPaint(
painter: position == DecorationPosition.background? painter : null,
foregroundPainter: position == DecorationPosition.foreground? painter : null,
child: Padding(
padding: imageRecord.padding,
child: child,
),
);
// return customPaint;
return ConstrainedBox(
constraints: BoxConstraints(
minWidth: imageRecord.image.width - imageRecord.centerSlice.width + 1,
minHeight: imageRecord.image.height - imageRecord.centerSlice.height + 1,
),
child: customPaint,
);
}
class _NinePatchDecoder {
_NinePatchDecoder({
required this.imageProvider,
});
final ImageProvider imageProvider;
List<_ImageRecord> processImage(ByteData data, ImageInfo imageInfo, List<Rect> rects) {
final width = imageInfo.image.width;
final records = <_ImageRecord>[];
int frame = 1;
for (final rect in rects) {
final centerH = _rangeH(data, width, rect, rect.top.toInt(), false, _pos('top', frame, rect))!;
final centerV = _rangeV(data, width, rect, rect.left.toInt(), false, _pos('left', frame, rect))!;
final paddingH = _rangeH(data, width, rect, rect.bottom.toInt() - 1, true, _pos('bottom', frame, rect)) ?? centerH;
final paddingV = _rangeV(data, width, rect, rect.right.toInt() - 1, true, _pos('right', frame, rect)) ?? centerV;
final centerSlice = Rect.fromLTWH(
centerH.start.toDouble(),
centerV.start.toDouble(),
centerH.length.toDouble(),
centerV.length.toDouble(),
);
final padding = EdgeInsets.fromLTRB(
paddingH.start.toDouble(),
paddingV.start.toDouble(),
rect.width - (paddingH.start + paddingH.length + 2),
rect.height - (paddingV.start + paddingV.length + 2),
);
records.add((
image: _cropImage(imageInfo.image, rect),
centerSlice: centerSlice,
padding: padding,
debugLabel: imageInfo.debugLabel,
));
frame++;
}
imageInfo.dispose();
return records;
}
ui.Image _cropImage(ui.Image image, Rect rect) {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
rect = rect.deflate(1);
canvas.drawImageRect(image, rect, Offset.zero & rect.size, Paint());
return recorder
.endRecording()
.toImageSync(rect.width.toInt(), rect.height.toInt());
}
int _alpha(ByteData data, int width, int x, int y) {
final byteOffset = 4 * (x + (y * width));
return data.getUint32(byteOffset) & 0xff;
}
_Range? _rangeH(ByteData data, int width, Rect rect, int y, bool allowEmpty, String position) {
final baseX = rect.left.toInt() + 1;
final alphas = List.generate(rect.width.toInt() - 2, (x) => _alpha(data, width, baseX + x, y));
return _range(alphas, allowEmpty, position);
}
_Range? _rangeV(ByteData data, int width, Rect rect, int x, bool allowEmpty, String position) {
final baseY = rect.top.toInt() + 1;
final alphas = List.generate(rect.height.toInt() - 2, (y) => _alpha(data, width, x, baseY + y));
return _range(alphas, allowEmpty, position);
}
_Range? _range(List<int> alphas, bool allowEmpty, String position) {
if (alphas.any((alpha) => 0 < alpha && alpha < 255)) {
final suspects = alphas
.mapIndexed((index, alpha) => (index, alpha))
.where((record) => 0 < record.$2 && record.$2 < 255)
.join(' ');
throw 'found neither fully transparent nor fully opaque pixels along $position, (offset, alpha):\n$suspects';
}
final ranges = alphas.splitBetween((first, second) => first != second).toList();
int start = 0;
List<_Range> rangeRecords = [];
for (final range in ranges) {
if (range[0] != 0) {
rangeRecords.add((start: start, length: range.length));
}
start += range.length;
}
if (rangeRecords.length > 1) {
final rangesStr = rangeRecords.map((r) => '${r.start}..${r.start + r.length - 1}').join(' ');
throw 'multiple opaque ranges along $position\n${_decorate(alphas)}\nfound ranges: $rangesStr';
}
if (!allowEmpty && rangeRecords.isEmpty) {
throw 'no opaque range along $position';
}
// print('$alphas $rangeRecords');
return rangeRecords.firstOrNull;
}
String _pos(String pos, int frame, Rect rect) => 'the $pos edge of frame #$frame ($rect) of $imageProvider';
String _decorate(List<int> alphas) => alphas.map((a) => a == 0? '○' : '●').join();
}
class _NinePatchPainter extends CustomPainter {
_NinePatchPainter({
required this.image,
required this.centerSlice,
required this.colorFilter,
required this.opacity,
required this.fit,
required this.notifier,
this.debugLabel,
});
final ui.Image image;
final Rect centerSlice;
final double opacity;
final ColorFilter? colorFilter;
final BoxFit? fit;
final ValueNotifier<Size> notifier;
final String? debugLabel;
@override
void paint(Canvas canvas, Size size) {
// print('paint $image');
final widthFits = size.width > image.width - centerSlice.width;
final heightFits = size.height > image.height - centerSlice.height;
if (notifier.value != size && (!widthFits || !heightFits)) {
notifier.value = size;
final buffer = StringBuffer('''$debugLabel
current size is not big enough to paint the image
current size: $size
image: $image
centerSlice: $centerSlice, ${centerSlice.size}
reason(s):
''');
if (!widthFits) buffer.writeln(' width does not fit because ${size.width} <= ${image.width} - ${centerSlice.width}');
if (!heightFits) buffer.writeln(' height does not fit because ${size.height} <= ${image.height} - ${centerSlice.height}');
FlutterError.reportError(
FlutterErrorDetails(
exception: buffer.toString(),
)
);
}
paintImage(
canvas: canvas,
rect: Offset.zero & size,
image: image,
opacity: opacity,
colorFilter: colorFilter,
fit: fit,
centerSlice: widthFits && heightFits? centerSlice : null,
);
}
@override
bool shouldRepaint(_NinePatchPainter oldDelegate) => true;
}
mixin _NinePatchImageProviderStateMixin<T extends StatefulWidget> on State<T> {
ImageStream? _imageStream;
late final decoder = _NinePatchDecoder(
imageProvider: imageProvider,
);
@override
void didChangeDependencies() {
// debugPrintBeginFrameBanner = true;
super.didChangeDependencies();
// We call getImage here because createLocalImageConfiguration() needs to
// be called again if the dependencies changed, in case the changes relate
// to the DefaultAssetBundle, MediaQuery, etc, which that method uses.
_getImage();
}
void _getImage() {
// TODO short circuit: does it pay off?
// final imageRecord = _lruMap[widget.imageProvider];
// if (imageRecord != null) {
// setState(() {
// // Trigger a build whenever the image changes.
// _imageRecord = imageRecord;
// });
// return;
// }
final ImageStream? oldImageStream = _imageStream;
_imageStream = imageProvider.resolve(createLocalImageConfiguration(context));
if (_imageStream!.key != oldImageStream?.key) {
// If the keys are the same, then we got the same image back, and so we don't
// need to update the listeners. If the key changed, though, we must make sure
// to switch our listeners to the new image stream.
final ImageStreamListener listener = ImageStreamListener(updateImage);
oldImageStream?.removeListener(listener);
_imageStream!.addListener(listener);
}
}
ImageProvider get imageProvider;
bool didProviderChange(T widget, T oldWidget);
void updateImage(ImageInfo imageInfo, bool synchronousCall);
@override
void didUpdateWidget(T oldWidget) {
super.didUpdateWidget(oldWidget);
if (didProviderChange(widget, oldWidget)) {
_getImage();
}
}
@override
void dispose() {
_imageStream?.removeListener(ImageStreamListener(updateImage));
super.dispose();
}
}
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'nine_patch.dart';
main() {
runApp(MaterialApp(
scrollBehavior: const MaterialScrollBehavior().copyWith(
dragDevices: {ui.PointerDeviceKind.mouse, ui.PointerDeviceKind.touch},
),
theme: ThemeData.light(useMaterial3: false),
home: _NinePatchExamples(),
));
}
class _NinePatchExamples extends StatefulWidget {
@override
State<_NinePatchExamples> createState() => _NinePatchExamplesState();
}
class _NinePatchExamplesState extends State<_NinePatchExamples> {
int cnt = 0;
bool down = false;
@override
Widget build(BuildContext context) {
// timeDilation = 5;
return Scaffold(
floatingActionButton: FloatingActionButton.extended(
onPressed: () => setState(() => cnt++),
label: const Text('animate'),
icon: const Icon(Icons.animation),
),
body: SingleChildScrollView(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(4),
child: Row(
children: [
NinePatch(
debugLabel: 'android button',
imageProvider: const AssetImage('images/webp_btn_default_normal.9.webp'),
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () {},
splashColor: Colors.white54,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 100),
child: const Text('android button with maxWidth == 100'),
),
),
),
),
Expanded(
child: Builder(
builder: (context) {
// final text = ConstrainedBox(
// constraints: const BoxConstraints(minWidth: 11, minHeight: 34),
// child: const Text('Anim occaecat esse ullamco id aute veniam sunt incididunt elit mollit consequat.'),
// );
final text = const Text('Anim occaecat esse ullamco id aute veniam sunt incididunt elit mollit consequat.');
return Stack(
children: [
Transform.translate(
offset: const Offset(2, 2),
child: ImageFiltered(
imageFilter: ui.ImageFilter.blur(sigmaX: 3, sigmaY: 3),
child: AnimatedNinePatch(
duration: const Duration(milliseconds: 500),
imageProvider: const AssetImage('images/webp_balloon.9.webp'),
color: Colors.black,
opacity: cnt.isEven? 0 : 1,
child: text,
),
),
),
AnimatedNinePatch(
duration: const Duration(milliseconds: 500),
imageProvider: const AssetImage('images/webp_balloon.9.webp'),
color: cnt.isEven? Colors.green.shade300 : Colors.green.shade100,
blendMode: BlendMode.modulate,
child: text,
),
],
);
}
),
),
],
),
),
AnimatedMultiNinePatch(
imageProvider: AssetImage('images/webp_balloon_multi_frame.9.webp'),
// 'images/webp_balloon_multi_frame.9.webp' contains 10 fixed
// size frames (with size Size(69, 49)) aligned horizontally
// so the whole image size is Size(10 * 69, 49)
frameBuilder: horizontalFixedSizeFrameBuilder(10, const Size(69, 49)),
color: down? Colors.yellow.shade600 : Colors.teal.shade400,
blendMode: BlendMode.modulate,
phase: down? 0 : 1,
// index: down? 0 : 9,
duration: const Duration(milliseconds: 300),
// debugLabel: 'multi',
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200),
child: GestureDetector(
onTapDown: (d) => setState(() => down = true),
onTapUp: (d) => setState(() => down = false),
onTapCancel: () => setState(() => down = false),
child: const Text('press me and hold your finger for a while...'),
),
),
),
Padding(
padding: const EdgeInsets.all(4),
child: NinePatch(
colorFilter: ColorFilter.mode(cnt.isOdd? Colors.deepOrange : Colors.orange, BlendMode.modulate),
imageProvider: const AssetImage('images/webp_flag.9.webp'),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 250),
child: OutlinedButton(
style: const ButtonStyle(
foregroundColor: MaterialStatePropertyAll(Colors.black),
),
onPressed: () {
invalidateNinePatchCacheItem(const AssetImage('images/webp_flag.9.webp'));
},
child: const Text('NinePatch with "centerSlice" and "padding" embedded in image'),
),
),
),
),
Padding(
padding: const EdgeInsets.all(4),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
AnimatedNinePatch(
duration: const Duration(milliseconds: 500),
color: cnt.isOdd? Colors.yellow.shade600 : Colors.red.shade600,
blendMode: BlendMode.modulate,
imageProvider: cnt.isOdd? const AssetImage('images/webp_flag.9.webp') : const AssetImage('images/webp_flag1.9.webp'),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 100),
child: const Text('this is AnimetedNinePatch widget'),
),
),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200),
child: AnimatedNinePatch(
duration: const Duration(milliseconds: 500),
color: cnt.isOdd? Colors.red.shade600 : Colors.yellow.shade600,
blendMode: BlendMode.modulate,
imageProvider: cnt.isOdd? const AssetImage('images/webp_flag1.9.webp') : const AssetImage('images/webp_flag.9.webp'),
child: const Text('this is AnimetedNinePatch widget'),
),
),
],
),
),
),
Stack(
alignment: Alignment.center,
children: [
AnimatedNinePatch(
duration: const Duration(milliseconds: 1000),
imageProvider: const AssetImage('images/webp_balloon.9.webp'),
color: cnt.isEven? null : Colors.green.shade100,
blendMode: BlendMode.modulate,
opacity: cnt.isEven? 0.33 : 1,
curve: Curves.easeOut,
child: AnimatedSize(
curve: Curves.easeOutCubic,
duration: const Duration(milliseconds: 600),
child: ConstrainedBox(
// constraints: const BoxConstraints(minWidth: 11, maxWidth: 150, minHeight: 34),
constraints: const BoxConstraints(maxWidth: 225),
child: cnt.isEven? const UnconstrainedBox() : const Text('Est Lorem non eu cupidatat. Sint occaecat excepteur minim nisi sint elit culpa eu officia dolor ut.')
),
),
),
NinePatch(
imageProvider: const AssetImage('images/webp_balloon_empty.9.webp'),
opacity: cnt.isEven? 0.33 : 1,
child: AnimatedSize(
curve: Curves.easeIn,
duration: const Duration(milliseconds: 400),
child: ConstrainedBox(
// constraints: const BoxConstraints(minWidth: 11, maxWidth: 150, minHeight: 34),
constraints: const BoxConstraints(maxWidth: 225),
child: cnt.isEven? const UnconstrainedBox() : const Opacity(opacity: 0, child: Text('Est Lorem non eu cupidatat. Sint occaecat excepteur minim nisi sint elit culpa eu officia dolor ut.'))
),
),
),
],
),
AnimatedNinePatch(
duration: const Duration(milliseconds: 1000),
imageProvider: const AssetImage('images/webp_balloon.9.webp'),
color: cnt.isEven? null : Colors.green.shade200,
blendMode: BlendMode.modulate,
opacity: cnt.isEven? 0.33 : 1,
curve: Curves.easeOut,
debugLabel: 'balloon',
child: AnimatedSize(
duration: const Duration(milliseconds: 500),
child: ConstrainedBox(
// constraints: const BoxConstraints(minWidth: 11, maxWidth: 150, minHeight: 34),
constraints: const BoxConstraints(maxWidth: 225),
child: cnt.isEven? const UnconstrainedBox() : const Text('Est Lorem non eu cupidatat. Sint occaecat excepteur minim nisi sint elit culpa eu officia dolor ut.')
),
),
),
Padding(
padding: const EdgeInsets.all(4),
child: Builder(
builder: (context) {
final child = ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200),
child: const Text('Sit occaecat tempor incididunt pariatur id amet aliqua magna mollit irure consequat commodo.'),
);
return Stack(
children: [
ImageFiltered(
imageFilter: ui.ImageFilter.blur(sigmaX: 3, sigmaY: 3),
child: Transform.translate(
offset: const Offset(2, 2),
child: NinePatch(
colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcATop),
imageProvider: const AssetImage('images/webp_flag.9.webp'),
child: child,
),
),
),
NinePatch(
debugLabel: 'yellow flag',
colorFilter: ColorFilter.mode(Colors.yellow.shade300, BlendMode.modulate),
imageProvider: const AssetImage('images/webp_flag.9.webp'),
child: child,
),
],
);
}
),
),
Padding(
padding: const EdgeInsets.all(4),
child: SizedBox(
height: 150,
child: PageView.builder(
itemCount: 10,
itemBuilder: (ctx, i) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 50),
child: NinePatch(
colorFilter: ColorFilter.mode(i.isOdd? Colors.blue : Colors.blueGrey, BlendMode.modulate),
imageProvider: const AssetImage('images/webp_flag.9.webp'),
child: FittedBox(child: Text('item $i')),
),
),
),
),
),
],
),
),
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment