scroll_physics.dart 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import 'dart:math' as math;
  2. import 'package:eye_video/framework/uikit/refresher/indicator/core/abstract_footer.dart';
  3. import 'package:eye_video/framework/uikit/refresher/indicator/core/abstract_header.dart';
  4. import 'package:eye_video/framework/uikit/refresher/pretty_refresher.dart';
  5. import 'package:flutter/gestures.dart';
  6. import 'package:flutter/physics.dart';
  7. import 'package:flutter/widgets.dart';
  8. class RefreshPhysics extends ScrollPhysics {
  9. // 任务状态
  10. final ValueNotifier<TaskState> taskNotifier;
  11. // 回弹设置
  12. final ValueNotifier<BouncingSettings> bouncingNotifier;
  13. // 指示器越界
  14. final ValueNotifier<RefreshIndicator> indicatorNotifier;
  15. // Creates scroll physics that bounce back from the edge.
  16. const RefreshPhysics({
  17. ScrollPhysics parent,
  18. this.taskNotifier,
  19. this.bouncingNotifier,
  20. this.indicatorNotifier,
  21. }) : super(parent: parent);
  22. @override
  23. RefreshPhysics applyTo(ScrollPhysics ancestor) {
  24. return RefreshPhysics(
  25. parent: buildParent(ancestor),
  26. taskNotifier: taskNotifier,
  27. bouncingNotifier: bouncingNotifier,
  28. indicatorNotifier: indicatorNotifier,
  29. );
  30. }
  31. Header get header => indicatorNotifier.value.header;
  32. bool get headerOverScroll {
  33. if (header != null) {
  34. return !header.enableInfiniteRefresh || header.overScroll;
  35. }
  36. return true;
  37. }
  38. double get headerExtent => header.extent;
  39. Footer get footer => indicatorNotifier.value.footer;
  40. bool get footerOverScroll {
  41. if (footer != null) {
  42. return !footer.enableInfiniteLoad || footer.overScroll;
  43. }
  44. return true;
  45. }
  46. double get footerExtent => footer.extent;
  47. // The multiple applied to overscroll to make it appear that scrolling past
  48. // the edge of the scrollable contents is harder than scrolling the list.
  49. // This is done by reducing the ratio of the scroll effect output vs the
  50. // scroll gesture input.
  51. //
  52. // This factor starts at 0.52 and progressively becomes harder to overscroll
  53. // as more of the area past the edge is dragged in (represented by an increasing
  54. // `overscrollFraction` which starts at 0 when there is no overscroll).
  55. double frictionFactor(double overscrollFraction) =>
  56. 0.52 * math.pow(1 - overscrollFraction, 2);
  57. @override
  58. bool shouldAcceptUserOffset(ScrollMetrics position) {
  59. return true;
  60. }
  61. @override
  62. double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
  63. assert(offset != 0.0);
  64. assert(position.minScrollExtent <= position.maxScrollExtent);
  65. if (!position.outOfRange) return offset;
  66. final double overscrollPastStart =
  67. math.max(position.minScrollExtent - position.pixels, 0.0);
  68. final double overscrollPastEnd =
  69. math.max(position.pixels - position.maxScrollExtent, 0.0);
  70. final double overscrollPast =
  71. math.max(overscrollPastStart, overscrollPastEnd);
  72. final bool easing = (overscrollPastStart > 0.0 && offset < 0.0) ||
  73. (overscrollPastEnd > 0.0 && offset > 0.0);
  74. final double friction = easing
  75. // Apply less resistance when easing the overscroll vs tensioning.
  76. ? frictionFactor(
  77. (overscrollPast - offset.abs()) / position.viewportDimension)
  78. : frictionFactor(overscrollPast / position.viewportDimension);
  79. final double direction = offset.sign;
  80. return direction * _applyFriction(overscrollPast, offset.abs(), friction);
  81. }
  82. static double _applyFriction(
  83. double extentOutside, double absDelta, double gamma) {
  84. assert(absDelta > 0);
  85. double total = 0.0;
  86. if (extentOutside > 0) {
  87. final double deltaToLimit = extentOutside / gamma;
  88. if (absDelta < deltaToLimit) return absDelta * gamma;
  89. total += extentOutside;
  90. absDelta -= deltaToLimit;
  91. }
  92. return total + absDelta;
  93. }
  94. @override
  95. double applyBoundaryConditions(ScrollMetrics position, double value) {
  96. if (!bouncingNotifier.value.top ||
  97. (!headerOverScroll &&
  98. (taskNotifier.value.refreshing ||
  99. taskNotifier.value.refreshNoMore))) {
  100. if (value < position.pixels &&
  101. position.pixels <= position.minScrollExtent) // underscroll
  102. return value - position.pixels;
  103. if (value < position.minScrollExtent &&
  104. position.minScrollExtent < position.pixels) // hit top edge
  105. return value - position.minScrollExtent;
  106. }
  107. if (!headerOverScroll && value - position.minScrollExtent < 0.0) {
  108. // 防止越界超过header高度
  109. return value - position.minScrollExtent;
  110. }
  111. if (!bouncingNotifier.value.bottom ||
  112. (!footerOverScroll &&
  113. (taskNotifier.value.loading || taskNotifier.value.loadNoMore))) {
  114. if (position.maxScrollExtent <= position.pixels &&
  115. position.pixels < value) {
  116. // overscroll
  117. return value - position.pixels;
  118. }
  119. if (position.pixels < position.maxScrollExtent &&
  120. position.maxScrollExtent < value) // hit bottom edge
  121. return value - position.maxScrollExtent;
  122. }
  123. if (!footerOverScroll && value - position.maxScrollExtent > 0.0) {
  124. // 防止越界超过footer高度
  125. return value - position.maxScrollExtent;
  126. }
  127. return 0.0;
  128. }
  129. @override
  130. Simulation createBallisticSimulation(
  131. ScrollMetrics position, double velocity) {
  132. final Tolerance tolerance = this.tolerance;
  133. if (velocity.abs() >= tolerance.velocity || position.outOfRange) {
  134. return BouncingScrollSimulation(
  135. spring: spring,
  136. position: position.pixels,
  137. velocity: velocity * 0.91,
  138. // TODO(abarth): We should move this constant closer to the drag end.
  139. leadingExtent: position.minScrollExtent,
  140. trailingExtent: position.maxScrollExtent,
  141. tolerance: tolerance,
  142. );
  143. }
  144. return null;
  145. }
  146. // The ballistic simulation here decelerates more slowly than the one for
  147. // ClampingScrollPhysics so we require a more deliberate input gesture
  148. // to trigger a fling.
  149. @override
  150. double get minFlingVelocity => kMinFlingVelocity * 2.0;
  151. // Methodology:
  152. // 1- Use https://github.com/flutter/scroll_overlay to test with Flutter and
  153. // platform scroll views superimposed.
  154. // 2- Record incoming speed and make rapid flings in the test app.
  155. // 3- If the scrollables stopped overlapping at any moment, adjust the desired
  156. // output value of this function at that input speed.
  157. // 4- Feed input/output set into a power curve fitter. Change function
  158. // and repeat from 2.
  159. // 5- Repeat from 2 with medium and slow flings.
  160. // Momentum build-up function that mimics iOS's scroll speed increase with repeated flings.
  161. //
  162. // The velocity of the last fling is not an important factor. Existing speed
  163. // and (related) time since last fling are factors for the velocity transfer
  164. // calculations.
  165. @override
  166. double carriedMomentum(double existingVelocity) {
  167. return existingVelocity.sign *
  168. math.min(0.000816 * math.pow(existingVelocity.abs(), 1.967).toDouble(),
  169. 40000.0);
  170. }
  171. // Eyeballed from observation to counter the effect of an unintended scroll
  172. // from the natural motion of lifting the finger after a scroll.
  173. @override
  174. double get dragStartDistanceMotionThreshold => 3.5;
  175. }
  176. // 回弹设置
  177. class BouncingSettings {
  178. bool top;
  179. bool bottom;
  180. BouncingSettings({this.top = true, this.bottom = true});
  181. BouncingSettings copy({bool top, bool bottom}) {
  182. return BouncingSettings(
  183. top: top ?? this.top,
  184. bottom: bottom ?? this.bottom,
  185. );
  186. }
  187. }