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

Progressive blur for Flutter: iOS style edge blur for scroll views, toolbars and headers. A GPU shader on the backdrop, no snapshots, cheap while scrolling.

example/lib/main.dart

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

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await EdgeBlur.precache();
  runApp(const DemoApp());
}

const _seed = Color(0xFFE0548B);
const _headerHeight = 52.0;
const _toolbarHeight = 60.0;
const _toolbarGap = 12.0;

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Scroll Edge Blur',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: _seed)),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: _seed,
          brightness: Brightness.dark,
        ),
      ),
      home: const GalleryPage(),
    );
  }
}

@immutable
class DemoSettings {
  const DemoSettings({
    this.sigma = 16,
    this.topExtent = 72,
    this.bottomExtent = 110,
    this.tinted = true,
    this.curve = Curves.easeIn,
    this.top = true,
    this.bottom = true,
  });

  final double sigma;
  final double topExtent;
  final double bottomExtent;
  final bool tinted;
  final Curve curve;
  final bool top;
  final bool bottom;

  DemoSettings copyWith({
    double? sigma,
    double? topExtent,
    double? bottomExtent,
    bool? tinted,
    Curve? curve,
    bool? top,
    bool? bottom,
  }) {
    return DemoSettings(
      sigma: sigma ?? this.sigma,
      topExtent: topExtent ?? this.topExtent,
      bottomExtent: bottomExtent ?? this.bottomExtent,
      tinted: tinted ?? this.tinted,
      curve: curve ?? this.curve,
      top: top ?? this.top,
      bottom: bottom ?? this.bottom,
    );
  }
}

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

  @override
  State<GalleryPage> createState() => _GalleryPageState();
}

class _GalleryPageState extends State<GalleryPage> {
  final _settings = ValueNotifier(const DemoSettings());

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

  void _openTuner() {
    showModalBottomSheet<void>(
      context: context,
      showDragHandle: true,
      // size to the controls instead of the default 9/16 cap
      isScrollControlled: true,
      builder: (context) => _Tuner(settings: _settings),
    );
  }

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    final safe = MediaQuery.paddingOf(context);
    // a light wash of the background toward each edge keeps the title readable
    final tint = scheme.surface.withValues(alpha: 0.55);

    return Scaffold(
      body: ValueListenableBuilder<DemoSettings>(
        valueListenable: _settings,
        builder: (context, s, _) {
          return Stack(
            children: [
              ScrollEdgeBlur(
                top:
                    s.top
                        ? EdgeBlurStyle(
                          extent: s.topExtent,
                          sigma: s.sigma,
                          curve: s.curve,
                          tint: s.tinted ? tint : null,
                          fallbackColor: scheme.surface,
                        )
                        : null,
                bottom:
                    s.bottom
                        ? EdgeBlurStyle(
                          extent: s.bottomExtent,
                          sigma: s.sigma,
                          curve: s.curve,
                          tint: s.tinted ? tint : null,
                          fallbackColor: scheme.surface,
                        )
                        : null,
                child: _PhotoGrid(
                  top: safe.top + _headerHeight + 8,
                  bottom: safe.bottom + _toolbarHeight + _toolbarGap * 2,
                ),
              ),
              Positioned(
                top: safe.top,
                left: 20,
                right: 20,
                height: _headerHeight,
                child: const _Header(),
              ),
              Positioned(
                left: 0,
                right: 0,
                bottom: safe.bottom + _toolbarGap,
                child: Center(child: _Toolbar(onTune: _openTuner)),
              ),
            ],
          );
        },
      ),
    );
  }
}

class _Header extends StatelessWidget {
  const _Header();

  @override
  Widget build(BuildContext context) {
    final text = Theme.of(context).textTheme;
    return Row(
      children: [
        Text(
          'Library',
          style: text.headlineMedium?.copyWith(fontWeight: FontWeight.w800),
        ),
        const Spacer(),
        IconButton.filledTonal(
          onPressed: () {},
          icon: const Icon(Icons.search),
        ),
      ],
    );
  }
}

class _Toolbar extends StatelessWidget {
  const _Toolbar({required this.onTune});

  final VoidCallback onTune;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return Material(
      color: scheme.surfaceContainerHighest.withValues(alpha: 0.7),
      shape: const StadiumBorder(),
      child: SizedBox(
        height: _toolbarHeight,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 8),
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              IconButton(onPressed: () {}, icon: const Icon(Icons.photo)),
              IconButton(onPressed: () {}, icon: const Icon(Icons.favorite)),
              IconButton(onPressed: () {}, icon: const Icon(Icons.share)),
              const SizedBox(width: 4),
              FilledButton.icon(
                onPressed: onTune,
                icon: const Icon(Icons.tune),
                label: const Text('Tune'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

/// Colourful generated tiles, so there's plenty of detail to blur and no
/// network images.
class _PhotoGrid extends StatelessWidget {
  const _PhotoGrid({required this.top, required this.bottom});

  final double top;
  final double bottom;

  static const _icons = [
    Icons.landscape,
    Icons.local_florist,
    Icons.pets,
    Icons.beach_access,
    Icons.music_note,
    Icons.directions_bike,
    Icons.icecream,
    Icons.nightlife,
    Icons.sailing,
    Icons.park,
  ];

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      padding: EdgeInsets.fromLTRB(4, top, 4, bottom),
      gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
        maxCrossAxisExtent: 160,
        mainAxisSpacing: 4,
        crossAxisSpacing: 4,
      ),
      itemCount: 240,
      itemBuilder: (context, i) {
        final hue = (i * 23) % 360.0;
        final a = HSLColor.fromAHSL(1, hue, 0.85, 0.62).toColor();
        final b = HSLColor.fromAHSL(1, (hue + 70) % 360, 0.9, 0.42).toColor();
        return DecoratedBox(
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(i % 7 == 0 ? 28 : 10),
            gradient: LinearGradient(
              begin: Alignment(-1 + (i % 3), -1),
              end: const Alignment(1, 1),
              colors: [a, b],
            ),
          ),
          child: Stack(
            children: [
              Positioned(
                right: -10,
                bottom: -10,
                child: Icon(
                  _icons[i % _icons.length],
                  size: 84,
                  color: Colors.white.withValues(alpha: 0.28),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(10),
                child: Text(
                  '#${i + 1}',
                  style: const TextStyle(
                    color: Colors.white,
                    fontWeight: FontWeight.w800,
                  ),
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

class _Tuner extends StatelessWidget {
  const _Tuner({required this.settings});

  final ValueNotifier<DemoSettings> settings;

  static const _curves = {
    'Ease in': Curves.easeIn,
    'Linear': Curves.linear,
    'Ease out': Curves.easeOut,
  };

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<DemoSettings>(
      valueListenable: settings,
      builder: (context, s, _) {
        void set(DemoSettings next) => settings.value = next;
        return SafeArea(
          child: SingleChildScrollView(
            padding: const EdgeInsets.fromLTRB(20, 0, 20, 12),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                _Slider(
                  label: 'Blur',
                  value: s.sigma,
                  max: 40,
                  onChanged: (v) => set(s.copyWith(sigma: v)),
                ),
                _Slider(
                  label: 'Top extent',
                  value: s.topExtent,
                  max: 240,
                  onChanged: (v) => set(s.copyWith(topExtent: v)),
                ),
                _Slider(
                  label: 'Bottom extent',
                  value: s.bottomExtent,
                  max: 240,
                  onChanged: (v) => set(s.copyWith(bottomExtent: v)),
                ),
                const SizedBox(height: 8),
                SegmentedButton<Curve>(
                  segments: [
                    for (final entry in _curves.entries)
                      ButtonSegment(value: entry.value, label: Text(entry.key)),
                  ],
                  selected: {s.curve},
                  onSelectionChanged: (v) => set(s.copyWith(curve: v.first)),
                ),
                const SizedBox(height: 4),
                SwitchListTile(
                  contentPadding: EdgeInsets.zero,
                  title: const Text('Tint'),
                  value: s.tinted,
                  onChanged: (v) => set(s.copyWith(tinted: v)),
                ),
                Wrap(
                  spacing: 8,
                  children: [
                    FilterChip(
                      label: const Text('Top edge'),
                      selected: s.top,
                      onSelected: (v) => set(s.copyWith(top: v)),
                    ),
                    FilterChip(
                      label: const Text('Bottom edge'),
                      selected: s.bottom,
                      onSelected: (v) => set(s.copyWith(bottom: v)),
                    ),
                  ],
                ),
                if (!EdgeBlur.isSupported)
                  Padding(
                    padding: const EdgeInsets.only(top: 12),
                    child: Text(
                      'This renderer has no shader filters (needs Impeller), '
                      'so a gradient is shown instead of the blur.',
                      style: Theme.of(context).textTheme.bodySmall,
                    ),
                  ),
              ],
            ),
          ),
        );
      },
    );
  }
}

class _Slider extends StatelessWidget {
  const _Slider({
    required this.label,
    required this.value,
    required this.max,
    required this.onChanged,
  });

  final String label;
  final double value;
  final double max;
  final ValueChanged<double> onChanged;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        SizedBox(width: 110, child: Text(label)),
        Expanded(child: Slider(value: value, max: max, onChanged: onChanged)),
        SizedBox(width: 36, child: Text(value.round().toString())),
      ],
    );
  }
}
0
likes
160
points
--
downloads

Documentation

API reference

Publisher

verified publisherumarghaffar.is-a.dev

Weekly Downloads

Progressive blur for Flutter: iOS style edge blur for scroll views, toolbars and headers. A GPU shader on the backdrop, no snapshots, cheap while scrolling.

Repository (GitHub)
View/report issues

Topics

#blur #shader #ui #effects #glassmorphism

License

MIT (license)

Dependencies

flutter

More

Packages that depend on scroll_edge_blur