liquid_reveal 0.1.1 copy "liquid_reveal: ^0.1.1" to clipboard
liquid_reveal: ^0.1.1 copied to clipboard

A liquid page reveal that measures its own box instead of the screen, so it works inside a card, a list row or a full page. Render object, no rebuilds.

example/lib/main.dart

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

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

const List<({Color colour, String title, String blurb})>
kPages = <({Color colour, String title, String blurb})>[
  (colour: Color(0xFF4F46E5), title: 'Measured', blurb: 'against its own box'),
  (
    colour: Color(0xFFEC4899),
    title: 'Not the screen',
    blurb: 'never MediaQuery',
  ),
  (colour: Color(0xFF06B6D4), title: 'One render object', blurb: 'no rebuilds'),
  (colour: Color(0xFF16A34A), title: 'Any size', blurb: 'card, row or page'),
];

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

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'liquid_reveal',
    debugShowCheckedModeBanner: false,
    theme: ThemeData(
      colorScheme: ColorScheme.fromSeed(
        seedColor: const Color(0xFF4F46E5),
        brightness: Brightness.dark,
      ),
      useMaterial3: true,
    ),
    home: const HomePage(),
  );
}

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

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

class _HomePageState extends State<HomePage> {
  final LiquidRevealController _card = LiquidRevealController();
  final LiquidRevealController _row = LiquidRevealController();
  RevealSide _side = RevealSide.right;

  @override
  void dispose() {
    _card.dispose();
    _row.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);

    return Scaffold(
      appBar: AppBar(title: const Text('liquid_reveal')),
      body: ListView(
        padding: const EdgeInsets.all(24),
        children: <Widget>[
          Text(
            'The same widget at three sizes. Drag any of them either way - '
            'forwards reveals the next page from one edge, backwards reveals '
            'the previous one from the other. The reveal is measured against '
            'the box it sits in, so a short row needs a short drag and a full '
            'page needs a long one.',
            style: theme.textTheme.bodyLarge,
          ),
          const SizedBox(height: 20),
          SegmentedButton<RevealSide>(
            segments: const <ButtonSegment<RevealSide>>[
              ButtonSegment<RevealSide>(
                value: RevealSide.right,
                label: Text('From the right'),
              ),
              ButtonSegment<RevealSide>(
                value: RevealSide.left,
                label: Text('From the left'),
              ),
              ButtonSegment<RevealSide>(
                value: RevealSide.bottom,
                label: Text('From the bottom'),
              ),
            ],
            selected: <RevealSide>{_side},
            onSelectionChanged: (Set<RevealSide> s) =>
                setState(() => _side = s.first),
          ),
          if (_side.axis == Axis.vertical)
            Padding(
              padding: const EdgeInsets.only(top: 12),
              child: Text(
                'Heads up: a vertical reveal inside this scrolling page loses '
                'the gesture to the scroll view. That is Flutter deciding, not '
                'the package - put a vertical reveal somewhere that does not '
                'scroll, or use the button below.',
                style: theme.textTheme.bodySmall?.copyWith(
                  color: theme.colorScheme.tertiary,
                ),
              ),
            ),
          const SizedBox(height: 28),

          _Labelled(
            title: 'A hero card, 220 pixels tall',
            trailing: _Indicator(controller: _card),
            child: ClipRRect(
              borderRadius: BorderRadius.circular(20),
              child: SizedBox(
                height: 220,
                child: LiquidReveal(
                  itemCount: kPages.length,
                  controller: _card,
                  settings: LiquidRevealSettings(side: _side),
                  itemBuilder: (BuildContext context, int i) => _Page(index: i),
                ),
              ),
            ),
          ),

          _Labelled(
            title: 'A list row, 84 pixels tall - the same settings',
            child: ClipRRect(
              borderRadius: BorderRadius.circular(12),
              child: SizedBox(
                height: 84,
                child: LiquidReveal(
                  itemCount: kPages.length,
                  controller: _row,
                  settings: LiquidRevealSettings(
                    side: _side,
                    duration: const Duration(milliseconds: 420),
                  ),
                  itemBuilder: (BuildContext context, int i) =>
                      _Page(index: i, compact: true),
                ),
              ),
            ),
          ),

          _Labelled(
            title: 'One way only: allowReverse is off here',
            child: ClipRRect(
              borderRadius: BorderRadius.circular(16),
              child: SizedBox(
                height: 120,
                child: LiquidReveal(
                  itemCount: kPages.length,
                  settings: LiquidRevealSettings(
                    side: _side,
                    allowReverse: false,
                  ),
                  itemBuilder: (BuildContext context, int i) =>
                      _Page(index: i, compact: true),
                ),
              ),
            ),
          ),

          _Labelled(
            title: 'Softer wave, and a drag only from the edge',
            child: ClipRRect(
              borderRadius: BorderRadius.circular(20),
              child: SizedBox(
                height: 160,
                child: LiquidReveal(
                  itemCount: kPages.length,
                  settings: LiquidRevealSettings.soft.copyWith(
                    side: _side,
                    dragEdgeFraction: 0.25,
                  ),
                  itemBuilder: (BuildContext context, int i) =>
                      _Page(index: i, compact: true),
                ),
              ),
            ),
          ),

          Center(
            child: Wrap(
              spacing: 12,
              alignment: WrapAlignment.center,
              children: <Widget>[
                OutlinedButton.icon(
                  onPressed: () {
                    _card.previous();
                    _row.previous();
                  },
                  icon: const Icon(Icons.skip_previous),
                  label: const Text('Previous from code'),
                ),
                FilledButton.icon(
                  onPressed: () {
                    _card.next();
                    _row.next();
                  },
                  icon: const Icon(Icons.skip_next),
                  label: const Text('Reveal from code'),
                ),
              ],
            ),
          ),
          const SizedBox(height: 24),
        ],
      ),
    );
  }
}

class _Labelled extends StatelessWidget {
  const _Labelled({required this.title, required this.child, this.trailing});

  final String title;
  final Widget child;
  final Widget? trailing;

  @override
  Widget build(BuildContext context) => Padding(
    padding: const EdgeInsets.only(bottom: 32),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Row(
          children: <Widget>[
            Expanded(
              child: Text(
                title,
                style: Theme.of(context).textTheme.titleMedium,
              ),
            ),
            ?trailing,
          ],
        ),
        const SizedBox(height: 12),
        child,
      ],
    ),
  );
}

/// Watches the controller rather than the pages, so nothing else rebuilds.
class _Indicator extends StatelessWidget {
  const _Indicator({required this.controller});

  final LiquidRevealController controller;

  @override
  Widget build(BuildContext context) => ListenableBuilder(
    listenable: controller,
    builder: (BuildContext context, Widget? _) => Row(
      children: <Widget>[
        for (int i = 0; i < kPages.length; i++)
          Padding(
            padding: const EdgeInsets.only(left: 6),
            child: _Dot(
              // The controller reports the reveal in flight, so the dots
              // slide instead of jumping.
              filled: _weight(i, controller.offset),
            ),
          ),
      ],
    ),
  );

  double _weight(int index, double offset) {
    final double distance = (offset % kPages.length) - index;
    return (1 - distance.abs()).clamp(0.0, 1.0);
  }
}

class _Dot extends StatelessWidget {
  const _Dot({required this.filled});

  final double filled;

  @override
  Widget build(BuildContext context) => Container(
    width: 8 + filled * 10,
    height: 8,
    decoration: BoxDecoration(
      borderRadius: BorderRadius.circular(4),
      color: Color.lerp(
        Theme.of(context).colorScheme.outlineVariant,
        Theme.of(context).colorScheme.primary,
        filled,
      ),
    ),
  );
}

class _Page extends StatelessWidget {
  const _Page({required this.index, this.compact = false});

  final int index;
  final bool compact;

  @override
  Widget build(BuildContext context) {
    final ({Color colour, String title, String blurb}) page =
        kPages[index % kPages.length];

    return DecoratedBox(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: <Color>[
            page.colour,
            Color.lerp(page.colour, Colors.black, 0.35)!,
          ],
        ),
      ),
      child: Padding(
        padding: EdgeInsets.all(compact ? 16 : 24),
        child: Row(
          children: <Widget>[
            Expanded(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text(
                    page.title,
                    style: TextStyle(
                      fontSize: compact ? 20 : 34,
                      fontWeight: FontWeight.bold,
                      color: Colors.white,
                    ),
                  ),
                  Text(
                    page.blurb,
                    style: const TextStyle(color: Colors.white70),
                  ),
                ],
              ),
            ),
            Text(
              '${index + 1}',
              style: TextStyle(
                fontSize: compact ? 28 : 56,
                fontWeight: FontWeight.bold,
                color: Colors.white24,
              ),
            ),
          ],
        ),
      ),
    );
  }
}
0
likes
160
points
112
downloads

Documentation

API reference

Publisher

verified publisherbomsamdi.com

Weekly Downloads

A liquid page reveal that measures its own box instead of the screen, so it works inside a card, a list row or a full page. Render object, no rebuilds.

Repository (GitHub)
View/report issues

Topics

#animation #transition #render-object #carousel #onboarding

License

MIT (license)

Dependencies

flutter

More

Packages that depend on liquid_reveal