Skip to main content

arcline 1.0.0 copy "arcline: ^1.0.0" to clipboard
arcline: ^1.0.0 copied to clipboard

An animated arc gauge for Flutter: a spring-driven fill, an over-target lap, milestone markers and a counting centre label.

example/lib/main.dart

import 'package:arcline/arcline.dart';
import 'package:flutter/material.dart';

import 'controls.dart';

void main() => runApp(const ArclineDemo());

const _bg = Color(0xFF050505);
const _panel = Color(0xFF0E0E10);
const _line = Color(0x1AFFFFFF);
const _muted = Color(0xFFA1A1AA);
const _accent = Color(0xFFD9F99D);

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Arcline — animated arc gauge for Flutter',
      debugShowCheckedModeBanner: false,
      theme: demoControlsTheme(
        ThemeData(
          brightness: Brightness.dark,
          scaffoldBackgroundColor: _bg,
          colorScheme: const ColorScheme.dark(primary: _accent, surface: _panel),
        ),
      ),
      home: const DemoPage(),
    );
  }
}

class DemoPage extends StatefulWidget {
  const DemoPage({super.key});

  @override
  State<DemoPage> createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> {
  static const _target = 2454.0;
  double _eaten = 1745;
  double _sweep = 240;
  double _thickness = 22;
  double _damping = 14;

  void _add(double kcal) => setState(() => _eaten = (_eaten + kcal).clamp(0, _target * 2));

  @override
  Widget build(BuildContext context) {
    final remaining = _target - _eaten;
    return Scaffold(
      body: SafeArea(
        bottom: false,
        child: Center(
          child: SingleChildScrollView(
            padding: EdgeInsets.fromLTRB(20, 28, 20, 40 + MediaQuery.paddingOf(context).bottom),
            child: ConstrainedBox(
              constraints: const BoxConstraints(maxWidth: 920),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const _Header(),
                  const SizedBox(height: 28),
                  Wrap(
                    spacing: 20,
                    runSpacing: 20,
                    children: [
                      _Panel(
                        width: 440,
                        child: Column(
                          children: [
                            Arcline(
                              value: _eaten,
                              max: _target,
                              size: 280,
                              thickness: _thickness,
                              sweepDegrees: _sweep,
                              damping: _damping,
                              markers: const [_target * 0.8],
                              semanticLabel: 'Calories today',
                              center: (context, value) => Column(
                                mainAxisSize: MainAxisSize.min,
                                children: [
                                  Text(
                                    '${value.round()}',
                                    style: const TextStyle(
                                      fontSize: 46,
                                      fontWeight: FontWeight.w800,
                                      letterSpacing: -1.5,
                                      fontFeatures: [FontFeature.tabularFigures()],
                                    ),
                                  ),
                                  const Text('of 2,454 kcal', style: TextStyle(color: _muted)),
                                ],
                              ),
                            ),
                            const SizedBox(height: 4),
                            Text(
                              remaining >= 0
                                  ? '${remaining.round()} kcal left today'
                                  : '${(-remaining).round()} kcal over target',
                              style: TextStyle(
                                color: remaining >= 0 ? _muted : const Color(0xFFFB923C),
                                fontWeight: FontWeight.w600,
                              ),
                            ),
                            const SizedBox(height: 18),
                            Wrap(
                              spacing: 8,
                              runSpacing: 8,
                              alignment: WrapAlignment.center,
                              children: [
                                _Chip(label: '+ Snack 180', onTap: () => _add(180)),
                                _Chip(label: '+ Lunch 620', onTap: () => _add(620)),
                                _Chip(label: '− 250', onTap: () => _add(-250)),
                                _Chip(label: 'Reset', onTap: () => setState(() => _eaten = 0)),
                              ],
                            ),
                          ],
                        ),
                      ),
                      _Panel(
                        width: 440,
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            const Text(
                              'Feel',
                              style: TextStyle(color: _muted, fontWeight: FontWeight.w600),
                            ),
                            _Setting(
                              label: 'Sweep',
                              value: '${_sweep.round()}°',
                              slider: Slider(
                                value: _sweep,
                                min: 120,
                                max: 360,
                                onChanged: (v) => setState(() => _sweep = v),
                              ),
                            ),
                            _Setting(
                              label: 'Thickness',
                              value: '${_thickness.round()} px',
                              slider: Slider(
                                value: _thickness,
                                min: 6,
                                max: 40,
                                onChanged: (v) => setState(() => _thickness = v),
                              ),
                            ),
                            _Setting(
                              label: 'Damping',
                              value: _damping.toStringAsFixed(0),
                              slider: Slider(
                                value: _damping,
                                min: 4,
                                max: 30,
                                onChanged: (v) => setState(() => _damping = v),
                              ),
                            ),
                            const SizedBox(height: 8),
                            const Text(
                              'Tap the buttons quickly — the arc keeps its momentum instead of restarting. '
                              'Go past the target to see the second lap. The tick marks 80% of the goal.',
                              style: TextStyle(color: _muted, height: 1.6),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 20),
                  const _Panel(
                    child: Wrap(
                      spacing: 28,
                      runSpacing: 20,
                      alignment: WrapAlignment.spaceAround,
                      children: [
                        _Mini(
                          label: 'Steps',
                          value: 7420,
                          max: 10000,
                          unit: '',
                          colors: [Color(0xFF93C5FD), Color(0xFFC4B5FD)],
                        ),
                        _Mini(
                          label: 'Water',
                          value: 1.8,
                          max: 2.5,
                          unit: ' L',
                          decimals: 1,
                          colors: [Color(0xFF67E8F9), Color(0xFF7DD3FC)],
                        ),
                        _Mini(
                          label: 'Sleep',
                          value: 8.4,
                          max: 8,
                          unit: ' h',
                          decimals: 1,
                          colors: [Color(0xFFF9A8D4), Color(0xFFFDA4AF)],
                        ),
                      ],
                    ),
                  ),
                  const SizedBox(height: 24),
                  const SizedBox(
                    width: double.infinity,
                    child: Text(
                      'MIT © 2026 Yagnik Barasiya · github.com/YagnikBarasiya23/arcline',
                      style: TextStyle(color: _muted, fontSize: 13),
                      textAlign: TextAlign.center,
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _Header extends StatelessWidget {
  const _Header();

  @override
  Widget build(BuildContext context) {
    return const Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          'ARCLINE · FLUTTER',
          style: TextStyle(color: Color(0xFF71717A), letterSpacing: 3.5, fontSize: 12, fontWeight: FontWeight.w600),
        ),
        SizedBox(height: 12),
        Text(
          'Progress that springs,\nnot ticks.',
          style: TextStyle(fontSize: 44, height: 1.02, fontWeight: FontWeight.w800, letterSpacing: -1.8),
        ),
        SizedBox(height: 14),
        Text(
          'An arc gauge with a spring-driven fill, a second lap past the target, milestone ticks and a centre that counts along.',
          style: TextStyle(color: _muted, fontSize: 16, height: 1.6),
        ),
      ],
    );
  }
}

class _Panel extends StatelessWidget {
  const _Panel({required this.child, this.width});

  final Widget child;
  final double? width;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: width,
      padding: const EdgeInsets.all(24),
      decoration: BoxDecoration(
        color: _panel,
        borderRadius: BorderRadius.circular(22),
        border: Border.all(color: _line),
      ),
      child: child,
    );
  }
}

class _Chip extends StatelessWidget {
  const _Chip({required this.label, required this.onTap});

  final String label;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return PillButton(label: label, onPressed: onTap);
  }
}

class _Setting extends StatelessWidget {
  const _Setting({required this.label, required this.value, required this.slider});

  final String label;
  final String value;
  final Widget slider;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(top: 14),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Text(label, style: const TextStyle(color: _muted)),
              const Spacer(),
              Text(value),
            ],
          ),
          slider,
        ],
      ),
    );
  }
}

class _Mini extends StatelessWidget {
  const _Mini({
    required this.label,
    required this.value,
    required this.max,
    required this.unit,
    required this.colors,
    this.decimals = 0,
  });

  final String label;
  final double value;
  final double max;
  final String unit;
  final List<Color> colors;
  final int decimals;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Arcline(
          value: value,
          max: max,
          size: 150,
          thickness: 12,
          sweepDegrees: 360,
          colors: colors,
          semanticLabel: label,
          center: (context, v) => Text(
            '${v.toStringAsFixed(decimals)}$unit',
            style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
          ),
        ),
        const SizedBox(height: 6),
        Text(label, style: const TextStyle(color: _muted)),
      ],
    );
  }
}
0
likes
0
points
79
downloads

Publisher

unverified uploader

Weekly Downloads

An animated arc gauge for Flutter: a spring-driven fill, an over-target lap, milestone markers and a counting centre label.

Homepage
Repository (GitHub)
View/report issues

Topics

#animation #gauge #progress #widget #ui

License

unknown (license)

Dependencies

flutter

More

Packages that depend on arcline