just_bubble 0.0.1 copy "just_bubble: ^0.0.1" to clipboard
just_bubble: ^0.0.1 copied to clipboard

A flexible and highly customizable bubble widget for Flutter. Easily create chat bubbles, tooltips, or any speech bubble style with custom tails, borders, gradients, images, and shadows.

example/lib/main.dart

import 'package:flex_color_picker/flex_color_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:just_bubble/just_bubble.dart';

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

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return CupertinoApp(home: HomePage());
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final _bgImages = [
    'https://picsum.photos/id/972/400/100.webp?grayscale&blur=2',
    'https://picsum.photos/id/299/400/100.webp?grayscale&blur=2',
  ];

  bool _enableBorder = true;
  double _borderRadius = 10;
  double _borderWidth = 4;
  Color? _borderColor1 = Color(0xFF8E2DE2);
  Color? _bgColor1,
      _bgColor2 = Colors.white,
      _borderColor2 = Color(0xFF4A00E0),
      _textColor = Color(0xFF4A00E0);

  final _shadowColors = <_Wrap<Color?>>[_Wrap(Colors.black26)];
  int _tailStyle = 1;
  int _tailJoin = 0;
  int? _bgImage;
  double _tailGap = 5;
  BubbleAlignment _tailAlign = BubbleAlignment.bottomLeft;
  double _tailAlignValue = 0;
  late Tail _tail = Tail.triangle(
    edgeGap: _tailGap,
    alignment: _tailAlign,
    tailJoin: TailJoin.rounded,
  );

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      child: Column(
        children: [
          const SafeArea(child: SizedBox.shrink()),
          _buildBubble(),
          _buildOptions(),
        ],
      ),
    );
  }

  Widget _buildBubble() {
    final image = _bgImage != null
        ? DecorationImage(
            image: NetworkImage(_bgImages[_bgImage!]),
            fit: BoxFit.cover,
          )
        : null;
    final borderGradient = _borderColor1 != null && _borderColor2 != null
        ? LinearGradient(colors: [_borderColor1!, _borderColor2!])
        : null;
    final shadows = _shadowColors.where((e) => e.color != null).map((e) {
      return BoxShadow(blurRadius: 10, offset: Offset(0, 4), color: e.color!);
    }).toList();
    final gradient = _bgColor1 != null && _bgColor2 != null
        ? LinearGradient(colors: [_bgColor1!, _bgColor2!])
        : null;

    final bg = gradient == null ? _bgColor1 ?? _bgColor2 : null;

    Widget widget = Bubble(
      padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
      gradient: gradient,
      color: bg,
      image: image,
      shadows: shadows,
      border: BubbleBorder(
        tail: _tail,
        color: _borderColor2,
        gradient: borderGradient,
        borderRadius: BorderRadius.circular(_borderRadius),
        width: _enableBorder ? _borderWidth : 0,
      ),
      child: Text(
        'Action is the foundational key to all success.',
        style: TextStyle(fontSize: 18, color: _textColor),
      ),
    );
    return ConstrainedBox(
      constraints: BoxConstraints(minHeight: 100),
      child: Center(child: widget),
    );
  }

  Widget _buildOptions() {
    Widget widget = Column(
      children: [
        CupertinoFormSection.insetGrouped(
          header: Text('Options'),
          children: [
            CupertinoListTile(
              title: Text('Text Color'),
              trailing: _buildColorBlock(
                color: _textColor,
                onPressed: () => _showColorPickerDialog(
                  _textColor,
                  (c) => setState(() => _textColor = c),
                ),
              ),
            ),
            CupertinoListTile(
              title: Text('Background Color'),
              trailing: Row(
                children: [
                  _buildColorBlock(
                    color: _bgColor1,
                    onPressed: () => _showColorPickerDialog(
                      _bgColor1,
                      (c) => setState(() => _bgColor1 = c),
                    ),
                  ),
                  _buildColorBlock(
                    color: _bgColor2,
                    onPressed: () => _showColorPickerDialog(
                      _bgColor2,
                      (c) => setState(() => _bgColor2 = c),
                    ),
                  ),
                ],
              ),
            ),
            CupertinoListTile(
              title: Text('Image'),
              trailing: Row(
                children: List.generate(
                  _bgImages.length,
                  (index) => _buildImageBlock(
                    _bgImages[index],
                    selected: _bgImage == index,
                    onPressed: () => _onSelectImageBlock(index),
                  ),
                ),
              ),
            ),
          ],
        ),
        CupertinoFormSection.insetGrouped(
          header: Text('Border'),
          children: [
            CupertinoListTile(
              title: Text('Enable'),
              trailing: CupertinoSwitch(
                value: _enableBorder,
                onChanged: (value) => setState(() => _enableBorder = value),
              ),
            ),
          ],
        ),
        AnimatedAlign(
          heightFactor: _enableBorder ? 1 : 0,
          alignment: Alignment.topCenter,
          duration: kThemeAnimationDuration,
          child: CupertinoFormSection.insetGrouped(
            children: [
              CupertinoListTile(
                title: Text('Color'),
                trailing: Row(
                  children: [
                    _buildColorBlock(
                      color: _borderColor1,
                      onPressed: () => _showColorPickerDialog(
                        _borderColor1,
                        (c) => setState(() => _borderColor1 = c),
                      ),
                    ),
                    _buildColorBlock(
                      color: _borderColor2,
                      onPressed: () => _showColorPickerDialog(
                        _borderColor2,
                        (c) => setState(() => _borderColor2 = c),
                      ),
                    ),
                  ],
                ),
              ),
              CupertinoListTile(
                title: Text('Width'),
                additionalInfo: Text(_borderWidth.toStringAsFixed(1)),
                trailing: CupertinoSlider(
                  value: _borderWidth,
                  min: 0,
                  max: 20,
                  onChanged: (v) => setState(() => _borderWidth = v),
                ),
              ),
              CupertinoListTile(
                title: Text('Radius'),
                additionalInfo: Text(_borderRadius.toStringAsFixed(1)),
                trailing: CupertinoSlider(
                  value: _borderRadius,
                  min: 0,
                  max: 20,
                  onChanged: (v) => setState(() => _borderRadius = v),
                ),
              ),
            ],
          ),
        ),
        CupertinoFormSection.insetGrouped(
          header: Text('Shadow'),
          children: [
            CupertinoListTile(
              title: Text('Add'),
              trailing: CupertinoButton(
                child: Icon(CupertinoIcons.add),
                onPressed: () {
                  setState(() => _shadowColors.add(_Wrap(null)));
                },
              ),
            ),
            ...List.generate(
              _shadowColors.length,
              (index) => Dismissible(
                key: Key('shadow ${_shadowColors[index].hashCode}'),
                onUpdate: _dismissibleHaptic,
                onDismissed: (_) {
                  setState(() => _shadowColors.removeAt(index));
                },
                background: Container(
                  color: Colors.red,
                  alignment: Alignment.centerRight,
                  padding: EdgeInsets.only(right: 16),
                  child: Text('Remove', style: TextStyle(color: Colors.white)),
                ),
                direction: DismissDirection.endToStart,
                child: CupertinoListTile(
                  title: Text('Shadow $index'),
                  trailing: _buildColorBlock(
                    color: _shadowColors[index].color,
                    onPressed: () => _showColorPickerDialog(
                      _shadowColors[index].color,
                      (c) => setState(() => _shadowColors[index].color = c),
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
        CupertinoFormSection.insetGrouped(
          header: Text('Tail'),
          children: [
            CupertinoListTile(
              title: Text('Preset Style'),
              trailing: CupertinoSlidingSegmentedControl<int>(
                children: {0: Text('None'), 1: Text('Triangle')},
                groupValue: _tailStyle,
                onValueChanged: (value) => setState(() {
                  _tailStyle = value!;
                  _applyTail();
                }),
              ),
            ),
          ],
        ),
        ClipRect(
          child: AnimatedAlign(
            heightFactor: _tailStyle != 0 ? 1 : 0,
            alignment: Alignment.topCenter,
            duration: kThemeAnimationDuration,
            child: CupertinoFormSection.insetGrouped(
              children: [
                CupertinoListTile(
                  title: Text('Alignment'),
                  trailing: Flexible(
                    child: SingleChildScrollView(
                      scrollDirection: Axis.horizontal,
                      child: Row(
                        children: List.generate(
                          BubbleAlignment.values.length,
                          (index) => _buildBubbleAlignBlock(
                            BubbleAlignment.values[index],
                            selected:
                                _tailAlign == BubbleAlignment.values[index],
                            onPressed: () => setState(() {
                              _tailAlign = BubbleAlignment.values[index];
                              _applyTail();
                            }),
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
                CupertinoListTile(
                  title: Text('Tail Join'),
                  trailing: CupertinoSlidingSegmentedControl<int>(
                    children: {0: Text('Sharp'), 1: Text('Rounded')},
                    groupValue: _tailJoin,
                    onValueChanged: (value) => setState(() {
                      _tailJoin = value!;
                      _applyTail();
                    }),
                  ),
                ),
                CupertinoListTile(
                  title: Text('Custom Alignment'),
                  additionalInfo: Text(_tailAlignValue.toStringAsFixed(1)),
                  trailing: CupertinoSlider(
                    value: _tailAlignValue,
                    min: -1,
                    max: 1,
                    onChanged: (v) => setState(() {
                      _tailAlign = BubbleAlignment(_tailAlign.direction, v);
                      _applyTail();
                    }),
                  ),
                ),
                CupertinoListTile(
                  title: Text('EdgeGap'),
                  additionalInfo: Text(_tailGap.toStringAsFixed(1)),
                  trailing: CupertinoSlider(
                    value: _tailGap,
                    min: 0,
                    max: 20,
                    onChanged: (v) => setState(() {
                      _tailGap = v;
                      _applyTail();
                    }),
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    );
    return Flexible(
      child: ColoredBox(
        color: CupertinoColors.systemGroupedBackground,
        child: SingleChildScrollView(child: widget),
      ),
    );
  }

  Widget _buildColorBlock({Color? color, VoidCallback? onPressed}) {
    return CupertinoButton(
      onPressed: onPressed,
      sizeStyle: CupertinoButtonSize.small,
      child: SizedBox.square(
        dimension: 28,
        child: DecoratedBox(
          decoration: BoxDecoration(
            color: color,
            border: Border.all(color: Colors.black12),
            borderRadius: BorderRadius.all(Radius.circular(2)),
          ),
        ),
      ),
    );
  }

  Widget _buildBubbleAlignBlock(
    BubbleAlignment align, {
    VoidCallback? onPressed,
    bool selected = false,
  }) {
    final borderSide = BorderSide(
      color: selected ? Colors.black45 : Colors.black12,
    );
    final activeBorderSide = const BorderSide(color: Colors.black, width: 2);
    return CupertinoButton(
      onPressed: onPressed,
      sizeStyle: CupertinoButtonSize.small,
      child: SizedBox.square(
        dimension: 28,
        child: DecoratedBox(
          decoration: BoxDecoration(
            border: Border(
              top: align.direction == BubbleDirection.top
                  ? activeBorderSide
                  : borderSide,
              right: align.direction == BubbleDirection.right
                  ? activeBorderSide
                  : borderSide,
              bottom: align.direction == BubbleDirection.bottom
                  ? activeBorderSide
                  : borderSide,
              left: align.direction == BubbleDirection.left
                  ? activeBorderSide
                  : borderSide,
            ),
          ),
          child: Align(
            alignment: align.alignment,
            child: SizedBox.square(
              dimension: 10,
              child: ColoredBox(color: Colors.black),
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildImageBlock(
    String url, {
    VoidCallback? onPressed,
    bool selected = false,
  }) {
    const borderRadius = BorderRadius.all(Radius.circular(2));
    Widget child = ClipRRect(
      borderRadius: borderRadius,
      child: Image.network(
        url,
        cacheWidth: 28,
        width: 28,
        height: 28,
        fit: BoxFit.cover,
      ),
    );
    if (selected) {
      child = DecoratedBox(
        position: DecorationPosition.foreground,
        decoration: BoxDecoration(
          borderRadius: borderRadius,
          border: Border.all(),
        ),
        child: child,
      );
    }
    return CupertinoButton(
      onPressed: onPressed,
      sizeStyle: CupertinoButtonSize.small,
      child: child,
    );
  }

  void _applyTail() {
    final tailJoin = switch (_tailJoin) {
      1 => TailJoin.rounded,
      _ => TailJoin.sharp,
    };
    _tail = switch (_tailStyle) {
      1 => Tail.triangle(
        edgeGap: _tailGap,
        alignment: _tailAlign,
        tailJoin: tailJoin,
      ),
      _ => const Tail.none(),
    };
    _tailAlignValue = _tailAlign.v;
  }

  void _onSelectImageBlock(int? index) {
    if (_bgImage == index) index = null;
    setState(() => _bgImage = index);
  }

  void _dismissibleHaptic(details) {
    if (details.reached && !details.previousReached ||
        !details.reached && details.previousReached) {
      HapticFeedback.lightImpact();
    }
  }

  void _showColorPickerDialog(Color? color, ValueSetter<Color> onColorChanged) {
    setState(() {});
    return;
    showCupertinoModalPopup(
      context: context,
      builder: (context) => CupertinoPopupSurface(
        child: ColorPicker(
          mainAxisSize: MainAxisSize.min,
          color: color ?? Colors.blue,
          onColorChanged: onColorChanged,
          pickersEnabled: {
            ColorPickerType.both: false,
            ColorPickerType.primary: false,
            ColorPickerType.accent: false,
            ColorPickerType.bw: false,
            ColorPickerType.custom: false,
            ColorPickerType.customSecondary: false,
            ColorPickerType.wheel: true,
          },
        ),
      ),
    );
  }
}

class _Wrap<T> {
  T color;

  _Wrap(this.color);
}
2
likes
150
points
78
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A flexible and highly customizable bubble widget for Flutter. Easily create chat bubbles, tooltips, or any speech bubble style with custom tails, borders, gradients, images, and shadows.

Repository (GitHub)
View/report issues

Topics

#bubble #chat #border #bubble-border

License

MIT (license)

Dependencies

flutter

More

Packages that depend on just_bubble