Guest User

Untitled

a guest
May 23rd, 2018
123
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 14.41 KB | None | 0 0
  1. // Copyright 2014 The Chromium Authors. All rights reserved.
  2.  
  3. // Redistribution and use in source and binary forms, with or without
  4. // modification, are permitted provided that the following conditions are
  5. // met:
  6.  
  7. // * Redistributions of source code must retain the above copyright
  8. // notice, this list of conditions and the following disclaimer.
  9. // * Redistributions in binary form must reproduce the above
  10. // copyright notice, this list of conditions and the following
  11. // disclaimer in the documentation and/or other materials provided
  12. // with the distribution.
  13. // * Neither the name of Google Inc. nor the names of its
  14. // contributors may be used to endorse or promote products derived
  15. // from this software without specific prior written permission.
  16.  
  17. // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  18. // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  19. // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  20. // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  21. // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  22. // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  23. // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  24. // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  25. // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  27. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28.  
  29.  
  30. import 'dart:async';
  31. import 'dart:math' as math;
  32.  
  33. import 'package:flutter/foundation.dart';
  34. import 'package:flutter/material.dart';
  35. import 'package:flutter/widgets.dart';
  36.  
  37. // The over-scroll distance that moves the indicator to its maximum displacement, as a percentage of the scrollable's container extent.
  38. const double _kDragContainerExtentPercentage = 0.25;
  39.  
  40. // How much the scroll's drag gesture can overshoot the RefreshIndicator's displacement; max displacement = _kDragSizeFactorLimit * displacement.
  41. const double _kDragSizeFactorLimit = 1.5;
  42.  
  43. // When the scroll ends, the duration of the refresh indicator's animation to the RefreshIndicator's displacement.
  44. const Duration _kIndicatorSnapDuration = const Duration(milliseconds: 150);
  45.  
  46. // The duration of the ScaleTransition that starts when the refresh action has completed.
  47. const Duration _kIndicatorScaleDuration = const Duration(milliseconds: 200);
  48.  
  49. /// The signature for a function that's called when the user has dragged a [ReactiveRefreshIndicator] far enough to demonstrate that they want to
  50. /// instigate a refresh.
  51. typedef void RefreshCallback();
  52.  
  53. // The state machine moves through these modes only when the scrollable identified by scrollableKey has been scrolled to its min or max limit.
  54. enum _RefreshIndicatorMode {
  55. drag, // Pointer is down.
  56. armed, // Dragged far enough that an up event will run the onRefresh callback.
  57. snap, // Animating to the indicator's final "displacement".
  58. refresh, // Running the refresh callback.
  59. done, // Animating the indicator's fade-out after refreshing.
  60. canceled, // Animating the indicator's fade-out after not arming.
  61. }
  62.  
  63. /// This is a customization of the [RefreshIndicator] widget that is reactive in design. This makes it much easier to integrate into code
  64. /// that has multiple avenues of refresh instigation. That is, refreshing in response to the user pulling down a [ListView], but also in
  65. /// response to some other stimuli, like swiping a header left or right.
  66. ///
  67. /// Instead of [onRefresh] being asynchronous as it is in [RefreshIndicator], it is synchronous. Consequently, instead of determining the
  68. /// visibility of the refresh indicator on your behalf, you must tell the control yourself via [isRefreshing]. The [onRefresh] callback is
  69. /// only executed if the user instigates a refresh via a pull-to-refresh gesture.
  70. class ReactiveRefreshIndicator extends StatefulWidget {
  71. const ReactiveRefreshIndicator({
  72. Key key,
  73. @required this.child,
  74. this.displacement: 40.0,
  75. @required this.isRefreshing,
  76. @required this.onRefresh,
  77. this.color,
  78. this.backgroundColor,
  79. this.notificationPredicate: defaultScrollNotificationPredicate,
  80. }) : assert(child != null),
  81. assert(onRefresh != null),
  82. assert(notificationPredicate != null),
  83. assert(isRefreshing != null),
  84. super(key: key);
  85.  
  86. final Widget child;
  87.  
  88. final double displacement;
  89.  
  90. final bool isRefreshing;
  91.  
  92. final RefreshCallback onRefresh;
  93.  
  94. final Color color;
  95.  
  96. final Color backgroundColor;
  97.  
  98. final ScrollNotificationPredicate notificationPredicate;
  99.  
  100. @override
  101. ReactiveRefreshIndicatorState createState() => new ReactiveRefreshIndicatorState();
  102. }
  103.  
  104. class ReactiveRefreshIndicatorState extends State<ReactiveRefreshIndicator> with TickerProviderStateMixin {
  105. AnimationController _positionController;
  106. AnimationController _scaleController;
  107. Animation<double> _positionFactor;
  108. Animation<double> _scaleFactor;
  109. Animation<double> _value;
  110. Animation<Color> _valueColor;
  111.  
  112. _RefreshIndicatorMode _mode;
  113. bool _isIndicatorAtTop;
  114. double _dragOffset;
  115.  
  116. @override
  117. void initState() {
  118. super.initState();
  119.  
  120. _positionController = new AnimationController(vsync: this);
  121. _positionFactor = new Tween<double>(
  122. begin: 0.0,
  123. end: _kDragSizeFactorLimit,
  124. ).animate(_positionController);
  125. _value = new Tween<double>( // The "value" of the circular progress indicator during a drag.
  126. begin: 0.0,
  127. end: 0.75,
  128. ).animate(_positionController);
  129.  
  130. _scaleController = new AnimationController(vsync: this);
  131. _scaleFactor = new Tween<double>(
  132. begin: 1.0,
  133. end: 0.0,
  134. ).animate(_scaleController);
  135. }
  136.  
  137. @override
  138. void didChangeDependencies() {
  139. final ThemeData theme = Theme.of(context);
  140. _valueColor = new ColorTween(
  141. begin: (widget.color ?? theme.accentColor).withOpacity(0.0),
  142. end: (widget.color ?? theme.accentColor).withOpacity(1.0)
  143. ).animate(new CurvedAnimation(
  144. parent: _positionController,
  145. curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit)
  146. ));
  147.  
  148. super.didChangeDependencies();
  149. }
  150.  
  151. @override
  152. void didUpdateWidget(ReactiveRefreshIndicator oldWidget) {
  153. if (widget.isRefreshing) {
  154. if (_mode != _RefreshIndicatorMode.refresh) {
  155. // Doing this work immediately triggers an assertion failure in the case when the refresh indicator is visible
  156. // upon first display:
  157. //
  158. // I/flutter (21441): The following assertion was thrown building ReactiveRefreshIndicator(state:
  159. // I/flutter (21441): ReactiveRefreshIndicatorState#26328(tickers: tracking 2 tickers)):
  160. // I/flutter (21441): 'package:flutter/src/rendering/object.dart': Failed assertion: line 1792 pos 12: '() {
  161. // I/flutter (21441): final AbstractNode parent = this.parent;
  162. // I/flutter (21441): if (parent is RenderObject)
  163. // I/flutter (21441): return parent._needsCompositing;
  164. // I/flutter (21441): return true;
  165. // I/flutter (21441): }()': is not true.
  166. //
  167. // Therefore, we schedule it via a future instead.
  168.  
  169. new Future(() {
  170. _start(AxisDirection.down);
  171. _show();
  172. });
  173. }
  174. } else {
  175. if (_mode != null && _mode != _RefreshIndicatorMode.done) {
  176. _dismiss(_RefreshIndicatorMode.done);
  177. }
  178. }
  179.  
  180. super.didUpdateWidget(oldWidget);
  181. }
  182.  
  183. @override
  184. void dispose() {
  185. _positionController.dispose();
  186. _scaleController.dispose();
  187. super.dispose();
  188. }
  189.  
  190. bool _handleScrollNotification(ScrollNotification notification) {
  191. if (!widget.notificationPredicate(notification)) {
  192. return false;
  193. }
  194.  
  195. if (notification is ScrollStartNotification && notification.metrics.extentBefore == 0.0 &&
  196. _mode == null && _start(notification.metrics.axisDirection)) {
  197. setState(() {
  198. _mode = _RefreshIndicatorMode.drag;
  199. });
  200. return false;
  201. }
  202.  
  203. bool indicatorAtTopNow;
  204.  
  205. switch (notification.metrics.axisDirection) {
  206. case AxisDirection.down:
  207. indicatorAtTopNow = true;
  208. break;
  209. case AxisDirection.up:
  210. indicatorAtTopNow = false;
  211. break;
  212. case AxisDirection.left:
  213. case AxisDirection.right:
  214. indicatorAtTopNow = null;
  215. break;
  216. }
  217.  
  218. if (indicatorAtTopNow != _isIndicatorAtTop) {
  219. if (_mode == _RefreshIndicatorMode.drag || _mode == _RefreshIndicatorMode.armed) {
  220. _dismiss(_RefreshIndicatorMode.canceled);
  221. }
  222. } else if (notification is ScrollUpdateNotification) {
  223. if (_mode == _RefreshIndicatorMode.drag || _mode == _RefreshIndicatorMode.armed) {
  224. if (notification.metrics.extentBefore > 0.0) {
  225. _dismiss(_RefreshIndicatorMode.canceled);
  226. } else {
  227. _dragOffset -= notification.scrollDelta;
  228. _checkDragOffset(notification.metrics.viewportDimension);
  229. }
  230. }
  231. } else if (notification is OverscrollNotification) {
  232. if (_mode == _RefreshIndicatorMode.drag || _mode == _RefreshIndicatorMode.armed) {
  233. _dragOffset -= notification.overscroll / 2.0;
  234. _checkDragOffset(notification.metrics.viewportDimension);
  235. }
  236. } else if (notification is ScrollEndNotification) {
  237. switch (_mode) {
  238. case _RefreshIndicatorMode.armed:
  239. _show(userInstigated: true);
  240. break;
  241. case _RefreshIndicatorMode.drag:
  242. _dismiss(_RefreshIndicatorMode.canceled);
  243. break;
  244. default:
  245. // do nothing
  246. break;
  247. }
  248. }
  249.  
  250. return false;
  251. }
  252.  
  253. bool _handleGlowNotification(OverscrollIndicatorNotification notification) {
  254. if (notification.depth != 0 || !notification.leading) {
  255. return false;
  256. }
  257.  
  258. if (_mode == _RefreshIndicatorMode.drag) {
  259. notification.disallowGlow();
  260. return true;
  261. }
  262.  
  263. return false;
  264. }
  265.  
  266. bool _start(AxisDirection direction) {
  267. assert(_mode == null);
  268. assert(_isIndicatorAtTop == null);
  269. assert(_dragOffset == null);
  270.  
  271. switch (direction) {
  272. case AxisDirection.down:
  273. _isIndicatorAtTop = true;
  274. break;
  275. case AxisDirection.up:
  276. _isIndicatorAtTop = false;
  277. break;
  278. case AxisDirection.left:
  279. case AxisDirection.right:
  280. _isIndicatorAtTop = null;
  281. // we do not support horizontal scroll views.
  282. return false;
  283. }
  284.  
  285. _dragOffset = 0.0;
  286. _scaleController.value = 0.0;
  287. _positionController.value = 0.0;
  288.  
  289. return true;
  290. }
  291.  
  292. void _checkDragOffset(double containerExtent) {
  293. assert(_mode == _RefreshIndicatorMode.drag || _mode == _RefreshIndicatorMode.armed);
  294.  
  295. double newValue = _dragOffset / (containerExtent * _kDragContainerExtentPercentage);
  296.  
  297. if (_mode == _RefreshIndicatorMode.armed) {
  298. newValue = math.max(newValue, 1.0 / _kDragSizeFactorLimit);
  299. }
  300.  
  301. _positionController.value = newValue.clamp(0.0, 1.0); // this triggers various rebuilds
  302.  
  303. if (_mode == _RefreshIndicatorMode.drag && _valueColor.value.alpha == 0xFF) {
  304. _mode = _RefreshIndicatorMode.armed;
  305. }
  306. }
  307.  
  308. Future<Null> _dismiss(_RefreshIndicatorMode newMode) async {
  309. // This can only be called from _show() when refreshing and
  310. // _handleScrollNotification in response to a ScrollEndNotification or
  311. // direction change.
  312. assert(newMode == _RefreshIndicatorMode.canceled || newMode == _RefreshIndicatorMode.done);
  313.  
  314. setState(() {
  315. _mode = newMode;
  316. });
  317.  
  318. switch (_mode) {
  319. case _RefreshIndicatorMode.done:
  320. await _scaleController.animateTo(1.0, duration: _kIndicatorScaleDuration);
  321. break;
  322. case _RefreshIndicatorMode.canceled:
  323. await _positionController.animateTo(0.0, duration: _kIndicatorScaleDuration);
  324. break;
  325. default:
  326. assert(false);
  327. }
  328.  
  329. if (mounted && _mode == newMode) {
  330. _dragOffset = null;
  331. _isIndicatorAtTop = null;
  332.  
  333. setState(() => _mode = null);
  334. }
  335. }
  336.  
  337. void _show({bool userInstigated = false}) {
  338. assert(_mode != _RefreshIndicatorMode.refresh);
  339. assert(_mode != _RefreshIndicatorMode.snap);
  340.  
  341. _mode = _RefreshIndicatorMode.snap;
  342. _positionController
  343. .animateTo(1.0 / _kDragSizeFactorLimit, duration: _kIndicatorSnapDuration)
  344. .then<void>((Null value) {
  345. if (mounted && _mode == _RefreshIndicatorMode.snap) {
  346. assert(widget.onRefresh != null);
  347.  
  348. setState(() => _mode = _RefreshIndicatorMode.refresh);
  349.  
  350. if (userInstigated) {
  351. widget.onRefresh();
  352. }
  353. }
  354. });
  355. }
  356.  
  357. final GlobalKey _key = new GlobalKey();
  358.  
  359. @override
  360. Widget build(BuildContext context) {
  361. final Widget child = new NotificationListener<ScrollNotification>(
  362. key: _key,
  363. onNotification: _handleScrollNotification,
  364. child: new NotificationListener<OverscrollIndicatorNotification>(
  365. onNotification: _handleGlowNotification,
  366. child: widget.child,
  367. ),
  368. );
  369.  
  370. if (_mode == null) {
  371. assert(_dragOffset == null);
  372. assert(_isIndicatorAtTop == null);
  373.  
  374. return child;
  375. }
  376.  
  377. assert(_dragOffset != null);
  378. assert(_isIndicatorAtTop != null);
  379.  
  380. final bool showIndeterminateIndicator = _mode == _RefreshIndicatorMode.refresh || _mode == _RefreshIndicatorMode.done;
  381.  
  382. return new Stack(
  383. children: <Widget>[
  384. child,
  385. new Positioned(
  386. top: _isIndicatorAtTop ? 0.0 : null,
  387. bottom: !_isIndicatorAtTop ? 0.0 : null,
  388. left: 0.0,
  389. right: 0.0,
  390. child: new SizeTransition(
  391. axisAlignment: _isIndicatorAtTop ? 1.0 : -1.0,
  392. sizeFactor: _positionFactor, // this is what brings it down
  393. child: new Container(
  394. padding: _isIndicatorAtTop
  395. ? new EdgeInsets.only(top: widget.displacement)
  396. : new EdgeInsets.only(bottom: widget.displacement),
  397. alignment: _isIndicatorAtTop
  398. ? Alignment.topCenter
  399. : Alignment.bottomCenter,
  400. child: new ScaleTransition(
  401. scale: _scaleFactor,
  402. child: new AnimatedBuilder(
  403. animation: _positionController,
  404. builder: (BuildContext context, Widget child) {
  405. return new RefreshProgressIndicator(
  406. value: showIndeterminateIndicator ? null : _value.value,
  407. valueColor: _valueColor,
  408. backgroundColor: widget.backgroundColor,
  409. );
  410. },
  411. ),
  412. ),
  413. ),
  414. ),
  415. ),
  416. ],
  417. );
  418. }
  419. }
Add Comment
Please, Sign In to add comment