visibility_detector 0.2.0-nullsafety.1 copy "visibility_detector: ^0.2.0-nullsafety.1" to clipboard
visibility_detector: ^0.2.0-nullsafety.1 copied to clipboard

outdated

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 = _rowHeight - 2 * _rowPadding;

/// The height of each row of the pseudo-table.  This includes [_rowPadding] on
/// top and bottom.
const double _rowHeight = 75;

/// The external padding around each row of the pseudo-table.
const double _rowPadding = 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');

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: title,
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const VisibilityDetectorDemoPage(),
    );
  }
}

/// The main page [VisibilityDetectorDemo].
class VisibilityDetectorDemoPage extends StatefulWidget {
  const VisibilityDetectorDemoPage({Key? key}) : super(key: key);

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

class VisibilityDetectorDemoPageState
    extends State<VisibilityDetectorDemoPage> {
  /// Whether the pseudo-table should be shown.
  bool _tableShown = true;

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

  @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
        : ListView.builder(
            key: mainListKey,
            scrollDirection: Axis.vertical,
            itemExtent: _rowHeight,
            itemBuilder: (BuildContext context, int rowIndex) {
              return DemoPageRow(rowIndex: rowIndex);
            },
          );

    return Scaffold(
      appBar: AppBar(title: const Text(title)),
      floatingActionButton: FloatingActionButton(
        shape: const Border(),
        onPressed: _toggleTable,
        child: _tableShown ? const Text('Hide') : const Text('Show'),
      ),
      body: Column(
        children: <Widget>[
          _tableShown ? Expanded(child: table!) : const Spacer(),
          const VisibilityReport(title: 'Visibility'),
        ],
      ),
    );
  }
}

/// An individual row for the pseudo-table of [VisibilityDetector] widgets.
class DemoPageRow extends StatelessWidget {
  const DemoPageRow({Key? key, required this.rowIndex}) : super(key: key);

  final int rowIndex;

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      scrollDirection: Axis.horizontal,
      padding: const EdgeInsets.all(_rowPadding),
      itemBuilder: (BuildContext context, int columnIndex) {
        return DemoPageCell(rowIndex: rowIndex, columnIndex: columnIndex);
      },
    );
  }
}

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

  final int rowIndex;
  final int columnIndex;

  /// 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(rowIndex, columnIndex), info);
    }
  }

  @override
  Widget build(BuildContext context) {
    return VisibilityDetector(
      key: cellKey(rowIndex, columnIndex),
      onVisibilityChanged: _handleVisibilityChanged,
      child: Container(
        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.headline4),
        ),
      ),
    );
  }
}

/// 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.headline6!.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 => hashValues(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.dev/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* {
  assert(iterables != null);

  final iterators = [for (final iterable in iterables) iterable.iterator];
  if (iterators.isEmpty) {
    return;
  }

  // ignore: literal_only_boolean_expressions, https://github.com/dart-lang/linter/issues/453
  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;
      }
    }
  }
}
969
likes
0
pub points
99%
popularity

Publisher

verified publishergoogle.dev

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

Homepage
Repository (GitHub)
View/report issues

License

unknown (LICENSE)

Dependencies

flutter

More

Packages that depend on visibility_detector