Skip to content

Commit 0380b85

Browse files
committed
Add animated tray icon generator and controls
Introduces AnimatedIconGenerator for generating animated tray icons with various effects (spinner, pulse, blink, progress, wave, rotating square). Updates main.dart to integrate the generator, provide icon management (asset and widget conversion), and add UI controls for starting/stopping animations on tray icons.
1 parent f279fb6 commit 0380b85

2 files changed

Lines changed: 573 additions & 0 deletions

File tree

Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
import 'dart:async';
2+
import 'dart:convert';
3+
import 'dart:ui' as ui;
4+
import 'dart:math' as math;
5+
6+
import 'package:flutter/material.dart' hide Image;
7+
import 'package:nativeapi/nativeapi.dart';
8+
9+
/// A generator for creating animated icons for TrayIcon.
10+
///
11+
/// This class provides several built-in pixel animations that can be
12+
/// continuously updated on a TrayIcon's icon.
13+
///
14+
/// Example:
15+
/// ```dart
16+
/// final generator = AnimatedIconGenerator(size: 32);
17+
/// final trayIcon = TrayIcon();
18+
///
19+
/// // Start a spinner animation
20+
/// generator.startSpinner(
21+
/// onFrame: (image) {
22+
/// trayIcon.icon = image;
23+
/// },
24+
/// );
25+
///
26+
/// // Stop animation when done
27+
/// generator.stop();
28+
/// ```
29+
class AnimatedIconGenerator {
30+
final int size;
31+
final Color foregroundColor;
32+
final Color backgroundColor;
33+
final double devicePixelRatio;
34+
35+
Timer? _animationTimer;
36+
int _currentFrame = 0;
37+
38+
AnimatedIconGenerator({
39+
this.size = 32, // Higher default size for better quality
40+
this.foregroundColor = Colors.blue,
41+
this.backgroundColor = Colors.transparent,
42+
double? devicePixelRatio,
43+
}) : devicePixelRatio = devicePixelRatio ?? ui.window.devicePixelRatio;
44+
45+
/// Start a spinning loader animation.
46+
///
47+
/// The animation continuously rotates a circular loader. Update interval
48+
/// controls how fast the animation runs (lower = faster).
49+
Future<void> startSpinner({
50+
required Future<void> Function(Image) onFrame,
51+
Duration updateInterval = const Duration(milliseconds: 100),
52+
}) async {
53+
stop();
54+
55+
_animationTimer = Timer.periodic(updateInterval, (timer) async {
56+
final image = await _generateSpinnerFrame();
57+
await onFrame(image);
58+
});
59+
}
60+
61+
/// Start a pulsing dot animation.
62+
///
63+
/// Creates a pulsing circular dot that expands and contracts.
64+
Future<void> startPulse({
65+
required Future<void> Function(Image) onFrame,
66+
Duration updateInterval = const Duration(milliseconds: 150),
67+
}) async {
68+
stop();
69+
70+
_animationTimer = Timer.periodic(updateInterval, (timer) async {
71+
final image = await _generatePulseFrame();
72+
await onFrame(image);
73+
});
74+
}
75+
76+
/// Start a blinking dot animation.
77+
///
78+
/// Creates a simple on/off blinking effect.
79+
Future<void> startBlink({
80+
required Future<void> Function(Image) onFrame,
81+
Duration updateInterval = const Duration(milliseconds: 500),
82+
}) async {
83+
stop();
84+
85+
_animationTimer = Timer.periodic(updateInterval, (timer) async {
86+
final image = await _generateBlinkFrame();
87+
await onFrame(image);
88+
});
89+
}
90+
91+
/// Start a progress bar animation.
92+
///
93+
/// Shows a horizontal progress bar that fills from left to right.
94+
Future<void> startProgress({
95+
required Future<void> Function(Image) onFrame,
96+
Duration updateInterval = const Duration(milliseconds: 80),
97+
}) async {
98+
stop();
99+
100+
_animationTimer = Timer.periodic(updateInterval, (timer) async {
101+
final image = await _generateProgressFrame();
102+
await onFrame(image);
103+
});
104+
}
105+
106+
/// Start a wave animation.
107+
///
108+
/// Creates a vertical wave pattern that moves left to right.
109+
Future<void> startWave({
110+
required Future<void> Function(Image) onFrame,
111+
Duration updateInterval = const Duration(milliseconds: 100),
112+
}) async {
113+
stop();
114+
115+
_animationTimer = Timer.periodic(updateInterval, (timer) async {
116+
final image = await _generateWaveFrame();
117+
await onFrame(image);
118+
});
119+
}
120+
121+
/// Start a rotating square animation.
122+
///
123+
/// Rotates a square icon continuously.
124+
Future<void> startRotatingSquare({
125+
required Future<void> Function(Image) onFrame,
126+
Duration updateInterval = const Duration(milliseconds: 100),
127+
}) async {
128+
stop();
129+
130+
_animationTimer = Timer.periodic(updateInterval, (timer) async {
131+
final image = await _generateRotatingSquareFrame();
132+
await onFrame(image);
133+
});
134+
}
135+
136+
/// Stop the current animation.
137+
void stop() {
138+
_animationTimer?.cancel();
139+
_animationTimer = null;
140+
_currentFrame = 0;
141+
}
142+
143+
/// Dispose resources.
144+
void dispose() {
145+
stop();
146+
}
147+
148+
// Setup canvas with high DPI scaling
149+
void _setupCanvas(Canvas canvas) {
150+
canvas.scale(devicePixelRatio, devicePixelRatio);
151+
}
152+
153+
// Generate spinner frame
154+
Future<Image> _generateSpinnerFrame() async {
155+
final recorder = ui.PictureRecorder();
156+
final canvas = Canvas(recorder);
157+
_setupCanvas(canvas);
158+
159+
final paint = Paint()
160+
..color = foregroundColor
161+
..style = PaintingStyle.stroke
162+
..strokeWidth = 3.0; // Thicker line for better visibility
163+
164+
final center = Offset(size / 2, size / 2);
165+
final radius = size / 2 - 3; // Adjust for thicker line
166+
167+
final angle = (_currentFrame * 30) % 360;
168+
final rotationMatrix = Matrix4.identity()
169+
..translate(center.dx, center.dy)
170+
..rotateZ(angle * math.pi / 180)
171+
..translate(-center.dx, -center.dy);
172+
173+
canvas.save();
174+
canvas.transform(rotationMatrix.storage);
175+
176+
canvas.drawArc(
177+
Rect.fromCircle(center: center, radius: radius),
178+
0,
179+
math.pi,
180+
false,
181+
paint,
182+
);
183+
184+
canvas.restore();
185+
186+
return await _imageFromCanvas(recorder);
187+
}
188+
189+
// Generate pulse frame
190+
Future<Image> _generatePulseFrame() async {
191+
final recorder = ui.PictureRecorder();
192+
final canvas = Canvas(recorder);
193+
_setupCanvas(canvas);
194+
195+
final progress = (_currentFrame % 10) / 10.0;
196+
final scale = 0.35 + (progress * 0.65); // Slightly larger minimum size
197+
198+
final paint = Paint()
199+
..color = foregroundColor.withOpacity(0.9); // More opaque for better visibility
200+
201+
final center = Offset(size / 2, size / 2);
202+
final radius = (size / 2 - 2) * scale; // More padding for clarity
203+
204+
canvas.drawCircle(center, radius, paint);
205+
206+
return await _imageFromCanvas(recorder);
207+
}
208+
209+
// Generate blink frame
210+
Future<Image> _generateBlinkFrame() async {
211+
final recorder = ui.PictureRecorder();
212+
final canvas = Canvas(recorder);
213+
_setupCanvas(canvas);
214+
215+
final isOn = (_currentFrame % 2) == 0;
216+
217+
if (isOn) {
218+
final paint = Paint()..color = foregroundColor;
219+
final center = Offset(size / 2, size / 2);
220+
final radius = size / 2 - 2; // More padding for better visibility
221+
222+
canvas.drawCircle(center, radius, paint);
223+
}
224+
225+
return await _imageFromCanvas(recorder);
226+
}
227+
228+
// Generate progress frame
229+
Future<Image> _generateProgressFrame() async {
230+
final recorder = ui.PictureRecorder();
231+
final canvas = Canvas(recorder);
232+
_setupCanvas(canvas);
233+
234+
final progress = (_currentFrame % 13) / 12.0;
235+
236+
final paint = Paint()..color = foregroundColor;
237+
238+
final padding = 2.0; // Larger padding for better visibility
239+
final barHeight = size - 2 * padding;
240+
final barWidth = (size - 2 * padding) * progress;
241+
242+
canvas.drawRect(
243+
Rect.fromLTWH(padding, padding, barWidth, barHeight),
244+
paint,
245+
);
246+
247+
return await _imageFromCanvas(recorder);
248+
}
249+
250+
// Generate wave frame
251+
Future<Image> _generateWaveFrame() async {
252+
final recorder = ui.PictureRecorder();
253+
final canvas = Canvas(recorder);
254+
_setupCanvas(canvas);
255+
256+
final paint = Paint()
257+
..color = foregroundColor
258+
..style = PaintingStyle.stroke
259+
..strokeWidth = 2.5; // Thicker line for better visibility
260+
261+
final path = Path();
262+
final offset = (_currentFrame % size).toDouble();
263+
264+
for (int i = 0; i < size; i++) {
265+
final x = i.toDouble();
266+
final y = size / 2 +
267+
(size / 4) *
268+
math.sin((i + offset) * 2 * math.pi / size);
269+
270+
if (i == 0) {
271+
path.moveTo(x, y);
272+
} else {
273+
path.lineTo(x, y);
274+
}
275+
}
276+
277+
canvas.drawPath(path, paint);
278+
279+
return await _imageFromCanvas(recorder);
280+
}
281+
282+
// Generate rotating square frame
283+
Future<Image> _generateRotatingSquareFrame() async {
284+
final recorder = ui.PictureRecorder();
285+
final canvas = Canvas(recorder);
286+
_setupCanvas(canvas);
287+
288+
final paint = Paint()
289+
..color = foregroundColor
290+
..style = PaintingStyle.stroke
291+
..strokeWidth = 3.0; // Thicker line for better visibility
292+
293+
final center = Offset(size / 2, size / 2);
294+
final angle = (_currentFrame * 10) % 360;
295+
296+
final rotationMatrix = Matrix4.identity()
297+
..translate(center.dx, center.dy)
298+
..rotateZ(angle * math.pi / 180)
299+
..translate(-center.dx, -center.dy);
300+
301+
canvas.save();
302+
canvas.transform(rotationMatrix.storage);
303+
304+
final squareSize = (size - 6).toDouble(); // Adjust for thicker line
305+
final rect = Rect.fromLTWH(
306+
(size - squareSize.toInt()) / 2,
307+
(size - squareSize.toInt()) / 2,
308+
squareSize,
309+
squareSize,
310+
);
311+
312+
canvas.drawRect(rect, paint);
313+
314+
canvas.restore();
315+
316+
return await _imageFromCanvas(recorder);
317+
}
318+
319+
// Convert canvas to Image object with high DPI support
320+
Future<Image> _imageFromCanvas(ui.PictureRecorder recorder) async {
321+
final picture = recorder.endRecording();
322+
323+
// Calculate high DPI image size (canvas already scaled by devicePixelRatio)
324+
final imageWidth = (size * devicePixelRatio).toInt();
325+
final imageHeight = (size * devicePixelRatio).toInt();
326+
327+
final img = await picture.toImage(imageWidth, imageHeight);
328+
final byteData = await img.toByteData(format: ui.ImageByteFormat.png);
329+
330+
if (byteData == null) {
331+
throw Exception('Failed to convert image to bytes');
332+
}
333+
334+
final pngBytes = byteData.buffer.asUint8List();
335+
final base64String = 'data:image/png;base64,${base64Encode(pngBytes)}';
336+
337+
_currentFrame++;
338+
339+
return Image.fromBase64(base64String)!;
340+
}
341+
}
342+

0 commit comments

Comments
 (0)