Flutter 你想知道的Widget可視區(qū)域,相對位置,大小

半夜睡不著覺畜挨,把心情寫成代碼蝌蹂,只好到這里水一篇bug


image

FlutterCandies QQ群:181398081
說起來這些東西,其實是一個怨念,從一個issue開始临扮。

NestedScrollView Issue NestedScrollView里面有2個Scroll Control,一個outer(header),一個是inner(body)论矾,當inner里面有PageView/TabBarView,并且每個page被緩存(AutomaticKeepAliveClientMixin or PageStorageKey)的杆勇,滑動inner會對全部的列表都有影響

之前通過key的方式來判斷哪個一個列表是當前可視區(qū)域里面激活的贪壳,讓NestedScrollView滑動只對它有影響,之前的解決方案蚜退。

其實我一開始就想知道怎么知道一個widget是不是在可視區(qū)域闰靴,日夜苦讀,終于找到個可行的方案來優(yōu)美的解決這個問題钻注。

文字圖代碼會比較多蚂且。建議準備好瓜子水。邊看邊吃幅恋。杏死。

我找到的第一個API是getOffsetToReveal

 /// The optional `rect` parameter describes which area of that `target` object
  /// should be revealed in the viewport. If `rect` is null, the entire
  /// `target` [RenderObject] (as defined by its [RenderObject.paintBounds])
  /// will be revealed. If `rect` is provided it has to be given in the
  /// coordinate system of the `target` object.
  ///
  /// The `alignment` argument describes where the target should be positioned
  /// after applying the returned offset. If `alignment` is 0.0, the child must
  /// be positioned as close to the leading edge of the viewport as possible. If
  /// `alignment` is 1.0, the child must be positioned as close to the trailing
  /// edge of the viewport as possible. If `alignment` is 0.5, the child must be
  /// positioned as close to the center of the viewport as possible.
  ///
  /// The target might not be a direct child of this viewport but it must be a
  /// descendant of the viewport and there must not be any other
  /// [RenderAbstractViewport] objects between the target and this object.
  ///
  /// This method assumes that the content of the viewport moves linearly, i.e.
  /// when the offset of the viewport is changed by x then `target` also moves
  /// by x within the viewport.
  ///
  /// See also:
  ///
  ///  * [RevealedOffset], which describes the return value of this method.
  RevealedOffset getOffsetToReveal(RenderObject target, double alignment, {Rect rect});

簡單說下,就是獲得目標RenderOject跟Viewport的距離,下面是主要用法

        RenderAbstractViewport viewport =
                RenderAbstractViewport.of(renderObject);

            /// Distance between top edge of screen and MyWidget bottom edge
            var offsetToRevealLeading =
                viewport.getOffsetToReveal(renderObject, 0.0);

            /// Distance between bottom edge of screen and MyWidget top edge
            var offsetToRevealTrailingEdge =
                viewport.getOffsetToReveal(renderObject, 1.0);

demo地址See your widget demo, demo中展示了怎么判斷一個ListView里面一個Widget是否進入可視區(qū)域的

這是一個新的發(fā)現(xiàn)佳遣,嚇的我趕快在TabBarView里面試了一下识埋。凡伊。結(jié)果零渐。。系忙。

image

這個方法能判斷出每個Tab相對于自己PageView/TabBarView可視區(qū)域的相對位置诵盼。通過判斷PageView/TabBarView的position.pixels 與offsetToRevealLeading是否相等,來判斷當前激活的Tab,但是當有多個PageView/TabBarView的時候银还。你就搞不清楚到底是哪個算是激活的风宁,因為你需要先判斷父PageView/TabBarView是否激活,然后才是子PageView/TabBarView

image

因為暫時沒發(fā)現(xiàn)有什么好的方法區(qū)分蛹疯,只是先暫時放棄戒财,如果你有好的idea,請告訴我捺弦,萬分感謝

后來我又找到個一個API (localToGlobal)

/// Convert the given point from the local coordinate system for this box to
  /// the global coordinate system in logical pixels.
  ///
  /// If `ancestor` is non-null, this function converts the given point to the
  /// coordinate system of `ancestor` (which must be an ancestor of this render
  /// object) instead of to the global coordinate system.
  ///
  /// This method is implemented in terms of [getTransformTo].
  Offset localToGlobal(Offset point, { RenderObject ancestor }) {
    return MatrixUtils.transformPoint(getTransformTo(ancestor), point);
  }

大概的意思是饮寞。×泻穑可以算出目標跟指定對象(ancestor)的相對位置幽崩。。結(jié)果如下

image

你能看出來什么嗎寞钥? 哇塞慌申,跟我想的一樣,完美理郑,用一個圖表示為

image

這看起來是一條路蹄溉。咨油。

現(xiàn)在我們回到最上面那個issue,想解決這個issue我們還將遇到以下問題:

1.我們需要知道什么時候TabBarView/PageView的Page改變了类缤。

為此我再次使用了熟悉的好東西NotificationListener
我的Flutter Candies當中大量使用到它

    if (widget.keepOnlyOneInnerNestedScrollPositionActive) {
      ///get notifications and compute active one in _innerController.nestedPositions
      return NotificationListener<ScrollNotification>(
          onNotification: (ScrollNotification notification) {
            if (notification is ScrollEndNotification &&
                notification.metrics is PageMetrics &&
                notification.metrics.axis == Axis.horizontal) {
              final PageMetrics metrics = notification.metrics;
              var depth = notification.depth;
              final int currentPage = metrics.page.round();
              var page = _pageMetricsList[depth];
              //ComputeActivatedNestedPosition only when page changed
              if (page != currentPage) {
                print("Page changed ${currentPage}");
                _coordinator._innerController
                    ._computeActivatedNestedPosition(notification);
              }
              _pageMetricsList[depth] = currentPage;
            }
            return false;
          },
          child: child);

使用NotificationListener監(jiān)聽PageMetrics臼勉,并且在Page changed時候通知去計算當前在可視區(qū)域的NestedPosition.

2.只用localToGlobal 這個玩意就足夠了嗎?餐弱?

答案是不夠的,因為ScrollEndNotification的時機還是不足夠精確,導(dǎo)致會出現(xiàn)0.4宴霸,0.9之類的誤差。膏蚓。


image

解決方法:

1.加了一個100 milliseconds的延遲來執(zhí)行計算

2.最后在結(jié)算與0的相比的值的時候做了個誤差計算(因為不同Page的差至少為一個屏幕的差距瓢谢,所以1的誤差是可以忍受的)


image
void _computeActivatedNestedPosition(ScrollNotification notification,
      {Duration delay: const Duration(milliseconds: 100)}) {
    ///if layout is not completed, the data will has some gap.
    ///need more accurate time to compute
    ///delay it in case.
    ///to do
    Future.delayed(delay, () {
      /// this is the page changed of PageView's renderBox,
      /// it maybe not the renderBox of [nestedPositions]
      /// because it maybe has more one tabbarview or pageview in NestedScrollView body
      final RenderBox pageChangedRenderBox =
          notification.context.findRenderObject();
      int activeCount = 0;
      nestedPositions.forEach((item) {
        item._computeActived(pageChangedRenderBox);
        if (item._isActived) activeCount++;
      });

      if (activeCount > 1) {
        print(
            "activeCount more than 1, please report to zmtzawqlp@live.com and show your case.");
      }

      coordinator.updateCanDrag();
    });
  }

3.你以為這樣就可以搞定了嗎?

錯了驮瞧,我們忘記考慮padding和margin.


image

比如我給TabBarView的每個頁面的List加了個一個PaddingEdgeInsets.only(left: 190.0),氓扛,讓我們看看會有什么效果。

image

那我們怎么處理這個問題呢论笔?從原因上面看通過_NestedScrollPosition的context得到的RenderBox只是這個List的RenderBox的區(qū)域采郎,它跟PageView/TabBarView的RenderBox的相對位置不一定總會存在offset.x為0的狀況,就像上面加了padding和margin一樣

解決方式如下:
position 是List跟PageView/TabBarView的相對位置
size 是List跟PageView/TabBarView 大小的差距

通過這樣的計算就能抵消padding和margin的影響狂魔,當然我這里沒有再考慮transform這種東西了蒜埋。。放過我吧最楷。整份。

順手送個Size的獲取方式,RenderBox 有個Size屬性

  final Offset position = child.localToGlobal(Offset.zero, ancestor: parent);
    ///remove the margin/padding
    final Offset size = Offset(parentSize.width - child.size.width,
        parentSize.height - child.size.height);

    ///if layout is not completed, the data will has some gap.
    ///need more accurate time to compute
    ///to do
    bool childIsActivedInViewport = ((position.dx - size.dx).abs() < 1 &&
        (position.dy - size.dy).abs() < 1);

4.完美籽孙,perfect烈评,beautiful?犯建?

忘記考慮多個TabBarView/PageView對結(jié)果的影響


image
image

為啥會出現(xiàn)這種情況呢讲冠? 因為開始我是使用的從ScrollEndNotification的Context計算出來的RenderBox,注意這個是不管你是哪個TabBarView/PageView的Page發(fā)生變化的适瓦,

但是其實上竿开,比如Tab0切換到Tab1的時候。你應(yīng)該關(guān)心的是Tab1 下面的Tab10犹菇,Tab11德迹,Tab12,Tab13的狀態(tài)揭芍,Tab0下面應(yīng)該都是不激活的.

其實我們應(yīng)該還要找到_NestedScrollPosition所對應(yīng)的PageView/TabBarView胳搞,計算_NestedScrollPosition和PageView/TabBarView的相對位置。

所以判斷_NestedScrollPosition是否為當前可視區(qū)域的激活的條件應(yīng)該如下:

1.ScrollEndNotification的RenderBox和_NestedScrollPosition的RenderBox的相對位置符合

2._NestedScrollPosition對應(yīng)的PageView/TabBarView的RenderBox跟_NestedScrollPosition的RenderBox的相對位置符合

打印結(jié)果也證明了這點:


image

5.結(jié)束了?肌毅?

沒有筷转,localToGlobal這個方法,在一種情況下會報錯悬而。

image

進入localToGlobal中呜舒,再進去getTransformTo

 Matrix4 getTransformTo(RenderObject ancestor) {
    assert(attached);
    if (ancestor == null) {
      final AbstractNode rootNode = owner.rootNode;
      if (rootNode is RenderObject)
        ancestor = rootNode;
    }
    final List<RenderObject> renderers = <RenderObject>[];
    for (RenderObject renderer = this; renderer != ancestor; renderer = renderer.parent) {
      assert(renderer != null); // Failed to find ancestor in parent chain.
      renderers.add(renderer);
    }
    final Matrix4 transform = Matrix4.identity();
    for (int index = renderers.length - 1; index > 0; index -= 1)
      renderers[index].applyPaintTransform(renderers[index - 1], transform);
    return transform;
  }

這里可能會觸發(fā)

assert(renderer != null); // Failed to find ancestor in parent

分析:說明你提供的ancestor 跟_NestedScrollPosition 沒有關(guān)聯(lián),這時候我們直接try catch, 設(shè)置為不激活狀態(tài)就好了笨奠。袭蝗。

6.應(yīng)該可以睡覺了吧

可以,但是我還想說2點般婆。

1.如果當計算之后到腥,有超過2個的nestedPositions,請告訴我一下蔚袍,看看你那個復(fù)雜的case是啥(實際上乡范,demo里面栗子已經(jīng)是很復(fù)雜的了)

 int activeCount = 0;
    nestedPositions.forEach((item) {
      item._computeActived(pageChangedRenderBox);
      if (item._isActived) activeCount++;
    });

    if (activeCount > 1) {
      print(
          "activeCount more than 1, please report to zmtzawqlp@live.com and show your case.");
    }

2.extended_nested_scroll_view

我只考慮了NestedScrollView滾動方向是垂直而且PageView/TabBarView是水平滾動的情況.

如果你有啥子妖魔鬼怪的布局,你可以試試老的extended_nested_scroll_view

最后放上 Github extended_nested_scroll_view啤咽,如果你有什么不明白的地方晋辆,請告訴我。

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末宇整,一起剝皮案震驚了整個濱河市瓶佳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌没陡,老刑警劉巖涩哟,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件索赏,死亡現(xiàn)場離奇詭異盼玄,居然都是意外死亡,警方通過查閱死者的電腦和手機潜腻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門埃儿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人融涣,你說我怎么就攤上這事童番。” “怎么了威鹿?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵剃斧,是天一觀的道長。 經(jīng)常有香客問我忽你,道長幼东,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮根蟹,結(jié)果婚禮上脓杉,老公的妹妹穿的比我還像新娘。我一直安慰自己简逮,他們只是感情好球散,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著散庶,像睡著了一般蕉堰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上悲龟,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天嘁灯,我揣著相機與錄音,去河邊找鬼躲舌。 笑死丑婿,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的没卸。 我是一名探鬼主播羹奉,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼约计!你這毒婦竟也來了诀拭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤煤蚌,失蹤者是張志新(化名)和其女友劉穎耕挨,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尉桩,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡筒占,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蜘犁。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片翰苫。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖这橙,靈堂內(nèi)的尸體忽然破棺而出奏窑,到底是詐尸還是另有隱情,我是刑警寧澤屈扎,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布埃唯,位于F島的核電站,受9級特大地震影響鹰晨,放射性物質(zhì)發(fā)生泄漏墨叛。R本人自食惡果不足惜滑沧,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望巍实。 院中可真熱鬧滓技,春花似錦、人聲如沸棚潦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽丸边。三九已至叠必,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間妹窖,已是汗流浹背纬朝。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留骄呼,地道東北人共苛。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像蜓萄,于是被迫代替她去往敵國和親隅茎。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容

  • 感恩父母給予我生命嫉沽,并且把我健康的撫養(yǎng)長大辟犀,祝福爸爸媽媽身體健康,長命百歲绸硕,每天都能開開心心堂竟,感恩母親節(jié)馬上到來,...
    危志霞閱讀 164評論 0 0
  • 今天碩碩自己拿書閱讀玻佩,沒用我管自己主動看的書出嘹,孩子真是越來越懂事了。
    萍水相逢_05b5閱讀 187評論 0 0
  • 中信書院閱讀 158評論 0 1
  • 中午夺蛇,三個朋友小聚疚漆。 性別都是女酣胀,加起來一百二十多歲了刁赦。 新年的歡欣氣氛尚未褪盡,三個中年姑娘的聚會闻镶,卻帶了一丟丟...
    天涯17閱讀 153評論 2 2