anim_svg 0.0.4 copy "anim_svg: ^0.0.4" to clipboard
anim_svg: ^0.0.4 copied to clipboard

Flutter widget for animated SVG. Transpiles SMIL, CSS @keyframes, and Svgator animations to Lottie JSON, rendered by the native thorvg engine.

example/lib/main.dart

import 'package:anim_svg/anim_svg.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  /// 6 distinct SVGs, repeated [_repeatCount] times to make the list long
  /// enough that some items are visible and the rest sit below the fold.
  /// Scrolling them past the viewport (and pausing at the bottom) is the
  /// only way to exercise the visibility-driven dispose path.
  static const _baseAssets = [
    'assets/svg_anim_1.svg',
    'assets/svg_anim_2.svg',
    'assets/svg_anim_3.svg',
    'assets/svg_anim_4.svg',
    'assets/svg_anim_5.svg',
    'assets/svg_anim_6.svg',
  ];

  static const int _repeatCount = 4; // → 24 items total
  static const double _cellHeight = 220.0;
  static const double _gap = 8.0;

  @override
  Widget build(BuildContext context) {
    final itemCount = _baseAssets.length * _repeatCount;
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('anim_svg demo')),
        body: ListView.separated(
          padding: const EdgeInsets.all(_gap),
          itemCount: itemCount,
          separatorBuilder: (_, _) => const SizedBox(height: _gap),
          itemBuilder: (context, index) {
            final path = _baseAssets[index % _baseAssets.length];
            return _DemoTile(
              index: index,
              assetPath: path,
              height: _cellHeight,
            );
          },
        ),
      ),
    );
  }
}

/// One row in the demo list. The `Stack` overlay shows the item index and
/// asset filename so a reader can correlate live logs with the visible
/// position. The `AnimSvgView` itself is wrapped in `_AspectFittedSvg` so
/// thorvg's render buffer hugs the source aspect rather than the cell box.
class _DemoTile extends StatelessWidget {
  const _DemoTile({
    required this.index,
    required this.assetPath,
    required this.height,
  });

  final int index;
  final String assetPath;
  final double height;

  @override
  Widget build(BuildContext context) {
    final cellW = MediaQuery.sizeOf(context).width - MyApp._gap * 2;
    return SizedBox(
      height: height,
      child: DecoratedBox(
        decoration: BoxDecoration(
          border: Border.all(color: Colors.black12),
          borderRadius: BorderRadius.circular(8),
        ),
        child: ClipRRect(
          borderRadius: BorderRadius.circular(8),
          child: Stack(
            children: [
              _AspectFittedSvg(
                assetPath: assetPath,
                maxWidth: cellW,
                maxHeight: height,
                index: index,
              ),
              Positioned(
                top: 6,
                left: 6,
                child: _IndexBadge(index: index, asset: assetPath),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

/// Top-left badge showing the item's position in the list and the asset
/// filename. Pure visual aid for debug — no functional role.
class _IndexBadge extends StatelessWidget {
  const _IndexBadge({required this.index, required this.asset});

  final int index;
  final String asset;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
      decoration: BoxDecoration(
        color: Colors.black.withValues(alpha: 0.55),
        borderRadius: BorderRadius.circular(4),
      ),
      child: Text(
        '#$index · ${asset.split('/').last}',
        style: const TextStyle(
          color: Colors.white,
          fontSize: 10,
          fontFeatures: [FontFeature.tabularFigures()],
        ),
      ),
    );
  }
}

/// Loads the SVG asset once, reads the intrinsic `viewBox`/`width`/`height`,
/// then renders `AnimSvgView` sized so it fits inside `maxWidth x maxHeight`
/// while preserving the source aspect ratio.
class _AspectFittedSvg extends StatefulWidget {
  const _AspectFittedSvg({
    required this.assetPath,
    required this.maxWidth,
    required this.maxHeight,
    required this.index,
  });

  final String assetPath;
  final double maxWidth;
  final double maxHeight;
  final int index;

  @override
  State<_AspectFittedSvg> createState() => _AspectFittedSvgState();
}

class _AspectFittedSvgState extends State<_AspectFittedSvg> {
  late final Future<_Size> _sizeFuture = _resolveSize();

  Future<_Size> _resolveSize() async {
    final text = await rootBundle.loadString(widget.assetPath);
    return _parseSize(text) ?? const _Size(1, 1);
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<_Size>(
      future: _sizeFuture,
      builder: (context, snap) {
        if (!snap.hasData) {
          return SizedBox(width: widget.maxWidth, height: widget.maxHeight);
        }
        final size = snap.data!;
        final scale = (widget.maxWidth / size.w).clamp(0.0, widget.maxHeight / size.h);
        final renderW = size.w * scale;
        final renderH = size.h * scale;
        return Center(
          child: SizedBox(
            width: renderW,
            height: renderH,
            child: AnimSvgView.asset(
              widget.assetPath,
              width: renderW,
              height: renderH,
              renderScale: 2.0,
              // Default DeveloperLogger() emits to dart:developer. The
              // explicit pass keeps this demo self-documenting: every
              // pipeline stage (load / convert / engine / visibility /
              // hide / show) is tagged with `[anim_svg] [...]` in the
              // run console so the reader can see exactly which steps
              // are firing for which list item.
              logger: DeveloperLogger(),
              controller: AnimSvgController(),
              errorBuilder: (ctx, err, _) => Padding(
                padding: const EdgeInsets.all(12),
                child: Center(
                  child: Text(
                    '#${widget.index} ${widget.assetPath.split('/').last}\n\n$err',
                    style: const TextStyle(fontSize: 10, color: Colors.red),
                    textAlign: TextAlign.center,
                  ),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

class _Size {
  const _Size(this.w, this.h);
  final double w;
  final double h;
}

/// Extracts SVG intrinsic dimensions: prefers `viewBox` (last two numbers),
/// falls back to `width`/`height`. Returns null if neither is usable.
_Size? _parseSize(String svg) {
  final viewBox = RegExp(r'viewBox\s*=\s*"([^"]+)"').firstMatch(svg);
  if (viewBox != null) {
    final parts = viewBox.group(1)!.split(RegExp(r'[\s,]+')).where((s) => s.isNotEmpty).toList();
    if (parts.length == 4) {
      final w = double.tryParse(parts[2]);
      final h = double.tryParse(parts[3]);
      if (w != null && h != null && w > 0 && h > 0) return _Size(w, h);
    }
  }
  final wAttr = RegExp(r'<svg\b[^>]*\swidth\s*=\s*"([^"]+)"').firstMatch(svg);
  final hAttr = RegExp(r'<svg\b[^>]*\sheight\s*=\s*"([^"]+)"').firstMatch(svg);
  if (wAttr != null && hAttr != null) {
    final w = double.tryParse(wAttr.group(1)!.replaceAll(RegExp(r'[^\d.]'), ''));
    final h = double.tryParse(hAttr.group(1)!.replaceAll(RegExp(r'[^\d.]'), ''));
    if (w != null && h != null && w > 0 && h > 0) return _Size(w, h);
  }
  return null;
}
2
likes
140
points
335
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter widget for animated SVG. Transpiles SMIL, CSS @keyframes, and Svgator animations to Lottie JSON, rendered by the native thorvg engine.

Repository (GitHub)
View/report issues

Topics

#svg #animation #lottie #thorvg #smil

License

MIT (license)

Dependencies

ffi, flutter, flutter_cache_manager, http, image, meta, thorvg_plus, visibility_detector, xml

More

Packages that depend on anim_svg

Packages that implement anim_svg