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

半夜睡不着覺,把心情寫成代碼,只好到這裏水一篇bug html

FlutterCandies QQ羣:181398081 提及來這些東西,實際上是一個怨念,從一個issue開始。

NestedScrollView Issue NestedScrollView裏面有2個Scroll Control,一個outer(header),一個是inner(body),當inner裏面有PageView/TabBarView,而且每一個page被緩存(AutomaticKeepAliveClientMixin or PageStorageKey)的,滑動inner會對所有的列表都有影響git

以前經過key的方式來判斷哪一個一個列表是當前可視區域裏面激活的,讓NestedScrollView滑動只對它有影響,以前的解決方案github

其實我一開始就想知道怎麼知道一個widget是否是在可視區域,日夜苦讀,終於找到個可行的方案來優美的解決這個問題。面試

文字圖代碼會比較多。建議準備好瓜子水。邊看邊吃。。緩存

我找到的第一個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的距離,下面是主要用法markdown

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是否進入可視區域的app

這是一個新的發現,嚇的我趕快在TabBarView裏面試了一下。。結果。。。ide

這個方法能判斷出每一個Tab相對於本身PageView/TabBarView可視區域的相對位置。經過判斷PageView/TabBarView的position.pixels 與offsetToRevealLeading是否相等,來判斷當前激活的Tab,可是當有多個PageView/TabBarView的時候。你就搞不清楚究竟是哪一個算是激活的,由於你須要先判斷父PageView/TabBarView是否激活,而後纔是子PageView/TabBarViewsvg

由於暫時沒發現有什麼好的方法區分,只是先暫時放棄,若是你有好的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)的相對位置。。結果以下佈局

你能看出來什麼嗎? 哇塞,跟我想的同樣,完美,用一個圖表示爲

這看起來是一條路。。

如今咱們回到最上面那個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監聽PageMetrics,而且在Page changed時候通知去計算當前在可視區域的NestedPosition.

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

答案是不夠的,由於ScrollEndNotification的時機仍是不足夠精確,致使會出現0.4,0.9之類的偏差。。

解決方法:

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

2.最後在結算與0的相比的值的時候作了個偏差計算(由於不一樣Page的差至少爲一個屏幕的差距,因此1的偏差是能夠忍受的)

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.

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

那咱們怎麼處理這個問題呢?從緣由上面看經過_NestedScrollPosition的context獲得的RenderBox只是這個List的RenderBox的區域,它跟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對結果的影響

爲啥會出現這種狀況呢? 由於開始我是使用的從ScrollEndNotification的Context計算出來的RenderBox,注意這個是無論你是哪一個TabBarView/PageView的Page發生變化的,

可是其實上,好比Tab0切換到Tab1的時候。你應該關心的是Tab1 下面的Tab10,Tab11,Tab12,Tab13的狀態,Tab0下面應該都是不激活的.

其實咱們應該還要找到_NestedScrollPosition所對應的PageView/TabBarView,計算_NestedScrollPosition和PageView/TabBarView的相對位置。

因此判斷_NestedScrollPosition是否爲當前可視區域的激活的條件應該以下:

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

2._NestedScrollPosition對應的PageView/TabBarView的RenderBox跟_NestedScrollPosition的RenderBox的相對位置符合

打印結果也證實了這點:

5.結束了??

沒有,localToGlobal這個方法,在一種狀況下會報錯。

進入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;
  }
複製代碼

這裏可能會觸發

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

分析:說明你提供的ancestor 跟_NestedScrollPosition 沒有關聯,這時候咱們直接try catch, 設置爲不激活狀態就行了。。

6.應該能夠睡覺了吧

能夠,可是我還想說2點。

1.若是當計算以後,有超過2個的nestedPositions,請告訴我一下,看看你那個複雜的case是啥(實際上,demo裏面栗子已是很複雜的了)

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,若是你有什麼不明白的地方,請告訴我。

pub package

相關文章
相關標籤/搜索