半夜睡不著覺畜挨,把心情寫成代碼蝌蹂,只好到這里水一篇bug
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é)果零渐。。系忙。
這個方法能判斷出每個Tab相對于自己PageView/TabBarView可視區(qū)域的相對位置诵盼。通過判斷PageView/TabBarView的position.pixels 與offsetToRevealLeading是否相等,來判斷當前激活的Tab,但是當有多個PageView/TabBarView的時候银还。你就搞不清楚到底是哪個算是激活的风宁,因為你需要先判斷父PageView/TabBarView是否激活,然后才是子PageView/TabBarView
因為暫時沒發(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é)果如下
你能看出來什么嗎寞钥? 哇塞慌申,跟我想的一樣,完美理郑,用一個圖表示為
這看起來是一條路蹄溉。咨油。
現(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之類的誤差。膏蚓。
解決方法:
1.加了一個100 milliseconds的延遲來執(zhí)行計算
2.最后在結(jié)算與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的區(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é)果的影響
為啥會出現(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é)果也證明了這點:
5.結(jié)束了?肌毅?
沒有筷转,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;
}
這里可能會觸發(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.");
}
我只考慮了NestedScrollView滾動方向是垂直而且PageView/TabBarView是水平滾動的情況.
如果你有啥子妖魔鬼怪的布局,你可以試試老的extended_nested_scroll_view
最后放上 Github extended_nested_scroll_view啤咽,如果你有什么不明白的地方晋辆,請告訴我。