contour_gradient 1.0.3 copy "contour_gradient: ^1.0.3" to clipboard
contour_gradient: ^1.0.3 copied to clipboard

A ShapeBorder and InputBorder that paint a gradient along the length of any OutlinedBorder shape, for containers, buttons, cards, chips and text fields.

example/lib/main.dart

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

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

const List<Color> _sunset = <Color>[
  Color(0xFF7F00FF),
  Color(0xFF00C6FF),
  Color(0xFFFF4E50),
  Color(0xFF7F00FF),
];

const List<Color> _aurora = <Color>[
  Color(0xFF00F260),
  Color(0xFF0575E6),
  Color(0xFFE100FF),
  Color(0xFFFFC300),
  Color(0xFF00F260),
];

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Contour Gradient',
      theme: ThemeData(colorSchemeSeed: Colors.deepPurple),
      darkTheme: ThemeData(
        colorSchemeSeed: Colors.deepPurple,
        brightness: Brightness.dark,
      ),
      home: const ExamplePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Contour Gradient')),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        shape: const ContourGradientBorder(
          colors: _aurora,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.all(Radius.circular(16)),
          ),
          side: BorderSide(width: 2),
        ),
        child: const Icon(Icons.add),
      ),
      body: ListView(
        padding: const EdgeInsets.fromLTRB(16, 16, 16, 96),
        children: const <Widget>[
          _Section(
            title: 'Shapes',
            description:
                'Any OutlinedBorder can be used as the shape. The gradient '
                'follows the outline, so each color gets the same share of '
                'it whatever the shape.',
            child: _ShapesGallery(),
          ),
          _Section(
            title: 'Buttons',
            description:
                'Buttons apply their own side to the shape, so set the width '
                'with the style\'s side.',
            child: _Buttons(),
          ),
          _Section(title: 'Cards, chips and inputs', child: _MaterialWidgets()),
          _Section(
            title: 'Animated start offset',
            description:
                'Animating startOffset from 0.0 to 1.0 moves the gradient '
                'once around the border, and repeats seamlessly.',
            child: _RotatingBorders(),
          ),
          _Section(
            title: 'Morphing shapes',
            description:
                'ContourGradientBorder lerps, so AnimatedContainer can morph '
                'between shapes, widths and gradients. Tap to change.',
            child: _MorphingBorder(),
          ),
          _Section(
            title: 'Stops',
            description:
                'Stops place colors along the border. Equal stops make a '
                'hard edge.',
            child: _Stops(),
          ),
        ],
      ),
    );
  }
}

class _Section extends StatelessWidget {
  const _Section({required this.title, this.description, required this.child});

  final String title;
  final String? description;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final TextTheme text = Theme.of(context).textTheme;
    return Padding(
      padding: const EdgeInsets.only(bottom: 32),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(title, style: text.titleLarge),
          if (description != null) ...<Widget>[
            const SizedBox(height: 4),
            Text(description!, style: text.bodyMedium),
          ],
          const SizedBox(height: 16),
          child,
        ],
      ),
    );
  }
}

/// A labelled box painted with [shape].
class _ShapeTile extends StatelessWidget {
  const _ShapeTile({
    required this.label,
    required this.shape,
    this.colors = _sunset,
    this.width = 120,
    this.height = 80,
    this.startOffset = 0.0,
    this.stops,
  });

  final String label;
  final OutlinedBorder shape;
  final List<Color> colors;
  final List<double>? stops;
  final double width;
  final double height;
  final double startOffset;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        Container(
          width: width,
          height: height,
          decoration: ShapeDecoration(
            shape: ContourGradientBorder(
              colors: colors,
              stops: stops,
              startOffset: startOffset,
              shape: shape,
              side: const BorderSide(width: 4),
            ),
          ),
        ),
        const SizedBox(height: 8),
        Text(label, style: Theme.of(context).textTheme.labelMedium),
      ],
    );
  }
}

class _ShapesGallery extends StatelessWidget {
  const _ShapesGallery();

  @override
  Widget build(BuildContext context) {
    return const Wrap(
      spacing: 24,
      runSpacing: 24,
      children: <Widget>[
        _ShapeTile(label: 'Rectangle', shape: RoundedRectangleBorder()),
        _ShapeTile(
          label: 'Rounded rectangle',
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.all(Radius.circular(20)),
          ),
        ),
        _ShapeTile(label: 'Stadium', shape: StadiumBorder()),
        _ShapeTile(
          label: 'Beveled',
          shape: BeveledRectangleBorder(
            borderRadius: BorderRadius.all(Radius.circular(16)),
          ),
        ),
        _ShapeTile(label: 'Circle', shape: CircleBorder(), width: 80),
        _ShapeTile(label: 'Oval', shape: OvalBorder()),
        _ShapeTile(
          label: 'Superellipse',
          shape: RoundedSuperellipseBorder(
            borderRadius: BorderRadius.all(Radius.circular(28)),
          ),
        ),
        _ShapeTile(
          label: 'Star',
          shape: StarBorder(innerRadiusRatio: 0.45),
          width: 90,
          height: 90,
        ),
        _ShapeTile(
          label: 'Rounded star',
          shape: StarBorder(
            points: 7,
            innerRadiusRatio: 0.65,
            pointRounding: 0.5,
            valleyRounding: 0.3,
          ),
          width: 90,
          height: 90,
        ),
        _ShapeTile(
          label: 'Hexagon',
          shape: StarBorder.polygon(sides: 6, pointRounding: 0.2),
          width: 90,
          height: 90,
        ),
        // A LinearBorder runs the gradient along only the edges it draws.
        _ShapeTile(
          label: 'Linear, bottom',
          shape: LinearBorder(bottom: LinearBorderEdge()),
        ),
        _ShapeTile(
          label: 'Linear, top and bottom',
          shape: LinearBorder(
            bottom: LinearBorderEdge(),
            top: LinearBorderEdge(),
          ),
        ),
        _ShapeTile(
          label: 'Linear, start and bottom',
          shape: LinearBorder(
            start: LinearBorderEdge(),
            bottom: LinearBorderEdge(),
          ),
        ),
        _ShapeTile(
          label: 'Linear, half bottom',
          shape: LinearBorder(bottom: LinearBorderEdge(size: 0.5)),
        ),
      ],
    );
  }
}

class _Buttons extends StatelessWidget {
  const _Buttons();

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 16,
      runSpacing: 16,
      crossAxisAlignment: WrapCrossAlignment.center,
      children: <Widget>[
        OutlinedButton(
          style: OutlinedButton.styleFrom(
            shape: const ContourGradientBorder(
              colors: _sunset,
              shape: StadiumBorder(),
            ),
            side: const BorderSide(width: 2),
          ),
          onPressed: () {},
          child: const Text('OutlinedButton'),
        ),
        OutlinedButton.icon(
          style: OutlinedButton.styleFrom(
            shape: const ContourGradientBorder(
              colors: _aurora,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.all(Radius.circular(8)),
              ),
            ),
            side: const BorderSide(width: 2),
          ),
          onPressed: () {},
          icon: const Icon(Icons.auto_awesome),
          label: const Text('With icon'),
        ),
        IconButton(
          style: IconButton.styleFrom(
            shape: const ContourGradientBorder(
              colors: _aurora,
              shape: CircleBorder(),
              side: BorderSide(width: 2),
            ),
          ),
          onPressed: () {},
          icon: const Icon(Icons.favorite),
        ),
      ],
    );
  }
}

class _MaterialWidgets extends StatelessWidget {
  const _MaterialWidgets();

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        const Card(
          shape: ContourGradientBorder(
            colors: _sunset,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.all(Radius.circular(16)),
            ),
            side: BorderSide(width: 2),
          ),
          child: ListTile(
            leading: Icon(Icons.gradient),
            title: Text('Card'),
            subtitle: Text('A Card with a gradient border.'),
          ),
        ),
        const SizedBox(height: 16),
        Wrap(
          spacing: 8,
          runSpacing: 8,
          children: <Widget>[
            for (final String label in <String>['Chip', 'Another', 'Third'])
              Chip(
                label: Text(label),
                // Chips apply their own side to the shape.
                side: const BorderSide(width: 1.5),
                shape: const ContourGradientBorder(
                  colors: _aurora,
                  shape: StadiumBorder(),
                ),
              ),
            ActionChip(
              avatar: const Icon(Icons.bolt, size: 18),
              label: const Text('ActionChip'),
              onPressed: () {},
              side: const BorderSide(width: 1.5),
              shape: const ContourGradientBorder(
                colors: _sunset,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.all(Radius.circular(8)),
                ),
              ),
            ),
          ],
        ),
        const SizedBox(height: 16),
        // Text fields take a ContourGradientInputBorder. The field applies
        // its own side, so the border gets thicker when focused, and a
        // floating label gets a gap in the top of the border.
        const TextField(
          decoration: InputDecoration(
            labelText: 'Name',
            border: ContourGradientInputBorder(
              colors: _sunset,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.all(Radius.circular(16)),
              ),
            ),
          ),
        ),
        const SizedBox(height: 16),
        const TextField(
          decoration: InputDecoration(
            hintText: 'Search',
            prefixIcon: Icon(Icons.search),
            border: ContourGradientInputBorder(
              colors: _aurora,
              shape: StadiumBorder(),
            ),
          ),
        ),
      ],
    );
  }
}

class _RotatingBorders extends StatefulWidget {
  const _RotatingBorders();

  @override
  State<_RotatingBorders> createState() => _RotatingBordersState();
}

class _RotatingBordersState extends State<_RotatingBorders>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller = AnimationController(
    vsync: this,
    duration: const Duration(seconds: 3),
  )..repeat();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (BuildContext context, Widget? child) {
        final double offset = _controller.value;
        return Wrap(
          spacing: 24,
          runSpacing: 24,
          crossAxisAlignment: WrapCrossAlignment.center,
          children: <Widget>[
            _ShapeTile(
              label: 'Stadium',
              shape: const StadiumBorder(),
              colors: _aurora,
              startOffset: offset,
            ),
            _ShapeTile(
              label: 'Star',
              shape: const StarBorder(innerRadiusRatio: 0.45),
              width: 90,
              height: 90,
              startOffset: offset,
            ),
            // A single bright streak chasing around the border.
            _ShapeTile(
              label: 'Streak',
              shape: const RoundedRectangleBorder(
                borderRadius: BorderRadius.all(Radius.circular(20)),
              ),
              colors: const <Color>[
                Color(0x3300C6FF),
                Color(0x3300C6FF),
                Color(0xFF00C6FF),
                Color(0x3300C6FF),
              ],
              stops: const <double>[0.0, 0.7, 0.85, 1.0],
              startOffset: offset,
            ),
            OutlinedButton(
              style: OutlinedButton.styleFrom(
                shape: ContourGradientBorder(
                  colors: _sunset,
                  startOffset: offset,
                  shape: const StadiumBorder(),
                ),
                side: const BorderSide(width: 2),
              ),
              onPressed: () {},
              child: const Text('Animated button'),
            ),
          ],
        );
      },
    );
  }
}

class _MorphingBorder extends StatefulWidget {
  const _MorphingBorder();

  @override
  State<_MorphingBorder> createState() => _MorphingBorderState();
}

class _MorphingBorderState extends State<_MorphingBorder> {
  static const List<ContourGradientBorder> _borders = <ContourGradientBorder>[
    ContourGradientBorder(
      colors: _sunset,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.all(Radius.circular(12)),
      ),
      side: BorderSide(width: 3),
    ),
    ContourGradientBorder(
      colors: _aurora,
      shape: StadiumBorder(),
      side: BorderSide(width: 8),
    ),
    ContourGradientBorder(
      colors: _sunset,
      startOffset: 0.5,
      shape: StarBorder(innerRadiusRatio: 0.5),
      side: BorderSide(width: 5),
    ),
  ];

  int _index = 0;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _index = (_index + 1) % _borders.length),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 600),
        curve: Curves.easeInOut,
        width: 200,
        height: 120,
        alignment: Alignment.center,
        decoration: ShapeDecoration(shape: _borders[_index]),
        child: const Text('Tap me'),
      ),
    );
  }
}

class _Stops extends StatelessWidget {
  const _Stops();

  @override
  Widget build(BuildContext context) {
    return const Wrap(
      spacing: 24,
      runSpacing: 24,
      children: <Widget>[
        _ShapeTile(
          label: 'Custom stops',
          shape: StadiumBorder(),
          colors: <Color>[Colors.pink, Colors.orange],
          stops: <double>[0.0, 0.3],
        ),
        _ShapeTile(
          label: 'Hard stops',
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.all(Radius.circular(12)),
          ),
          colors: <Color>[Colors.red, Colors.red, Colors.blue, Colors.blue],
          stops: <double>[0.0, 0.5, 0.5, 1.0],
        ),
        _ShapeTile(
          label: 'Single color',
          shape: StadiumBorder(),
          colors: <Color>[Colors.teal],
        ),
      ],
    );
  }
}
2
likes
160
points
87
downloads
screenshot

Documentation

API reference

Publisher

verified publisherbszarlej.de

Weekly Downloads

A ShapeBorder and InputBorder that paint a gradient along the length of any OutlinedBorder shape, for containers, buttons, cards, chips and text fields.

Repository (GitHub)
View/report issues

Topics

#gradient #border #ui #animation

License

MIT (license)

Dependencies

flutter

More

Packages that depend on contour_gradient