半夜睡不着觉,把心情写成代码,只好到这里水一篇bug html
NestedScrollView Issue NestedScrollView里面有2个Scroll Control,一个outer(header),一个是inner(body),当inner里面有PageView/TabBarView,而且每一个page被缓存(AutomaticKeepAliveClientMixin or PageStorageKey)的,滑动inner会对所有的列表都有影响git
以前经过key的方式来判断哪一个一个列表是当前可视区域里面激活的,让NestedScrollView滑动只对它有影响,以前的解决方案。github
其实我一开始就想知道怎么知道一个widget是否是在可视区域,日夜苦读,终于找到个可行的方案来优美的解决这个问题。面试
文字图代码会比较多。建议准备好瓜子水。边看边吃。。缓存
/// 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
/// 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咱们还将遇到如下问题:
为此我再次使用了熟悉的好东西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.
答案是不够的,由于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();
});
}
复制代码
错了,咱们忘记考虑padding和margin.
好比我给TabBarView的每一个页面的List加了个一个PaddingEdgeInsets.only(left: 190.0),
,让咱们看看会有什么效果。
解决方式以下: 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);
复制代码
忘记考虑多个TabBarView/PageView对结果的影响
可是其实上,好比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的相对位置符合
打印结果也证实了这点:
没有,localToGlobal这个方法,在一种状况下会报错。
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, 设置为不激活状态就行了。。
能够,可是我还想说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.");
}
复制代码
我只考虑了NestedScrollView滚动方向是垂直并且PageView/TabBarView是水平滚动的状况.
若是你有啥子妖魔鬼怪的布局,你能够试试老的extended_nested_scroll_view
最后放上 Github extended_nested_scroll_view,若是你有什么不明白的地方,请告诉我。