likes
comments
collection
share

【Flutter】熊孩子拆组件系列之拆ListView(三)—— GlowingOverscrollIndicator

作者站长头像
站长
· 阅读数 6

前言

上一篇中对Widget树种最顶层的两个进行了分析;

下面看一下第三个,GlowingOverscrollIndicator 是个什么东西,过度滚动效果是怎么搞出来的;

目录

首先还是概念解析

首先还是注释部分:

 A visual indication that a scroll view has overscrolled.

 A [GlowingOverscrollIndicator] listens for [ScrollNotification]s in order to control the overscroll indication. These notifications are typically generated by a [ScrollView], such as a [ListView] or a [GridView].

 [GlowingOverscrollIndicator] generates [OverscrollIndicatorNotification] before showing an overscroll indication. To prevent the indicator from showing the indication, call [OverscrollIndicatorNotification.disallowGlow] on the notification.

 Created automatically by [ScrollBehavior.buildOverscrollIndicator] on platforms (e.g., Android) that commonly use this type of overscroll indication.

 In a [MaterialApp], the edge glow color is the overall theme's [ColorScheme.secondary] color.

 ## Customizing the Glow Position for Advanced Scroll Views

 When building a [CustomScrollView] with a [GlowingOverscrollIndicator], the indicator will apply to the entire scrollable area, regardless of what slivers the CustomScrollView contains.

 For example, if your CustomScrollView contains a SliverAppBar in the first position, the GlowingOverscrollIndicator will overlay the SliverAppBar. To manipulate the position of the GlowingOverscrollIndicator in this case, you can either make use of a [NotificationListener] and provide a [OverscrollIndicatorNotification.paintOffset] to the notification, or use a [NestedScrollView].

 {@tool dartpad --template=stateless_widget_scaffold}

 This example demonstrates how to use a [NotificationListener] to manipulate the placement of a [GlowingOverscrollIndicator] when building a [CustomScrollView]. Drag the scrollable to see the bounds of the overscroll indicator.

 Widget build(BuildContext context) {
   final double leadingPaintOffset = MediaQuery.of(context).padding.top + AppBar().preferredSize.height;
   return NotificationListener<OverscrollIndicatorNotification>(
     onNotification: (OverscrollIndicatorNotification notification) {
       if (notification.leading) {
         notification.paintOffset = leadingPaintOffset;
       }
       return false;
     },
     child: CustomScrollView(
       slivers: <Widget>[
         const SliverAppBar(title: Text('Custom PaintOffset')),
         SliverToBoxAdapter(
           child: Container(
             color: Colors.amberAccent,
             height: 100,
             child: const Center(child: Text('Glow all day!')),
           ),
         ),
         const SliverFillRemaining(child: FlutterLogo()),
       ],
     ),
   );
 }
 {@end-tool}

 {@tool dartpad --template=stateless_widget_scaffold}

 This example demonstrates how to use a [NestedScrollView] to manipulate the placement of a [GlowingOverscrollIndicator] when building a [CustomScrollView]. Drag the scrollable to see the bounds of the overscroll indicator.

 Widget build(BuildContext context) {
   return NestedScrollView(
     headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
       return const <Widget>[         SliverAppBar(title: Text('Custom NestedScrollViews')),       ];
     },
     body: CustomScrollView(
       slivers: <Widget>[
         SliverToBoxAdapter(
           child: Container(
             color: Colors.amberAccent,
             height: 100,
             child: const Center(child: Text('Glow all day!')),
           ),
         ),
         const SliverFillRemaining(child: FlutterLogo()),
       ],
     ),
   );
 }
 {@end-tool}

从注释上来看,GlowingOverscrollIndicator 就是一个通过监听 OverscrollIndicatorNotification 事件,然后根据此事件作出相应反应,并绘制出来的 Widget ;

好像没啥特殊的?

GlowingOverscrollIndicator 从何而来

回到ListView 的 build 部分;在其中,有这么一句:

_configuration.buildOverscrollIndicator(context, result, details),

首先呢,这个_configuration 其实可以不用管,这个是来自app部分的,相当于theme配置之类的东西,用来规定Scrollable是如何表现的,但在OverScroollIndicator这块并没有很实际参与到,从OverScroollIndicator的角度可以无视掉;

经过追踪,来到了buildViewportChrome方法,在这里做了一次筛选:

Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
  switch (getPlatform(context)) {
    case TargetPlatform.iOS:
    case TargetPlatform.linux:
    case TargetPlatform.macOS:
    case TargetPlatform.windows:
      return child;
    case TargetPlatform.android:
    case TargetPlatform.fuchsia:
    return GlowingOverscrollIndicator(
      child: child,
      axisDirection: axisDirection,
      color: _kDefaultGlowColor,
    );
  }
}

可以看到 GlowingOverscrollIndicator 就这么构造出来的,不过也因此得知,只有fuchsia、android,才会有这个 GlowingOverscrollIndicator ;

出于熊孩子的直觉,在这里做下封装修改,是否可以实现一个蕾丝自定义刷新头、刷新脚之类的东西呢?

话说,fuchsia 已经开始适配了啊,不知道何时能看到fuchsia开始推广呢

回正题,由来得知了,下面看一下具体内容

GlowingOverscrollIndicator 构造

还是惯例,来到build方法这里:

@override
Widget build(BuildContext context) {
  return NotificationListener<ScrollNotification>(
    onNotification: _handleScrollNotification,
    child: RepaintBoundary(
      child: CustomPaint(
        foregroundPainter: _GlowingOverscrollIndicatorPainter(
          leadingController: widget.showLeading ? _leadingController : null,
          trailingController: widget.showTrailing ? _trailingController : null,
          axisDirection: widget.axisDirection,
          repaint: _leadingAndTrailingListener,
        ),
        child: RepaintBoundary(
          child: widget.child,
        ),
      ),
    ),
  );
}

这build方法平平无奇啊,说白了,就是往ListView覆盖了一层画布,用来画 OverScroll 效果而已;

那看下具体怎么实习绘制的,按照注释说明,实现方式是通过监听 OverscrollIndicatorNotification 事件来实现的,那么自然就要看下 NotificationListener 的 onNotification 方法喽:

这个方法,看上去挺长的,但拆开来看,好像也没啥;

1、首先结合边界情况,计算了一下偏移量(github.com/flutter/flu…

_leadingController!._paintOffsetScrollPixels =
  -math.min(notification.metrics.pixels - notification.metrics.minScrollExtent, _leadingController!._paintOffset);
_trailingController!._paintOffsetScrollPixels =
  -math.min(notification.metrics.maxScrollExtent - notification.metrics.pixels, _trailingController!._paintOffset);

2、看一下往哪偏的,应该下一步会对绘制器的哪个controller来做操作:

if (notification.overscroll < 0.0) {
  controller = _leadingController;
} else if (notification.overscroll > 0.0) {
  controller = _trailingController;
} else {
  assert(false);
}

3、不让过度的绘制效果一直显示个没完,在这里做个截断:

if (_lastNotificationType != OverscrollNotification) {
  final OverscrollIndicatorNotification confirmationNotification = OverscrollIndicatorNotification(leading: isLeading);
  confirmationNotification.dispatch(context);
  _accepted[isLeading] = confirmationNotification._accepted;
  if (_accepted[isLeading]!) {
    controller!._paintOffset = confirmationNotification.paintOffset;
  }
}

4、如果没被截断,那么根据 overScroll 的情况做出相应绘制:

比如说,如果是那种有惯性的,那么调用 absorbImpact ,绘制那种有惯性的绘制效果;

if (_accepted[isLeading]!) {
  if (notification.velocity != 0.0) {
    assert(notification.dragDetails == null);
    controller!.absorbImpact(notification.velocity.abs());
  } else {
    assert(notification.overscroll != 0.0);
    if (notification.dragDetails != null) {
      assert(notification.dragDetails!.globalPosition != null);
      final RenderBox renderer = notification.context!.findRenderObject()! as RenderBox;
      assert(renderer != null);
      assert(renderer.hasSize);
      final Size size = renderer.size;
      final Offset position = renderer.globalToLocal(notification.dragDetails!.globalPosition);
      switch (notification.metrics.axis) {
        case Axis.horizontal:
          controller!.pull(notification.overscroll.abs(), size.width, position.dy.clamp(0.0, size.height), size.height);
          break;
        case Axis.vertical:
          controller!.pull(notification.overscroll.abs(), size.height, position.dx.clamp(0.0, size.width), size.width);
          break;
      }
    }
  }
}

5.如果不拉了或者别的结束情况,绘制结束,记录当前操作,用于对比

} else if (notification is ScrollEndNotification || notification is ScrollUpdateNotification) {
  if ((notification as dynamic).dragDetails != null) {
    _leadingController!.scrollEnd();
    _trailingController!.scrollEnd();
  }
}
_lastNotificationType = notification.runtimeType;

至此,onNotification 部分的方法大体分析完成;

上面提到的controller ,本质上是 ChangeNotify ,在build的时候传给了 CustomPaint;重绘通知这块,也通过一个Listener.merge 组合监听那俩controller来处理,也就是build方法出现的那个_leadingAndTrailingListener;

之后就是平平无奇的根据 ChangeNotify 的变化通知,来做相应处理:

@override
void paint(Canvas canvas, Size size) {
  _paintSide(canvas, size, leadingController, axisDirection, GrowthDirection.reverse);
  _paintSide(canvas, size, trailingController, axisDirection, GrowthDirection.forward);
}

@override
bool shouldRepaint(_GlowingOverscrollIndicatorPainter oldDelegate) {
  return oldDelegate.leadingController != leadingController
      || oldDelegate.trailingController != trailingController;
}

_paintSide 方法:

void _paintSide(Canvas canvas, Size size, _GlowController? controller, AxisDirection axisDirection, GrowthDirection growthDirection) {
  if (controller == null)
    return;
  switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
    case AxisDirection.up:
      controller.paint(canvas, size);
      break;
    case AxisDirection.down:
      canvas.save();
      canvas.translate(0.0, size.height);
      canvas.scale(1.0, -1.0);
      controller.paint(canvas, size);
      canvas.restore();
      break;
    case AxisDirection.left:
      canvas.save();
      canvas.rotate(piOver2);
      canvas.scale(1.0, -1.0);
      controller.paint(canvas, Size(size.height, size.width));
      canvas.restore();
      break;
    case AxisDirection.right:
      canvas.save();
      canvas.translate(size.width, 0.0);
      canvas.rotate(piOver2);
      controller.paint(canvas, Size(size.height, size.width));
      canvas.restore();
      break;
  }
}

controller的paint方法:

void paint(Canvas canvas, Size size) {
  if (_glowOpacity.value == 0.0)
    return;
  final double baseGlowScale = size.width > size.height ? size.height / size.width : 1.0;
  final double radius = size.width * 3.0 / 2.0;
  final double height = math.min(size.height, size.width * _widthToHeightFactor);
  final double scaleY = _glowSize.value * baseGlowScale;
  final Rect rect = Rect.fromLTWH(0.0, 0.0, size.width, height);
  final Offset center = Offset((size.width / 2.0) * (0.5 + _displacement), height - radius);
  final Paint paint = Paint()..color = color.withOpacity(_glowOpacity.value);
  canvas.save();
  canvas.translate(0.0, _paintOffset + _paintOffsetScrollPixels);
  canvas.scale(1.0, scaleY);
  canvas.clipRect(rect);
  canvas.drawCircle(center, radius, paint);
  canvas.restore();
}

文章太长不看的懒人总结篇

OverScrollableIndicator 本质上就是一个组合型Widget,当收到 OverscrollIndicatorNotification 事件的时候,计算处理overScroll效果,并通知CustomPainter来做出相应绘制操作,就这么简单;