【Flutter】熊孩子拆元件系列之拆ListView(三)—— GlowingOverscrollIndicator
theme: condensed-night-purple
小知識,大挑戰!本文正在參與「程式設計師必備小知識」創作活動
本文已參與 「掘力星計劃」 ,贏取創作大禮包,挑戰創作激勵金。
前言
上一篇中對Widget樹種最頂層的兩個進行了分析;
下面看一下第三個,GlowingOverscrollIndicator
是個什麼東西,過度滾動效果是怎麼搞出來的;
目錄
https://juejin.cn/post/7016538861580845063
首先還是概念解析
首先還是註釋部分:
``` 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
{@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
從註釋上來看,
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、首先結合邊界情況,計算了一下偏移量(https://github.com/flutter/flutter/issues/64149) :
_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來做出相應繪製操作,就這麼簡單;
- 【Flutter】小說閱讀器改版 —— 翻頁動畫(三)
- 【Flutter】小說閱讀器改版 (六)—— 在動畫播放中攔截手勢
- 【Flutter】小說閱讀器改版 (五)—— 整合ScrollActivity
- 【Flutter】小說閱讀器改版 (四)—— 讓ScrollActivity追蹤手勢最新位置
- 【Flutter】小說閱讀器改版 (三)—— 實現支援 Drag 的ScrollActivity
- 【Flutter】小說閱讀器改版 (二)—— 改進一下模擬翻頁的效果
- 【Flutter】小說閱讀器改版 (一)—— 模擬翻頁的思路優化
- 【Flutter】自定義ListView開發記錄(五)—— 提供手勢等資訊
- 【Flutter】自定義ListView開發記錄(四)—— 關於ParentData的設想和分析與簡單實踐
- 【Flutter】自定義ListView開發記錄(三)—— 處理HitTest手勢事件
- 【Flutter】自定義ListView開發記錄(二)——設計LayoutManager
- 【Flutter】自定義ListView開發記錄(一)——設計滑動效果的處理方式
- 【Flutter】熊孩子拆元件系列之拆ListView(十)—— 按自己的方式組裝修改ListView
- 【Flutter】熊孩子拆元件系列之拆ListView(九)—— AutomaticKeepAlive和KeepAlive
- 【Flutter】熊孩子拆元件系列之拆ListView(八)—— SliverList的運作機制
- 【Flutter】熊孩子拆元件系列之拆ListView(七)—— SliverList的基礎結構
- 【Flutter】熊孩子拆元件系列之拆ListView(六)—— SliverPadding
- 【Flutter】熊孩子拆元件系列之拆ListView(五)—— ViewPort
- 【Flutter】熊孩子拆元件系列之拆ListView(四)—— _ScrollableScope
- 【Flutter】熊孩子拆元件系列之拆ListView(三)—— GlowingOverscrollIndicator