Hibok
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 
 

215 lignes
7.3 KiB

  1. import 'dart:collection';
  2. import 'package:flutter/widgets.dart';
  3. import 'package:flutter/rendering.dart';
  4. /// Helps [child] stay visible by resizing it to avoid the given [areaToAvoid].
  5. ///
  6. /// Wraps the [child] in a [AnimatedContainer] that adjusts its bottom [padding] to accommodate the given area.
  7. ///
  8. /// If [autoScroll] is true and the [child] contains a focused widget such as a [TextField],
  9. /// automatically scrolls so that it is just visible above the keyboard, plus any additional [overscroll].
  10. class BottomAreaAvoider extends StatefulWidget {
  11. static const Duration defaultDuration = Duration(milliseconds: 100);
  12. static const Curve defaultCurve = Curves.easeIn;
  13. static const double defaultOverscroll = 12.0;
  14. static const bool defaultAutoScroll = false;
  15. /// The child to embed.
  16. ///
  17. /// If the [child] is not a [ScrollView], it is automatically embedded in a [SingleChildScrollView].
  18. /// If the [child] is a [ScrollView], it must have a [ScrollController].
  19. final Widget child;
  20. /// Amount of bottom area to avoid. For example, the height of the currently-showing system keyboard, or
  21. /// any custom bottom overlays.
  22. final double areaToAvoid;
  23. /// Whether to auto-scroll to the focused widget after the keyboard appears. Defaults to false.
  24. /// Could be expensive because it searches all the child objects in this widget's render tree.
  25. final bool autoScroll;
  26. /// Extra amount to scroll past the focused widget. Defaults to [defaultOverscroll].
  27. /// Useful in case the focused widget is inside a parent widget that you also want to be visible.
  28. final double overscroll;
  29. /// Duration of the resize animation. Defaults to [defaultDuration]. To disable, set to [Duration.zero].
  30. final Duration duration;
  31. /// Animation curve. Defaults to [defaultCurve]
  32. final Curve curve;
  33. final ScrollController scrollTo;
  34. BottomAreaAvoider({
  35. Key key,
  36. @required this.child,
  37. @required this.areaToAvoid,
  38. this.autoScroll = false,
  39. this.duration = defaultDuration,
  40. this.curve = defaultCurve,
  41. this.scrollTo,
  42. this.overscroll = defaultOverscroll,
  43. }) : //assert(child is ScrollView ? child.controller != null : true),
  44. assert(areaToAvoid >= 0, 'Cannot avoid a negative area'),
  45. super(key: key);
  46. BottomAreaAvoiderState createState() => BottomAreaAvoiderState();
  47. }
  48. class BottomAreaAvoiderState extends State<BottomAreaAvoider> {
  49. final _animationKey = new GlobalKey<ImplicitlyAnimatedWidgetState>();
  50. Function(AnimationStatus) _animationListener;
  51. ScrollController _scrollController;
  52. double _previousAreaToAvoid;
  53. scroll() {
  54. _scrollController.position.moveTo(
  55. 200,
  56. duration: Duration(milliseconds: 100),
  57. curve: widget.curve,
  58. );
  59. }
  60. @override
  61. void initState() {
  62. super.initState();
  63. }
  64. @override
  65. void didUpdateWidget(BottomAreaAvoider oldWidget) {
  66. _previousAreaToAvoid = oldWidget.areaToAvoid;
  67. super.didUpdateWidget(oldWidget);
  68. }
  69. @override
  70. void dispose() {
  71. _animationKey.currentState?.animation
  72. ?.removeStatusListener(_animationListener);
  73. super.dispose();
  74. }
  75. @override
  76. Widget build(BuildContext context) {
  77. // Add a status listener to the animation after the initial build.
  78. // Wait a frame so that _animationKey.currentState is not null.
  79. if (_animationListener == null) {
  80. WidgetsBinding.instance.addPostFrameCallback((_) {
  81. _animationListener = _paddingAnimationStatusChanged;
  82. _animationKey.currentState.animation
  83. .addStatusListener(_animationListener);
  84. });
  85. }
  86. // If [child] is a [ScrollView], get its [ScrollController]
  87. // and embed the [child] directly in an [AnimatedContainer].
  88. if (widget.child is ScrollView) {
  89. var scrollView = widget.child as ScrollView;
  90. _scrollController =
  91. scrollView.controller ?? PrimaryScrollController.of(context);
  92. return _buildAnimatedContainer(widget.child);
  93. }
  94. // If [child] is not a [ScrollView], and [autoScroll] is true,
  95. // embed the [child] in a [SingleChildScrollView] to make
  96. // it possible to scroll to the focused widget.
  97. if (widget.autoScroll) {
  98. _scrollController = widget.scrollTo;
  99. // widget.scrollTo =_scrollController;
  100. return _buildAnimatedContainer(
  101. LayoutBuilder(
  102. builder: (context, constraints) {
  103. return SingleChildScrollView(
  104. controller: _scrollController,
  105. child: ConstrainedBox(
  106. constraints: BoxConstraints(
  107. minHeight: constraints.maxHeight,
  108. ),
  109. child: widget.child,
  110. ),
  111. );
  112. },
  113. ),
  114. );
  115. }
  116. // Just embed the [child] directly in an [AnimatedContainer].
  117. return _buildAnimatedContainer(widget.child);
  118. }
  119. Widget _buildAnimatedContainer(Widget child) {
  120. return AnimatedContainer(
  121. key: _animationKey,
  122. padding: EdgeInsets.only(bottom: widget.areaToAvoid),
  123. duration: widget.duration,
  124. curve: widget.curve,
  125. child: child,
  126. );
  127. }
  128. /// Called whenever the status of our padding animation changes.
  129. ///
  130. /// If the animation has completed, we added overlap, and scroll is on, scroll to that.
  131. void _paddingAnimationStatusChanged(AnimationStatus status) {
  132. if (status != AnimationStatus.completed) {
  133. return; // Only check when the animation is finishing
  134. }
  135. if (!widget.autoScroll) {
  136. return; // auto scroll is not enabled, do nothing
  137. }
  138. if (widget.areaToAvoid <= _previousAreaToAvoid) {
  139. return; // decreased-- do nothing. We only scroll when area to avoid is added (keyboard shown).
  140. }
  141. // Need to wait a frame to get the new size (todo: is this still needed? we dont use mediaquery anymore)
  142. WidgetsBinding.instance.addPostFrameCallback((_) {
  143. if (context == null || !mounted) {
  144. return; // context is no longer valid
  145. }
  146. final focused = findFocusedObject(context.findRenderObject());
  147. if (focused == null) {
  148. return; // no focused object found
  149. }
  150. scrollToObject(focused, _scrollController, widget.duration, widget.curve,
  151. widget.overscroll);
  152. });
  153. }
  154. }
  155. /// Utility helper methods
  156. /// Finds the first focused focused child of [root] using a breadth-first search.
  157. RenderObject findFocusedObject(RenderObject root) {
  158. final q = Queue<RenderObject>();
  159. q.add(root);
  160. while (q.isNotEmpty) {
  161. final RenderObject node = q.removeFirst();
  162. final config = SemanticsConfiguration();
  163. node.describeSemanticsConfiguration(config);
  164. if (config.isFocused) {
  165. return node;
  166. }
  167. node.visitChildrenForSemantics((child) {
  168. q.add(child);
  169. });
  170. }
  171. return null;
  172. }
  173. /// Scroll to the given [object], which must be inside [scrollController]s viewport.
  174. scrollToObject(RenderObject object, ScrollController scrollController,
  175. Duration duration, Curve curve, double overscroll) {
  176. // Calculate the offset needed to show the object in the [ScrollView]
  177. // so that its bottom touches the top of the keyboard.
  178. final viewport = RenderAbstractViewport.of(object);
  179. final offset = viewport.getOffsetToReveal(object, 1.0).offset + overscroll;
  180. // If the object is covered by the keyboard, scroll to reveal it,
  181. // and add [focusPadding] between it and top of the keyboard.
  182. if (offset > scrollController.position.pixels) {
  183. scrollController.position.moveTo(
  184. offset,
  185. duration: duration,
  186. curve: curve,
  187. );
  188. }
  189. }