jpnurmi / item_selector

@@ -0,0 +1,235 @@
Loading
1 +
// MIT License
2 +
//
3 +
// Copyright (c) 2020 J-P Nurmi
4 +
//
5 +
// The ItemSelector library is based on:
6 +
// Multi Select GridView in Flutter - by Simon Lightfoot:
7 +
// https://gist.github.com/slightfoot/a002dd1e031f5f012f810c6d5da14a11
8 +
//
9 +
// Copyright (c) 2019 Simon Lightfoot
10 +
//
11 +
// Permission is hereby granted, free of charge, to any person obtaining a copy
12 +
// of this software and associated documentation files (the "Software"), to deal
13 +
// in the Software without restriction, including without limitation the rights
14 +
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 +
// copies of the Software, and to permit persons to whom the Software is
16 +
// furnished to do so, subject to the following conditions:
17 +
//
18 +
// The above copyright notice and this permission notice shall be included in all
19 +
// copies or substantial portions of the Software.
20 +
//
21 +
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 +
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 +
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 +
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 +
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 +
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27 +
// SOFTWARE.
28 +
//
29 +
// Thanks to Hugo Passos.
30 +
//
31 +
import 'package:flutter/gestures.dart';
32 +
import 'package:flutter/widgets.dart';
33 +
34 +
import 'item_selection.dart';
35 +
import 'item_selection_hit_tester.dart';
36 +
import 'item_selection_types.dart';
37 +
38 +
/// Controls a selection within indexed child widgets.
39 +
///
40 +
/// ItemSelectionController detects tap, long-press, and drag gestures,
41 +
/// and performs appropriate selection actions based on its selection mode.
42 +
///
43 +
/// ### Example
44 +
///
45 +
///     Widget build(BuildContext context) {
46 +
///       return ItemSelectionController(
47 +
///         child: ListView.builder(
48 +
///           itemCount: 100,
49 +
///           itemBuilder: (BuildContext context, int index) {
50 +
///             return ItemSelectionBuilder(
51 +
///               index: index,
52 +
///               builder: (BuildContext context, int index, bool selected) {
53 +
///                 return Text('$index: $selected');
54 +
///               },
55 +
///             );
56 +
///           },
57 +
///         ),
58 +
///       );
59 +
///     }
60 +
class ItemSelectionController extends StatefulWidget {
61 +
  final Widget child;
62 +
  final ItemSelection _selection;
63 +
  final ItemSelectionMode _selectionMode;
64 +
  final ItemSelectionActionCallback _onSelectionStart;
65 +
  final ItemSelectionActionCallback _onSelectionUpdate;
66 +
  final ItemSelectionActionCallback _onSelectionEnd;
67 +
  final EdgeInsets _scrollInsets;
68 +
  final Duration _scrollDuration;
69 +
70 +
  /// Creates an item selection controller.
71 +
  ///
72 +
  /// A [selection] may be provided to tell ItemSelectionController to operate
73 +
  /// on a specific instance. This allows you to add listeners on the selection,
74 +
  /// for example.
75 +
  ///
76 +
  /// [selectionMode] specifies whether the controller performs single-selection
77 +
  /// by tap, or multi-selection by long-press and drag gestures. The default
78 +
  /// selection mode is [ItemSelectionMode.multi].
79 +
  ///
80 +
  /// The `onSelectionStart`, `onSelectionUpdate`, and `onSelectionEnd` callbacks
81 +
  /// are called when an interactive item selection starts, updates, or ends,
82 +
  /// respectively. Return `true` to indicate that the callback has handled the
83 +
  /// action, or `false` to let the controller perform the default action.
84 +
  ///
85 +
  /// The default selection actions:
86 +
  ///   - `onSelectionStart:
87 +
  ///     - clear any existing selection
88 +
  ///     - add a new selection with [start] index
89 +
  ///   - `onSelectionUpdate`:
90 +
  ///     - replace the existing selection with [start] to [end]
91 +
  ///   - `onSelectionEnd`:
92 +
  ///     - none, reserved for future use
93 +
  ///
94 +
  /// Furthermore, should a selection occur on a child widget that is partly
95 +
  /// outside the viewport, [scrollInsets] and [scrollDuration] may be specified
96 +
  /// to control auto-scrolling. The defaults are _48 px_ and _125 ms_.
97 +
  ItemSelectionController(
98 +
      {Key key,
99 +
      @required this.child,
100 +
      ItemSelection selection,
101 +
      ItemSelectionMode selectionMode,
102 +
      ItemSelectionActionCallback onSelectionStart,
103 +
      ItemSelectionActionCallback onSelectionUpdate,
104 +
      ItemSelectionActionCallback onSelectionEnd,
105 +
      EdgeInsets scrollInsets,
106 +
      Duration scrollDuration})
107 +
      : _selection = selection ?? ItemSelection(),
108 +
        _selectionMode = selectionMode ?? ItemSelectionMode.multi,
109 +
        _scrollInsets = scrollInsets ?? const EdgeInsets.all(48),
110 +
        _scrollDuration = scrollDuration ?? const Duration(milliseconds: 125),
111 +
        _onSelectionStart = onSelectionStart,
112 +
        _onSelectionUpdate = onSelectionUpdate,
113 +
        _onSelectionEnd = onSelectionEnd,
114 +
        super(key: key);
115 +
116 +
  /// Returns the current selection.
117 +
  ItemSelection get selection => _selection;
118 +
119 +
  bool _startSelection(int start, int end) {
120 +
    if (_onSelectionStart == null) return false;
121 +
    return _onSelectionStart(start, end);
122 +
  }
123 +
124 +
  bool _updateSelection(int start, int end) {
125 +
    if (_onSelectionUpdate == null) return false;
126 +
    return _onSelectionUpdate(start, end);
127 +
  }
128 +
129 +
  bool _endSelection(int start, int end) {
130 +
    if (_onSelectionEnd == null) return false;
131 +
    return _onSelectionEnd(start, end);
132 +
  }
133 +
134 +
  /// Returns an ItemSelectionController instance for the given build [context],
135 +
  /// or `null` if not found.
136 +
  static ItemSelectionController of(BuildContext context) {
137 +
    final _ItemSelectionScope scope =
138 +
        context.dependOnInheritedWidgetOfExactType<_ItemSelectionScope>();
139 +
    return scope?.controller;
140 +
  }
141 +
142 +
  @override
143 +
  _ItemSelectionControllerState createState() =>
144 +
      _ItemSelectionControllerState();
145 +
}
146 +
147 +
class _ItemSelectionControllerState extends State<ItemSelectionController> {
148 +
  int _start = -1;
149 +
  int _end = -1;
150 +
151 +
  @override
152 +
  Widget build(BuildContext context) {
153 +
    final canSelect = widget._selectionMode != ItemSelectionMode.none;
154 +
    final multiSelect = widget._selectionMode == ItemSelectionMode.multi;
155 +
    return _ItemSelectionScope(
156 +
      controller: widget,
157 +
      child: ItemSelectionHitTester(
158 +
        child: GestureDetector(
159 +
          child: widget.child,
160 +
          onTapUp: canSelect ? _onTapUp : null,
161 +
          onLongPressStart: canSelect ? _onLongPressStart : null,
162 +
          onLongPressMoveUpdate: multiSelect ? _onLongPressMoveUpdate : null,
163 +
          onLongPressEnd: multiSelect ? _onLongPressEnd : null,
164 +
        ),
165 +
      ),
166 +
    );
167 +
  }
168 +
169 +
  void _onTapUp(TapUpDetails details) {
170 +
    final index = _hitTestAt(details.localPosition);
171 +
    _startSelection(index);
172 +
    _endSelection(index);
173 +
  }
174 +
175 +
  void _onLongPressStart(LongPressStartDetails details) {
176 +
    final index = _hitTestAt(details.localPosition);
177 +
    _startSelection(index);
178 +
  }
179 +
180 +
  void _onLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
181 +
    final index = _hitTestAt(details.localPosition);
182 +
    _updateSelection(index);
183 +
  }
184 +
185 +
  void _onLongPressEnd(LongPressEndDetails details) {
186 +
    final index = _hitTestAt(details.localPosition);
187 +
    _updateSelection(index);
188 +
    _endSelection(index);
189 +
  }
190 +
191 +
  int _hitTestAt(Offset position) {
192 +
    final RenderItemSelectionHitTester hitTester = context.findRenderObject();
193 +
    return hitTester?.hitTestAt(
194 +
            position, widget._scrollInsets, widget._scrollDuration) ??
195 +
        -1;
196 +
  }
197 +
198 +
  void _startSelection(int index) {
199 +
    if (index < 0) return;
200 +
    _start = _end = index;
201 +
    if (!widget._startSelection(index, index)) {
202 +
      widget.selection.clear();
203 +
      widget.selection.add(index);
204 +
    }
205 +
  }
206 +
207 +
  void _updateSelection(int index) {
208 +
    if (index < 0 || _start < 0 || _end == index) return;
209 +
    _end = index;
210 +
    if (!widget._updateSelection(_start, index)) {
211 +
      widget.selection.replace(_start, index);
212 +
    }
213 +
  }
214 +
215 +
  void _endSelection(int index) {
216 +
    if (index < 0 || _start < 0) return;
217 +
    widget._endSelection(_start, index);
218 +
    _start = _end = -1;
219 +
  }
220 +
}
221 +
222 +
class _ItemSelectionScope extends InheritedWidget {
223 +
  final ItemSelectionController controller;
224 +
225 +
  const _ItemSelectionScope({
226 +
    Key key,
227 +
    Widget child,
228 +
    this.controller,
229 +
  }) : super(key: key, child: child);
230 +
231 +
  @override
232 +
  bool updateShouldNotify(_ItemSelectionScope old) {
233 +
    return controller != old.controller;
234 +
  }
235 +
}

@@ -0,0 +1,63 @@
Loading
1 +
// MIT License
2 +
//
3 +
// Copyright (c) 2020 J-P Nurmi
4 +
//
5 +
// The ItemSelector library is based on:
6 +
// Multi Select GridView in Flutter - by Simon Lightfoot:
7 +
// https://gist.github.com/slightfoot/a002dd1e031f5f012f810c6d5da14a11
8 +
//
9 +
// Copyright (c) 2019 Simon Lightfoot
10 +
//
11 +
// Permission is hereby granted, free of charge, to any person obtaining a copy
12 +
// of this software and associated documentation files (the "Software"), to deal
13 +
// in the Software without restriction, including without limitation the rights
14 +
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 +
// copies of the Software, and to permit persons to whom the Software is
16 +
// furnished to do so, subject to the following conditions:
17 +
//
18 +
// The above copyright notice and this permission notice shall be included in all
19 +
// copies or substantial portions of the Software.
20 +
//
21 +
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 +
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 +
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 +
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 +
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 +
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27 +
// SOFTWARE.
28 +
//
29 +
// Thanks to Hugo Passos.
30 +
//
31 +
import 'package:flutter/widgets.dart';
32 +
33 +
/// Specifies how an item selection controller responds to user interaction.
34 +
///
35 +
/// See also [ItemSelectionController()].
36 +
enum ItemSelectionMode {
37 +
  /// No interactive selection.
38 +
  none,
39 +
40 +
  /// Single-selection by tap.
41 +
  single,
42 +
43 +
  /// Multi-selection by long-press and drag (default).
44 +
  multi,
45 +
}
46 +
47 +
/// Signature for a callback function that is called by [ItemSelectionController]
48 +
/// when items are interactively selected by the user.
49 +
typedef ItemSelectionActionCallback = bool Function(int start, int end);
50 +
51 +
/// Signature for a callback function that is called by [ItemSelection] when an
52 +
/// item selection state changes to [selected] at the specified [index].
53 +
typedef ItemSelectionChangeCallback = void Function(int index, bool selected);
54 +
55 +
/// Signature for a builder function that is called by [ItemSelectionBuilder] to
56 +
/// create a widget for a given [index] and [selected] state.
57 +
typedef ItemSelectionWidgetBuilder = Widget Function(
58 +
    BuildContext context, int index, bool selected);
59 +
60 +
class ItemSelectionMetaData {
61 +
  final int index;
62 +
  ItemSelectionMetaData({@required this.index});
63 +
}

@@ -0,0 +1,130 @@
Loading
1 +
// Copyright 2020 J-P Nurmi
2 +
//
3 +
// ItemSelectionNotifier is based on Flutter's ChangeNotifier:
4 +
// https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/foundation/change_notifier.dart
5 +
//
6 +
// Copyright 2014 The Flutter Authors. All rights reserved.
7 +
//
8 +
// Redistribution and use in source and binary forms, with or without modification,
9 +
// are permitted provided that the following conditions are met:
10 +
//
11 +
//     * Redistributions of source code must retain the above copyright
12 +
//       notice, this list of conditions and the following disclaimer.
13 +
//     * Redistributions in binary form must reproduce the above
14 +
//       copyright notice, this list of conditions and the following
15 +
//       disclaimer in the documentation and/or other materials provided
16 +
//       with the distribution.
17 +
//     * Neither the name of Google Inc. nor the names of its
18 +
//       contributors may be used to endorse or promote products derived
19 +
//       from this software without specific prior written permission.
20 +
//
21 +
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22 +
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 +
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 +
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
25 +
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26 +
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27 +
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
28 +
// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 +
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 +
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 +
32 +
import 'dart:collection';
33 +
34 +
import 'package:flutter/foundation.dart';
35 +
36 +
import 'item_selection_types.dart';
37 +
38 +
@visibleForTesting
39 +
abstract class ItemSelectionNotifier {
40 +
  /// Adds a [listener] callback for selection changes at any index.
41 +
  void addListener(ItemSelectionChangeCallback listener) {
42 +
    assert(_debugAssertNotDisposed());
43 +
    _listeners.add(listener);
44 +
  }
45 +
46 +
  /// Removes a [listener] callback for selection changes at any index.
47 +
  void removeListener(ItemSelectionChangeCallback listener) {
48 +
    assert(_debugAssertNotDisposed());
49 +
    _listeners.remove(listener);
50 +
  }
51 +
52 +
  /// Adds a [listener] callback for selection changes at a specific [index] only.
53 +
  void addIndexListener(int index, ItemSelectionChangeCallback listener) {
54 +
    assert(_debugAssertNotDisposed());
55 +
    if (!_indexListeners.containsKey(index)) {
56 +
      _indexListeners[index] = ObserverList<ItemSelectionChangeCallback>();
57 +
    }
58 +
    _indexListeners[index].add(listener);
59 +
  }
60 +
61 +
  /// Removes a [listener] callback for selection changes at a specific [index] only.
62 +
  void removeIndexListener(int index, ItemSelectionChangeCallback listener) {
63 +
    assert(_debugAssertNotDisposed());
64 +
    if (_indexListeners.containsKey(index)) {
65 +
      _indexListeners[index].remove(listener);
66 +
    }
67 +
  }
68 +
69 +
  /// Notifies all the registered listeners.
70 +
  @protected
71 +
  @visibleForTesting
72 +
  void notifyListeners(int index, bool selected) {
73 +
    assert(_debugAssertNotDisposed());
74 +
    if (_listeners != null) {
75 +
      final allListeners = List<ItemSelectionChangeCallback>.from(
76 +
          [..._listeners, ...?_indexListeners[index]]);
77 +
      for (final listener in allListeners) {
78 +
        try {
79 +
          listener(index, selected);
80 +
        } catch (exception, stack) {
81 +
          FlutterError.reportError(
82 +
            FlutterErrorDetails(
83 +
              exception: exception,
84 +
              stack: stack,
85 +
              library: 'item_selector',
86 +
              context: ErrorDescription(
87 +
                  'while dispatching notifications for $runtimeType'),
88 +
              informationCollector: () sync* {
89 +
                yield DiagnosticsProperty<ItemSelectionNotifier>(
90 +
                  'The $runtimeType sending notification was',
91 +
                  this,
92 +
                  style: DiagnosticsTreeStyle.errorProperty,
93 +
                );
94 +
              },
95 +
            ),
96 +
          );
97 +
        }
98 +
      }
99 +
    }
100 +
  }
101 +
102 +
  /// Discards the listener callbacks. After this method is called, the notifier
103 +
  /// object is no longer in a usable state and should be discarded (calls to
104 +
  /// [addListener] and [removeListener] will throw after the object is
105 +
  /// disposed).
106 +
  ///
107 +
  /// This method should only be called by the object's owner.
108 +
  @mustCallSuper
109 +
  void dispose() {
110 +
    assert(_debugAssertNotDisposed());
111 +
    _indexListeners = null;
112 +
    _listeners = null;
113 +
  }
114 +
115 +
  bool _debugAssertNotDisposed() {
116 +
    assert(() {
117 +
      if (_listeners == null || _indexListeners == null) {
118 +
        throw FlutterError('A $runtimeType was used after being disposed.\n'
119 +
            'Once you have called dispose() on a $runtimeType, it can no longer be used.');
120 +
      }
121 +
      return true;
122 +
    }());
123 +
    return true;
124 +
  }
125 +
126 +
  ObserverList<ItemSelectionChangeCallback> _listeners =
127 +
      ObserverList<ItemSelectionChangeCallback>();
128 +
  HashMap<int, ObserverList<ItemSelectionChangeCallback>> _indexListeners =
129 +
      HashMap<int, ObserverList<ItemSelectionChangeCallback>>();
130 +
}

@@ -0,0 +1,83 @@
Loading
1 +
// MIT License
2 +
//
3 +
// Copyright (c) 2020 J-P Nurmi
4 +
//
5 +
// The ItemSelector library is based on:
6 +
// Multi Select GridView in Flutter - by Simon Lightfoot:
7 +
// https://gist.github.com/slightfoot/a002dd1e031f5f012f810c6d5da14a11
8 +
//
9 +
// Copyright (c) 2019 Simon Lightfoot
10 +
//
11 +
// Permission is hereby granted, free of charge, to any person obtaining a copy
12 +
// of this software and associated documentation files (the "Software"), to deal
13 +
// in the Software without restriction, including without limitation the rights
14 +
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 +
// copies of the Software, and to permit persons to whom the Software is
16 +
// furnished to do so, subject to the following conditions:
17 +
//
18 +
// The above copyright notice and this permission notice shall be included in all
19 +
// copies or substantial portions of the Software.
20 +
//
21 +
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 +
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 +
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 +
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 +
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 +
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27 +
// SOFTWARE.
28 +
//
29 +
// Thanks to Hugo Passos.
30 +
//
31 +
import 'dart:math';
32 +
33 +
import 'package:flutter/gestures.dart';
34 +
import 'package:flutter/rendering.dart';
35 +
import 'package:flutter/widgets.dart';
36 +
37 +
import 'item_selection_types.dart';
38 +
39 +
class ItemSelectionHitTester extends SingleChildRenderObjectWidget {
40 +
  ItemSelectionHitTester({
41 +
    Key key,
42 +
    @required Widget child,
43 +
  }) : super(key: key, child: child);
44 +
45 +
  @override
46 +
  RenderItemSelectionHitTester createRenderObject(BuildContext context) =>
47 +
      RenderItemSelectionHitTester();
48 +
}
49 +
50 +
class RenderItemSelectionHitTester extends RenderProxyBox {
51 +
  int hitTestAt(
52 +
      Offset position, EdgeInsets scrollInsets, Duration scrollDuration) {
53 +
    final result = BoxHitTestResult();
54 +
    final hit = hitTestChildren(result, position: _withinPaintBounds(position));
55 +
    if (hit) {
56 +
      for (final entry in result.path) {
57 +
        final target = entry.target;
58 +
        if (target is RenderMetaData) {
59 +
          target.showOnScreen(
60 +
            duration: scrollDuration,
61 +
            rect: scrollInsets.inflateRect(Offset.zero & target.size),
62 +
          );
63 +
          final metaData = target.metaData;
64 +
          if (metaData is ItemSelectionMetaData) {
65 +
            return metaData.index;
66 +
          }
67 +
        }
68 +
      }
69 +
    }
70 +
    return -1;
71 +
  }
72 +
73 +
  Offset _withinPaintBounds(Offset position) {
74 +
    double clamp(double val, double a, double b) {
75 +
      return max(a, min(val, b));
76 +
    }
77 +
78 +
    return Offset(
79 +
      clamp(position.dx, paintBounds.left + 1, paintBounds.right - 1),
80 +
      clamp(position.dy, paintBounds.top + 1, paintBounds.bottom - 1),
81 +
    );
82 +
  }
83 +
}

@@ -0,0 +1,194 @@
Loading
1 +
// MIT License
2 +
//
3 +
// Copyright (c) 2020 J-P Nurmi
4 +
//
5 +
// The ItemSelector library is based on:
6 +
// Multi Select GridView in Flutter - by Simon Lightfoot:
7 +
// https://gist.github.com/slightfoot/a002dd1e031f5f012f810c6d5da14a11
8 +
//
9 +
// Copyright (c) 2019 Simon Lightfoot
10 +
//
11 +
// Permission is hereby granted, free of charge, to any person obtaining a copy
12 +
// of this software and associated documentation files (the "Software"), to deal
13 +
// in the Software without restriction, including without limitation the rights
14 +
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 +
// copies of the Software, and to permit persons to whom the Software is
16 +
// furnished to do so, subject to the following conditions:
17 +
//
18 +
// The above copyright notice and this permission notice shall be included in all
19 +
// copies or substantial portions of the Software.
20 +
//
21 +
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 +
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 +
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 +
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 +
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 +
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27 +
// SOFTWARE.
28 +
//
29 +
// Thanks to Hugo Passos.
30 +
//
31 +
import 'dart:collection';
32 +
33 +
import 'package:interval_tree/interval_tree.dart';
34 +
35 +
import 'item_selection_notifier.dart';
36 +
37 +
/// Manages a selection of items.
38 +
///
39 +
/// ItemSelection is an [Iterable] collection offering all standard iterable
40 +
/// operations, such as querying whether it [contains] specific indexes, easily
41 +
/// accessing the [first] or [last] index, or iterating all indexes in the
42 +
/// selection with [iterator].
43 +
///
44 +
/// It is often not necessary to create an ItemSelection instance yourself,
45 +
/// because [ItemSelectionController] will create one internally if necessary.
46 +
/// However, creating an ItemSelection instance gives you more control over the
47 +
/// selection. First of all, it allows you to specify an initial selection, and
48 +
/// secondly, you can listen to selection state changes for individual items.
49 +
///
50 +
/// ### Example
51 +
///
52 +
///     Widget build(BuildContext context) {
53 +
///       // specify initial selection
54 +
///       final mySelection = ItemSelection(0, 9);
55 +
///
56 +
///       // listen to selection changes
57 +
///       mySelection.addListener((int index, bool selected) {
58 +
///         print('$index: $selected');
59 +
///       });
60 +
///
61 +
///       // pass the selection to the controller
62 +
///       return ItemSelectionController(
63 +
///         selection: mySelection,
64 +
///         // ...
65 +
///       );
66 +
///     }
67 +
class ItemSelection extends ItemSelectionNotifier with IterableMixin<int> {
68 +
  /// Creates a selection, optionally with an initial selection range from
69 +
  /// [start] to [end].
70 +
  ItemSelection([int start, int end]) {
71 +
    if (start != null) {
72 +
      _tree.add([start, end ?? start]);
73 +
    }
74 +
  }
75 +
76 +
  /// Returns `true` if this selection is empty.
77 +
  bool get isEmpty => _tree.isEmpty;
78 +
79 +
  /// Returns `true` if this selection is not empty.
80 +
  bool get isNotEmpty => _tree.isNotEmpty;
81 +
82 +
  /// Returns the first index in this selection.
83 +
  int get first => _tree.first?.start;
84 +
85 +
  /// Returns the last index in this selection.
86 +
  int get last => _tree.last?.end;
87 +
88 +
  /// Returns an iterator for iterating the indexes this selection.
89 +
  Iterator<int> get iterator => _ItemSelectionIterator(_tree.iterator);
90 +
91 +
  /// Returns `true` if this selection contains the specified [index].
92 +
  bool contains(covariant int index) {
93 +
    return _tree.contains([index, index]);
94 +
  }
95 +
96 +
  /// Adds a selection range from [start] to [end].
97 +
  void add(int start, [int end]) {
98 +
    end ??= start;
99 +
    final addition = IntervalTree([start, end]);
100 +
    addition.removeAll(_tree.intersection(addition));
101 +
    for (final range in addition) {
102 +
      for (int i = range.start; i <= range.end; ++i) {
103 +
        notifyListeners(i, true);
104 +
      }
105 +
    }
106 +
    _tree.add([start, end]);
107 +
  }
108 +
109 +
  /// Removes the selection range from [start] to [end].
110 +
  void remove(int start, [int end]) {
111 +
    end ??= start;
112 +
    if (_tree.isEmpty) return;
113 +
    final removal = IntervalTree([start - 1, end + 1]);
114 +
    removal.removeAll(_tree.intersection(removal));
115 +
    for (int i = start; i <= end; ++i) {
116 +
      notifyListeners(i, false);
117 +
    }
118 +
    _tree.remove([start - 1, end + 1]);
119 +
  }
120 +
121 +
  /// Replaces the existing selection with a selection range from [start] to
122 +
  /// [end] so that no changes are notified for the overlapping range.
123 +
  void replace(int start, [int end]) {
124 +
    end ??= start;
125 +
126 +
    final newTree = IntervalTree([start, end]);
127 +
    final overlap = _tree.intersection(newTree);
128 +
129 +
    final removal = IntervalTree.of(_tree);
130 +
    removal.removeAll(newTree);
131 +
    for (final range in removal) {
132 +
      for (int i = range.start; i <= range.end; ++i) {
133 +
        if (!overlap.contains([i, i])) {
134 +
          notifyListeners(i, false);
135 +
        }
136 +
      }
137 +
    }
138 +
139 +
    final addition = IntervalTree.of(newTree);
140 +
    addition.removeAll(removal);
141 +
    for (final range in addition) {
142 +
      for (int i = range.start; i <= range.end; ++i) {
143 +
        if (!overlap.contains([i, i])) {
144 +
          notifyListeners(i, true);
145 +
        }
146 +
      }
147 +
    }
148 +
149 +
    _tree = newTree;
150 +
  }
151 +
152 +
  /// Clears this selection.
153 +
  void clear() {
154 +
    for (final range in _tree) {
155 +
      for (int i = range.start; i <= range.end; ++i) {
156 +
        notifyListeners(i, false);
157 +
      }
158 +
    }
159 +
    _tree.clear();
160 +
  }
161 +
162 +
  IntervalTree _tree = IntervalTree();
163 +
}
164 +
165 +
class _ItemSelectionIterator extends Iterator<int> {
166 +
  _ItemSelectionIterator(this._ranges);
167 +
168 +
  /// Returns the current value.
169 +
  @override
170 +
  int get current => _current;
171 +
172 +
  /// Iterates to the next value and returns `true` on success, or `false`
173 +
  /// otherwise.
174 +
  @override
175 +
  bool moveNext() {
176 +
    if (_current == null) {
177 +
      if (!_ranges.moveNext()) return false;
178 +
      _current = _ranges.current.start;
179 +
    } else {
180 +
      ++_current;
181 +
    }
182 +
    if (!_ranges.current.contains(Interval(_current, _current))) {
183 +
      if (!_ranges.moveNext()) {
184 +
        _current = null;
185 +
        return false;
186 +
      }
187 +
      _current = _ranges.current.start;
188 +
    }
189 +
    return true;
190 +
  }
191 +
192 +
  int _current;
193 +
  Iterator<Interval> _ranges;
194 +
}

@@ -0,0 +1,160 @@
Loading
1 +
// MIT License
2 +
//
3 +
// Copyright (c) 2020 J-P Nurmi
4 +
//
5 +
// The ItemSelector library is based on:
6 +
// Multi Select GridView in Flutter - by Simon Lightfoot:
7 +
// https://gist.github.com/slightfoot/a002dd1e031f5f012f810c6d5da14a11
8 +
//
9 +
// Copyright (c) 2019 Simon Lightfoot
10 +
//
11 +
// Permission is hereby granted, free of charge, to any person obtaining a copy
12 +
// of this software and associated documentation files (the "Software"), to deal
13 +
// in the Software without restriction, including without limitation the rights
14 +
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 +
// copies of the Software, and to permit persons to whom the Software is
16 +
// furnished to do so, subject to the following conditions:
17 +
//
18 +
// The above copyright notice and this permission notice shall be included in all
19 +
// copies or substantial portions of the Software.
20 +
//
21 +
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 +
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 +
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 +
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 +
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 +
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27 +
// SOFTWARE.
28 +
//
29 +
// Thanks to Hugo Passos.
30 +
//
31 +
import 'package:flutter/widgets.dart';
32 +
33 +
import 'item_selection_controller.dart';
34 +
import 'item_selection_types.dart';
35 +
36 +
/// Builds itself based on selection state changes.
37 +
///
38 +
/// ItemSelectionBuilder is used in conjunction with [ItemSelectionController].
39 +
/// Widget rebuilding is scheduled by appropriate selection changes in the
40 +
/// controller.
41 +
///
42 +
/// Notice that you must tell ItemSelectionBuilder the [index] it represents,
43 +
/// and a [builder] method that is called when the selection state of that
44 +
/// particular index changes.
45 +
///
46 +
/// ### Example
47 +
///
48 +
///     Widget build(BuildContext context) {
49 +
///       return ItemSelectionController(
50 +
///         child: Column(
51 +
///           children: List.generate(5, (int index) {
52 +
///             // tell the builder which index it represents
53 +
///             return ItemSelectionBuilder(
54 +
///               index: index,
55 +
///               builder: (BuildContext context, int index, bool selected) {
56 +
///                 // build a selectable item
57 +
///                 return Text('$index: $selected');
58 +
///               },
59 +
///             );
60 +
///           }),
61 +
///         ),
62 +
///       );
63 +
///     }
64 +
class ItemSelectionBuilder extends StatefulWidget {
65 +
  /// Creates an item selection builder.
66 +
  ///
67 +
  /// Notice that you must tell ItemSelectionBuilder the [index] it represents,
68 +
  /// and a [builder] method that is called when the selection state of that
69 +
  /// particular index changes.
70 +
  ItemSelectionBuilder({
71 +
    Key key,
72 +
    @required this.index,
73 +
    ItemSelectionController controller,
74 +
    @required ItemSelectionWidgetBuilder builder,
75 +
  })  : assert(index != -1),
76 +
        assert(builder != null),
77 +
        _controller = controller,
78 +
        _builder = builder,
79 +
        super(key: key ?? ValueKey(index));
80 +
81 +
  /// The index of this builder widget.
82 +
  final int index;
83 +
84 +
  @override
85 +
  _ItemSelectionBuilderState createState() => _ItemSelectionBuilderState();
86 +
87 +
  final ItemSelectionWidgetBuilder _builder;
88 +
  final ItemSelectionController _controller;
89 +
}
90 +
91 +
class _ItemSelectionBuilderState extends State<ItemSelectionBuilder> {
92 +
  bool _selected = false;
93 +
  ItemSelectionController _controller;
94 +
95 +
  @override
96 +
  Widget build(BuildContext context) {
97 +
    return MetaData(
98 +
      metaData: ItemSelectionMetaData(index: widget.index),
99 +
      child: widget._builder(context, widget.index, _selected),
100 +
    );
101 +
  }
102 +
103 +
  @override
104 +
  void dispose() {
105 +
    if (_controller != null) {
106 +
      _controller.selection.removeIndexListener(widget.index, _updateSelection);
107 +
    }
108 +
    _controller = null;
109 +
    super.dispose();
110 +
  }
111 +
112 +
  @override
113 +
  void didChangeDependencies() {
114 +
    super.didChangeDependencies();
115 +
    _updateController();
116 +
  }
117 +
118 +
  @override
119 +
  void didUpdateWidget(ItemSelectionBuilder oldWidget) {
120 +
    super.didUpdateWidget(oldWidget);
121 +
    if (widget._controller != oldWidget._controller) _updateController();
122 +
  }
123 +
124 +
  void _updateController() {
125 +
    final newController =
126 +
        widget._controller ?? ItemSelectionController.of(context);
127 +
    assert(() {
128 +
      if (newController == null) {
129 +
        throw FlutterError(
130 +
            'No ItemSelectionController for ${widget.runtimeType}.\n'
131 +
            'When creating a ${widget.runtimeType}, you must either provide an explicit '
132 +
            'ItemSelectionController using the "controller" property, or you must ensure that there '
133 +
            'is a ItemSelectionController above the ${widget.runtimeType}.\n'
134 +
            'In this case, there was neither an explicit controller nor a default controller.');
135 +
      }
136 +
      return true;
137 +
    }());
138 +
139 +
    if (newController == _controller) return;
140 +
141 +
    final index = widget.index;
142 +
    if (_controller != null) {
143 +
      _controller.selection.removeIndexListener(index, _updateSelection);
144 +
    }
145 +
    _controller = newController;
146 +
    if (_controller != null) {
147 +
      _controller.selection.addIndexListener(index, _updateSelection);
148 +
      _updateSelection(index, _controller.selection.contains(index));
149 +
    }
150 +
  }
151 +
152 +
  void _updateSelection(int index, bool selected) {
153 +
    assert(index == widget.index);
154 +
    if (_selected != selected) {
155 +
      setState(() {
156 +
        _selected = selected;
157 +
      });
158 +
    }
159 +
  }
160 +
}
Files Coverage
lib/src 93.97%
Project Totals (6 files) 93.97%
Notifications are pending CI completion. Waiting for GitHub's status webhook to queue notifications. Push notifications now.

No yaml found.

Create your codecov.yml to customize your Codecov experience

Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading