Flutter开发,scroll_to_index适配flutter_list_view

问题描述:

在使用Flutter做项目的过程中,使用了flutter_list_view作为消息展示,选择flutter_list_view的原因是,比起原生ListView,FlutterListView可以使消息反转,并且可以设置firstItemAlign: FirstItemAlign.end,属性,实现聊天最新消息在视图最下方,并且实现顶端对齐。但是在开发消息引用过程中,准备使用scroll_to_index实现消息跳转,但是它只支持原生ListView,却不支持FlutterListView,这里不使用ListView的原因是消息反转后无法实现顶端对齐。

为此,查阅了scroll_to_index源码,对其进行改造,使之适配FlutterListView。

Dart 复制代码
import 'dart:async';
import 'dart:math' as math;

import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_list_view/flutter_list_view.dart';
import 'package:scroll_to_index/util.dart';

// import 'util.dart';

const defaultScrollDistanceOffset = 100.0;
const defaultDurationUnit = 40;

const _millisecond = Duration(milliseconds: 1);
const _highlightDuration = const Duration(seconds: 3);
const scrollAnimationDuration = Duration(milliseconds: 250);

typedef Rect ViewportBoundaryGetter();
typedef double AxisValueGetter(Rect rect);

Rect defaultViewportBoundaryGetter() => Rect.zero;

abstract class AutoScrollController implements ScrollController {
  factory AutoScrollController(
      {double initialScrollOffset: 0.0,
        bool keepScrollOffset: true,
        double? suggestedRowHeight,
        ViewportBoundaryGetter viewportBoundaryGetter:
        defaultViewportBoundaryGetter,
        Axis? axis,
        String? debugLabel,
        AutoScrollController? copyTagsFrom}) {
    return SimpleAutoScrollController(
        initialScrollOffset: initialScrollOffset,
        keepScrollOffset: keepScrollOffset,
        suggestedRowHeight: suggestedRowHeight,
        viewportBoundaryGetter: viewportBoundaryGetter,
        beginGetter: axis == Axis.horizontal ? (r) => r.left : (r) => r.top,
        endGetter: axis == Axis.horizontal ? (r) => r.right : (r) => r.bottom,
        copyTagsFrom: copyTagsFrom,
        debugLabel: debugLabel);
  }

  /// used to quick scroll to a index if the row height is the same
  double? get suggestedRowHeight;

  /// used to make the additional boundary for viewport
  /// e.g. a sticky header which covers the real viewport of a list view
  ViewportBoundaryGetter get viewportBoundaryGetter;

  /// used to choose which direction you are using.
  /// e.g. axis == Axis.horizontal ? (r) => r.left : (r) => r.top
  AxisValueGetter get beginGetter;
  AxisValueGetter get endGetter;

  /// detect if it's in scrolling (scrolling is a async process)
  bool get isAutoScrolling;

  /// all layout out states will be put into this map
  Map<int, AutoScrollTagState> get tagMap;

  /// used to chaining parent scroll controller
  set parentController(ScrollController parentController);

  /// check if there is a parent controller
  bool get hasParentController;

  /// scroll to the giving index
  Future scrollToIndex(int index,
      {Duration duration: scrollAnimationDuration,
        AutoScrollPosition? preferPosition});

  /// highlight the item
  Future highlight(int index,
      {bool cancelExistHighlights: true,
        Duration highlightDuration: _highlightDuration,
        bool animated: true});

  /// cancel all highlight item immediately.
  void cancelAllHighlights();

  /// check if the state is created. that is, is the indexed widget is layout out.
  /// NOTE: state created doesn't mean it's in viewport. it could be a buffer range, depending on flutter's implementation.
  bool isIndexStateInLayoutRange(int index);
}

// 新增:为FlutterListView创建的控制器接口
abstract class FlutterListAutoScrollController implements FlutterListViewController {
  factory FlutterListAutoScrollController() {
    return SimpleFlutterListAutoScrollController();
  }

  /// all layout out states will be put into this map
  Map<int, AutoScrollTagState> get tagMap;

  /// detect if it's in scrolling (scrolling is a async process)
  bool get isAutoScrolling;

  /// scroll to the giving index
  Future scrollToIndex(int index,
      {Duration duration: scrollAnimationDuration,
        AutoScrollPosition? preferPosition});

  /// highlight the item
  Future highlight(int index,
      {bool cancelExistHighlights: true,
        Duration highlightDuration: _highlightDuration,
        bool animated: true});

  /// cancel all highlight item immediately.
  void cancelAllHighlights();

  /// check if the state is created. that is, is the indexed widget is layout out.
  /// NOTE: state created doesn't mean it's in viewport. it could be a buffer range, depending on flutter's implementation.
  bool isIndexStateInLayoutRange(int index);
}

class SimpleAutoScrollController extends ScrollController
    with AutoScrollControllerMixin {
  @override
  final double? suggestedRowHeight;
  @override
  final ViewportBoundaryGetter viewportBoundaryGetter;
  @override
  final AxisValueGetter beginGetter;
  @override
  final AxisValueGetter endGetter;

  SimpleAutoScrollController(
      {double initialScrollOffset: 0.0,
        bool keepScrollOffset: true,
        this.suggestedRowHeight,
        this.viewportBoundaryGetter: defaultViewportBoundaryGetter,
        required this.beginGetter,
        required this.endGetter,
        AutoScrollController? copyTagsFrom,
        String? debugLabel})
      : super(
      initialScrollOffset: initialScrollOffset,
      keepScrollOffset: keepScrollOffset,
      debugLabel: debugLabel) {
    if (copyTagsFrom != null) tagMap.addAll(copyTagsFrom.tagMap);
  }
}

// 新增:FlutterListView的控制器实现
class SimpleFlutterListAutoScrollController extends FlutterListViewController
    with FlutterListAutoScrollControllerMixin {
}

class PageAutoScrollController extends PageController
    with AutoScrollControllerMixin {
  @override
  final double? suggestedRowHeight;
  @override
  final ViewportBoundaryGetter viewportBoundaryGetter;
  @override
  final AxisValueGetter beginGetter = (r) => r.left;
  @override
  final AxisValueGetter endGetter = (r) => r.right;

  PageAutoScrollController(
      {int initialPage: 0,
        bool keepPage: true,
        double viewportFraction: 1.0,
        this.suggestedRowHeight,
        this.viewportBoundaryGetter: defaultViewportBoundaryGetter,
        AutoScrollController? copyTagsFrom,
        String? debugLabel})
      : super(
      initialPage: initialPage,
      keepPage: keepPage,
      viewportFraction: viewportFraction) {
    if (copyTagsFrom != null) tagMap.addAll(copyTagsFrom.tagMap);
  }
}

enum AutoScrollPosition { begin, middle, end }

// 新增一个类型定义,用于兼容两种控制器类型
typedef AutoScrollControllerBase = dynamic;

mixin AutoScrollControllerMixin on ScrollController
implements AutoScrollController {
  @override
  final Map<int, AutoScrollTagState> tagMap = <int, AutoScrollTagState>{};
  double? get suggestedRowHeight;
  ViewportBoundaryGetter get viewportBoundaryGetter;
  AxisValueGetter get beginGetter;
  AxisValueGetter get endGetter;

  bool __isAutoScrolling = false;
  set _isAutoScrolling(bool isAutoScrolling) {
    __isAutoScrolling = isAutoScrolling;
    if (!isAutoScrolling &&
        hasClients) //after auto scrolling, we should sync final scroll position without flag on
      notifyListeners();
  }

  @override
  bool get isAutoScrolling => __isAutoScrolling;

  ScrollController? _parentController;
  @override
  set parentController(ScrollController parentController) {
    if (_parentController == parentController) return;

    final isNotEmpty = positions.isNotEmpty;
    if (isNotEmpty && _parentController != null) {
      for (final p in _parentController!.positions)
        if (positions.contains(p)) _parentController!.detach(p);
    }

    _parentController = parentController;

    if (isNotEmpty && _parentController != null)
      for (final p in positions) _parentController!.attach(p);
  }

  @override
  bool get hasParentController => _parentController != null;

  @override
  void attach(ScrollPosition position) {
    super.attach(position);

    _parentController?.attach(position);
  }

  @override
  void detach(ScrollPosition position) {
    _parentController?.detach(position);

    super.detach(position);
  }

  static const maxBound = 30; // 0.5 second if 60fps
  @override
  Future scrollToIndex(int index,
      {Duration duration: scrollAnimationDuration,
        AutoScrollPosition? preferPosition: AutoScrollPosition.middle}) async {
    return co(
        this,
            () => _scrollToIndex(index,
            duration: duration, preferPosition: preferPosition));
  }

  Future _scrollToIndex(int index,
      {Duration duration: scrollAnimationDuration,
        AutoScrollPosition? preferPosition}) async {
    assert(duration > Duration.zero);

    // In listView init or reload case, widget state of list item may not be ready for query.
    // this prevent from over scrolling becoming empty screen or unnecessary scroll bounce.
    Future makeSureStateIsReady() async {
      for (var count = 0; count < maxBound; count++) {
        if (_isEmptyStates) {
          await _waitForWidgetStateBuild();
        } else
          return null;
      }

      return null;
    }

    await makeSureStateIsReady();

    if (!hasClients) return null;

    // two cases,
    // 1. already has state. it's in viewport layout
    // 2. doesn't have state yet. it's not in viewport so we need to start scrolling to make it into layout range.
    if (isIndexStateInLayoutRange(index)) {
      _isAutoScrolling = true;

      await _bringIntoViewportIfNeed(index, preferPosition,
              (double offset) async {
            await animateTo(offset, duration: duration, curve: Curves.ease);
            await _waitForWidgetStateBuild();
            return null;
          });

      _isAutoScrolling = false;
    } else {
      // the idea is scrolling based on either
      // 1. suggestedRowHeight or
      // 2. testDistanceOffset
      double prevOffset = offset - 1;
      double currentOffset = offset;
      bool contains = false;
      Duration spentDuration = const Duration();
      double lastScrollDirection = 0.5; // alignment, default center;
      final moveDuration = duration ~/ defaultDurationUnit;

      _isAutoScrolling = true;

      /// ideally, the suggest row height will move to the final corrent offset approximately in just one scroll(iteration).
      /// if the given suggest row height is the minimal/maximal height in variable row height enviroment,
      /// we can just use viewport calculation to reach the final offset in other iteration.
      bool usedSuggestedRowHeightIfAny = true;
      while (prevOffset != currentOffset &&
          !(contains = isIndexStateInLayoutRange(index))) {
        prevOffset = currentOffset;
        final nearest = _getNearestIndex(index);

        if (tagMap[nearest ?? 0] == null)
          return null;

        final moveTarget =
        _forecastMoveUnit(index, nearest, usedSuggestedRowHeightIfAny)!;

        // assume suggestRowHeight will move to correct offset in just one time.
        // if the rule doesn't work (in variable row height case), we will use backup solution (non-suggested way)
        final suggestedDuration =
        usedSuggestedRowHeightIfAny && suggestedRowHeight != null
            ? duration
            : null;
        usedSuggestedRowHeightIfAny = false; // just use once
        lastScrollDirection = moveTarget - prevOffset > 0 ? 1 : 0;
        currentOffset = moveTarget;
        spentDuration += suggestedDuration ?? moveDuration;
        final oldOffset = offset;
        await animateTo(currentOffset,
            duration: suggestedDuration ?? moveDuration, curve: Curves.ease);
        await _waitForWidgetStateBuild();
        if (!hasClients || offset == oldOffset) {
          // already scroll to begin or end
          contains = isIndexStateInLayoutRange(index);
          break;
        }
      }
      _isAutoScrolling = false;

      if (contains && hasClients) {
        await _bringIntoViewportIfNeed(
            index, preferPosition ?? _alignmentToPosition(lastScrollDirection),
                (finalOffset) async {
              if (finalOffset != offset) {
                _isAutoScrolling = true;
                final remaining = duration - spentDuration;
                await animateTo(finalOffset,
                    duration: remaining <= Duration.zero ? _millisecond : remaining,
                    curve: Curves.ease);
                await _waitForWidgetStateBuild();

                // not sure why it doesn't scroll to the given offset, try more within 3 times
                if (hasClients && offset != finalOffset) {
                  final count = 3;
                  for (var i = 0;
                  i < count && hasClients && offset != finalOffset;
                  i++) {
                    await animateTo(finalOffset,
                        duration: _millisecond, curve: Curves.ease);
                    await _waitForWidgetStateBuild();
                  }
                }
                _isAutoScrolling = false;
              }
            });
      }
    }

    return null;
  }

  @override
  Future highlight(int index,
      {bool cancelExistHighlights: true,
        Duration highlightDuration: _highlightDuration,
        bool animated: true}) async {
    final tag = tagMap[index];
    return tag == null
        ? null
        : await tag.highlight(
        cancelExisting: cancelExistHighlights,
        highlightDuration: highlightDuration,
        animated: animated);
  }

  @override
  void cancelAllHighlights() {
    _cancelAllHighlights();
  }

  @override
  bool isIndexStateInLayoutRange(int index) => tagMap[index] != null;

  /// this means there is no widget state existing, usually happened before build.
  /// we should wait for next frame.
  bool get _isEmptyStates => tagMap.isEmpty;

  /// wait until the [SchedulerPhase] in [SchedulerPhase.persistentCallbacks].
  /// it means if we do animation scrolling to a position, the Future call back will in [SchedulerPhase.midFrameMicrotasks].
  /// if we want to search viewport element depending on Widget State, we must delay it to [SchedulerPhase.persistentCallbacks].
  /// which is the phase widget build/layout/draw
  Future _waitForWidgetStateBuild() => SchedulerBinding.instance.endOfFrame;

  /// NOTE: this is used to forcase the nearestIndex. if the the index equals targetIndex,
  /// we will use the function, calling _directionalOffsetToRevealInViewport to get move unit.
  double? _forecastMoveUnit(
      int targetIndex, int? currentNearestIndex, bool useSuggested) {
    assert(targetIndex != currentNearestIndex);
    currentNearestIndex = currentNearestIndex ?? 0; //null as none of state

    final alignment = targetIndex > currentNearestIndex ? 1.0 : 0.0;
    double? absoluteOffsetToViewport;

    if (useSuggested && suggestedRowHeight != null) {
      final indexDiff = (targetIndex - currentNearestIndex);
      final offsetToLastState = _offsetToRevealInViewport(
          currentNearestIndex, indexDiff <= 0 ? 0 : 1)!;
      absoluteOffsetToViewport = math.max(
          offsetToLastState.offset + indexDiff * suggestedRowHeight!, 0);
    } else {
      final offsetToLastState =
      _offsetToRevealInViewport(currentNearestIndex, alignment);

      absoluteOffsetToViewport = offsetToLastState?.offset;
      if (absoluteOffsetToViewport == null)
        absoluteOffsetToViewport = defaultScrollDistanceOffset;
    }

    return absoluteOffsetToViewport;
  }

  int? _getNearestIndex(int index) {
    final list = tagMap.keys;
    if (list.isEmpty) return null;

    final sorted = list.toList()
      ..sort((int first, int second) => first.compareTo(second));
    final min = sorted.first;
    final max = sorted.last;
    return (index - min).abs() < (index - max).abs() ? min : max;
  }

  /// bring the state node (already created but all of it may not be fully in the viewport) into viewport
  Future _bringIntoViewportIfNeed(int index, AutoScrollPosition? preferPosition,
      Future move(double offset)) async {

    if (preferPosition != null) {
      double targetOffset = _directionalOffsetToRevealInViewport(
          index, _positionToAlignment(preferPosition));

      // The content preferred position might be impossible to reach
      // for items close to the edges of the scroll content, e.g.
      // we cannot put the first item at the end of the viewport or
      // the last item at the beginning. Trying to do so might lead
      // to a bounce at either the top or bottom, unless the scroll
      // physics are set to clamp. To prevent this, we limit the
      // offset to not overshoot the extent in either direction.
      targetOffset = targetOffset.clamp(
          position.minScrollExtent, position.maxScrollExtent);

      await move(targetOffset);
    } else {
      final begin = _directionalOffsetToRevealInViewport(index, 0);
      final end = _directionalOffsetToRevealInViewport(index, 1);

      final alreadyInViewport = offset < begin && offset > end;
      if (!alreadyInViewport) {
        double value;
        if ((end - offset).abs() < (begin - offset).abs())
          value = end;
        else
          value = begin;

        await move(value > 0 ? value : 0);
      }
    }
  }

  double _positionToAlignment(AutoScrollPosition position) {
    return position == AutoScrollPosition.begin
        ? 0
        : position == AutoScrollPosition.end
        ? 1
        : 0.5;
  }

  AutoScrollPosition _alignmentToPosition(double alignment) => alignment == 0
      ? AutoScrollPosition.begin
      : alignment == 1
      ? AutoScrollPosition.end
      : AutoScrollPosition.middle;

  /// return offset, which is a absolute offset to bring the target index object into the location(depends on [direction]) of viewport
  /// see also: _offsetYToRevealInViewport()
  double _directionalOffsetToRevealInViewport(int index, double alignment) {
    assert(alignment == 0 || alignment == 0.5 || alignment == 1);
    // 1.0 bottom, 0.5 center, 0.0 begin if list is vertically from begin to end
    final tagOffsetInViewport = _offsetToRevealInViewport(index, alignment);

    if (tagOffsetInViewport == null) {
      return -1;
    } else {
      double absoluteOffsetToViewport = tagOffsetInViewport.offset;
      if (alignment == 0.5) {
        return absoluteOffsetToViewport;
      } else if (alignment == 0) {
        return absoluteOffsetToViewport - beginGetter(viewportBoundaryGetter());
      } else {
        return absoluteOffsetToViewport + endGetter(viewportBoundaryGetter());
      }
    }
  }

  /// return offset, which is a absolute offset to bring the target index object into the center of the viewport
  /// see also: _directionalOffsetToRevealInViewport()
  RevealedOffset? _offsetToRevealInViewport(int index, double alignment) {
    final ctx = tagMap[index]?.context;
    if (ctx == null) return null;

    final renderBox = ctx.findRenderObject()!;
    assert(Scrollable.of(ctx) != null);
    final RenderAbstractViewport viewport =
    RenderAbstractViewport.of(renderBox)!;
    final revealedOffset = viewport.getOffsetToReveal(renderBox, alignment);

    return revealedOffset;
  }
}

// 新增:为FlutterListView实现的mixin
mixin FlutterListAutoScrollControllerMixin on FlutterListViewController
implements FlutterListAutoScrollController {
  @override
  final Map<int, AutoScrollTagState> tagMap = <int, AutoScrollTagState>{};

  bool __isAutoScrolling = false;
  set _isAutoScrolling(bool isAutoScrolling) {
    __isAutoScrolling = isAutoScrolling;
  }

  @override
  bool get isAutoScrolling => __isAutoScrolling;

  static const maxBound = 30; // 0.5 second if 60fps
  @override
  Future scrollToIndex(int index,
      {Duration duration: scrollAnimationDuration,
        AutoScrollPosition? preferPosition}) async {
    return co(
        this,
            () => _scrollToIndex(index,
            duration: duration, preferPosition: preferPosition));
  }

  Future _scrollToIndex(int index,
      {Duration duration: scrollAnimationDuration,
        AutoScrollPosition? preferPosition}) async {
    assert(duration > Duration.zero);

    // In listView init or reload case, widget state of list item may not be ready for query.
    // this prevent from over scrolling becoming empty screen or unnecessary scroll bounce.
    Future makeSureStateIsReady() async {
      for (var count = 0; count < maxBound; count++) {
        if (_isEmptyStates) {
          await _waitForWidgetStateBuild();
        } else
          return null;
      }

      return null;
    }

    await makeSureStateIsReady();

    // For FlutterListView, we directly use the controller's animateToIndex method
    _isAutoScrolling = true;
    // try {
    //   // 使用FlutterListView的animateToIndex方法
    //   await sliverController.animateToIndex(
    //     index,
    //     duration: duration,
    //     curve: Curves.ease,
    //     // offset: 0.0, // 添加默认偏移量
    //     offset: 0.0, // 添加默认偏移量
    //     // basedOnBottom: false, // 添加默认参数
    //   );
    // } finally {
    //   _isAutoScrolling = false;
    // }
    try {
      // 使用FlutterListView的animateToIndex方法
      // 根据preferPosition参数设置offsetBasedOnBottom参数
      bool offsetBasedOnBottom = false;
      double offset = 0.0;

      if (preferPosition == AutoScrollPosition.end) {
        offsetBasedOnBottom = true;
        offset = 0.0;
      } else if (preferPosition == AutoScrollPosition.middle) {
        // 对于middle位置,我们可以设置一个适中的偏移量
        // 这里需要获取视口高度来计算中间位置,暂时设置为0
        offset = 350.0;
      } else {
        // 默认或begin位置
        offsetBasedOnBottom = false;
        offset = 0.0;
      }

      await sliverController.animateToIndex(
        index,
        duration: duration,
        curve: Curves.ease,
        offset: offset,
      );
    } finally {
      _isAutoScrolling = false;
    }

    return null;
  }

  @override
  Future highlight(int index,
      {bool cancelExistHighlights: true,
        Duration highlightDuration: _highlightDuration,
        bool animated: true}) async {
    final tag = tagMap[index];
    return tag == null
        ? null
        : await tag.highlight(
        cancelExisting: cancelExistHighlights,
        highlightDuration: highlightDuration,
        animated: animated);
  }

  @override
  void cancelAllHighlights() {
    _cancelAllHighlights();
  }

  @override
  bool isIndexStateInLayoutRange(int index) => tagMap[index] != null;

  /// this means there is no widget state existing, usually happened before build.
  /// we should wait for next frame.
  bool get _isEmptyStates => tagMap.isEmpty;

  /// wait until the [SchedulerPhase] in [SchedulerPhase.persistentCallbacks].
  /// it means if we do animation scrolling to a position, the Future call back will in [SchedulerPhase.midFrameMicrotasks].
  /// if we want to search viewport element depending on Widget State, we must delay it to [SchedulerPhase.persistentCallbacks].
  /// which is the phase widget build/layout/draw
  Future _waitForWidgetStateBuild() => SchedulerBinding.instance.endOfFrame;
}

void _cancelAllHighlights([AutoScrollTagState? state]) {
  for (final tag in _highlights.keys)
    tag._cancelController(reset: tag != state);

  _highlights.clear();
}

typedef Widget TagHighlightBuilder(BuildContext context, Animation<double> highlight);
class AutoScrollTag extends StatefulWidget {
  final dynamic controller; // 修改为dynamic类型以接受两种控制器类型
  final int index;
  final Widget? child;
  final TagHighlightBuilder? builder;
  final Color? color;
  final Color? highlightColor;
  final bool disabled;

  AutoScrollTag(
      {required Key key,
        required this.controller,
        required this.index,
        this.child,
        this.builder,
        this.color,
        this.highlightColor,
        this.disabled: false})
      : assert(child != null || builder != null), super(key: key);

  @override
  AutoScrollTagState createState() {
    return new AutoScrollTagState<AutoScrollTag>();
  }
}

Map<AutoScrollTagState, AnimationController?> _highlights =
<AutoScrollTagState, AnimationController?>{};

class AutoScrollTagState<W extends AutoScrollTag> extends State<W>
    with TickerProviderStateMixin {
  AnimationController? _controller;

  @override
  void initState() {
    super.initState();
    if (!widget.disabled) {
      register(widget.index);
    }
  }

  @override
  void dispose() {
    _cancelController();
    if (!widget.disabled) {
      unregister(widget.index);
    }
    _controller = null;
    _highlights.remove(this);
    super.dispose();
  }

  @override
  void didUpdateWidget(W oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.index != widget.index ||
        oldWidget.key != widget.key ||
        oldWidget.disabled != widget.disabled) {
      if (!oldWidget.disabled) unregister(oldWidget.index);

      if (!widget.disabled) register(widget.index);
    }
  }

  void register(int index) {
    // the caller in initState() or dispose() is not in the order of first dispose and init
    // so we can't assert there isn't a existing key
    // assert(!widget.controller.tagMap.keys.contains(index));
    widget.controller.tagMap[index] = this;
  }

  void unregister(int index) {
    _cancelController();
    _highlights.remove(this);
    // the caller in initState() or dispose() is not in the order of first dispose and init
    // so we can't assert there isn't a existing key
    // assert(widget.controller.tagMap.keys.contains(index));
    if (widget.controller.tagMap[index] == this)
      widget.controller.tagMap.remove(index);
  }

  @override
  Widget build(BuildContext context) {
    final animation = _controller ?? kAlwaysDismissedAnimation;
    return widget.builder?.call(context, animation)
        ?? buildHighlightTransition(context: context, highlight: animation, child: widget.child!,
            background: widget.color, highlightColor: widget.highlightColor);
  }

  //used to make sure we will drop the old highlight
  //it's rare that we call it more than once in same millisecond, so we just make the time stamp as the unique key
  DateTime? _startKey;

  /// this function can be called multiple times. every call will reset the highlight style.
  Future highlight(
      {bool cancelExisting: true,
        Duration highlightDuration: _highlightDuration,
        bool animated: true}) async {
    if (!mounted) return null;

    if (cancelExisting) {
      _cancelAllHighlights(this);
    }

    if (_highlights.containsKey(this)) {
      assert(_controller != null);
      _controller!.stop();
    }

    if (_controller == null) {
      _controller = new AnimationController(vsync: this);
      _highlights[this] = _controller;
    }

    final startKey0 = _startKey = DateTime.now();
    const animationShow = 1.0;
    setState(() {});
    if (animated)
      await catchAnimationCancel(_controller!
          .animateTo(animationShow, duration: scrollAnimationDuration));
    else
      _controller!.value = animationShow;
    await Future.delayed(highlightDuration);

    if (startKey0 == _startKey) {
      if (mounted) {
        setState(() {});
        const animationHide = 0.0;
        if (animated)
          await catchAnimationCancel(_controller!
              .animateTo(animationHide, duration: scrollAnimationDuration));
        else
          _controller!.value = animationHide;
      }

      if (startKey0 == _startKey) {
        _controller = null;
        _highlights.remove(this);
      }
    }
    return null;
  }

  void _cancelController({bool reset: true}) {
    if (_controller != null) {
      if (_controller!.isAnimating) _controller!.stop();

      if (reset && _controller!.value != 0.0) _controller!.value = 0.0;
    }
  }
}

// Widget buildHighlightTransition({required BuildContext context, required Animation<double> highlight,
//   required Widget child, Color? background, Color? highlightColor}) {
//   return DecoratedBoxTransition(
//       decoration: DecorationTween(
//           begin: background != null ?
//           BoxDecoration(color: background) :
//           BoxDecoration(),
//           end: background != null ?
//           BoxDecoration(color: background) :
//           BoxDecoration(color: highlightColor)
//       ).animate(highlight),
//       child: child
//   );
// }

Widget buildHighlightTransition({required BuildContext context, required Animation<double> highlight,
  required Widget child, Color? background, Color? highlightColor}) {
  return Stack(
    children: [
      child,
      Positioned.fill(
        child: IgnorePointer(
          ignoring: true,
          child: FadeTransition(
            opacity: highlight,
            child: DecoratedBox(
              decoration: BoxDecoration(
                color: highlightColor,
                // borderRadius: BorderRadius.circular(10.0)
                borderRadius: const BorderRadius.all(Radius.circular(10)),
              ),
            ),
          ),
        ),
      )
    ],
  );

}

代码中还重新写了buildHighlightTransition,使源码中的高亮显示在控件的最底层改为最上层,读者可以自行更改

在使用中,Controller可以做如下定义:

Dart 复制代码
  /// 消息列表滚动控制器
  SimpleFlutterListAutoScrollController? _listViewController;

  /// 获取滚动控制器
  SimpleFlutterListAutoScrollController get listViewController {
    _listViewController ??= SimpleFlutterListAutoScrollController();
    return _listViewController!;
  }

对于其他按照scroll_to_index正常使用就好

相关推荐
HIT_Weston40 分钟前
49、【Ubuntu】【Gitlab】拉出内网 Web 服务:http.server 单/多线程分析(一)
前端·ubuntu·gitlab
松☆41 分钟前
Flutter + OpenHarmony 构建工业巡检 App:离线采集、多端协同与安全上报
安全·flutter
涔溪42 分钟前
Vue3 中ref和reactive的核心区别是什么?
前端·vue.js·typescript
●VON43 分钟前
Flutter for OpenHarmony前置知识《Flutter 路由与导航完整教程》
学习·flutter·华为·openharmony·开源鸿蒙
Ya-Jun1 小时前
架构设计模式:模块化设计方案
flutter
吉星9527ABC1 小时前
表示离散量的echarts图型示例
前端·arcgis·echarts·离散量web展示
光影少年1 小时前
web3学习路线
前端·学习·前端框架·web3
克喵的水银蛇1 小时前
Flutter 状态管理:Provider 入门到实战(替代 setState)
前端·javascript·flutter