From 521a26fe3abd0e8eef5fe3ad6839ad18c9768e00 Mon Sep 17 00:00:00 2001 From: Deepak Gupta Date: Mon, 8 Jan 2024 02:40:03 -0800 Subject: [PATCH] Support widget animations (#14) * Setup * Refactor * Refactor * refactor * Refactor, improve generics * bump version --------- Co-authored-by: souvikdas --- example/lib/main.dart | 129 +---------------- .../lib}/predefined_animations.dart | 56 +++----- example/lib/text_demo.dart | 127 +++++++++++++++++ example/lib/widget_effects_demo.dart | 47 +++++++ lib/blend_animation_kit.dart | 7 +- lib/src/animation_input.dart | 50 ------- ...lder.dart => blend_animation_builder.dart} | 22 +-- .../blend_animation_input.dart | 43 ++++++ .../character_animation_input.dart | 77 ++++++++++ .../widget_animation_input.dart | 38 +++++ lib/src/blend_animation_widget.dart | 59 ++++++++ lib/src/box_info.dart | 23 +++ lib/src/pipeline/delay.dart | 4 +- lib/src/pipeline/opacity.dart | 2 +- lib/src/pipeline/pipeline_step.dart | 2 +- lib/src/pipeline/transform.dart | 2 +- lib/src/pipeline/wait.dart | 4 +- lib/src/text_animation_widget.dart | 132 ------------------ pubspec.yaml | 2 +- 19 files changed, 460 insertions(+), 366 deletions(-) rename {lib/src => example/lib}/predefined_animations.dart (59%) create mode 100644 example/lib/text_demo.dart create mode 100644 example/lib/widget_effects_demo.dart delete mode 100644 lib/src/animation_input.dart rename lib/src/{text_animation_builder.dart => blend_animation_builder.dart} (73%) create mode 100644 lib/src/blend_animation_input/blend_animation_input.dart create mode 100644 lib/src/blend_animation_input/character_animation_input.dart create mode 100644 lib/src/blend_animation_input/widget_animation_input.dart create mode 100644 lib/src/blend_animation_widget.dart create mode 100644 lib/src/box_info.dart delete mode 100644 lib/src/text_animation_widget.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 52a26d0..8bb0a70 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,4 +1,4 @@ -import 'package:blend_animation_kit/blend_animation_kit.dart'; +import 'package:example/text_demo.dart'; import 'package:flutter/material.dart'; void main() { @@ -16,132 +16,7 @@ class MyApp extends StatelessWidget { theme: ThemeData.dark( useMaterial3: true, ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - List get widgets => [ - variant2(customText ?? "Great Thinkers", const TextStyle(fontSize: 40)), - variant3( - customText ?? "Coffee mornings", const TextStyle(fontSize: 30)), - variant4( - customText ?? "Beautiful Questions", const TextStyle(fontSize: 30)), - variant5(customText ?? "Animation 6", const TextStyle(fontSize: 40)), - variant6(customText ?? "Animation 7", const TextStyle(fontSize: 40)), - ]; - - String? customText; - final textFieldController = TextEditingController(); - - @override - void dispose() { - textFieldController.dispose(); - super.dispose(); - } - - double sliderVal = 1; - bool sliderActive = false; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: SafeArea( - child: Center( - child: Column( - children: [ - TextField( - controller: textFieldController, - decoration: InputDecoration( - hintText: "Enter any text", - suffixIcon: TextButton.icon( - onPressed: () { - if (!mounted) return; - setState(() { - customText = null; - }); - textFieldController.clear(); - FocusManager.instance.primaryFocus?.unfocus(); - }, - icon: const Icon(Icons.cancel), - label: const Text("Cancel"), - ), - ), - onChanged: (val) { - if (!mounted) return; - setState(() { - customText = val; - }); - }, - ), - Expanded( - child: Scrollbar( - child: ListView.separated( - key: ValueKey(customText), - padding: const EdgeInsets.all(10), - separatorBuilder: (context, index) => const SizedBox( - height: 10, - ), - itemCount: widgets.length, - itemBuilder: (context, index) { - return Center( - child: LayoutBuilder(builder: (context, constraints) { - return Container( - width: sliderActive - ? constraints.maxWidth * sliderVal - : null, - decoration: BoxDecoration( - border: Border.all(color: Colors.red)), - child: widgets[index], - ); - }), - ); - }, - ), - ), - ), - Slider( - min: 0, - max: 1, - value: sliderVal, - onChanged: sliderActive - ? (val) { - setState(() { - sliderVal = val; - }); - } - : null, - ), - Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text("Change Width"), - Checkbox( - value: sliderActive, - onChanged: (val) { - setState(() { - sliderActive = val ?? false; - sliderVal = 1.0; - }); - }), - ], - ), - ) - ], - ), - ), - ), + home: const TextDemo(title: 'Flutter Demo Home Page'), ); } } diff --git a/lib/src/predefined_animations.dart b/example/lib/predefined_animations.dart similarity index 59% rename from lib/src/predefined_animations.dart rename to example/lib/predefined_animations.dart index 532d365..f151319 100644 --- a/lib/src/predefined_animations.dart +++ b/example/lib/predefined_animations.dart @@ -1,10 +1,6 @@ import 'dart:math'; -import 'package:blend_animation_kit/src/animation_input.dart'; -import 'package:blend_animation_kit/src/pipeline/opacity.dart'; -import 'package:blend_animation_kit/src/pipeline/pipeline_helpers.dart'; -import 'package:blend_animation_kit/src/pipeline/pipeline_step.dart'; -import 'package:blend_animation_kit/src/text_animation_widget.dart'; +import 'package:blend_animation_kit/blend_animation_kit.dart'; import 'package:flutter/material.dart'; final PipelineStep variant2Pipeline = const OpacityStep( @@ -16,12 +12,10 @@ final PipelineStep variant2Pipeline = const OpacityStep( ) + PipelineHelpers.waitAndFadeOutAll(); -Widget variant2(String text, TextStyle? textStyle) => - TextAnimationWidget.fromInput( - animationInput: CharacterAnimationInput(text: text), - textStyle: textStyle, - pipelineStep: variant2Pipeline, - ); +Widget variant2(String text, TextStyle? textStyle) => BlendAnimationWidget( + builder: BlendAnimationBuilder(CharacterAnimationInput( + text: text, textStyle: textStyle, textAlign: TextAlign.end)) + .add(variant2Pipeline)); final PipelineStep variant3Pipeline = PipelineHelpers.opacityAndTransform( initialOpacity: 1.0, @@ -35,12 +29,10 @@ final PipelineStep variant3Pipeline = PipelineHelpers.opacityAndTransform( ) + PipelineHelpers.waitAndFadeOutAll(); -Widget variant3(String text, TextStyle? textStyle) => - TextAnimationWidget.fromInput( - animationInput: CharacterAnimationInput(text: text), - textStyle: textStyle, - pipelineStep: variant3Pipeline, - ); +Widget variant3(String text, TextStyle? textStyle) => BlendAnimationWidget( + builder: BlendAnimationBuilder( + CharacterAnimationInput(text: text, textStyle: textStyle)) + .add(variant3Pipeline)); final PipelineStep variant4Pipeline = PipelineHelpers.opacityAndTransform( initialOpacity: 0.0, @@ -53,12 +45,10 @@ final PipelineStep variant4Pipeline = PipelineHelpers.opacityAndTransform( ) + PipelineHelpers.waitAndFadeOutAll(); -Widget variant4(String text, TextStyle? textStyle) => - TextAnimationWidget.fromInput( - animationInput: CharacterAnimationInput(text: text), - textStyle: textStyle, - pipelineStep: variant4Pipeline, - ); +Widget variant4(String text, TextStyle? textStyle) => BlendAnimationWidget( + builder: BlendAnimationBuilder( + CharacterAnimationInput(text: text, textStyle: textStyle)) + .add(variant4Pipeline)); final PipelineStep variant5Pipeline = PipelineHelpers.opacityAndTransform( initialOpacity: 0.0, @@ -71,12 +61,10 @@ final PipelineStep variant5Pipeline = PipelineHelpers.opacityAndTransform( ) + PipelineHelpers.waitAndFadeOutAll(); -Widget variant5(String text, TextStyle? textStyle) => - TextAnimationWidget.fromInput( - animationInput: CharacterAnimationInput(text: text), - textStyle: textStyle, - pipelineStep: variant5Pipeline, - ); +Widget variant5(String text, TextStyle? textStyle) => BlendAnimationWidget( + builder: BlendAnimationBuilder( + CharacterAnimationInput(text: text, textStyle: textStyle)) + .add(variant5Pipeline)); final PipelineStep variant6Pipeline = PipelineHelpers.opacityAndTransform( initialOpacity: 0.0, @@ -89,9 +77,7 @@ final PipelineStep variant6Pipeline = PipelineHelpers.opacityAndTransform( ) + PipelineHelpers.waitAndFadeOutAll(); -Widget variant6(String text, TextStyle? textStyle) => - TextAnimationWidget.fromInput( - animationInput: CharacterAnimationInput(text: text), - textStyle: textStyle, - pipelineStep: variant6Pipeline, - ); +Widget variant6(String text, TextStyle? textStyle) => BlendAnimationWidget( + builder: BlendAnimationBuilder( + CharacterAnimationInput(text: text, textStyle: textStyle)) + .add(variant6Pipeline)); diff --git a/example/lib/text_demo.dart b/example/lib/text_demo.dart new file mode 100644 index 0000000..cad13f6 --- /dev/null +++ b/example/lib/text_demo.dart @@ -0,0 +1,127 @@ +import 'package:example/predefined_animations.dart'; +import 'package:flutter/material.dart'; + +class TextDemo extends StatefulWidget { + const TextDemo({super.key, required this.title}); + + final String title; + + @override + State createState() => _TextDemoState(); +} + +class _TextDemoState extends State { + List get widgets => [ + variant2(customText ?? "Great Thinkers", const TextStyle(fontSize: 40)), + variant3( + customText ?? "Coffee mornings", const TextStyle(fontSize: 30)), + variant4( + customText ?? "Beautiful Questions", const TextStyle(fontSize: 30)), + variant5(customText ?? "Animation 6", const TextStyle(fontSize: 40)), + variant6(customText ?? "Animation 7", const TextStyle(fontSize: 40)), + ]; + + String? customText; + final textFieldController = TextEditingController(); + + @override + void dispose() { + textFieldController.dispose(); + super.dispose(); + } + + double sliderVal = 1; + bool sliderActive = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Center( + child: Column( + children: [ + TextField( + controller: textFieldController, + decoration: InputDecoration( + hintText: "Enter any text", + suffixIcon: TextButton.icon( + onPressed: () { + if (!mounted) return; + setState(() { + customText = null; + }); + textFieldController.clear(); + FocusManager.instance.primaryFocus?.unfocus(); + }, + icon: const Icon(Icons.cancel), + label: const Text("Cancel"), + ), + ), + onChanged: (val) { + if (!mounted) return; + setState(() { + customText = val; + }); + }, + ), + Expanded( + child: Scrollbar( + child: ListView.separated( + key: ValueKey(customText), + padding: const EdgeInsets.all(10), + separatorBuilder: (context, index) => const SizedBox( + height: 10, + ), + itemCount: widgets.length, + itemBuilder: (context, index) { + return Center( + child: LayoutBuilder(builder: (context, constraints) { + return Container( + width: sliderActive + ? constraints.maxWidth * sliderVal + : null, + decoration: BoxDecoration( + border: Border.all(color: Colors.red)), + child: widgets[index], + ); + }), + ); + }, + ), + ), + ), + Slider( + min: 0, + max: 1, + value: sliderVal, + onChanged: sliderActive + ? (val) { + setState(() { + sliderVal = val; + }); + } + : null, + ), + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Change Width"), + Checkbox( + value: sliderActive, + onChanged: (val) { + setState(() { + sliderActive = val ?? false; + sliderVal = 1.0; + }); + }), + ], + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/example/lib/widget_effects_demo.dart b/example/lib/widget_effects_demo.dart new file mode 100644 index 0000000..ced075d --- /dev/null +++ b/example/lib/widget_effects_demo.dart @@ -0,0 +1,47 @@ +import 'package:blend_animation_kit/blend_animation_kit.dart'; +import 'package:example/predefined_animations.dart'; +import 'package:flutter/material.dart'; + +class WidgetEffectsDemo extends StatefulWidget { + const WidgetEffectsDemo({super.key, required this.title}); + + final String title; + + @override + State createState() => _WidgetEffectsDemoState(); +} + +class _WidgetEffectsDemoState extends State { + @override + Widget build(BuildContext context) { + final builder = BlendAnimationBuilder( + WidgetAnimationInput( + widget: const SampleIcon(), size: const Size(200, 200)), + ).add(variant2Pipeline); + return SafeArea( + child: Stack(children: [ + BlendAnimationWidget(builder: builder), + ]), + ); + } +} + +class SampleIcon extends StatelessWidget { + const SampleIcon({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 200, + height: 200, + child: Container( + color: Colors.blue, + child: const Icon( + Icons.favorite, + color: Colors.white, + size: 200.0, + ), + ), + ); + } +} diff --git a/lib/blend_animation_kit.dart b/lib/blend_animation_kit.dart index 35af2a9..9e6e8b3 100644 --- a/lib/blend_animation_kit.dart +++ b/lib/blend_animation_kit.dart @@ -2,7 +2,9 @@ library blend_animation_kit; export 'package:simple_animations/animation_developer_tools/animation_developer_tools.dart'; -export 'src/animation_input.dart'; +export 'src/blend_animation_builder.dart'; +export 'src/blend_animation_input/blend_animation_input.dart'; +export 'src/blend_animation_widget.dart'; export 'src/matrix4_alignment_tween.dart'; export 'src/pipeline/delay.dart'; export 'src/pipeline/opacity.dart'; @@ -10,6 +12,3 @@ export 'src/pipeline/pipeline_helpers.dart'; export 'src/pipeline/pipeline_step.dart'; export 'src/pipeline/transform.dart'; export 'src/pipeline/wait.dart'; -export 'src/predefined_animations.dart'; -export 'src/text_animation_builder.dart'; -export 'src/text_animation_widget.dart'; diff --git a/lib/src/animation_input.dart b/lib/src/animation_input.dart deleted file mode 100644 index 80e20cc..0000000 --- a/lib/src/animation_input.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:blend_animation_kit/src/animation_property.dart'; -import 'package:flutter/material.dart'; - -abstract class AnimationInput { - String get text; - - Iterable get groups; - - List get animationProperties; -} - -class CharacterAnimationInput extends AnimationInput { - @override - final String text; - - @override - final Iterable groups; - - @override - late final List animationProperties; - - CharacterAnimationInput({ - required this.text, - }) : groups = text.characters { - animationProperties = - List.generate(groups.length, (index) => AnimationProperty()); - } -} - -class WordAnimationInput extends AnimationInput { - @override - late final Iterable groups; - - @override - late final List animationProperties; - - @override - final String text; - - WordAnimationInput({ - required this.text, - }) { - final re = RegExp(r"\w+|\s+|[^\s\w]+"); - groups = re.allMatches(text).map((m) => m.group(0) ?? ''); - animationProperties = List.generate( - groups.length, - (index) => AnimationProperty(), - ); - } -} diff --git a/lib/src/text_animation_builder.dart b/lib/src/blend_animation_builder.dart similarity index 73% rename from lib/src/text_animation_builder.dart rename to lib/src/blend_animation_builder.dart index 520e8eb..7b7d0e4 100644 --- a/lib/src/text_animation_builder.dart +++ b/lib/src/blend_animation_builder.dart @@ -4,38 +4,38 @@ import 'package:flutter/material.dart'; import 'package:simple_animations/movie_tween/movie_tween.dart'; @immutable -class TextAnimationBuilder { +class BlendAnimationBuilder { late final Iterable sceneItems; late final Duration begin; - late final MovieTween tween = _generateTween(); + late final MovieTween tween = generateTween(); List get animationProperties => animationInput.animationProperties; - final AnimationInput animationInput; + final BlendAnimationInput animationInput; - TextAnimationBuilder(this.animationInput) + BlendAnimationBuilder(this.animationInput) : begin = Duration.zero, sceneItems = []; - TextAnimationBuilder._({ + BlendAnimationBuilder._({ required this.animationInput, required this.sceneItems, required this.begin, }); - TextAnimationBuilder copyWith( + BlendAnimationBuilder copyWith( {List? sceneItems, Duration? begin}) { - return TextAnimationBuilder._( + return BlendAnimationBuilder._( animationInput: animationInput, sceneItems: sceneItems ?? this.sceneItems, begin: begin ?? this.begin, ); } - MovieTween _generateTween() { + MovieTween generateTween() { MovieTween movieTween = MovieTween(); for (final element in sceneItems) { element.attachToScene(movieTween); @@ -43,9 +43,11 @@ class TextAnimationBuilder { return movieTween; } - TextAnimationBuilder add(final PipelineStep pipelineStep) { + BlendAnimationBuilder add( + final PipelineStep pipelineStep, + ) { PipelineStep? pipelineIterator = pipelineStep; - TextAnimationBuilder updatedBuilder = this; + BlendAnimationBuilder updatedBuilder = this; while (pipelineIterator != null) { updatedBuilder = pipelineIterator.updatedBuilder(updatedBuilder); pipelineIterator = pipelineIterator.nextStep; diff --git a/lib/src/blend_animation_input/blend_animation_input.dart b/lib/src/blend_animation_input/blend_animation_input.dart new file mode 100644 index 0000000..ab18bd6 --- /dev/null +++ b/lib/src/blend_animation_input/blend_animation_input.dart @@ -0,0 +1,43 @@ +import 'package:blend_animation_kit/src/animation_property.dart'; +import 'package:blend_animation_kit/src/box_info.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:simple_animations/simple_animations.dart'; + +export 'character_animation_input.dart'; +export 'widget_animation_input.dart'; + +abstract class BlendAnimationInput { + Iterable get groups; + + @nonVirtual + late final List animationProperties = + groups.map((e) => AnimationProperty()).toList(growable: false); + + GroupDetails getAnimationGroupDetails( + BuildContext context, BoxConstraints constraints); + + Widget renderGroupItem(AnimationBoxInfo info); + + Alignment get alignment => Alignment.center; + + @nonVirtual + Positioned renderAnimation(AnimationBoxInfo info, Movie movie) { + final animationProperty = animationProperties.elementAt(info.index); + return Positioned( + top: info.rect.top, + left: info.rect.left, + child: Opacity( + opacity: animationProperty.opacity.fromOrDefault(movie).clamp(0, 1), + child: Transform( + alignment: animationProperty.transformation + .fromOrDefault(movie) + .transformAlignment, + transform: + animationProperty.transformation.fromOrDefault(movie).matrix, + child: renderGroupItem(info), + ), + ), + ); + } +} diff --git a/lib/src/blend_animation_input/character_animation_input.dart b/lib/src/blend_animation_input/character_animation_input.dart new file mode 100644 index 0000000..f0f255b --- /dev/null +++ b/lib/src/blend_animation_input/character_animation_input.dart @@ -0,0 +1,77 @@ +import 'dart:ui'; + +import 'package:blend_animation_kit/blend_animation_kit.dart'; +import 'package:blend_animation_kit/src/box_info.dart'; +import 'package:blend_animation_kit/src/extensions.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +class CharacterAnimationInput extends BlendAnimationInput { + final String text; + + final TextAlign textAlign; + + final TextStyle? textStyle; + + @override + Alignment get alignment => textAlign.toAlignment(); + + @override + final Iterable groups; + + @override + GroupDetails getAnimationGroupDetails( + BuildContext context, BoxConstraints constraints) { + final defaultTextStyle = DefaultTextStyle.of(context); + final textStyle = defaultTextStyle.style.merge(this.textStyle); + final textPainter = TextPainter( + text: TextSpan(text: text, style: textStyle), + textDirection: TextDirection.ltr, + textAlign: textAlign, + textScaler: MediaQuery.textScalerOf(context), + maxLines: defaultTextStyle.maxLines, + textWidthBasis: defaultTextStyle.textWidthBasis, + textHeightBehavior: defaultTextStyle.textHeightBehavior ?? + DefaultTextHeightBehavior.maybeOf(context), + ); + textPainter.layout(maxWidth: constraints.maxWidth); + + final boxes = >[]; + int charOffset = 0; + text.characters.forEachIndexed((i, char) { + final selectionRects = textPainter.getBoxesForSelection( + TextSelection( + baseOffset: charOffset, extentOffset: charOffset + char.length), + boxHeightStyle: BoxHeightStyle.max, + boxWidthStyle: BoxWidthStyle.max, + ); + charOffset += char.length; + if (selectionRects.isNotEmpty) { + boxes.add( + AnimationBoxInfo( + subject: char, + index: i, + rect: selectionRects.first.toRect(), + ), + ); + } + }); + final boxSize = textPainter.size; + textPainter.dispose(); + return GroupDetails(boxSize, boxes); + } + + @override + Widget renderGroupItem(AnimationBoxInfo info) { + return Text.rich( + TextSpan(text: info.subject), + style: textStyle, + ); + } + + CharacterAnimationInput({ + required this.text, + this.textAlign = TextAlign.left, + this.textStyle, + }) : groups = text.characters; +} diff --git a/lib/src/blend_animation_input/widget_animation_input.dart b/lib/src/blend_animation_input/widget_animation_input.dart new file mode 100644 index 0000000..3695ec6 --- /dev/null +++ b/lib/src/blend_animation_input/widget_animation_input.dart @@ -0,0 +1,38 @@ +import 'package:blend_animation_kit/blend_animation_kit.dart'; +import 'package:blend_animation_kit/src/box_info.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +class WidgetAnimationInput extends BlendAnimationInput { + final Widget widget; + final Size size; + + WidgetAnimationInput({ + required this.widget, + required this.size, + }); + + @override + GroupDetails getAnimationGroupDetails( + BuildContext context, BoxConstraints constraints) { + final boxes = groups.mapIndexed((index, e) { + return AnimationBoxInfo( + subject: e, + rect: Offset.zero & size, + index: index, + ); + }).toList(growable: false); + return GroupDetails(size, boxes); + } + + @override + Iterable get groups => [widget]; + + @override + Widget renderGroupItem(AnimationBoxInfo info) { + return SizedBox.fromSize( + size: info.rect.size, + child: widget, + ); + } +} diff --git a/lib/src/blend_animation_widget.dart b/lib/src/blend_animation_widget.dart new file mode 100644 index 0000000..3148fc1 --- /dev/null +++ b/lib/src/blend_animation_widget.dart @@ -0,0 +1,59 @@ +import 'package:blend_animation_kit/blend_animation_kit.dart'; +import 'package:blend_animation_kit/src/animation_property.dart'; +import 'package:flutter/material.dart'; +import 'package:simple_animations/simple_animations.dart'; + +class BlendAnimationWidget extends StatelessWidget { + final BlendAnimationBuilder builder; + + final bool loop; + + const BlendAnimationWidget({ + super.key, + required this.builder, + this.loop = true, + }); + + factory BlendAnimationWidget.fromInput({ + required BlendAnimationInput animationInput, + required PipelineStep pipelineStep, + }) { + return BlendAnimationWidget( + builder: BlendAnimationBuilder(animationInput).add(pipelineStep), + ); + } + + MovieTween get tween => builder.tween; + + List get animationProperties => + builder.animationProperties; + + BlendAnimationInput get animationInput => builder.animationInput; + + @override + Widget build(BuildContext context) { + return Align( + alignment: animationInput.alignment, + child: LayoutBuilder(builder: (context, constraints) { + final boxInfo = + animationInput.getAnimationGroupDetails(context, constraints); + return SizedBox.fromSize( + size: boxInfo.overallBoxSize, + child: CustomAnimationBuilder( + tween: tween, + control: loop ? Control.loop : Control.play, + builder: (context, movie, child) { + return Stack( + clipBehavior: Clip.none, + children: boxInfo.boxes + .map((info) => animationInput.renderAnimation(info, movie)) + .toList(growable: false), + ); + }, + duration: tween.duration, + ), + ); + }), + ); + } +} diff --git a/lib/src/box_info.dart b/lib/src/box_info.dart new file mode 100644 index 0000000..c9b82b9 --- /dev/null +++ b/lib/src/box_info.dart @@ -0,0 +1,23 @@ +import 'dart:ui'; + +class AnimationBoxInfo { + final T subject; + + final Rect rect; + + final int index; + + const AnimationBoxInfo({ + required this.subject, + required this.rect, + required this.index, + }); +} + +class GroupDetails { + final Size overallBoxSize; + + final List> boxes; + + const GroupDetails(this.overallBoxSize, this.boxes); +} diff --git a/lib/src/pipeline/delay.dart b/lib/src/pipeline/delay.dart index d417f7e..8ac5b89 100644 --- a/lib/src/pipeline/delay.dart +++ b/lib/src/pipeline/delay.dart @@ -2,7 +2,7 @@ import 'dart:collection'; import 'package:blend_animation_kit/src/animation_property.dart'; import 'package:blend_animation_kit/src/pipeline/pipeline_step.dart'; -import 'package:blend_animation_kit/src/text_animation_builder.dart'; +import 'package:blend_animation_kit/src/blend_animation_builder.dart'; class DelayStep extends PipelineStep { final Duration delay; @@ -20,7 +20,7 @@ class DelayStep extends PipelineStep { } @override - TextAnimationBuilder updatedBuilder(TextAnimationBuilder builder) { + BlendAnimationBuilder updatedBuilder(BlendAnimationBuilder builder) { final newBegin = delay + builder.tween.duration; return builder.copyWith( begin: newBegin, diff --git a/lib/src/pipeline/opacity.dart b/lib/src/pipeline/opacity.dart index 2c8825d..d6db6b6 100644 --- a/lib/src/pipeline/opacity.dart +++ b/lib/src/pipeline/opacity.dart @@ -40,7 +40,7 @@ class OpacityStep extends PipelineStep { } @override - TextAnimationBuilder updatedBuilder(TextAnimationBuilder builder) { + BlendAnimationBuilder updatedBuilder(BlendAnimationBuilder builder) { final newSceneItems = List.of(builder.sceneItems); for (var (index, _) in builder.animationInput.groups.indexed) { final property = builder.animationProperties.elementAt(index).opacity; diff --git a/lib/src/pipeline/pipeline_step.dart b/lib/src/pipeline/pipeline_step.dart index 6873eb6..a1e31d6 100644 --- a/lib/src/pipeline/pipeline_step.dart +++ b/lib/src/pipeline/pipeline_step.dart @@ -7,7 +7,7 @@ abstract class PipelineStep { String get tag; - TextAnimationBuilder updatedBuilder(TextAnimationBuilder builder); + BlendAnimationBuilder updatedBuilder(BlendAnimationBuilder builder); PipelineStep chain(PipelineStep next) { if (nextStep != null) { diff --git a/lib/src/pipeline/transform.dart b/lib/src/pipeline/transform.dart index 9fd4e52..aa3c02f 100644 --- a/lib/src/pipeline/transform.dart +++ b/lib/src/pipeline/transform.dart @@ -47,7 +47,7 @@ class TransformStep extends PipelineStep { } @override - TextAnimationBuilder updatedBuilder(TextAnimationBuilder builder) { + BlendAnimationBuilder updatedBuilder(BlendAnimationBuilder builder) { final newSceneItems = List.of(builder.sceneItems); for (var (index, _) in builder.animationInput.groups.indexed) { final property = diff --git a/lib/src/pipeline/wait.dart b/lib/src/pipeline/wait.dart index c36422a..1b4c1fc 100644 --- a/lib/src/pipeline/wait.dart +++ b/lib/src/pipeline/wait.dart @@ -1,7 +1,7 @@ import 'dart:collection'; import 'package:blend_animation_kit/src/pipeline/pipeline_step.dart'; -import 'package:blend_animation_kit/src/text_animation_builder.dart'; +import 'package:blend_animation_kit/src/blend_animation_builder.dart'; class WaitStep extends PipelineStep { static String get wireName => "Wait"; @@ -17,7 +17,7 @@ class WaitStep extends PipelineStep { } @override - TextAnimationBuilder updatedBuilder(TextAnimationBuilder builder) { + BlendAnimationBuilder updatedBuilder(BlendAnimationBuilder builder) { final begin = builder.tween.duration; return builder.copyWith(begin: begin); } diff --git a/lib/src/text_animation_widget.dart b/lib/src/text_animation_widget.dart deleted file mode 100644 index 3fadb66..0000000 --- a/lib/src/text_animation_widget.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'dart:ui'; - -import 'package:blend_animation_kit/blend_animation_kit.dart'; -import 'package:blend_animation_kit/src/animation_property.dart'; -import 'package:blend_animation_kit/src/extensions.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:simple_animations/simple_animations.dart'; - -typedef TextBoxInfo = ({String character, TextBox box, int index}); - -class TextAnimationWidget extends StatelessWidget { - final TextStyle? textStyle; - - final TextAnimationBuilder builder; - - final TextAlign textAlign; - - final bool loop; - - const TextAnimationWidget({ - super.key, - required this.builder, - this.textStyle, - this.textAlign = TextAlign.center, - this.loop = true, - }); - - factory TextAnimationWidget.fromInput({ - required CharacterAnimationInput animationInput, - TextStyle? textStyle, - required PipelineStep pipelineStep, - TextAlign textAlign = TextAlign.center, - }) { - return TextAnimationWidget( - builder: TextAnimationBuilder(animationInput).add(pipelineStep), - textStyle: textStyle, - textAlign: textAlign, - ); - } - - MovieTween get tween => builder.tween; - - List get animationProperties => - builder.animationProperties; - - AnimationInput get animationInput => builder.animationInput; - - ({Size overallBoxSize, Iterable boxes}) getCharacterDetails( - BuildContext context, BoxConstraints constraints) { - final text = animationInput.text; - final defaultTextStyle = DefaultTextStyle.of(context); - final textStyle = defaultTextStyle.style.merge(this.textStyle); - final textPainter = TextPainter( - text: TextSpan(text: text, style: textStyle), - textDirection: TextDirection.ltr, - textAlign: textAlign, - textScaleFactor: MediaQuery.textScaleFactorOf(context), - maxLines: defaultTextStyle.maxLines, - textWidthBasis: defaultTextStyle.textWidthBasis, - textHeightBehavior: defaultTextStyle.textHeightBehavior ?? - DefaultTextHeightBehavior.maybeOf(context), - ); - textPainter.layout(maxWidth: constraints.maxWidth); - - final boxes = []; - int charOffset = 0; - text.characters.forEachIndexed((i, char) { - final selectionRects = textPainter.getBoxesForSelection( - TextSelection( - baseOffset: charOffset, extentOffset: charOffset + char.length), - boxHeightStyle: BoxHeightStyle.max, - boxWidthStyle: BoxWidthStyle.max, - ); - charOffset += char.length; - if (selectionRects.isNotEmpty) { - boxes.add((character: char, box: selectionRects.first, index: i)); - } - }); - final boxSize = textPainter.size; - textPainter.dispose(); - return (boxes: boxes, overallBoxSize: boxSize); - } - - @override - Widget build(BuildContext context) { - return Align( - alignment: textAlign.toAlignment(), - child: LayoutBuilder(builder: (context, constraints) { - final boxInfo = getCharacterDetails(context, constraints); - return SizedBox.fromSize( - size: boxInfo.overallBoxSize, - child: CustomAnimationBuilder( - tween: tween, - control: loop ? Control.loop : Control.play, - builder: (context, movie, child) { - return Stack( - clipBehavior: Clip.none, - children: boxInfo.boxes - .map((info) => renderCharacterAnimation(info, movie)) - .toList(growable: false), - ); - }, - duration: tween.duration, - ), - ); - }), - ); - } - - Positioned renderCharacterAnimation(TextBoxInfo info, Movie movie) { - final animationProperty = animationProperties.elementAt(info.index); - return Positioned( - top: info.box.top, - left: info.box.left, - child: Opacity( - opacity: animationProperty.opacity.fromOrDefault(movie).clamp(0, 1), - child: Transform( - alignment: animationProperty.transformation - .fromOrDefault(movie) - .transformAlignment, - transform: - animationProperty.transformation.fromOrDefault(movie).matrix, - child: Text.rich( - TextSpan(text: info.character), - style: textStyle, - ), - ), - ), - ); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 1e31079..2bad9e5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: blend_animation_kit description: A new Flutter package project. -version: 0.0.4 +version: 0.1.0 environment: sdk: '>=3.0.6 <4.0.0'