| // Copyright 2020 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| const Duration _animationCheckingInterval = Duration(milliseconds: 50); |
| |
| const Duration _scrollAnimationLength = Duration(milliseconds: 300); |
| |
| Offset _absoluteTopLeft(RenderObject renderObject) => |
| (renderObject as RenderBox).localToGlobal(Offset.zero); |
| |
| Rect _absoluteRect(RenderObject renderObject) => |
| _absoluteTopLeft(renderObject) & renderObject.paintBounds.size; |
| |
| Size _windowSize(BuildContext context) => MediaQuery.of(context).size; |
| |
| Rect _windowRect(BuildContext context) => Offset.zero & _windowSize(context); |
| |
| bool _isSuperset({required Rect large, required Rect small}) => |
| large.top <= small.top && |
| large.left <= small.left && |
| large.bottom >= small.bottom && |
| large.right >= small.right; |
| |
| const _minFreeRoomRequirement = 5.0; |
| |
| /// Whether [small] is a subset of [large] and has sufficient room |
| /// inside [large], at the end of [large] specified by [axisDirection]. |
| bool _hasSufficientFreeRoom({ |
| required Rect large, |
| required Rect small, |
| required AxisDirection axisDirection, |
| }) { |
| if (!_isSuperset(large: large, small: small)) { |
| return false; |
| } else { |
| bool result; |
| |
| switch (axisDirection) { |
| case AxisDirection.down: |
| result = large.bottom - small.bottom >= _minFreeRoomRequirement; |
| break; |
| case AxisDirection.right: |
| result = large.right - small.right >= _minFreeRoomRequirement; |
| break; |
| case AxisDirection.left: |
| result = small.left - large.left >= _minFreeRoomRequirement; |
| break; |
| case AxisDirection.up: |
| result = small.top - large.top >= _minFreeRoomRequirement; |
| break; |
| } |
| |
| return result; |
| } |
| } |
| |
| Future<void> animationStops() async { |
| if (!WidgetsBinding.instance.hasScheduledFrame) return; |
| |
| final Completer stopped = Completer<void>(); |
| |
| Timer.periodic(_animationCheckingInterval, (timer) { |
| if (!WidgetsBinding.instance.hasScheduledFrame) { |
| stopped.complete(); |
| timer.cancel(); |
| } |
| }); |
| |
| await stopped.future; |
| } |
| |
| Future<void> scrollUntilVisible({ |
| required Element element, |
| bool strict = false, |
| bool animated = true, |
| }) async { |
| final elementRenderObject = element.renderObject!; |
| final elementRect = _absoluteRect(elementRenderObject); |
| |
| final scrollable = Scrollable.of(element); |
| final viewport = RenderAbstractViewport.of(elementRenderObject)!; |
| |
| final visibleWindow = _absoluteRect(viewport).intersect(_windowRect(element)); |
| |
| // If there is free room between this demo button and the end of |
| // the scrollable, the next demo button is visible and can be tapped. |
| if (!strict && |
| _hasSufficientFreeRoom( |
| large: visibleWindow, |
| small: elementRect, |
| axisDirection: scrollable!.axisDirection, |
| )) { |
| return; |
| } |
| |
| late double pixelsToBeMoved; |
| switch (scrollable!.axisDirection) { |
| case AxisDirection.down: |
| pixelsToBeMoved = elementRect.top - visibleWindow.top; |
| break; |
| case AxisDirection.right: |
| pixelsToBeMoved = elementRect.left - visibleWindow.left; |
| break; |
| case AxisDirection.left: |
| pixelsToBeMoved = visibleWindow.right - elementRect.right; |
| break; |
| case AxisDirection.up: |
| pixelsToBeMoved = visibleWindow.bottom - elementRect.bottom; |
| break; |
| } |
| |
| final targetPixels = scrollable.position.pixels + pixelsToBeMoved; |
| final restrictedTargetPixels = targetPixels |
| .clamp( |
| scrollable.position.minScrollExtent, |
| scrollable.position.maxScrollExtent, |
| ) |
| .toDouble(); |
| |
| await scrollToPosition( |
| scrollable: scrollable, |
| pixels: restrictedTargetPixels, |
| animated: animated, |
| ); |
| } |
| |
| Future<void> scrollToExtreme({ |
| required ScrollableState? scrollable, |
| bool toEnd = false, |
| bool animated = true, |
| }) async { |
| final targetPixels = toEnd |
| ? scrollable!.position.maxScrollExtent |
| : scrollable!.position.minScrollExtent; |
| |
| await scrollToPosition( |
| scrollable: scrollable, |
| pixels: targetPixels, |
| animated: animated, |
| ); |
| } |
| |
| Future<void> scrollToPosition({ |
| required ScrollableState? scrollable, |
| required double pixels, |
| bool animated = true, |
| }) async { |
| if (animated) { |
| await scrollable!.position.animateTo( |
| pixels, |
| duration: _scrollAnimationLength, |
| curve: Curves.easeInOut, |
| ); |
| } else { |
| scrollable!.position.jumpTo(pixels); |
| } |
| |
| await animationStops(); |
| } |