anim_svg 0.0.1 copy "anim_svg: ^0.0.1" to clipboard
anim_svg: ^0.0.1 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';

void main() {
  runApp(const MyApp());
}

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

  static const _assets = [
    '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',
  ];

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('anim_svg demo')),
        body: LayoutBuilder(
          builder: (context, constraints) {
            const gap = 8.0;
            final cellW = constraints.maxWidth - gap * 2;
            final cellH = (constraints.maxHeight - gap * (_assets.length + 1)) / _assets.length;
            return ListView.separated(
              padding: const EdgeInsets.all(gap),
              itemCount: _assets.length,
              separatorBuilder: (_, __) => const SizedBox(height: gap),
              itemBuilder: (context, index) {
                final path = _assets[index];
                return SizedBox(
                  height: cellH,
                  child: DecoratedBox(
                    decoration: BoxDecoration(
                      border: Border.all(color: Colors.black12),
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: ClipRRect(
                      borderRadius: BorderRadius.circular(8),
                      child: _AspectFittedSvg(assetPath: path, maxWidth: cellW, maxHeight: cellH),
                    ),
                  ),
                );
              },
            );
          },
        ),
      ),
    );
  }
}

/// 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,
  });

  final String assetPath;
  final double maxWidth;
  final double maxHeight;

  @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,
              controller: AnimSvgController(),
              errorBuilder: (ctx, err, _) => Padding(
                padding: const EdgeInsets.all(12),
                child: Center(
                  child: Text(
                    '${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
0
points
335
downloads

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

unknown (license)

Dependencies

ffi, flutter, flutter_cache_manager, http, image, meta, thorvg, xml

More

Packages that depend on anim_svg

Packages that implement anim_svg