visibility_detector 0.4.0+2 copy "visibility_detector: ^0.4.0+2" to clipboard
visibility_detector: ^0.4.0+2 copied to clipboard

A widget that detects the visibility of its child and notifies a callback.

example/lib/main.dart

// Copyright 2018 the Dart project authors.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd

import 'dart:collection';

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

const String title = 'VisibilityDetector Demo';

/// The width of each cell of our pseudo-table of [VisibilityDetector] widgets.
const double cellWidth = 125;

//// The height of each cell of our pseudo-table.
const double cellHeight = 65;

/// The external padding around the primary row/column of the pseudo-table.
const double externalCellPadding = 5;

/// The internal padding for each cell of the pseudo-table.
const double _cellPadding = 10;

/// The external padding around the widgets in the visibility report section.
const double _reportPadding = 5;

/// The height of the visibility report.
const double _reportHeight = 200;

/// The [Key] to the main [ListView] widget.
const mainListKey = Key('MainList');

const scaleButtonKey = Key('scaleButton');

Key secondaryScrollableKey(int primaryIndex) =>
    ValueKey('secondary-$primaryIndex');

/// Returns the [Key] to the [VisibilityDetector] widget in each cell of the
/// pseudo-table.
Key cellKey(int row, int col) => Key('Cell-$row-$col');

/// Returns the [Key] to the content of the cell.
Key cellContentKey(int row, int col) => Key('Content-$row-$col');

/// A callback to be invoked by the [VisibilityDetector.onVisibilityChanged]
/// callback.  We use the extra level of indirection to allow widget tests to
/// reuse this demo app with a different callback.
final visibilityListeners =
    <void Function(RowColumn rc, VisibilityInfo info)>[];

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

/// Axis and growth direction of the table.
class Layout {
  const Layout(this.mainAxis, this.secondaryAxis, {this.reverse = false});

  final Axis mainAxis;
  final Axis secondaryAxis;

  /// Reverse direction of the secondary axis.
  final bool reverse;
}

/// The root widget for the demo app.
class VisibilityDetectorDemo extends StatelessWidget {
  const VisibilityDetectorDemo({Key? key, this.useSlivers = false})
      : super(key: key);

  final bool useSlivers;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: title,
      scrollBehavior:
          const MaterialScrollBehavior().copyWith(scrollbars: false),
      theme: ThemeData(primarySwatch: Colors.blue),
      home: VisibilityDetectorDemoPage(key: key, useSlivers: useSlivers),
    );
  }
}

/// The main page [VisibilityDetectorDemo].
class VisibilityDetectorDemoPage extends StatefulWidget {
  VisibilityDetectorDemoPage({Key? key, this.useSlivers = false})
      : super(key: key);

  final bool useSlivers;

  @override
  VisibilityDetectorDemoPageState createState() =>
      VisibilityDetectorDemoPageState();
}

class VisibilityDetectorDemoPageState
    extends State<VisibilityDetectorDemoPage> {
  VisibilityDetectorDemoPageState();

  var _layoutIndex = 0;

  /// Whether the pseudo-table should be shown.
  bool _tableShown = true;

  /// Whether to use slivers.
  bool _useSlivers = false;

  bool _useScale = false;

  /// The four layouts, that can be changed via pressing the Layout button.
  static const _layouts = [
    Layout(Axis.vertical, Axis.horizontal, reverse: false),
    Layout(Axis.vertical, Axis.horizontal, reverse: true),
    Layout(Axis.horizontal, Axis.vertical, reverse: false),
    Layout(Axis.horizontal, Axis.vertical, reverse: true),
  ];

  Layout get _layout => _layouts[_layoutIndex];

  @override
  void initState() {
    super.initState();
    _useSlivers = widget.useSlivers;
  }

  /// Toggles the visibility of the pseudo-table of [VisibilityDetector]
  /// widgets.
  void _toggleTable() {
    setState(() {
      _tableShown = !_tableShown;
    });
  }

  /// Toggles the visibility of the pseudo-table of [VisibilityDetector]
  /// widgets.
  void _toggleScale() {
    setState(() {
      _useScale = !_useScale;
    });
  }

  /// Toggles between the layouts.
  void _toggleLayout() {
    setState(() {
      _layoutIndex = (_layoutIndex + 1) % _layouts.length;
    });
  }

  /// Toggles between RenderBox and RenderSliver widgets.
  void _toggleSlivers() {
    setState(() {
      _useSlivers = !_useSlivers;
    });
  }

  @override
  Widget build(BuildContext context) {
    // Our pseudo-table of [VisibilityDetector] widgets.  We want to scroll both
    // vertically and horizontally, so we'll implement it as a [ListView] of
    // [ListView]s.
    final table = !_tableShown
        ? null
        : ClipRect(
            child: Container(
              width: _useScale ? 400 : null,
              height: _useScale ? 300 : null,
              child: ListView.builder(
                key: mainListKey,
                scrollDirection: _layout.mainAxis,
                itemExtent: (_layout.mainAxis == Axis.vertical
                        ? cellHeight
                        : cellWidth) +
                    2 * externalCellPadding,
                itemBuilder: (BuildContext context, int primaryIndex) {
                  return _useSlivers
                      ? SliverDemoPageSecondaryAxis(
                          key: secondaryScrollableKey(primaryIndex),
                          primaryIndex: primaryIndex,
                          secondaryAxis: _layout.secondaryAxis,
                          reverse: _layout.reverse,
                          useScale: _useScale,
                        )
                      : DemoPageSecondaryAxis(
                          key: secondaryScrollableKey(primaryIndex),
                          primaryIndex: primaryIndex,
                          secondaryAxis: _layout.secondaryAxis,
                          reverse: _layout.reverse,
                          useScale: _useScale,
                        );
                },
              ),
            ),
          );

    return Scaffold(
      appBar: AppBar(title: const Text(title)),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            shape: const Border(),
            onPressed: _toggleLayout,
            heroTag: null,
            child: Text('Layout'),
          ),
          const SizedBox(width: 8),
          FloatingActionButton(
            shape: const Border(),
            onPressed: _toggleSlivers,
            heroTag: null,
            child: _useSlivers
                ? const Text('RenderBox')
                : const Text('RenderSliver'),
          ),
          const SizedBox(width: 8),
          FloatingActionButton(
            shape: const Border(),
            onPressed: _toggleTable,
            heroTag: null,
            child: _tableShown ? const Text('Hide') : const Text('Show'),
          ),
          const SizedBox(width: 8),
          FloatingActionButton(
            key: scaleButtonKey,
            shape: const Border(),
            onPressed: _toggleScale,
            heroTag: null,
            child: _useScale ? const Text('Scale') : const Text('No scaling'),
          ),
        ],
      ),
      body: Column(
        children: <Widget>[
          _tableShown ? Expanded(child: table!) : const Spacer(),
          VisibilityReport(
              title:
                  'Visibility (${_useSlivers ? "RenderSliver" : "RenderBox"})'),
        ],
      ),
    );
  }
}

/// A secondary axis for the pseudo-table of [VisibilityDetector] widgets.
class DemoPageSecondaryAxis extends StatelessWidget {
  const DemoPageSecondaryAxis({
    Key? key,
    required this.primaryIndex,
    required this.secondaryAxis,
    required this.reverse,
    required this.useScale,
  }) : super(key: key);

  final Axis secondaryAxis;
  final int primaryIndex;
  final bool reverse;
  final bool useScale;

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      scrollDirection: secondaryAxis,
      reverse: reverse,
      padding: const EdgeInsets.all(externalCellPadding),
      itemBuilder: (BuildContext context, int secondaryIndex) {
        return DemoPageCell(
          primaryIndex: primaryIndex,
          secondaryIndex: secondaryIndex,
          useSlivers: false,
          useScale: useScale,
        );
      },
    );
  }
}

/// A Secondary axis using sliver cell widgets.
class SliverDemoPageSecondaryAxis extends StatelessWidget {
  const SliverDemoPageSecondaryAxis({
    Key? key,
    required this.primaryIndex,
    required this.secondaryAxis,
    required this.reverse,
    required this.useScale,
  }) : super(key: key);

  final Axis secondaryAxis;
  final int primaryIndex;
  final bool reverse;
  final bool useScale;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: secondaryAxis == Axis.horizontal
          ? const EdgeInsets.symmetric(vertical: externalCellPadding)
          : const EdgeInsets.symmetric(horizontal: externalCellPadding),
      child: CustomScrollView(
        scrollDirection: secondaryAxis,
        reverse: reverse,
        slivers: [
          SliverToBoxAdapter(
            child: SizedBox(
              width: externalCellPadding,
              height: externalCellPadding,
            ),
          ),
          // Sliver version renders up to 20 columns.
          for (var secondaryIndex = 0; secondaryIndex < 20; secondaryIndex++)
            DemoPageCell(
              primaryIndex: primaryIndex,
              secondaryIndex: secondaryIndex,
              useSlivers: true,
              useScale: useScale,
            ),
          SliverToBoxAdapter(
            child: SizedBox(
              width: externalCellPadding,
              height: externalCellPadding,
            ),
          ),
        ],
      ),
    );
  }
}

/// An individual cell for the pseudo-table of [VisibilityDetector] widgets.
class DemoPageCell extends StatelessWidget {
  DemoPageCell({
    Key? key,
    required this.primaryIndex,
    required this.secondaryIndex,
    required this.useSlivers,
    required this.useScale,
  })  : _cellName = 'Item $primaryIndex-$secondaryIndex',
        _backgroundColor = ((primaryIndex + secondaryIndex) % 2 == 0)
            ? Colors.pink[200]
            : Colors.yellow[200],
        super(key: key);

  final int primaryIndex;
  final int secondaryIndex;
  final bool useSlivers;
  final bool useScale;

  /// The text to show for the cell.
  final String _cellName;

  final Color? _backgroundColor;

  /// [VisibilityDetector] callback for when the visibility of the widget
  /// changes.  Triggers the [visibilityListeners] callbacks.
  void _handleVisibilityChanged(VisibilityInfo info) {
    for (final listener in visibilityListeners) {
      listener(RowColumn(primaryIndex, secondaryIndex), info);
    }
  }

  @override
  Widget build(BuildContext context) {
    final cell = Container(
      key: cellContentKey(primaryIndex, secondaryIndex),
      width: cellWidth,
      decoration: BoxDecoration(color: _backgroundColor),
      padding: const EdgeInsets.all(_cellPadding),
      alignment: Alignment.center,
      child: FittedBox(
        fit: BoxFit.scaleDown,
        child: Text(
          _cellName,
          style: Theme.of(context).textTheme.headlineMedium,
        ),
      ),
    );

    if (useSlivers) {
      return SliverVisibilityDetector(
        key: cellKey(primaryIndex, secondaryIndex),
        onVisibilityChanged: _handleVisibilityChanged,
        sliver: SliverToBoxAdapter(child: cell),
      );
    }

    var visibilityDetector = VisibilityDetector(
      key: cellKey(primaryIndex, secondaryIndex),
      onVisibilityChanged: _handleVisibilityChanged,
      child: cell,
    );

    if (useScale) {
      return Transform.scale(
        scale: 0.25,
        child: Padding(
          padding: const EdgeInsets.only(left: 100),
          child: visibilityDetector,
        ),
      );
    } else {
      return visibilityDetector;
    }
  }
}

/// A widget that lists the reported visibility percentages of the
/// [VisibilityDetector] widgets on the page.
class VisibilityReport extends StatelessWidget {
  const VisibilityReport({Key? key, required this.title}) : super(key: key);

  /// The text to use for the heading of the report.
  final String title;

  @override
  Widget build(BuildContext context) {
    final headingTextStyle =
        Theme.of(context).textTheme.titleLarge?.copyWith(color: Colors.white);

    final heading = Container(
      padding: const EdgeInsets.all(_reportPadding),
      alignment: Alignment.centerLeft,
      decoration: const BoxDecoration(color: Colors.black),
      child: Text(title, style: headingTextStyle),
    );

    final grid = Container(
      padding: const EdgeInsets.all(_reportPadding),
      decoration: BoxDecoration(color: Colors.grey[300]),
      child: const SizedBox(
        height: _reportHeight,
        child: VisibilityReportGrid(),
      ),
    );

    return Column(children: <Widget>[heading, grid]);
  }
}

/// The portion of [VisibilityReport] that shows data.
class VisibilityReportGrid extends StatefulWidget {
  const VisibilityReportGrid({Key? key}) : super(key: key);

  @override
  VisibilityReportGridState createState() => VisibilityReportGridState();
}

class VisibilityReportGridState extends State<VisibilityReportGrid> {
  /// Maps [row, column] indices to the visibility percentage of the
  /// corresponding [VisibilityDetector] widget.
  final _visibilities = SplayTreeMap<RowColumn, double>();

  /// The [Text] widgets used to fill our [GridView].
  List<Text>? _reportItems;

  /// See [State.initState].  Adds a callback to [visibilityListeners] to update
  /// the visibility report with the widget's visibility.
  @override
  void initState() {
    super.initState();

    visibilityListeners.add(_update);
    assert(visibilityListeners.contains(_update));
  }

  @override
  void dispose() {
    visibilityListeners.remove(_update);

    super.dispose();
  }

  /// Callback added to [visibilityListeners] to update the state.
  void _update(RowColumn rc, VisibilityInfo info) {
    setState(() {
      if (info.visibleFraction == 0) {
        _visibilities.remove(rc);
      } else {
        _visibilities[rc] = info.visibleFraction;
      }

      // Invalidate `_reportItems` so that we regenerate it lazily.
      _reportItems = null;
    });
  }

  /// Populates [_reportItems].
  List<Text> _generateReportItems() {
    final entries = _visibilities.entries;
    final items = <Text>[];

    for (final i in entries) {
      final visiblePercentage = (i.value * 100).toStringAsFixed(1);
      items.add(Text('${i.key}: $visiblePercentage%'));
    }

    // It's easier to read cells down than across, so sort by columns instead of
    // by rows.
    final tailIndex = items.length - items.length ~/ 3;
    final midIndex = tailIndex - tailIndex ~/ 2;
    final head = items.getRange(0, midIndex);
    final mid = items.getRange(midIndex, tailIndex);
    final tail = items.getRange(tailIndex, items.length);
    return collate([head, mid, tail]).toList(growable: false);
  }

  @override
  Widget build(BuildContext context) {
    _reportItems ??= _generateReportItems();

    return GridView.count(
      crossAxisCount: 3,
      childAspectRatio: 8,
      padding: const EdgeInsets.all(5),
      children: _reportItems!,
    );
  }
}

/// A class for storing a [row, column] pair.
class RowColumn extends Comparable<RowColumn> {
  RowColumn(this.row, this.column);

  final int row;
  final int column;

  @override
  bool operator ==(dynamic other) {
    if (other is RowColumn) {
      return row == other.row && column == other.column;
    }
    return false;
  }

  @override
  int get hashCode => Object.hash(row, column);

  /// See [Comparable.compareTo].  Sorts [RowColumn] objects in row-major order.
  @override
  int compareTo(RowColumn other) {
    if (row < other.row) {
      return -1;
    } else if (row > other.row) {
      return 1;
    }

    if (column < other.column) {
      return -1;
    } else if (column > other.column) {
      return 1;
    }

    return 0;
  }

  @override
  String toString() {
    return '[$row, $column]';
  }
}

/// Returns an [Iterable] containing the nth element (if it exists) of every
/// [Iterable] in `iterables` in sequence.
///
/// Unlike [zip](https://pub-web.flutter-io.cn/documentation/quiver/latest/quiver.iterables/zip.html),
/// returns a single sequence and continues until *all* [Iterable]s are
/// exhausted.
///
/// For example, `collate([[1, 4, 7], [2, 5, 8, 9], [3, 6]])` would return a
/// sequence `1, 2, 3, 4, 5, 6, 7, 8, 9`.
@visibleForTesting
Iterable<T> collate<T>(Iterable<Iterable<T>> iterables) sync* {
  final iterators = [for (final iterable in iterables) iterable.iterator];
  if (iterators.isEmpty) {
    return;
  }

  while (true) {
    var exhaustedCount = 0;
    for (final i in iterators) {
      if (i.moveNext()) {
        yield i.current;
        continue;
      }

      exhaustedCount += 1;
      if (exhaustedCount == iterators.length) {
        // All iterators are at their ends.
        return;
      }
    }
  }
}
965
likes
120
pub points
99%
popularity

Publisher

verified publishergoogle.dev

A widget that detects the visibility of its child and notifies a callback.

Repository (GitHub)
View/report issues
Contributing

Documentation

API reference

License

BSD-3-Clause (LICENSE)

Dependencies

flutter

More

Packages that depend on visibility_detector