From b86e06f2e6fe47bf3d7e862ce3285b57c1667da1 Mon Sep 17 00:00:00 2001 From: Thomas Ferreira <139474199+ThomasDevApps@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:08:07 +0200 Subject: [PATCH 1/6] CHORE: Setup FlutterHighlight --- lib/flutter_highlight.dart | 65 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/lib/flutter_highlight.dart b/lib/flutter_highlight.dart index e2300f0..a06e531 100644 --- a/lib/flutter_highlight.dart +++ b/lib/flutter_highlight.dart @@ -1,11 +1,72 @@ import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; -class FlutterHighlight extends StatelessWidget { +@immutable +class FlutterHighlight extends StatefulWidget { final Widget child; const FlutterHighlight({super.key, required this.child}); + @override + State createState() => _FlutterHighlightState(); +} + +class _FlutterHighlightState extends State { @override Widget build(BuildContext context) { - return child; + return _FlutterHighlightRender(child: widget.child); + } +} + +/// ---------------------------------------------------------------------------- + +@immutable +class _FlutterHighlightRender extends SingleChildRenderObjectWidget { + const _FlutterHighlightRender({super.child}); + + @override + _FlutterHighlightFilter createRenderObject(BuildContext context) { + return _FlutterHighlightFilter(); + } +} + +/// ---------------------------------------------------------------------------- + +class _FlutterHighlightFilter extends RenderProxyBox { + @override + ShaderMaskLayer? get layer => super.layer as ShaderMaskLayer?; + + @override + bool get alwaysNeedsCompositing => child != null; + + double _offset(double start, double end, double percent) { + return start + (end - start) * percent; + } + + @override + void paint(PaintingContext context, Offset offset) { + if (child != null) { + final double width = child!.size.width; + final double height = child!.size.height; + + Rect rect; + double dx, dy; + + dx = _offset(-width, width, 1); + dy = 0.0; + rect = Rect.fromLTWH(dx - width, dy, 3 * width, height); + + layer ??= ShaderMaskLayer(); + layer! + ..shader = LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [Colors.yellow, Colors.yellow], + ).createShader(rect) + ..maskRect = offset & size + ..blendMode = BlendMode.srcIn; + context.pushLayer(layer!, super.paint, offset); + } else { + layer = null; + } } } From f4c8fcc7325edb0c8e339ae845cb591a997ea85e Mon Sep 17 00:00:00 2001 From: Thomas Ferreira <139474199+ThomasDevApps@users.noreply.github.com> Date: Fri, 25 Apr 2025 12:10:03 +0200 Subject: [PATCH 2/6] FIX: replace srcIn by srcATop --- lib/flutter_highlight.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/flutter_highlight.dart b/lib/flutter_highlight.dart index a06e531..8784420 100644 --- a/lib/flutter_highlight.dart +++ b/lib/flutter_highlight.dart @@ -60,10 +60,13 @@ class _FlutterHighlightFilter extends RenderProxyBox { ..shader = LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, - colors: [Colors.yellow, Colors.yellow], + colors: [ + Colors.yellow.withValues(alpha: 0.6), + Colors.yellow.withValues(alpha: 0.6), + ], ).createShader(rect) ..maskRect = offset & size - ..blendMode = BlendMode.srcIn; + ..blendMode = BlendMode.srcATop; context.pushLayer(layer!, super.paint, offset); } else { layer = null; From 355d32537b7463614fcc1681646cf2f67e644afa Mon Sep 17 00:00:00 2001 From: Thomas Ferreira <139474199+ThomasDevApps@users.noreply.github.com> Date: Fri, 25 Apr 2025 12:52:27 +0200 Subject: [PATCH 3/6] FEAT: highlight with animation --- example/lib/main.dart | 1 + lib/flutter_highlight.dart | 80 ++++++++++++++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 2889af8..d469c53 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -34,6 +34,7 @@ class _MyHomePageState extends State { appBar: AppBar(), body: Center( child: FlutterHighlight( + duration: Duration(milliseconds: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/flutter_highlight.dart b/lib/flutter_highlight.dart index 8784420..1575440 100644 --- a/lib/flutter_highlight.dart +++ b/lib/flutter_highlight.dart @@ -3,17 +3,63 @@ import 'package:flutter/rendering.dart'; @immutable class FlutterHighlight extends StatefulWidget { + final Duration duration; + final int blinkNumber; final Widget child; - const FlutterHighlight({super.key, required this.child}); + + const FlutterHighlight({ + super.key, + required this.duration, + this.blinkNumber = 3, + required this.child, + }); @override State createState() => _FlutterHighlightState(); } -class _FlutterHighlightState extends State { +class _FlutterHighlightState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + + Future _runAnimation() async { + int count = 0; + while (count < widget.blinkNumber) { + await _animationController.forward(); + await _animationController.reverse(); + count++; + } + } + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: widget.duration, + upperBound: 0.8, + ); + _runAnimation(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return _FlutterHighlightRender(child: widget.child); + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return _FlutterHighlightRender( + percent: _animationController.value, + child: child, + ); + }, + child: widget.child, + ); } } @@ -21,23 +67,43 @@ class _FlutterHighlightState extends State { @immutable class _FlutterHighlightRender extends SingleChildRenderObjectWidget { - const _FlutterHighlightRender({super.child}); + final double percent; + + const _FlutterHighlightRender({required this.percent, required super.child}); @override _FlutterHighlightFilter createRenderObject(BuildContext context) { - return _FlutterHighlightFilter(); + return _FlutterHighlightFilter(percent); + } + + @override + void updateRenderObject( + BuildContext context, + _FlutterHighlightFilter filter, + ) { + filter.percent = percent; } } /// ---------------------------------------------------------------------------- class _FlutterHighlightFilter extends RenderProxyBox { + double _percent; + + _FlutterHighlightFilter(this._percent); + @override ShaderMaskLayer? get layer => super.layer as ShaderMaskLayer?; @override bool get alwaysNeedsCompositing => child != null; + set percent(double newValue) { + if (newValue == _percent) return; + _percent = newValue; + markNeedsPaint(); + } + double _offset(double start, double end, double percent) { return start + (end - start) * percent; } @@ -61,8 +127,8 @@ class _FlutterHighlightFilter extends RenderProxyBox { begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ - Colors.yellow.withValues(alpha: 0.6), - Colors.yellow.withValues(alpha: 0.6), + Colors.yellow.withValues(alpha: _percent), + Colors.yellow.withValues(alpha: _percent), ], ).createShader(rect) ..maskRect = offset & size From 10a6851e3a60e28c198794c9e6da6cc33875d46c Mon Sep 17 00:00:00 2001 From: Thomas Ferreira <139474199+ThomasDevApps@users.noreply.github.com> Date: Fri, 25 Apr 2025 12:54:52 +0200 Subject: [PATCH 4/6] FEAT: Can update color --- lib/flutter_highlight.dart | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/flutter_highlight.dart b/lib/flutter_highlight.dart index 1575440..a72a09a 100644 --- a/lib/flutter_highlight.dart +++ b/lib/flutter_highlight.dart @@ -5,12 +5,14 @@ import 'package:flutter/rendering.dart'; class FlutterHighlight extends StatefulWidget { final Duration duration; final int blinkNumber; + final Color? color; final Widget child; const FlutterHighlight({ super.key, required this.duration, this.blinkNumber = 3, + this.color, required this.child, }); @@ -55,6 +57,7 @@ class _FlutterHighlightState extends State builder: (context, child) { return _FlutterHighlightRender( percent: _animationController.value, + color: widget.color ?? Theme.of(context).colorScheme.surface, child: child, ); }, @@ -68,12 +71,17 @@ class _FlutterHighlightState extends State @immutable class _FlutterHighlightRender extends SingleChildRenderObjectWidget { final double percent; + final Color color; - const _FlutterHighlightRender({required this.percent, required super.child}); + const _FlutterHighlightRender({ + required this.percent, + required this.color, + required super.child, + }); @override _FlutterHighlightFilter createRenderObject(BuildContext context) { - return _FlutterHighlightFilter(percent); + return _FlutterHighlightFilter(percent, color); } @override @@ -82,6 +90,7 @@ class _FlutterHighlightRender extends SingleChildRenderObjectWidget { _FlutterHighlightFilter filter, ) { filter.percent = percent; + filter.color = color; } } @@ -89,8 +98,9 @@ class _FlutterHighlightRender extends SingleChildRenderObjectWidget { class _FlutterHighlightFilter extends RenderProxyBox { double _percent; + Color _color; - _FlutterHighlightFilter(this._percent); + _FlutterHighlightFilter(this._percent, this._color); @override ShaderMaskLayer? get layer => super.layer as ShaderMaskLayer?; @@ -104,6 +114,12 @@ class _FlutterHighlightFilter extends RenderProxyBox { markNeedsPaint(); } + set color(Color newValue) { + if (newValue == _color) return; + _color = newValue; + markNeedsPaint(); + } + double _offset(double start, double end, double percent) { return start + (end - start) * percent; } @@ -127,8 +143,8 @@ class _FlutterHighlightFilter extends RenderProxyBox { begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ - Colors.yellow.withValues(alpha: _percent), - Colors.yellow.withValues(alpha: _percent), + _color.withValues(alpha: _percent), + _color.withValues(alpha: _percent), ], ).createShader(rect) ..maskRect = offset & size From 4dab0f346f32294e70521d0da996ea28dd309dfc Mon Sep 17 00:00:00 2001 From: Thomas Ferreira <139474199+ThomasDevApps@users.noreply.github.com> Date: Fri, 25 Apr 2025 12:56:28 +0200 Subject: [PATCH 5/6] DESIGN: Update example --- example/lib/main.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index d469c53..4e5c717 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -35,6 +35,7 @@ class _MyHomePageState extends State { body: Center( child: FlutterHighlight( duration: Duration(milliseconds: 500), + color: Theme.of(context).colorScheme.primary, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ From 6ac0ea8f2f7409ee9c465fa544aa88fa2cdf5a47 Mon Sep 17 00:00:00 2001 From: Thomas Ferreira <139474199+ThomasDevApps@users.noreply.github.com> Date: Fri, 25 Apr 2025 12:56:38 +0200 Subject: [PATCH 6/6] CHORE: update UpperBound --- lib/flutter_highlight.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/flutter_highlight.dart b/lib/flutter_highlight.dart index a72a09a..df14117 100644 --- a/lib/flutter_highlight.dart +++ b/lib/flutter_highlight.dart @@ -39,7 +39,7 @@ class _FlutterHighlightState extends State _animationController = AnimationController( vsync: this, duration: widget.duration, - upperBound: 0.8, + upperBound: 0.6, ); _runAnimation(); }