鍓嶈█
extended_nested_scroll_view 鏄垜鐨勭涓€涓笂浼犲埌 pub.dev 鐨?Flutter 缁勪欢.
涓€鏅冪溂閮藉揩3骞翠簡锛岀粡鍘嗕簡43涓増鏈凯浠o紝鍔熻兘绋冲畾锛屼唬鐮佷笌瀹樻柟鍚屾銆?/p>
鑰屾垜鏈€杩戜竴鐩寸澶囩潃瀵瑰叾杩涜閲嶆瀯銆傛€庝箞璇翠簡锛屾帴瑙?Flutter 3骞翠簡锛岃鐭ヤ篃涓庡綋鍒濇湁鎵€涓嶅悓銆傛垜鐩镐俊鑷繁濡傛灉鐜板湪鍐嶉潰瀵?NestedScrollView
鐨勯棶棰橈紝鎴戝簲璇ヨ兘澶勭悊鍦版洿濂姐€?/p>
娉ㄦ剰锛?鍚庨潰鐢ㄥ埌鐨?SliverPinnedToBoxAdapter
鏄?extended_sliver閲岄潰涓€涓粍浠讹紝浣犳妸瀹冨綋浣?SliverPersistentHeader
( Pinned 涓?true锛宮inExtent = maxExtent) 灏卞ソ浜嗐€?/p>
NestedScrollView 鏄粈涔?/h2>
A scrolling view inside of which can be nested other scrolling views, with their scroll positions being intrinsically linked.
A scrolling view inside of which can be nested other scrolling views, with their scroll positions being intrinsically linked.
灏嗗閮ㄦ粴鍔?Header閮ㄥ垎)鍜屽唴閮ㄦ粴鍔?Body閮ㄥ垎)鑱斿姩璧锋潵銆傞噷闈㈡粴鍔ㄤ笉浜嗭紝婊氬姩澶栭潰銆傚闈㈡粴鍔ㄦ病浜嗭紝婊氬姩閲岄潰銆傞偅涔?NestedScrollView
鏄浣曞仛鍒扮殑鍛紵
NestedScrollView
鍏跺疄鏄竴涓?CustomScrollView
, 涓嬮潰涓轰吉浠g爜銆?/p>
CustomScrollView(
controller: outerController,
slivers: [
...<Widget>[Header1,Header2],
SliverFillRemaining()(
child: PrimaryScrollController(
controller: innerController,
child: body,
),
),
],
);
- outerController 鏄?
CustomScrollView
鐨?controller
锛?浠庡眰绾т笂鐪嬶紝灏辨槸澶栭儴 - 杩欓噷浣跨敤浜?
PrimaryScrollController
锛岄偅涔?body
閲岄潰鐨勪换浣曟粴鍔ㄧ粍浠讹紝鍦ㄤ笉鑷畾涔?controller
鐨勬儏鍐典笅锛岄兘灏嗗叕鐢?innerController
銆?/li>
鑷充簬涓轰粈涔堜細杩欐牱锛岄鍏堢湅涓€涓嬫瘡涓粴鍔ㄧ粍浠堕兘鏈夌殑灞炴€?primary锛屽鏋?controller 涓?null 锛屽苟涓旀槸绔栫洿鏂规硶锛屽氨榛樿涓?true 銆?/p>
primary = primary ?controller == null && identical(scrollDirection, Axis.vertical),
鐒跺悗 鍦?scroll_view.dart 涓紝濡傛灉 primary
涓?true锛屽氨鍘昏幏鍙?PrimaryScrollController
鐨?controller銆?/p>
final ScrollControllerscrollController =
primary PrimaryScrollController.of(context) : controller;
final Scrollable scrollable = Scrollable(
dragStartBehavior: dragStartBehavior,
axisDirection: axisDirection,
controller: scrollController,
physics: physics,
scrollBehavior: scrollBehavior,
semanticChildCount: semanticChildCount,
restorationId: restorationId,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return buildViewport(context, offset, axisDirection, slivers);
},
);
杩欎篃瑙i噴浜嗕负鍟ユ湁浜涘悓瀛︾粰 body 涓殑婊氬姩缁勪欢璁剧疆浜?controller锛屽氨浼氬彂鐜板唴澶栨粴鍔ㄤ笉鍐嶈仈鍔ㄤ簡銆?/p>
涓轰粈涔堣鎵╁睍瀹樻柟鐨?/h2>
鐞嗚В浜?NestedScrollView
鏄粈涔堬紝閭f垜涓哄暐瑕佹墿灞曞畼鏂圭粍浠跺憿锛?/p>
Header 涓寘鍚涓?Pinned Sliver 鏃跺€欑殑闂
鍒嗘瀽
鍏堢湅涓€涓浘锛屼綘瑙夊緱鍒楄〃鍚戜笂婊氬姩鏈€缁堢殑缁撴灉鏄粈涔堬紵浠g爜鍦ㄤ笅闈€?/p>
CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header: 100楂樺害'),
height: 100,
color: Colors.yellow.withOpacity(0.4),
),
),
SliverPinnedToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header: Pinned 100楂樺害'),
height: 100,
color: Colors.red.withOpacity(0.4),
),
),
SliverToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header: 100楂樺害'),
height: 100,
color: Colors.yellow.withOpacity(0.4),
),
),
SliverFillRemaining(
child: Column(
children: List.generate(
100,
(index) => Container(
alignment: Alignment.topCenter,
child: Text('body: 閲岄潰鐨勫唴瀹?index,楂樺害100'),
height: 100,
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.4),
border: Border.all(
color: Colors.black,
)),
)),
),
)
],
),
鍡紝娌¢敊锛屽垪琛ㄧ殑绗竴涓?Item 浼氭粴鍔ㄥ埌 Header1 涓嬮潰銆備絾瀹為檯涓婏紝鎴戜滑閫氬父鐨勯渶姹傛槸闇€瑕佸垪琛ㄥ仠鐣欏湪 Header1 搴曡竟銆?/p>
Flutter 瀹樻柟涔熸敞鎰忓埌浜嗚繖涓棶棰橈紝骞朵笖鎻愪緵浜?SliverOverlapAbsorber
SliverOverlapInjector
鏉ュ鐞嗚繖涓棶棰橈紝
-
SliverOverlapAbsorber
鏉ュ寘瑁?Pinned
涓?true
鐨?Sliver
- 鍦?body 涓娇鐢?
SliverOverlapInjector
鏉ュ崰浣?/li> - 鐢?
NestedScrollView._absorberHandle
鏉ュ疄鐜?SliverOverlapAbsorber
鍜?SliverOverlapInjector
鐨勪俊鎭紶閫掋€?/li>
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
// 鐩戝惉璁$畻楂樺害锛屽苟涓旈€氳繃 NestedScrollView._absorberHandle 灏?
// 鑷韩鐨勯珮搴?鍛婅瘔 SliverOverlapInjector
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverPinnedToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header: Pinned 100楂樺害'),
height: 100,
color: Colors.red.withOpacity(0.4),
),
)
)
];
},
body: Builder(
builder: (BuildContext context) {
return CustomScrollView(
// The "controller" and "primary" members should be left
// unset, so that the NestedScrollView can control this
// inner scroll view.
// If the "controller" property is set, then this scroll
// view will not be associated with the NestedScrollView.
slivers: <Widget>[
// 鍗犱綅锛屾帴鏀?SliverOverlapAbsorber 鐨勪俊鎭?
SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => ListTile(title: Text('Item $index')),
childCount: 30,
),
),
],
);
}
)
)
);
}
濡傛灉浣犺寰楄繖绉嶆柟娉曚笉娓呮锛岄偅鎴戠畝鍖栦竴涓嬶紝鐢ㄥ彟澶栫殑鏂瑰紡琛ㄨ揪銆傛垜浠篃澧炲姞涓€涓?100 鐨勫崰浣嶃€備笉杩囧疄闄呮搷浣滀腑鏄笉鍙兘杩欐牱鍋氱殑锛岃繖鏍蜂細瀵艰嚧鍒濆鍖栫殑鏃跺€欏垪琛ㄤ笂鏂逛細鐣欎笅 100 鐨勭┖浣嶃€?/p>
CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header0: 100楂樺害'),
height: 100,
color: Colors.yellow.withOpacity(0.4),
),
),
SliverPinnedToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header1: Pinned 100楂樺害'),
height: 100,
color: Colors.red.withOpacity(0.4),
),
),
SliverToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header2: 100楂樺害'),
height: 100,
color: Colors.yellow.withOpacity(0.4),
),
),
SliverFillRemaining(
child: Column(
children: <Widget>[
// 鎴戠浉褰撲簬 SliverOverlapAbsorber
Container(
height: 100,
),
Column(
children: List.generate(
100,
(index) => Container(
alignment: Alignment.topCenter,
child: Text('body: 閲岄潰鐨勫唴瀹?index,楂樺害100'),
height: 100,
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.4),
border: Border.all(
color: Colors.black,
)),
)),
),
],
),
)
],
),
閭i棶棰樻潵浜嗭紝濡傛灉 NestedScrollView
鐨?Header
涓寘鍚涓?Pinned
涓?true
鐨?Sliver
锛?閭d箞 SliverOverlapAbsorber
渚挎棤鑳戒负鍔涗簡锛孖ssue 浼犻€侀棬銆?/p>
瑙e喅
鎴戜滑鍐嶆潵鍥為【 NestedScrollView
闀夸粈涔堟牱瀛愮殑锛屽彲浠ョ湅鍑烘潵锛岃繖涓棶棰樺簲璇ヨ窡 outerController
鏈夊叧绯汇€傚弬鐓у墠闈㈢畝鍗?demo 鏉ョ湅锛屽彧瑕佹垜浠澶栭儴灏戞粴鍔?100锛屽氨鍙互璁╁垪琛ㄥ仠鐣欏湪 Pinned Header1 搴曢儴浜嗐€?/p>
CustomScrollView(
controller: outerController,
slivers: [
...<Widget>[Header1,Header2],
SliverFillRemaining()(
child: PrimaryScrollController(
controller: innerController,
child: body,
),
),
],
);
maxScrollExtent
鎴戜滑鍐嶆€濊€冧竴涓嬶紝鏄粈涔堜細褰卞搷涓€涓粴鍔ㄧ粍浠剁殑婊氬姩鏈€缁堣窛绂伙紵
绛旀鏄?ScrollPosition.maxScrollExtent
鐭ラ亾浜嗘槸浠€涔堜笢瑗垮奖鍝嶏紝鎴戜滑瑕佸仛鐨勫氨鏄湪鍚堥€傜殑鏃跺€欎慨鏀硅繖涓€硷紝閭d箞濡備綍鑾峰彇鏃舵満鍛紵
灏嗕笅闈唬鐮?/p>
@override
double get maxScrollExtent => _maxScrollExtent!;
double_maxScrollExtent;
鏀逛负浠ヤ笅浠g爜
@override
double get maxScrollExtent => _maxScrollExtent!;
//double_maxScrollExtent;
double__maxScrollExtent;
doubleget _maxScrollExtent => __maxScrollExtent;
set _maxScrollExtent(doublevalue) {
if (__maxScrollExtent != value) {
__maxScrollExtent = value;
}
}
杩欐牱鎴戜滑灏卞彲浠ュ湪 set 鏂规硶閲岄潰鎵撲笂 debug 鏂偣锛岀湅鐪嬫槸浠€涔堟椂鍊?_maxScrollExtent
琚祴鍊肩殑銆?/p>
杩愯渚嬪瓙 锛屽緱鍒颁互涓?Call Stack
銆?/p>
鐪嬪埌杩欓噷锛屾垜浠簲璇ョ煡閬擄紝鍙互閫氳繃 override applyContentDimensions
鏂规硶锛屽幓閲嶆柊璁剧疆 maxScrollExtent
ScrollPosition
鎯宠 override applyContentDimensions
灏辫鐭ラ亾 ScrollPosition
鍦ㄤ粈涔堟椂鍊欏垱寤虹殑锛岀户缁皟璇? 鎶婃柇鐐规墦鍒?ScrollPosition
鐨勬瀯閫犱笂闈€?/p>
graph TD
ScrollController.createScrollPosition --> ScrollPositionWithSingleContext --> ScrollPosition
鍙互鐪嬪埌濡傛灉涓嶆槸鐗瑰畾鐨?ScrollPosition
锛屾垜浠钩鏃朵娇鐢ㄧ殑鏄粯璁ょ殑
ScrollPositionWithSingleContext
锛屽苟涓斿湪 ScrollController
鐨?createScrollPosition
鏂规硶涓垱寤恒€?/p>
澧炲姞涓嬮潰鐨勪唬鐮侊紝骞朵笖缁?demo 涓殑 CustomScrollView
娣诲姞 controller
涓?MyScrollController
锛屾垜浠啀娆¤繍琛?demo锛屾槸涓嶆槸寰楀埌浜嗘垜浠兂瑕佺殑鏁堟灉鍛紵
class MyScrollController extends ScrollController {
@override
ScrollPosition createScrollPosition(ScrollPhysics physics,
ScrollContext context, ScrollPosition oldPosition) {
return MyScrollPosition(
physics: physics,
context: context,
initialPixels: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
}
}
class MyScrollPosition extends ScrollPositionWithSingleContext {
MyScrollPosition({
@required ScrollPhysics physics,
@required ScrollContext context,
double initialPixels = 0.0,
bool keepScrollOffset = true,
ScrollPosition oldPosition,
String debugLabel,
}) : super(
physics: physics,
context: context,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
initialPixels: initialPixels,
);
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
return super.applyContentDimensions(minScrollExtent, maxScrollExtent - 100);
}
}
_NestedScrollPosition
瀵瑰簲鍒?NestedScrollView
涓紝鍙互涓篲NestedScrollPosition 娣诲姞浠ヤ笅鐨勬柟娉曘€?/p>
pinnedHeaderSliverHeightBuilder
鍥炶皟鏄幏鍙?Header
褰撲腑涓€鍏辨湁鍝簺 Pinned
鐨?Sliver
銆?/p>
- 瀵逛簬 SliverAppbar 鏉ヨ锛屾渶缁堝浐瀹氱殑楂樺害搴旇鍖呮嫭
鐘舵€佹爮鐨勯珮搴?/code>(MediaQuery.of(context).padding.top) 鍜?
瀵艰埅鏍忕殑楂樺害
(kToolbarHeight) - 瀵逛簬
SliverPersistentHeader
( Pinned 涓?true )锛?鏈€缁堝浐瀹氶珮搴﹀簲璇ヤ负minExtent
- 濡傛灉鏈夊涓繖绉?Sliver锛?搴旇涓轰粬浠渶缁堝浐瀹氱殑楂樺害涔嬪拰銆?/li>
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
if (debugLabel == 'outer' &&
coordinator.pinnedHeaderSliverHeightBuilder != null) {
maxScrollExtent =
maxScrollExtent - coordinator.pinnedHeaderSliverHeightBuilder!();
maxScrollExtent = math.max(0.0, maxScrollExtent);
}
return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
}
Body 涓鍒楄〃婊氬姩浜掔浉褰卞搷鐨勯棶棰?/h3>
澶у涓€瀹氭湁杩欑闇€姹傦紝鍦?TabbarView
鎴栬€?PageView
涓殑鍒楄〃锛屽垏鎹㈢殑鏃跺€欏垪琛ㄧ殑婊氬姩浣嶇疆瑕佷繚鐣欍€傝繖涓娇鐢?AutomaticKeepAliveClientMixin
锛岄潪甯哥畝鍗曘€?/p>
浣嗘槸濡傛灉鎶?TabbarView
鎴栬€?PageView
鏀惧埌NestedScrollView
鐨?body
閲岄潰鐨勮瘽锛屼綘婊氬姩鍏朵腑涓€涓垪琛紝涔熶細鍙戠幇鍏朵粬鐨勫垪琛ㄤ篃浼氳窡鐫€鏀瑰彉浣嶇疆銆侷ssue 浼犻€侀棬
鍒嗘瀽
鍏堢湅 NestedScrollView
鐨勪吉浠g爜銆?code>NestedScrollView 涔嬫墍浠ヨ兘涓婂唴澶栬仈鍔紝灏辨槸鍦ㄤ簬 outerController
鍜?innerController
鐨勮仈鍔ㄣ€?/p>
CustomScrollView(
controller: outerController,
slivers: [
...<Widget>[Header1,Header2],
SliverFillRemaining()(
child: PrimaryScrollController(
controller: innerController,
child: body,
),
),
],
);
innerController
璐熻矗 Body
锛屽皢 Body
涓病鏈夎缃繃 controller 鐨勫垪琛ㄧ殑 ScrollPosition
閫氳繃 attach
鏂规硶锛屽姞杞借繘鏉ャ€?/p>
褰撲娇鐢ㄥ垪琛ㄧ紦瀛樼殑鏃跺€欙紝鍒囨崲 tab 鐨勬椂鍊欙紝鍘熷垪琛ㄥ皢涓嶄細
dispose
锛屽氨涓嶄細浠?controller 涓?detach
銆? innerController.positions 灏嗕笉姝竴涓€傝€?outerController
鍜?innerController
鐨勮仈鍔ㄨ绠楅兘鏄熀浜?positions 鏉ヨ繘琛岀殑銆傝繖灏辨槸瀵艰嚧杩欎釜闂鐨勫師鍥犮€?/p>
鍏蜂綋浠g爜浣撶幇鍦?br> https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/nested_scroll_view.dart#L1135
if (innerDelta != 0.0) {
for (final _NestedScrollPosition position in _innerPositions)
position.applyFullDragUpdate(innerDelta);
}
瑙e喅
涓嶇鏄?骞村墠杩樻槸鐜板湪鍐嶇湅杩欎釜闂锛岀涓€鎰熻锛屼笉灏辨槸鍙鎵惧埌褰撳墠
鏄剧ず
鐨勯偅涓垪琛紝鍙瀹冩粴鍔ㄥ氨鍙互浜嗗槢锛屼笉鏄緢绠€鍗曞悧
纭疄锛屼絾鏄偅鍙槸鐪嬭捣鏉ヨ寰楃畝鍗曪紝姣曠珶杩欎釜 issue 宸茬粡 open 3骞翠簡銆?/p>
鑰佹柟妗?/h5>
鍦?ScrollPosition
attach
鐨勬椂鍊欏幓閫氳繃 context
鎵惧埌杩欎釜鍒楄〃鎵€瀵瑰簲鐨勬爣蹇楋紝璺?TabbarView
鎴栬€?PageView
鐨?index 鍏宠仈杩涜瀵规瘮銆?br>
Flutter 鎵╁睍NestedScrollView 锛堜簩锛夊垪琛ㄦ粴鍔ㄥ悓姝ヨВ鍐?(juejin.cn)
閫氳繃璁$畻鍒楄〃鐨勭浉瀵逛綅缃紝鏉ョ‘瀹氬綋鍓?鏄剧ず
鐨勫垪琛ㄣ€?br>
Flutter 浣犳兂鐭ラ亾鐨刉idget鍙鍖哄煙,鐩稿浣嶇疆,澶у皬 (juejin.cn)
鍦?ScrollPosition
attach
鐨勬椂鍊欏幓閫氳繃 context
鎵惧埌杩欎釜鍒楄〃鎵€瀵瑰簲鐨勬爣蹇楋紝璺?TabbarView
鎴栬€?PageView
鐨?index 鍏宠仈杩涜瀵规瘮銆?br>
Flutter 鎵╁睍NestedScrollView 锛堜簩锛夊垪琛ㄦ粴鍔ㄥ悓姝ヨВ鍐?(juejin.cn)
閫氳繃璁$畻鍒楄〃鐨勭浉瀵逛綅缃紝鏉ョ‘瀹氬綋鍓?鏄剧ず
鐨勫垪琛ㄣ€?br>
Flutter 浣犳兂鐭ラ亾鐨刉idget鍙鍖哄煙,鐩稿浣嶇疆,澶у皬 (juejin.cn)
鎬讳綋鏉ヨ锛?/p>
- 1鏂规鏇村噯纭紝浣嗘槸鐢ㄦ硶姣旇緝绻佺悙銆?/li>
- 2鏂规鍙楀姩鐢诲奖鍝嶏紝鍦ㄤ竴浜涚壒娈婄殑鎯呭喌涓嬩細瀵艰嚧璁$畻涓嶆纭€?/li>
鏂版柟妗?/h5>
棣栧厛鎴戜滑鍏堝噯澶囦竴涓殑 demo 閲嶇幇闂銆?/p>
NestedScrollView(
headerSliverBuilder: (
BuildContext buildContext,
bool innerBoxIsScrolled,
) =>
<Widget>[
SliverToBoxAdapter(
child: Container(
color: Colors.red,
height: 200,
),
)
],
body: Column(
children: [
Container(
color: Colors.yellow,
height: 200,
),
Expanded(
child: PageView(
children: <Widget>[
ListItem(
tag: 'Tab0',
),
ListItem(
tag: 'Tab1',
),
],
),
),
],
),
),
class ListItem extends StatefulWidget {
const ListItem({
Key key,
this.tag,
}) : super(key: key);
final String tag;
@override
_ListItemState createState() => _ListItemState();
}
class _ListItemState extends State<ListItem>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
return ListView.builder(
itemBuilder: (BuildContext buildContext, int index) =>
Center(child: Text('${widget.tag}---$index')),
itemCount: 1000,
);
}
@override
bool get wantKeepAlive => true;
}
Drag
鐜板湪鍐嶇湅杩欎釜闂锛屾垜鍦ㄦ€濊€冿紝鎴戣嚜宸辨粴鍔ㄤ簡鍝釜鍒楄〃锛屾垜鑷繁涓嶇煡閬擄紵锛?/p>
鐪嬭繃涓婁竴绡?Flutter 閿佸畾琛屽垪鐨凢lexGrid - 鎺橀噾 (juejin.cn) 鐨勫皬浼欎即锛屽簲璇ョ煡閬撳湪鎷栨嫿鍒楄〃鐨勬椂鍊欐槸浼氱敓鎴愪竴涓?Drag
鐨勩€傞偅涔堟湁杩欎釜 Drag
鐨?ScrollPosition
涓嶅氨瀵瑰簲姝e湪鏄剧ず鐨勫垪琛ㄥ悧锛燂紵
鍏蜂綋鍒颁唬鐮侊紝鎴戜滑璇曡瘯鎵撴棩蹇楃湅鐪嬶紝
https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/nested_scroll_view.dart#L1625
@override
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
print(debugLabel);
return coordinator.drag(details, dragCancelCallback);
}
鐞嗘兂寰堝ソ锛屼絾鏄幇瀹炴槸楠ㄦ劅鐨勶紝涓嶇鎴戞槸婊氬姩 Header
杩樻槸 Body
锛岄兘鍙墦鍗颁簡 outer
銆?閭f剰鎬濇槸 Body 閲岄潰鐨勬墜鍔垮叏閮ㄨ鍚冧簡锛燂紵
涓嶇潃鎬ワ紝鎴戜滑鎵撳紑 DevTools
锛岀湅鐪?ListView
閲岄潰鐨?ScrollableState 鐨勭姸鎬併€?鍏蜂綋涓哄暐瑕佺湅杩欓噷闈紝鍙互鍘昏璇?Flutter 閿佸畾琛屽垪鐨?FlexGrid (juejin.cn))
鍝堝搱锛?code>gestures 灞呯劧涓?none
锛屽氨鏄 Body
閲岄潰娌℃湁娉ㄥ唽鎵嬪娍銆?/p>
https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/scrollable.dart#L543 setCanDrag
鏂规硶涓紝鎴戜滑鍙互鐪嬪埌鍙湁 canDrag
绛変簬 false
鐨勬椂鍊欙紝鎴戜滑鏄病鏈夋敞鍐屾墜鍔跨殑銆傚綋鐒朵篃鏈変竴绉嶅彲鑳斤紝setCanDrag
涔熻灏辨病鏈夎璋冪敤杩囷紝榛樿鐨?_gestureRecognizers
灏辨槸绌恒€?/p>
@override
@protected
void setCanDrag(bool canDrag) {
if (canDrag == _lastCanDrag && (!canDrag || widget.axis == _lastAxisDirection))
return;
if (!canDrag) {
_gestureRecognizers = const <Type, GestureRecognizerFactory>{};
// Cancel the active hold/drag (if any) because the gesture recognizers
// will soon be disposed by our RawGestureDetector, and we won't be
// receiving pointer up events to cancel the hold/drag.
_handleDragCancel();
} else {
switch (widget.axis) {
case Axis.vertical:
_gestureRecognizers = <Type, GestureRecognizerFactory>{
VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(),
(VerticalDragGestureRecognizer instance) {
instance
..onDown = _handleDragDown
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel
..minFlingDistance = _physics?.minFlingDistance
..minFlingVelocity = _physics?.minFlingVelocity
..maxFlingVelocity = _physics?.maxFlingVelocity
..velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context)
..dragStartBehavior = widget.dragStartBehavior;
},
),
};
break;
case Axis.horizontal:
_gestureRecognizers = <Type, GestureRecognizerFactory>{
HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
() => HorizontalDragGestureRecognizer(),
(HorizontalDragGestureRecognizer instance) {
instance
..onDown = _handleDragDown
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel
..minFlingDistance = _physics?.minFlingDistance
..minFlingVelocity = _physics?.minFlingVelocity
..maxFlingVelocity = _physics?.maxFlingVelocity
..velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context)
..dragStartBehavior = widget.dragStartBehavior;
},
),
};
break;
}
}
_lastCanDrag = canDrag;
_lastAxisDirection = widget.axis;
if (_gestureDetectorKey.currentState != null)
_gestureDetectorKey.currentState!.replaceGestureRecognizers(_gestureRecognizers);
}
鎴戜滑鍦?setCanDrag
鏂规硶涓墦涓€涓柇鐐癸紝鐪嬬湅璋冪敤鐨勬椂鏈恒€?/p>
- RenderViewport.performLayout
performLayout 鏂规硶涓绠楀嚭褰撳墠 ScrollPosition
鐨勬渶灏忔渶澶у€?/p>
if (offset.applyContentDimensions(
math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
))
- ScrollPosition.applyContentDimensions
璋冪敤 applyNewDimensions
鏂规硶
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
assert(minScrollExtent != null);
assert(maxScrollExtent != null);
assert(haveDimensions == (_lastMetrics != null));
if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) ||
!nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) ||
_didChangeViewportDimensionOrReceiveCorrection) {
assert(minScrollExtent != null);
assert(maxScrollExtent != null);
assert(minScrollExtent <= maxScrollExtent);
_minScrollExtent = minScrollExtent;
_maxScrollExtent = maxScrollExtent;
final ScrollMetricscurrentMetrics = haveDimensions copyWith() : null;
_didChangeViewportDimensionOrReceiveCorrection = false;
_pendingDimensions = true;
if (haveDimensions && !correctForNewDimensions(_lastMetrics!, currentMetrics!)) {
return false;
}
_haveDimensions = true;
}
assert(haveDimensions);
if (_pendingDimensions) {
applyNewDimensions();
_pendingDimensions = false;
}
assert(!_didChangeViewportDimensionOrReceiveCorrection, 'Use correctForNewDimensions() (and return true) to change the scroll offset during applyContentDimensions().');
_lastMetrics = copyWith();
return true;
}
- ScrollPositionWithSingleContext.applyNewDimensions
涓嶇壒娈婂畾涔夌殑璇濓紝榛樿 ScrollPosition
閮芥槸 ScrollPositionWithSingleContext
銆?code>context 鏄皝鍛紵
褰撶劧鏄?ScrollableState
@override
void applyNewDimensions() {
super.applyNewDimensions();
context.setCanDrag(physics.shouldAcceptUserOffset(this));
}
杩欓噷鎻愪簡涓€涓嬶紝骞虫椂鏈夊悓瀛﹂棶銆備笉婊′竴灞忓箷鐨勫垪琛? controller 娉ㄥ唽涓嶈Е鍙?鎴栬€?NotificationListener<ScrollUpdateNotification> 鐩戝惉涓嶈Е鍙戙€傚師鍥犲氨鍦ㄨ繖閲岋紝physics.shouldAcceptUserOffset(this) 杩斿洖鐨勬槸
false
銆傝€屾垜浠殑澶勭悊鍔炴硶灏辨槸 璁剧疆 physics 涓?AlwaysScrollableScrollPhysics
锛?shouldAcceptUserOffset 鏀?/p>
AlwaysScrollableScrollPhysics
鐨?shouldAcceptUserOffset
鏂规硶姘歌繙杩斿洖 true
銆?/p>
class AlwaysScrollableScrollPhysics extends ScrollPhysics {
/// Creates scroll physics that always lets the user scroll.
const AlwaysScrollableScrollPhysics({ ScrollPhysicsparent }) : super(parent: parent);
@override
AlwaysScrollableScrollPhysics applyTo(ScrollPhysicsancestor) {
return AlwaysScrollableScrollPhysics(parent: buildParent(ancestor));
}
@override
bool shouldAcceptUserOffset(ScrollMetrics position) => true;
}
- ScrollableState.setCanDrag
鏈€缁堣揪鍒拌繖閲岋紝鍘绘牴鎹?canDrag
鍜?axis
(姘村钩/鍨傜洿)
_NestedScrollCoordinator
閭f帴涓嬫潵锛屾垜浠氨鍘?NestedScrollView
浠g爜閲岄潰鎵炬壘鐪嬨€?/p>
https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/nested_scroll_view.dart#L1612
@override
void applyNewDimensions() {
super.applyNewDimensions();
coordinator.updateCanDrag();
}
杩欓噷鎴戜滑鐪嬪埌璋冪敤浜?coordinator.updateCanDrag()
銆?/p>
棣栧厛鎴戜滑鐪嬬湅 coordinator
鏄粈涔堬紵涓嶉毦鐪嬪嚭鏉ワ紝鐢ㄦ潵鍗忚皟 outerController
鍜?innerController
鐨勩€?/p>
class _NestedScrollCoordinator
implements ScrollActivityDelegate, ScrollHoldController {
_NestedScrollCoordinator(
this._state,
this._parent,
this._onHasScrolledBodyChanged,
this._floatHeaderSlivers,
) {
final double initialScrollOffset = _parent?.initialScrollOffset ?0.0;
_outerController = _NestedScrollController(
this,
initialScrollOffset: initialScrollOffset,
debugLabel: 'outer',
);
_innerController = _NestedScrollController(
this,
initialScrollOffset: 0.0,
debugLabel: 'inner',
);
}
閭d箞鎴戜滑鐪嬬湅 updateCanDrag
鏂规硶閲岄潰鍋氫簡浠€涔堛€?/p>
void updateCanDrag() {
if (!_outerPosition!.haveDimensions) return;
double maxInnerExtent = 0.0;
for (final _NestedScrollPosition position in _innerPositions) {
if (!position.haveDimensions) return;
maxInnerExtent = math.max(
maxInnerExtent,
position.maxScrollExtent - position.minScrollExtent,
);
}
// _NestedScrollPosition.updateCanDrag
_outerPosition!.updateCanDrag(maxInnerExtent);
}
_NestedScrollPosition.updateCanDrag
void updateCanDrag(double totalExtent) {
// 璋冪敤 ScrollableState 鐨?setCanDrag 鏂规硶
context.setCanDrag(totalExtent > (viewportDimension - maxScrollExtent) ||
minScrollExtent != maxScrollExtent);
}
鐭ラ亾鍘熷洜涔嬪悗锛屾垜浠瘯璇曞姩鎵嬫敼涓嬨€?/p>
- 淇敼
_NestedScrollCoordinator.updateCanDrag
涓哄涓?
void updateCanDrag({_NestedScrollPositionposition}) {
double maxInnerExtent = 0.0;
if (position != null && position.debugLabel == 'inner') {
if (position.haveDimensions) {
maxInnerExtent = math.max(
maxInnerExtent,
position.maxScrollExtent - position.minScrollExtent,
);
position.updateCanDrag(maxInnerExtent);
}
}
if (!_outerPosition!.haveDimensions) {
return;
}
for (final _NestedScrollPosition position in _innerPositions) {
if (!position.haveDimensions) {
return;
}
maxInnerExtent = math.max(
maxInnerExtent,
position.maxScrollExtent - position.minScrollExtent,
);
}
_outerPosition!.updateCanDrag(maxInnerExtent);
}
- 淇敼
_NestedScrollPosition.drag
鏂规硶涓哄涓?
bool _isActived = false;
@override
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
_isActived = true;
return coordinator.drag(details, () {
dragCancelCallback();
_isActived = false;
});
}
/// Whether is actived now
bool get isActived {
return _isActived;
}
- 淇敼
_NestedScrollCoordinator._innerPositions
涓哄涓?
Iterable<_NestedScrollPosition> get _innerPositions {
if (_innerController.nestedPositions.length > 1) {
final Iterable<_NestedScrollPosition> actived = _innerController
.nestedPositions
.where((_NestedScrollPosition element) => element.isActived);
print('${actived.length}');
if (actived.isNotEmpty) return actived;
}
return _innerController.nestedPositions;
}
鐜板湪鍐嶈繍琛?demo 锛?鍒囨崲鍒楄〃涔嬪悗婊氬姩鐪嬬湅锛屾槸鍚︷煈屼簡锛熺粨鏋滄槸澶辨湜鐨勩€?/p>
- 铏界劧鎴戜滑鍦?
drag
鎿嶄綔鐨勬椂鍊欙紝纭疄鍙互鍒ゆ柇鍒拌皝鏄縺娲荤殑锛屼絾鏄墜鎸?up 锛屽紑濮嬫儻鎬ф粦鍔ㄧ殑鏃跺€欙紝dragCancelCallback
鍥炶皟宸茬粡瑙﹀彂锛?code>_isActived 宸茬粡琚缃负false
銆?/li> - 褰撴垜浠湪鎿嶄綔
PageView
涓婃柟榛勮壊鍖哄煙鐨勬椂鍊?閫氬父鎯呭喌涓嬶紝杩欓儴鍒嗗彲鑳芥槸Tabbar
), 鐢变簬娌℃湁鍦ㄥ垪琛ㄤ笂闈㈣繘琛?drag
鎿嶄綔锛屾墍浠ヨ繖涓椂鍊?actived
鐨勫垪琛ㄤ负 0.
NestedScrollView(
headerSliverBuilder: (
BuildContext buildContext,
bool innerBoxIsScrolled,
) =>
<Widget>[
SliverToBoxAdapter(
child: Container(
color: Colors.red,
height: 200,
),
)
],
body: Column(
children: [
Container(
color: Colors.yellow,
height: 200,
),
Expanded(
child: PageView(
children: <Widget>[
ListItem(
tag: 'Tab0',
),
ListItem(
tag: 'Tab1',
),
],
),
),
],
),
),
鏄惁鍙
闂濂藉儚鍙堣蛋鍒颁簡鑰佸湴鏂癸紝鎬庝箞鍒ゆ柇涓€涓鍥炬槸鍙銆?/p>
棣栧厛锛屾垜浠繖閲岃兘鎷垮埌鏈€鐩存帴鐨勫氨鏄?_NestedScrollPosition
锛屾垜浠湅鐪嬭繖涓浼欐湁浠€涔堜笢瑗垮彲浠ュ埄鐢ㄣ€?/p>
涓€鐪煎氨鐪嬪埌浜?context(ScrollableState)
锛屾槸涓€涓?ScrollContext
锛岃€?ScrollableState
瀹炵幇浜?ScrollContext
銆?/p>
/// Where the scrolling is taking place.
///
/// Typically implemented by [ScrollableState].
final ScrollContext context;
鐪嬩竴鐪?ScrollContext
锛?code>notificationContext 鍜?storageContext
搴旇鏄浉鍏崇殑銆?/p>
abstract class ScrollContext {
/// The [BuildContext] that should be used when dispatching
/// [ScrollNotification]s.
///
/// This context is typically different that the context of the scrollable
/// widget itself. For example, [Scrollable] uses a context outside the
/// [Viewport] but inside the widgets created by
/// [ScrollBehavior.buildOverscrollIndicator] and [ScrollBehavior.buildScrollbar].
BuildContextget notificationContext;
/// The [BuildContext] that should be used when searching for a [PageStorage].
///
/// This context is typically the context of the scrollable widget itself. In
/// particular, it should involve any [GlobalKey]s that are dynamically
/// created as part of creating the scrolling widget, since those would be
/// different each time the widget is created.
// TODO(goderbauer): Deprecate this when state restoration supports all features of PageStorage.
BuildContext get storageContext;
/// A [TickerProvider] to use when animating the scroll position.
TickerProvider get vsync;
/// The direction in which the widget scrolls.
AxisDirection get axisDirection;
/// Whether the contents of the widget should ignore [PointerEvent] inputs.
///
/// Setting this value to true prevents the use from interacting with the
/// contents of the widget with pointer events. The widget itself is still
/// interactive.
///
/// For example, if the scroll position is being driven by an animation, it
/// might be appropriate to set this value to ignore pointer events to
/// prevent the user from accidentally interacting with the contents of the
/// widget as it animates. The user will still be able to touch the widget,
/// potentially stopping the animation.
void setIgnorePointer(bool value);
/// Whether the user can drag the widget, for example to initiate a scroll.
void setCanDrag(bool value);
/// Set the [SemanticsAction]s that should be expose to the semantics tree.
void setSemanticsActions(Set<SemanticsAction> actions);
/// Called by the [ScrollPosition] whenever scrolling ends to persist the
/// provided scroll `offset` for state restoration purposes.
///
/// The [ScrollContext] may pass the value back to a [ScrollPosition] by
/// calling [ScrollPosition.restoreOffset] at a later point in time or after
/// the application has restarted to restore the scroll offset.
void saveOffset(double offset);
}
鍐嶇湅鐪?ScrollableState
涓殑瀹炵幇銆?/p>
class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, RestorationMixin
implements ScrollContext {
@override
BuildContextget notificationContext => _gestureDetectorKey.currentContext;
@override
BuildContext get storageContext => context;
}
storageContext
鍏跺疄鏄?br>ScrollableState
鐨?context
銆?/p>notificationContext
鏌ユ壘涓嬪紩鐢紝鍙互鐪嬪埌銆?/p>
鏋滅劧锛岃皝瑙﹀彂鐨勪簨浠讹紝褰撶劧鏄?ScrollableState
閲岄潰鐨?RawGestureDetector
銆?/p>
NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollNotification) {
/// The build context of the widget that fired this notification.
///
/// This can be used to find the scrollable's render objects to determine the
/// size of the viewport, for instance.
// final BuildContextcontext;
print(scrollNotification.context);
return false;
},
);
鏈€缁堟垜浠繕鏄鍦?storageContext
涓婇潰涓嬪姛澶簡銆備箣鍓?# Flutter Sliver涓€鐢熶箣鏁?# 绯诲垪閲岄潰鎴戜滑瀵?Sliver
鐩稿叧鐭ヨ瘑杩涜杩囨⒊鐞嗐€傚浜?TabbarView
鎴栬€?PageView
褰撳墠鏄剧ず鐨勫厓绱狅紝鍦?RenderSliverFillViewport
褰撲腑搴旇鏄敮涓€鐨?闄ら潪浣犳妸 viewportFraction
鐨勫€艰缃负灏忎簬 1
鐨勬暟鍊?)銆傛垜浠彲浠ラ€氳繃 _NestedScrollPosition
鐨?br>
Context
鍚戜笂鎵惧埌 RenderSliverFillViewport
锛岀湅鐪?RenderSliverFillViewport
涓殑 child 鏄惁涓?_NestedScrollPosition
鐨?Context
銆?/p>
- 淇敼
_NestedScrollCoordinator._innerPositions
涓哄涓?
Iterable<_NestedScrollPosition> get _innerPositions {
if (_innerController.nestedPositions.length > 1) {
final Iterable<_NestedScrollPosition> actived = _innerController
.nestedPositions
.where((_NestedScrollPosition element) => element.isActived);
if (actived.isEmpty) {
for (final _NestedScrollPosition scrollPosition
in _innerController.nestedPositions) {
final RenderObjectrenderObject =
scrollPosition.context.storageContext.findRenderObject();
if (renderObject == null || !renderObject.attached) {
continue;
}
if (renderObjectIsVisible(renderObject, Axis.horizontal)) {
return <_NestedScrollPosition>[scrollPosition];
}
}
return _innerController.nestedPositions;
}
return actived;
} else {
return _innerController.nestedPositions;
}
}
- 鍦?
renderObjectIsVisible
鏂规硶涓煡鐪嬫槸鍚﹀瓨鍦ㄤ簬TabbarView
鎴栬€?PageView
涓紝骞朵笖鍏?axis
涓?ScrollPosition
鐨?axis
鐩稿瀭鐩淬€傚鏋滄湁鐨勮瘽锛岀敤RenderViewport
褰撳墠鐨?child
璋冪敤childIsVisible
鏂规硶楠岃瘉鏄惁鍖呭惈ScrollPosition
鎵€瀵瑰簲鐨?RenderObject
銆傛敞鎰忥紝杩欓噷璋冪敤浜?renderObjectIsVisible
鍥犱负鍙兘鏈夊祵濂?澶氱骇)鐨?TabbarView
鎴栬€?PageView
銆?/li>
bool renderObjectIsVisible(RenderObject renderObject, Axis axis) {
final RenderViewportparent = findParentRenderViewport(renderObject);
if (parent != null && parent.axis == axis) {
for (final RenderSliver childrenInPaint
in parent.childrenInHitTestOrder) {
return childIsVisible(childrenInPaint, renderObject) &&
renderObjectIsVisible(parent, axis);
}
}
return true;
}
- 鍚戜笂瀵绘壘
RenderViewport
锛屾垜浠彧鍦?NestedScrollView
鐨?body
鐨勪腑鎵撅紝鐩村埌_ExtendedRenderSliverFillRemainingWithScrollable
銆?/li>
RenderViewportfindParentRenderViewport(RenderObjectobject) {
if (object == null) {
return null;
}
object = object.parent as RenderObject?;
while (object != null) {
// 鍙湪 body 涓鎵?
if (object is _ExtendedRenderSliverFillRemainingWithScrollable) {
return null;
}
if (object is RenderViewport) {
return object;
}
object = object.parent as RenderObject?;
}
return null;
}
- 璋冪敤
visitChildrenForSemantics
閬嶅巻children
锛岀湅鏄惁鑳芥壘鍒?ScrollPosition
鎵€瀵瑰簲鐨?RenderObject
/// Return whether renderObject is visible in parent
bool childIsVisible(
RenderObject parent,
RenderObject renderObject,
) {
bool visible = false;
// The implementation has to return the children in paint order skipping all
// children that are not semantically relevant (e.g. because they are
// invisible).
parent.visitChildrenForSemantics((RenderObject child) {
if (renderObject == child) {
visible = true;
} else {
visible = childIsVisible(child, renderObject);
}
});
return visible;
}
杩樻湁鍏朵粬鏂规鍚?/h4>
鍏跺疄瀵逛簬 Body 涓鍒楄〃婊氬姩浜掔浉褰卞搷鐨勯棶棰?br>
锛屽鏋滀綘鍙槸瑕佹眰鍒楄〃淇濇寔浣嶇疆鐨勮瘽锛屼綘瀹屽叏鍙互鍒╃敤 PageStorageKey
鏉ヤ繚鎸佹粴鍔ㄥ垪琛ㄧ殑浣嶇疆銆傝繖鏍风殑璇濓紝TabbarView
鎴栬€?PageView
鍒囨崲鐨勬椂鍊欙紝ScrollableState
浼?dispose
锛屽苟涓斾粠灏?ScrollPosition
浠?innerController
涓?detach
鎺夈€?/p>
@override
void dispose() {
if (widget.controller != null) {
widget.controller!.detach(position);
} else {
_fallbackScrollController?.detach(position);
_fallbackScrollController?.dispose();
}
position.dispose();
_persistedScrollOffset.dispose();
super.dispose();
}
鑰屼綘闇€瑕佸仛鐨勬槸鍦ㄤ笂涓€灞傦紝鍒╃敤姣斿
provider | Flutter Package (flutter-io.cn) 鏉ヤ繚鎸佸垪琛ㄦ暟鎹垨鑰呭叾浠栨暟鎹姸鎬併€?/p>
NestedScrollView(
headerSliverBuilder: (
BuildContext buildContext,
bool innerBoxIsScrolled,
) =>
<Widget>[
SliverToBoxAdapter(
child: Container(
color: Colors.red,
height: 200,
),
)
],
body: Column(
children: <Widget>[
Container(
color: Colors.yellow,
height: 200,
),
Expanded(
child: PageView(
//controller: PageController(viewportFraction: 0.8),
children: <Widget>[
ListView.builder(
//store Page state
key: const PageStorageKey<String>('Tab0'),
physics: const ClampingScrollPhysics(),
itemBuilder: (BuildContext c, int i) {
return Container(
alignment: Alignment.center,
height: 60.0,
child:
Text(const Key('Tab0').toString() + ': ListView$i'),
);
},
itemCount: 50,
),
ListView.builder(
//store Page state
key: const PageStorageKey<String>('Tab1'),
physics: const ClampingScrollPhysics(),
itemBuilder: (BuildContext c, int i) {
return Container(
alignment: Alignment.center,
height: 60.0,
child:
Text(const Key('Tab1').toString() + ': ListView$i'),
);
},
itemCount: 50,
),
],
),
),
],
),
),
閲嶆瀯浠g爜
浣撳姏娲?/h3>
3骞翠笉鐭ヤ笉瑙夊氨鍐欎簡 18 涓?Flutter 缁勪欢搴撳拰 3 涓?Flutter 鐩稿叧 宸ュ叿銆?/p>
like_button | Flutter Package (flutter-io.cn)
extended_image_library | Flutter Package (pub.dev)
extended_nested_scroll_view | Flutter Package (flutter-io.cn)
extended_text | Flutter Package (flutter-io.cn)
extended_text_field | Flutter Package (flutter-io.cn)
extended_image | Flutter Package (flutter-io.cn)
extended_sliver | Flutter Package (flutter-io.cn)
pull_to_refresh_notification | Flutter Package (flutter-io.cn)
waterfall_flow | Flutter Package (flutter-io.cn)
loading_more_list | Flutter Package (flutter-io.cn)
extended_tabs | Flutter Package (flutter-io.cn)
http_client_helper | Dart Package (flutter-io.cn)
extended_text_library | Flutter Package (flutter-io.cn)
extended_list | Flutter Package (flutter-io.cn)
extended_list_library | Flutter Package (flutter-io.cn)
ff_annotation_route_library | Flutter Package (flutter-io.cn)
loading_more_list_library | Dart Package (flutter-io.cn)
ff_annotation_route | Dart Package (flutter-io.cn)
ff_annotation_route_core | Dart Package (flutter-io.cn)
flex_grid | Flutter Package (flutter-io.cn)
assets_generator | Dart Package (flutter-io.cn)
fluttercandies/JsonToDart: The tool to convert json to dart code, support Windows锛孧ac锛學eb. (github.com)
鍙互璇存瘡涓€娆″畼鏂瑰彂甯?Stable
鐗堟湰锛屽浜庢垜鏉ヨ閮芥槸涓€娆′綋鍔涙椿銆傜壒鍒槸 extended_nested_scroll_view锛宔xtended_text
, extended_text_field
, extended_image 杩?4 涓簱锛?code>merge 浠g爜鏄笉鍏夋槸浣撳姏娲伙紝涔熼渶瑕佽鐪熶粩缁嗗幓鐞嗚В鏂版敼鍔ㄣ€?/p>
缁撴瀯閲嶆瀯
杩欐涔樼潃杩欎釜鏀瑰姩鐨勬満浼氾紝鎴戝皢鏁翠釜缁撴瀯鍋氫簡璋冩暣銆?/p>
src/extended_nested_scroll_view.dart
涓哄畼鏂规簮鐮侊紝鍙仛浜嗕竴浜涘繀瑕佹敼鍔ㄣ€傛瘮濡傚鍔犲弬鏁帮紝鏇挎崲鎵╁睍绫诲瀷銆傛渶澶х▼搴︾殑淇濇寔瀹樻柟婧愮爜鐨勭粨鏋勫拰鏍煎紡銆?/p>src/extended_nested_scroll_view_part.dart
涓烘墿灞曞畼鏂圭粍浠跺姛鑳界殑閮ㄥ垎浠g爜銆傚鍔犱笅闈?涓墿灞曠被锛屽疄鐜版垜浠浉搴旂殑鎵╁睍鏂规硶銆?/p>
class _ExtendedNestedScrollCoordinator extends _NestedScrollCoordinator
class _ExtendedNestedScrollController extends _NestedScrollController
class _ExtendedNestedScrollPosition extends _NestedScrollPosition
鏈€鍚庡湪 src/extended_nested_scroll_view.dart
淇敼鍒濆鍖栦唬鐮佸嵆鍙€備互鍚庢垜鍙渶瑕佺敤 src/extended_nested_scroll_view.dart
璺熷畼鏂圭殑浠g爜杩涜 merge
鍗冲彲銆?/p>
_NestedScrollCoordinator_coordinator;
@override
void initState() {
super.initState();
_coordinator = _ExtendedNestedScrollCoordinator(
this,
widget.controller,
_handleHasScrolledBodyChanged,
widget.floatHeaderSlivers,
widget.pinnedHeaderSliverHeightBuilder,
widget.onlyOneScrollInBody,
widget.scrollDirection,
);
}
灏忕硸鏋滒煃?/h2>
濡傛灉浣犵湅鍒拌繖閲岋紝宸茬粡鐪嬩簡6000瀛楋紝鎰熻阿銆傞€佷笂涓€浜涚殑鎶€宸э紝甯屾湜鑳藉浣犳湁鎵€甯姪銆?/p>
CustomScrollView center
CustomScrollView.center
杩欎釜灞炴€ф垜鍏跺疄寰堟棭涔嬪墠灏辫杩囦簡锛?br>
Flutter Sliver涓€鐢熶箣鏁?(ScrollView) (juejin.cn)銆?br>
绠€鍗曞湴鏉ヨ:
-
center
鏄紑濮嬬粯鍒剁殑鍦版柟锛屾棦缁樺埗鍦?zero scroll offset
鐨勫湴鏂癸紝 鍚戝墠涓鸿礋锛屽悜鍚庝负姝c€?/li> -
center
涔嬪墠鐨?Sliver
鏄€掑簭缁樺埗銆?/li>
姣斿涓嬮潰浠g爜锛屼綘瑙夊緱鏈€缁堢殑鏁堟灉鏄粈涔堟牱瀛愮殑锛?/p>
CustomScrollView(
center: key,
slivers: <Widget>[
SliverList(),
SliverGrid(key:key),
]
)
鏁堟灉鍥惧涓嬶紝SliverGrid
琚粯鍒跺湪浜嗗紑濮嬩綅缃€備綘鍙互鍚戜笅婊氬姩锛岃繖涓椂鍊欙紝涓婇潰鐨?SliverList
鎵嶄細灞曠ず銆?/p>
CustomScrollView.anchor
鍙互鎺у埗 center
鐨勪綅缃€?br>
0 涓?viewport 鐨?leading锛? 涓?viewport 鐨?trailing锛屾棦杩欎釜鏄?viewport 楂樺害鍨傜洿(瀹藉害姘村钩)鐨勫崰姣斻€傛瘮濡傚鏋滄槸 0.5锛岄偅涔堢粯鍒?SliverGrid
鐨勫湴鏂瑰氨浼氬湪 viewport
鐨勪腑闂翠綅缃€?/p>
閫氳繃杩?涓睘鎬э紝鎴戜滑鍙互鍒涢€犱竴浜涙湁瓒g殑鏁堟灉銆?/p>
鑱婂ぉ鍒楄〃
flutter_instant_messaging/main.dart at master 路 fluttercandies/flutter_instant_messaging (github.com) 涓€骞村墠鍐欑殑灏?demo锛岀幇鍦ㄧЩ鍒?flutter_challenges/chat_sample.dart at main 路 fluttercandies/flutter_challenges (github.com) 缁熶竴缁存姢銆?/p>
ios 鍊掑簭鐩稿唽
flutter_challenges/ios_photo album.dart at main 路 fluttercandies/flutter_challenges (github.com) 浠g爜鍦ㄦ銆?/p>
璧锋簮浜庨┈甯堝倕缁?wechat_assets_picker | Flutter Package (flutter-io.cn)鎻愮殑闇€姹?灏炬閮芥病鏈夌粨)锛岃璁╃浉鍐屾煡鐪嬫晥鏋滆窡 Ios 鍘熺敓鐨勪竴鏍枫€?Ios 鐨勮璁℃灉鐒朵笉涓€鏍凤紝瀛︿範(chao)灏辨槸浜嗐€?/p>
鏂楅奔棣栭〉婊氬姩鏁堟灉
flutter_challenges/float_scroll.dart at main 路 fluttercandies/flutter_challenges (github.com) 浠g爜鍦ㄦ銆?/p>
涓嶅緱涓嶅啀鎻愭彁锛?code>NotificationListener锛屽畠鏄?Notification
鐨勭洃鍚€呫€傞€氳繃 Notification.dispatch
锛岄€氱煡浼氭部鐫€褰撳墠鑺傜偣锛圔uildContext锛夊悜涓婁紶閫掞紝灏辫窡鍐掓场涓€鏍凤紝浣犲彲浠ュ湪鐖惰妭鐐逛娇鐢?NotificationListener
鏉ユ帴鍙楅€氱煡銆?Flutter 涓粡甯镐娇鐢ㄥ埌鐨勬槸 ScrollNotification
锛岄櫎姝や箣澶栬繕鏈?code>SizeChangedLayoutNotification銆?code>KeepAliveNotification 銆?code>LayoutChangedNotification 绛夈€備綘涔熷彲浠ヨ嚜宸卞畾涔変竴涓€氱煡銆?/p>
import 'package:flutter/material.dart';
import 'package:oktoast/oktoast.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return OKToast(
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return NotificationListener<TextNotification>(
onNotification: (TextNotification notification) {
showToast('鏄熸槦鏀跺埌浜嗛€氱煡: ${notification.text}');
return true;
},
child: Scaffold(
appBar: AppBar(),
body: NotificationListener<TextNotification>(
onNotification: (TextNotification notification) {
showToast('澶у疂鏀跺埌浜嗛€氱煡: ${notification.text}');
// 濡傛灉杩欓噷鏀规垚 true, 鏄熸槦灏辨敹涓嶅埌淇℃伅浜嗭紝
return false;
},
child: Center(
child: Builder(
builder: (BuildContext context) {
return RaisedButton(
onPressed: () {
TextNotification('涓嬬彮浜?')..dispatch(context);
},
child: Text('鐐规垜'),
);
},
),
),
)),
);
}
}
class TextNotification extends Notification {
TextNotification(this.text);
final String text;
}
鑰屾垜浠粡甯镐娇鐢ㄧ殑涓嬫媺鍒锋柊鍜屼笂鎷夊姞杞芥洿澶氱殑缁勪欢涔熷彲浠ラ€氳繃鐩戝惉 ScrollNotification
鏉ュ畬鎴愩€?/p>
pull_to_refresh_notification | Flutter Package (flutter-io.cn)
loading_more_list | Flutter Package (flutter-io.cn)
ScrollPosition.ensureVisible
瑕佸畬鎴愯繖涓搷浣滐紝搴旇澶ч儴鍒嗕汉閮芥槸浼氱殑銆傚叾瀹炰竾鍙樹笉绂诲叾涓紝閫氳繃褰撳墠瀵硅薄鐨?RenderObject
鍘绘壘鍒板搴旂殑 RenderAbstractViewport
锛岀劧鍚庨€氳繃 getOffsetToReveal
鏂规硶鑾峰彇鐩稿浣嶇疆銆?/p>
/// Animates the position such that the given object is as visible as possible
/// by just scrolling this position.
///
/// See also:
///
/// * [ScrollPositionAlignmentPolicy] for the way in which `alignment` is
/// applied, and the way the given `object` is aligned.
Future<void> ensureVisible(
RenderObject object, {
double alignment = 0.0,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
}) {
assert(alignmentPolicy != null);
assert(object.attached);
final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
assert(viewport != null);
double target;
switch (alignmentPolicy) {
case ScrollPositionAlignmentPolicy.explicit:
target = viewport.getOffsetToReveal(object, alignment).offset.clamp(minScrollExtent, maxScrollExtent) as double;
break;
case ScrollPositionAlignmentPolicy.keepVisibleAtEnd:
target = viewport.getOffsetToReveal(object, 1.0).offset.clamp(minScrollExtent, maxScrollExtent) as double;
if (target < pixels) {
target = pixels;
}
break;
case ScrollPositionAlignmentPolicy.keepVisibleAtStart:
target = viewport.getOffsetToReveal(object, 0.0).offset.clamp(minScrollExtent, maxScrollExtent) as double;
if (target > pixels) {
target = pixels;
}
break;
}
if (target == pixels)
return Future<void>.value();
if (duration == Duration.zero) {
jumpTo(target);
return Future<void>.value();
}
return animateTo(target, duration: duration, curve: curve);
}
Demo 浠g爜鍦板潃: ensureVisible 婕旂ず (github.com)
鐣欎釜闂锛屽綋浣犵偣鍑? 涔嬪墠璺熸帢閲戝畼鏂规彁杩囷紝鏄惁鍙互澧炲姞 鎯呬汉鑺?+ 涓冨 杩欐槸涓嶆槸涓阀鍚?锛燂紵 瑕佹眰: 濡傛灉浣犺鐪熺湅瀹屼簡 澧炲姞鐐瑰嚮鍖哄煙锛岃繖搴旇鏄钩鏃跺簲璇ヤ細閬囧埌鐨勯渶姹傦紝閭d箞鍦?Flutter 涓簲璇ユ€庝箞瀹炵幇鍛紵 鍘熷浠g爜鍦板潃: 澧炲ぇ鐐瑰嚮鍖哄煙 (github.com) 涓轰簡娴嬭瘯鏂逛究锛岃娣诲姞鍦? 瑕佹眰: 瀹屾垚鏁堟灉濡備笅, 鎵╁ぇ鐨勮寖鍥寸悊璁轰笂鍙互闅忔剰璁剧疆銆?/p>
杩欑瘒鍐欑殑姣旇緝澶氾紝鎯冲埌浜嗕粈涔堝氨鍐欍€備笉绠℃槸浠€涔堟妧鏈紝鍙湁娣卞叆浜嗘墠鑳介浼氬叾涓殑閬撶悊銆傜淮鎶ゅ紑婧愮粍浠讹紝纭疄鏄竴浠跺緢绱殑浜嬫儏銆備絾鏄繖浼氫笉鏂己杩綘鍘诲涔狅紝鍦ㄤ笉鍋滄洿鏂拌凯浠e綋涓紝浣犻兘浼氬涔犲埌涓€浜涘钩鏃朵笉瀹规槗鎺ヨЕ鍒扮殑鐭ヨ瘑銆傜Н娌欐垚濉旓紝鎾搁亶 鐖? 鏈€鏈€鍚庢斁涓?Flutter Candies 鍏ㄥ妗讹紝鐪熼銆?/p>鐐规垜璺宠浆椤堕儴,鎴戞槸鍥哄畾鐨?/code> 杩欎釜鎸夐挳鐨勬椂鍊欙紝浣犵寽浼氬彂鐢熶粈涔堢幇璞°€?/p>
Flutter 鎸戞垬
浣犻棶鎴戠瓟
/ 浣犲嚭棰樻垜鎸戞垬
妯″潡锛屽鍔犵▼搴忓憳涔嬮棿鐨勪氦娴侊紝绋嬪簭鍛橀兘鏄笉鏈嶈緭鐨勶紝搴旇浼?馃敟 鍚э紵 鎯虫兂閮藉埡婵€銆傛垜鍒涘缓涓€涓柊鐨?FlutterChallenges qq 缇?321954965 鏉ヨ繘琛屼氦娴侊紱浠撳簱锛岀敤鏉ヨ璁哄拰瀛樻斁杩欎簺灏忔寫鎴樹唬鐮併€傚钩鏃舵敹闆嗕竴浜涘钩鏃舵湁涓€浜涢毦搴︾殑瀹為檯鍦烘櫙渚嬪瓙锛屼笉鍗曞崟鍙槸绉€鎶€鏈€傝繘缇ら渶瑕侀€氳繃鎺ㄨ崘鎴栬€呴獙璇侊紝娆㈣繋鍠滄鎶樿吘鑷繁鐨勭闉?br>
銆?/p>
缇庡洟楗夸簡涔堢偣椁愰〉闈?/h3>
NestedScrollView
锛屾垜鎯冲簲璇ユ湁鍔炴硶鏉ュ仛杩欑鍔熻兘浜嗐€?/p>
澧炲ぇ鐐瑰嚮鍖哄煙
pubspec.yaml
涓?娣诲姞璐㈢粡榫欏ぇ浣殑 oktoast
銆?/p>
oktoast: any
Stack
鎶婃暣涓?Item
閲嶅啓銆?/li>
缁撹
Flutter
婧愮爜涓嶅啀鏄ⅵ鎯炽€?/p>
Flutter
锛岀埍绯栨灉
锛屾杩庡姞鍏Flutter Candies]