当前位置: 首页>数据库>正文

Flutter 重识 NestedScrollView

鍓嶈█

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.

灏嗗閮ㄦ粴鍔?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>
  1. 鍦?ScrollPosition attach 鐨勬椂鍊欏幓閫氳繃 context 鎵惧埌杩欎釜鍒楄〃鎵€瀵瑰簲鐨勬爣蹇楋紝璺?TabbarView 鎴栬€?PageView 鐨?index 鍏宠仈杩涜瀵规瘮銆?br> Flutter 鎵╁睍NestedScrollView 锛堜簩锛夊垪琛ㄦ粴鍔ㄥ悓姝ヨВ鍐?(juejin.cn)

  2. 閫氳繃璁$畻鍒楄〃鐨勭浉瀵逛綅缃紝鏉ョ‘瀹氬綋鍓?鏄剧ず 鐨勫垪琛ㄣ€?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>

  1. RenderViewport.performLayout

performLayout 鏂规硶涓绠楀嚭褰撳墠 ScrollPosition 鐨勬渶灏忔渶澶у€?/p>

     if (offset.applyContentDimensions(
              math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
              math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
           ))
  1. 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;
  }
  1. 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;
}
  1. 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>

  1. 铏界劧鎴戜滑鍦?drag 鎿嶄綔鐨勬椂鍊欙紝纭疄鍙互鍒ゆ柇鍒拌皝鏄縺娲荤殑锛屼絾鏄墜鎸?up 锛屽紑濮嬫儻鎬ф粦鍔ㄧ殑鏃跺€欙紝dragCancelCallback 鍥炶皟宸茬粡瑙﹀彂锛?code>_isActived 宸茬粡琚缃负 false 銆?/li>
  2. 褰撴垜浠湪鎿嶄綔 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>

  1. like_button | Flutter Package (flutter-io.cn)

  2. extended_image_library | Flutter Package (pub.dev)

  3. extended_nested_scroll_view | Flutter Package (flutter-io.cn)

  4. extended_text | Flutter Package (flutter-io.cn)

  5. extended_text_field | Flutter Package (flutter-io.cn)

  6. extended_image | Flutter Package (flutter-io.cn)

  7. extended_sliver | Flutter Package (flutter-io.cn)

  8. pull_to_refresh_notification | Flutter Package (flutter-io.cn)

  9. waterfall_flow | Flutter Package (flutter-io.cn)

  10. loading_more_list | Flutter Package (flutter-io.cn)

  11. extended_tabs | Flutter Package (flutter-io.cn)

  12. http_client_helper | Dart Package (flutter-io.cn)

  13. extended_text_library | Flutter Package (flutter-io.cn)

  14. extended_list | Flutter Package (flutter-io.cn)

  15. extended_list_library | Flutter Package (flutter-io.cn)

  16. ff_annotation_route_library | Flutter Package (flutter-io.cn)

  17. loading_more_list_library | Dart Package (flutter-io.cn)

  18. ff_annotation_route | Dart Package (flutter-io.cn)

  19. ff_annotation_route_core | Dart Package (flutter-io.cn)

  20. flex_grid | Flutter Package (flutter-io.cn)

  21. assets_generator | Dart Package (flutter-io.cn)

  22. 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)

鐣欎釜闂锛屽綋浣犵偣鍑?鐐规垜璺宠浆椤堕儴,鎴戞槸鍥哄畾鐨?/code> 杩欎釜鎸夐挳鐨勬椂鍊欙紝浣犵寽浼氬彂鐢熶粈涔堢幇璞°€?/p>

Flutter 鎸戞垬

涔嬪墠璺熸帢閲戝畼鏂规彁杩囷紝鏄惁鍙互澧炲姞 浣犻棶鎴戠瓟/ 浣犲嚭棰樻垜鎸戞垬 妯″潡锛屽鍔犵▼搴忓憳涔嬮棿鐨勪氦娴侊紝绋嬪簭鍛橀兘鏄笉鏈嶈緭鐨勶紝搴旇浼?馃敟 鍚э紵 鎯虫兂閮藉埡婵€銆傛垜鍒涘缓涓€涓柊鐨?FlutterChallenges qq 缇?321954965 鏉ヨ繘琛屼氦娴侊紱浠撳簱锛岀敤鏉ヨ璁哄拰瀛樻斁杩欎簺灏忔寫鎴樹唬鐮併€傚钩鏃舵敹闆嗕竴浜涘钩鏃舵湁涓€浜涢毦搴︾殑瀹為檯鍦烘櫙渚嬪瓙锛屼笉鍗曞崟鍙槸绉€鎶€鏈€傝繘缇ら渶瑕侀€氳繃鎺ㄨ崘鎴栬€呴獙璇侊紝娆㈣繋鍠滄鎶樿吘鑷繁鐨勭闉?br> 銆?/p>

鎯呬汉鑺?+ 涓冨 杩欐槸涓嶆槸涓阀鍚?锛燂紵

缇庡洟楗夸簡涔堢偣椁愰〉闈?/h3>

瑕佹眰:

  1. 宸﹀彸2涓垪琛ㄨ兘鑱斿姩锛屾暣涓椤典笂涓嬫粴鍔ㄨ仈鍔?/li>
  2. 閫氱敤鎬э紝鍙垚缁勪欢

濡傛灉浣犺鐪熺湅瀹屼簡 NestedScrollView锛屾垜鎯冲簲璇ユ湁鍔炴硶鏉ュ仛杩欑鍔熻兘浜嗐€?/p>

澧炲ぇ鐐瑰嚮鍖哄煙

澧炲姞鐐瑰嚮鍖哄煙锛岃繖搴旇鏄钩鏃跺簲璇ヤ細閬囧埌鐨勯渶姹傦紝閭d箞鍦?Flutter 涓簲璇ユ€庝箞瀹炵幇鍛紵

鍘熷浠g爜鍦板潃: 澧炲ぇ鐐瑰嚮鍖哄煙 (github.com)

涓轰簡娴嬭瘯鏂逛究锛岃娣诲姞鍦?pubspec.yaml 涓?娣诲姞璐㈢粡榫欏ぇ浣殑 oktoast 銆?/p>

  oktoast: any

瑕佹眰:

  1. 涓嶈鏀瑰彉鏁翠釜缁撴瀯鍜屽昂瀵搞€?/li>
  2. 涓嶈鐩存帴 Stack 鎶婃暣涓?Item 閲嶅啓銆?/li>
  3. 閫氱敤鎬с€?/li>

瀹屾垚鏁堟灉濡備笅, 鎵╁ぇ鐨勮寖鍥寸悊璁轰笂鍙互闅忔剰璁剧疆銆?/p>

缁撹

杩欑瘒鍐欑殑姣旇緝澶氾紝鎯冲埌浜嗕粈涔堝氨鍐欍€備笉绠℃槸浠€涔堟妧鏈紝鍙湁娣卞叆浜嗘墠鑳介浼氬叾涓殑閬撶悊銆傜淮鎶ゅ紑婧愮粍浠讹紝纭疄鏄竴浠跺緢绱殑浜嬫儏銆備絾鏄繖浼氫笉鏂己杩綘鍘诲涔狅紝鍦ㄤ笉鍋滄洿鏂拌凯浠e綋涓紝浣犻兘浼氬涔犲埌涓€浜涘钩鏃朵笉瀹规槗鎺ヨЕ鍒扮殑鐭ヨ瘑銆傜Н娌欐垚濉旓紝鎾搁亶 Flutter 婧愮爜涓嶅啀鏄ⅵ鎯炽€?/p>

鐖?Flutter锛岀埍绯栨灉锛屾杩庡姞鍏Flutter Candies]

鏈€鏈€鍚庢斁涓?Flutter Candies 鍏ㄥ妗讹紝鐪熼銆?/p>


https://www.xamrdz.com/database/65y1994073.html

相关文章: