curtains 0.9.4+1 copy "curtains: ^0.9.4+1" to clipboard
curtains: ^0.9.4+1 copied to clipboard

Super simple scrim Curtains, or shadow decorations, by wrapping a scrollable child, alluding to unrevealed content while not at the start or end.

example/lib/main.dart

/// ## 📜 Curtains Demonstration
library curtains_demo;

import 'dart:math';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';

import 'package:curtains/curtains.dart';

import 'source_code.dart';

void main() => runApp(CurtainsDemo());

/// This demonstration.
late BuildContext curtainsDemo;

///     const DURATION = Duration(milliseconds: 600);
const DURATION = Duration(milliseconds: 600);

///     const CURVE = Curves.fastOutSlowIn;
const CURVE = Curves.fastOutSlowIn;

const TITLES = [
  '📜 Curtains Demo',
  '📜 Fancy Curtains: Axis.horizontal',
  '📜 Regal Curtains: Animations',
  '📜 Regal Curtains: BoxDecorations',
  '📜 Regal Curtains: Curtain Spread & clipBehavior',
  '📜 Regal Curtains: Sensitivity',
];

/// Starts at `TITLES[0]`, '📜 Curtains Demo', but changes as the
/// `>` IconButton over [CurrentDemo] is tapped.
var title = TITLES[0];

/// Hover in most IDEs for context tooltip
/// describing the significance of each Demo.
const DEMOS = [
  CurtainsDemoVertical(),
  FancyCurtainsDemoHorizontal(),
  RegalCurtainsDemoVertical1(),
  RegalCurtainsDemoVertical2(),
  RegalCurtainsDemoHorizontal(),
  RegalCurtainsDemoVertical3(),
];

/// Starts at `0`, [CurtainsDemoVertical], but changes as the
/// `>` IconButton over [CurrentDemo] is tapped.
var currentDemo = 0;

class CurtainsDemo extends StatefulWidget {
  @override
  _CurtainsDemoState createState() => _CurtainsDemoState();
}

class _CurtainsDemoState extends State<CurtainsDemo> {
  @override
  Widget build(BuildContext context) {
    curtainsDemo = context; // Allows easy rebuild from [CurrentDemo]
    return MaterialApp(
      title: title,
      theme: ThemeData(primarySwatch: Colors.red),
      home: SafeArea(
        child: Scaffold(
          appBar: AppBar(
            title: Text(title),
            automaticallyImplyLeading: false,
            toolbarHeight: (currentDemo == -1) ? 80 : null,
          ),
          body: const CurrentDemo(),
        ),
      ),
    );
  }
}

/// Setup the body of [CurtainsDemo] Scaffold
/// according to the [Axis] of the [currentDemo].
///
/// Provides an `>` IconButton to change [currentDemo].
class CurrentDemo extends StatefulWidget {
  const CurrentDemo({Key? key}) : super(key: key);
  _CurrentDemoState createState() => _CurrentDemoState();
}

class _CurrentDemoState extends State<CurrentDemo> {
  @override
  Widget build(BuildContext context) {
    final children = <Widget>[
      (currentDemo != 1 && currentDemo != 4)
          ? HEADER_VERTICAL
          : HEADER_HORIZONTAL,
      DEMOS[(currentDemo == -1) ? 0 : currentDemo],
      (currentDemo != 1 && currentDemo != 4)
          ? FOOTER_VERTICAL
          : FOOTER_HORIZONTAL,
    ];

    return WillPopScope(
      child: Stack(
        children: <Widget>[
          (currentDemo == -1)
              ? const SourceCode()
              : (currentDemo != 1 && currentDemo != 4)
                  ? Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: children,
                    )
                  : Row(children: children),
          IconButton(
            icon: Icon(Icons.arrow_forward_ios),
            color: (currentDemo == -1) ? Colors.white : Colors.black,
            iconSize: 70,
            onPressed: () => setState(
              () {
                HapticFeedback.vibrate();
                currentDemo =
                    (currentDemo == DEMOS.length - 1) ? 0 : currentDemo + 1;
                title = TITLES[currentDemo];
                (curtainsDemo as Element).markNeedsBuild();
              },
            ),
          ),
          Positioned(
            top: 0,
            right: (currentDemo != 1 && currentDemo != 4)
                ? (currentDemo == -1)
                    ? -1920
                    : 16
                : -4,
            child: IconButton(
              icon: Icon(Icons.code),
              color: (currentDemo != 1 && currentDemo != 4)
                  ? Colors.red
                  : Colors.blue,
              iconSize: (currentDemo != 1 && currentDemo != 4) ? 70 : 50,
              onPressed: () => setState(() {
                HapticFeedback.vibrate();
                currentDemo = -1;
                title = '📜 Curtains Demo: Source Code\n'
                    '🔎 Pinch to Zoom  👆 Tap and Hold to Select';
                (curtainsDemo as Element).markNeedsBuild();
              }),
            ),
          ),
        ],
      ),
      onWillPop: () async {
        if (currentDemo == -1) {
          setState(() {
            currentDemo = 0;
            title = TITLES[0];
            (curtainsDemo as Element).markNeedsBuild();
          });
          return false;
        }
        var willPop = false;
        Scaffold.of(context).showBottomSheet<void>(
          (_) => GestureDetector(
            onTap: () => Navigator.pop(context),
            child: Container(
              height: 200,
              color: Colors.red[900],
              child: Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: <Widget>[
                    const Text(
                      'Exit 📜 Curtains Demo?',
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 40,
                      ),
                    ),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                      children: [
                        ElevatedButton(
                          child: const Text('\n  E X I T  \n'),
                          onPressed: () {
                            willPop = true;
                            SystemChannels.platform.invokeMethod(
                              'SystemNavigator.pop',
                            );
                          },
                        ),
                        ElevatedButton(
                          child: const Text('\n  S T A Y  \n'),
                          onPressed: () => Navigator.pop(context),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
          ),
        );
        return willPop;
      },
    );
  }
}

/// 🕴 Consider [Material.elevation], but see [Elevation].
class CurtainsDemoVertical extends StatelessWidget {
  /// 🕴 Consider [Material.elevation], but see [Elevation].
  const CurtainsDemoVertical({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Curtains(
        elevation: 24, // Consider [Material.elevation], but see [Elevation].🕴
        // endCurtainInitVisible: false,
        child: ListView(
          physics: const BouncingScrollPhysics(),
          children: generatedListVertical,
        ),
      ),
    );
  }
}

class FancyCurtainsDemoHorizontal extends StatelessWidget {
  /// ↔ When instantiating, 📜 [Curtains] may be created
  /// on the [Axis.horizontal] if it needs to match a
  /// horizontally-scrolling `child`.
  ///
  /// 🔛 [Curtains.directionality] can manually
  /// trigger RTL (but Curtains does check).
  ///
  /// 🕴 Default constructor 📜 [Curtains] uses
  /// [Elevation.asBoxDecoration] to render its decorations;
  /// but feel free to use these static methods, too.
  /// - with 👥 [`package:shadows`](https://pub.dev/packages/shadows)
  const FancyCurtainsDemoHorizontal({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 🕴
    final List<BoxShadow> _shadow = (Elevation.asBoxShadows(
              24,
              color: Colors.red,
            ) +
            const [
              BoxShadow(color: Colors.yellow, spreadRadius: 10, blurRadius: 10),
              BoxShadow(color: Colors.green, spreadRadius: 5, blurRadius: 25),
            ])
        // Material elevation `List<BoxShadow>`s have 3x shadows
        // - Kept at Material-standard opacities with `kElevationShadowOpacityRamp`
        // We added 2x shadows which we will now modify.
        //
        // (Simply demonstrating `rampOpacity` util from
        // 👥 Shadows - https://pub.dev/packages/shadows)
        .rampOpacity(kElevationShadowOpacityRamp + [0.2, 0.25]);

    return Expanded(
      child: Curtains.fancy(
        scrollDirection: Axis.horizontal, // ↔
        // directionality: TextDirection.rtl, // Manually trigger RTL 🔛
        startCurtain: BoxDecoration(boxShadow: _shadow),
        endCurtain: Elevation.asBoxDecoration(12, color: Colors.purple), // 🕴
        child: ListView(
          scrollDirection: Axis.horizontal, // ↔
          itemExtent: 100.0,
          physics: const BouncingScrollPhysics(),
          children: generatedListHorizontal,
        ),
      ),
    );
  }
}

class RegalCurtainsDemoVertical1 extends StatelessWidget {
  /// ⏰ [Curtains.regal] has intrinsic animation support.
  /// Provide `duration` and/or `curve` or let 📜 [Curtains] default.
  const RegalCurtainsDemoVertical1({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Curtains.regal(
        startCurtain: Elevation.asBoxDecoration(12), // 🕴
        endCurtain: BoxDecoration(boxShadow: Elevation.asBoxShadows(24)), // 🕴
        duration: DURATION, // ⏰
        curve: CURVE, // ⏰
        child: ListView(
          physics: const BouncingScrollPhysics(),
          children: generatedListVertical,
        ),
      ),
    );
  }
}

class RegalCurtainsDemoVertical2 extends StatelessWidget {
  /// 🌈 [Curtains.fancy] and [Curtains.regal] support
  /// full-fat [BoxDecoration]s in lieu of `elevation`.
  const RegalCurtainsDemoVertical2({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Curtains.regal(
        startCurtain: FANCY_START, // 🌈
        endCurtain: buildFancyEnd(), // 🌈
        duration: DURATION,
        curve: CURVE,
        child: ListView(
          physics: const BouncingScrollPhysics(),
          children: generatedListVertical,
        ),
      ),
    );
  }
}

class RegalCurtainsDemoHorizontal extends StatelessWidget {
  /// ```
  /// // [buildFancyEnd] has a `gradient` that
  /// // is not visible without [Curtains.spread].
  /// endCurtain: buildFancyEnd(Axis.horizontal), // (explains [Axis] pass)
  /// spread: 25, // Provide "girth" to [_Curtain]s for [Gradient] support.
  ///
  /// // `BoxDecoration.gradient` clips itself, but [BoxShadows],
  /// // which the simplest 📜 [Curtains] relies on for [Curtains.elevation],
  /// // need a [ClipRect] in order to not overflow.
  /// // - Manually disable clipping with [clipBehavior] initialized `Clip.none`
  /// // - This will alter the Widget tree depth
  /// clipBehavior: Clip.none,
  /// ```
  const RegalCurtainsDemoHorizontal({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Curtains.regal(
        scrollDirection: Axis.horizontal,
        startCurtain: FANCY_START,
        // [buildFancyEnd] has a `gradient` that
        // is not visible without [Curtains.spread].
        endCurtain: buildFancyEnd(Axis.horizontal), // (explains [Axis] pass)
        spread: 25, // Provide "girth" to [_Curtain]s for [Gradient] support.
        duration: DURATION,
        curve: CURVE,
        // `BoxDecoration.gradient` clips itself, but [BoxShadows],
        // which the simplest 📜 [Curtains] relies on for [Curtains.elevation],
        // need a [ClipRect] in order to not overflow.
        // - Manually disable clipping with [clipBehavior] initialized `Clip.none`
        clipBehavior: Clip.none,
        child: ListView(
          scrollDirection: Axis.horizontal,
          itemExtent: 150.0,
          physics: const BouncingScrollPhysics(),
          children: generatedListHorizontal,
        ),
      ),
    );
  }
}

class RegalCurtainsDemoVertical3 extends StatelessWidget {
  /// ⚖ With a two-entry `List<double>` [Curtains.sensitivity],
  /// the 📜 [Curtains] scrims will appear later and disappear sooner.
  const RegalCurtainsDemoVertical3({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Curtains.regal(
        startCurtain: FANCY_START,
        endCurtain: buildFancyEnd(),
        // ⚖ `start` appears once scrolled `350` px beyond start
        // ⚖ `end` appears once scrolled `175` px beyond end
        sensitivity: const [350.0, 175.0], // ⚖
        duration: DURATION,
        curve: CURVE,
        child: ListView(
          physics: const BouncingScrollPhysics(),
          children: generatedListVertical,
        ),
      ),
    );
  }
}

const FANCY_START = BoxDecoration(
  boxShadow: [
    BoxShadow(
      color: Color(0xFFFF0000),
      spreadRadius: 1.0,
      blurRadius: 5.0,
    ),
    BoxShadow(
      color: Color(0xBBFF0000),
      spreadRadius: 10.0,
      blurRadius: 30.0,
    ),
    BoxShadow(
      color: Color(0x66FF0000),
      spreadRadius: 25.0,
      blurRadius: 150.0,
    ),
  ],
);

BoxDecoration buildFancyEnd([Axis? axis]) => BoxDecoration(
      // TODO: fix [backgroundBlendMode]
      // backgroundBlendMode: BlendMode.colorDodge,
      gradient: LinearGradient(
        colors: [Colors.green[400]!, Colors.green[900]!.withOpacity(0)],
        begin: (axis == Axis.vertical)
            ? Alignment.bottomCenter
            : Alignment.centerRight,
        end: (axis == Axis.vertical)
            ? Alignment.topCenter
            : Alignment.centerLeft,
      ),
      boxShadow: const [
        BoxShadow(
          color: Colors.blue,
          spreadRadius: 15.0,
          blurRadius: 20.0,
          offset: Offset(0, 15),
        ),
      ],
    );

final List<Widget> generatedListVertical = List.generate(
  35,
  (i) => Padding(
    padding: const EdgeInsets.only(top: 5.0),
    child: ListTile(
      tileColor: Colors.red.withOpacity(0.05),
      leading: buildLeading(Axis.vertical, i),
      title: TITLE_VERTICAL,
      subtitle: SUBTITLE_VERTICAL,
    ),
  ),
);

final List<Widget> generatedListHorizontal = List.generate(
  35,
  (i) => Padding(
    padding: const EdgeInsets.only(right: 10.0),
    child: Container(
      color: Colors.red.withOpacity(0.05),
      height: double.infinity,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: <Widget>[
          buildLeading(Axis.horizontal, i),
          TITLE_HORIZONTAL,
          SUBTITLE_HORIZONTAL,
        ],
      ),
    ),
  ),
);

Container buildLeading(Axis axis, int i) => Container(
      decoration: BoxDecoration(
          color: Colors.primaries[Random().nextInt(Colors.primaries.length)],
          border: Border.all(color: Colors.black, width: 2.0)),
      width: (axis == Axis.vertical) ? 50.0 : 75.0,
      height: (axis == Axis.vertical) ? 50.0 : 75.0,
      child: Text(
        '${i + 1}',
        style: TextStyle(
          fontSize: (axis == Axis.vertical) ? 20 : 34,
          fontWeight: FontWeight.bold,
          color: Colors.white,
          height: 1,
        ),
      ),
    );

const TITLE_VERTICAL = Text(
  'Foo Bar Boo Baz',
  style: TextStyle(fontSize: 20),
);

const SUBTITLE_VERTICAL = Text('Lorem ipsum dolor sit amet');

const TITLE_HORIZONTAL = Text(
  'Foo\nBar\nBoo\nBaz',
  textAlign: TextAlign.center,
  style: TextStyle(fontSize: 36),
);

const SUBTITLE_HORIZONTAL = Text(
  '\nLorem\nipsum\ndolor\nsit\namet\nlorem\nipsum\ndolor\nsit\namet',
  textAlign: TextAlign.center,
  style: TextStyle(fontSize: 22),
);

const STYLE = TextStyle(fontSize: 20, fontWeight: FontWeight.bold);

const HEADER_VERTICAL = SizedBox(
  height: 90,
  child: DecoratedBox(
    decoration: BoxDecoration(color: Color(0x44FF0000)),
    child: Align(
      alignment: Alignment.center,
      child: Text(
        'Vertical ListView 👤 Header',
        style: STYLE,
      ),
    ),
  ),
);

const FOOTER_VERTICAL = SizedBox(
  height: 60,
  child: DecoratedBox(
    decoration: BoxDecoration(color: Color(0x4400C3FF)),
    child: Align(
      alignment: Alignment.center,
      child: Text(
        'Vertical ListView 🦶 Footer',
        style: STYLE,
      ),
    ),
  ),
);

const HEADER_HORIZONTAL = SizedBox(
  width: 80,
  child: DecoratedBox(
    decoration: BoxDecoration(color: Color(0x44FF0000)),
    child: Center(
      child: Text(
        'H\nO\nR\nI\nZ\nO\nN\nT\nA\nL\n\n'
        '👤\n\nH\nE\nA\nD\nE\nR',
        textAlign: TextAlign.center,
        style: STYLE,
      ),
    ),
  ),
);

const FOOTER_HORIZONTAL = SizedBox(
  width: 60,
  child: DecoratedBox(
    decoration: BoxDecoration(color: Color(0x4400C3FF)),
    child: Center(
      child: Text(
        'H\nO\nR\nI\nZ\nO\nN\nT\nA\nL\n\n'
        '🦶\n\nF\nO\nO\nT\nE\nR',
        textAlign: TextAlign.center,
        style: STYLE,
      ),
    ),
  ),
);
7
likes
150
pub points
0%
popularity

Publisher

verified publisherzaba.app

Super simple scrim Curtains, or shadow decorations, by wrapping a scrollable child, alluding to unrevealed content while not at the start or end.

Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

flutter, shadows

More

Packages that depend on curtains