本来想跳过真个这个直接去说一下listview还有GridView的属性,但是想着既然是学习就不能只为了怎么用他而学习,应该是通过看源码学习他的思想和原理.
先来看一下ScrollView的构造方法
const ScrollView({
Key key,
///Scrollable属性,用来设置滑动的主轴方向
this.scrollDirection = Axis.vertical,
///是否按照阅读方向相反的方向滑动
this.reverse = false,
///Scrollable属性,控制器用来监听滚动和设置滚动距离
this.controller,
/// 指是否使用widget树中默认的PrimaryScrollController;当滑动方向为垂直方向
///(scrollDirection值为Axis.vertical)并且没有指定controller时,primary默认为true
///看到 属性介绍primary 如果为真的时候即使他没有足够的高度来实际滚动他也会滚动,
///但是要求 controller 为 null ,但是我哦试验了一下没有作用
bool primary,
///Scrollable 属性,完成拖拽后的动画响应
ScrollPhysics physics,
///如果滚动视图不收缩换行,则滚动视图将展开到scrollDirection中允许的最大大小。
///如果滚动视图在scrollDirection中具有无限约束,则shrinkWrap必须为true
/// 貌似这个属性可以解决listview嵌套的问题,但是这样更为消耗性能
this.shrinkWrap = false,
this.center,
///当scrollOffset = 0,第一个child在viewport的位置(0 <= anchor <= 1.0),
///0.0在开始,1.0在尾部,0.5在中间,只有
this.anchor = 0.0,
///缓存区域大小
this.cacheExtent,
///Scrollable 属性 语义子集数
this.semanticChildCount,
///Scrollable 属性,开始响应拖拽的时机
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
})
结合ListView 学习 ScrollView,
查看ScrollView你会发现他是一个抽象类
abstract class ScrollView extends StatelessWidget {
@protected
List<Widget> buildSlivers(BuildContext context);
}
通过原来的学习我们应该知道所有的控件在build方法里面构建的
我们先来看一下ScrollView 的build方法,
@override
Widget build(BuildContext context) {
final List<Widget> slivers = buildSlivers(context);
final AxisDirection axisDirection = getDirection(context);
final ScrollController scrollController =
primary ? PrimaryScrollController.of(context) : controller;
final Scrollable scrollable = Scrollable(
dragStartBehavior: dragStartBehavior,
axisDirection: axisDirection,
controller: scrollController,
physics: physics,
semanticChildCount: semanticChildCount,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return buildViewport(context, offset, axisDirection, slivers);
},
);
final Widget scrollableResult = primary && scrollController != null
? PrimaryScrollController.none(child: scrollable)
: scrollable;
if (keyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) {
return NotificationListener<ScrollUpdateNotification>(
child: scrollableResult,
onNotification: (ScrollUpdateNotification notification) {
final FocusScopeNode focusScope = FocusScope.of(context);
if (notification.dragDetails != null && focusScope.hasFocus) {
focusScope.unfocus();
}
return false;
},
);
} else {
return scrollableResult;
}
}
buildSlivers 这个方法被ScrollView 的子类BoxScrollView的实现了,但是BoxScrollView 也是一个抽象类
abstract class BoxScrollView extends ScrollView {
@protected
Widget buildChildLayout(BuildContext context);
}
@override
List<Widget> buildSlivers(BuildContext context) {
Widget sliver = buildChildLayout(context);
EdgeInsetsGeometry effectivePadding = padding;
if (padding == null) {
final MediaQueryData mediaQuery = MediaQuery.of(context, nullOk: true);
if (mediaQuery != null) {
// Automatically pad sliver with padding from MediaQuery.
final EdgeInsets mediaQueryHorizontalPadding =
mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0);
final EdgeInsets mediaQueryVerticalPadding =
mediaQuery.padding.copyWith(left: 0.0, right: 0.0);
// Consume the main axis padding with SliverPadding.
effectivePadding = scrollDirection == Axis.vertical
? mediaQueryVerticalPadding
: mediaQueryHorizontalPadding;
// Leave behind the cross axis padding.
sliver = MediaQuery(
data: mediaQuery.copyWith(
padding: scrollDirection == Axis.vertical
? mediaQueryHorizontalPadding
: mediaQueryVerticalPadding,
),
child: sliver,
);
}
}
if (effectivePadding != null)
sliver = SliverPadding(padding: effectivePadding, sliver: sliver);
return <Widget>[ sliver ];
}
BoxScrollView 将widget 封装成了Sliver
通过ListView 我发现这个方法被实现了
@override
Widget buildChildLayout(BuildContext context) {
if (itemExtent != null) {
return SliverFixedExtentList(
delegate: childrenDelegate,
itemExtent: itemExtent,
);
}
return SliverList(delegate: childrenDelegate);
}
好好分析一下这个过程,
1.ScrollView 想要构建就必须需要子类实现buildSlivers提供Slivers,
2.他的子类BoxScrollView 实现了buildSlivers 方法,在创建buildSlivers过程需要将Widget封装到Slivers里面,
3.Slivers构建需要子类提供Widget,再去查看ListView 的buildChildLayout 方法时发现这个widget是由childrenDelegate构造的SliverList这个提供的,childrenDelegate是在ListView 构造方法哪里初始化的,
class SliverList extends SliverMultiBoxAdaptorWidget {
/// Creates a sliver that places box children in a linear array.
const SliverList({
Key key,
@required SliverChildDelegate delegate,
}) : super(key: key, delegate: delegate);
@override
RenderSliverList createRenderObject(BuildContext context) {
final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement;
return RenderSliverList(childManager: element);
}
}
4.但是他们绘制方法在RenderSliverList 这个里面,在这个RenderSliverList 类中有一个performLayout这个方法,负责在顶部创建更多子级如果有必要的话,再沿着列表进行更新和布局
每一个孩子,如果有必要的话在最后加上更多,直到我们有足够的覆盖整个视口的子对象。
但是这里好多变量不知道他是什么意思,所以就理解不到他到底为什么这么做,只就看懂了一部分逻辑,从网上找的也是很久以前的源码解析了,Flutter 的代码更新的还是比较快的,不过能看出来这里包含回收和缓存,
@override
void performLayout() {
final SliverConstraints constraints = this.constraints;
childManager.didStartLayout();
childManager.setDidUnderflow(false);
final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
assert(scrollOffset >= 0.0);
final double remainingExtent = constraints.remainingCacheExtent;
assert(remainingExtent >= 0.0);
final double targetEndScrollOffset = scrollOffset + remainingExtent;
final BoxConstraints childConstraints = constraints.asBoxConstraints();
int leadingGarbage = 0;
int trailingGarbage = 0;
bool reachedEnd = false;
// This algorithm in principle is straight-forward: find the first child
// that overlaps the given scrollOffset, creating more children at the top
// of the list if necessary, then walk down the list updating and laying out
// each child and adding more at the end if necessary until we have enough
// children to cover the entire viewport.
//
// It is complicated by one minor issue, which is that any time you update
// or create a child, it's possible that the some of the children that
// haven't yet been laid out will be removed, leaving the list in an
// inconsistent state, and requiring that missing nodes be recreated.
//
// To keep this mess tractable, this algorithm starts from what is currently
// the first child, if any, and then walks up and/or down from there, so
// that the nodes that might get removed are always at the edges of what has
// already been laid out.
// Make sure we have at least one child to start from.
if (firstChild == null) {
if (!addInitialChild()) {
// There are no children.
geometry = SliverGeometry.zero;
childManager.didFinishLayout();
return;
}
}
// We have at least one child.
// These variables track the range of children that we have laid out. Within
// this range, the children have consecutive indices. Outside this range,
// it's possible for a child to get removed without notice.
RenderBox leadingChildWithLayout, trailingChildWithLayout;
RenderBox earliestUsefulChild = firstChild;
// A firstChild with null layout offset is likely a result of children
// reordering.
//
// We rely on firstChild to have accurate layout offset. In the case of null
// layout offset, we have to find the first child that has valid layout
// offset.
if (childScrollOffset(firstChild) == null) {
int leadingChildrenWithoutLayoutOffset = 0;
while (childScrollOffset(earliestUsefulChild) == null) {
earliestUsefulChild = childAfter(firstChild);
leadingChildrenWithoutLayoutOffset += 1;
}
// We should be able to destroy children with null layout offset safely,
// because they are likely outside of viewport
collectGarbage(leadingChildrenWithoutLayoutOffset, 0);
assert(firstChild != null);
}
// Find the last child that is at or before the scrollOffset.
earliestUsefulChild = firstChild;
for (double earliestScrollOffset = childScrollOffset(earliestUsefulChild);
earliestScrollOffset > scrollOffset;
earliestScrollOffset = childScrollOffset(earliestUsefulChild)) {
// We have to add children before the earliestUsefulChild.
earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
if (earliestUsefulChild == null) {
final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
if (scrollOffset == 0.0) {
// insertAndLayoutLeadingChild only lays out the children before
// firstChild. In this case, nothing has been laid out. We have
// to lay out firstChild manually.
firstChild.layout(childConstraints, parentUsesSize: true);
earliestUsefulChild = firstChild;
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
break;
} else {
// We ran out of children before reaching the scroll offset.
// We must inform our parent that this sliver cannot fulfill
// its contract and that we need a scroll offset correction.
geometry = SliverGeometry(
scrollOffsetCorrection: -scrollOffset,
);
return;
}
}
final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild);
// firstChildScrollOffset may contain double precision error
if (firstChildScrollOffset < -precisionErrorTolerance) {
// The first child doesn't fit within the viewport (underflow) and
// there may be additional children above it. Find the real first child
// and then correct the scroll position so that there's room for all and
// so that the trailing edge of the original firstChild appears where it
// was before the scroll offset correction.
// TODO(hansmuller): do this work incrementally, instead of all at once,
// i.e. find a way to avoid visiting ALL of the children whose offset
// is < 0 before returning for the scroll correction.
double correction = 0.0;
while (earliestUsefulChild != null) {
assert(firstChild == earliestUsefulChild);
correction += paintExtentOf(firstChild);
earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
}
earliestUsefulChild = firstChild;
if ((correction - earliestScrollOffset).abs() > precisionErrorTolerance) {
geometry = SliverGeometry(
scrollOffsetCorrection: correction - earliestScrollOffset,
);
final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
return;
}
}
final SliverMultiBoxAdaptorParentData childParentData = earliestUsefulChild.parentData as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = firstChildScrollOffset;
assert(earliestUsefulChild == firstChild);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
}
// At this point, earliestUsefulChild is the first child, and is a child
// whose scrollOffset is at or before the scrollOffset, and
// leadingChildWithLayout and trailingChildWithLayout are either null or
// cover a range of render boxes that we have laid out with the first being
// the same as earliestUsefulChild and the last being either at or after the
// scroll offset.
assert(earliestUsefulChild == firstChild);
assert(childScrollOffset(earliestUsefulChild) <= scrollOffset);
// Make sure we've laid out at least one child.
if (leadingChildWithLayout == null) {
earliestUsefulChild.layout(childConstraints, parentUsesSize: true);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout = earliestUsefulChild;
}
// Here, earliestUsefulChild is still the first child, it's got a
// scrollOffset that is at or before our actual scrollOffset, and it has
// been laid out, and is in fact our leadingChildWithLayout. It's possible
// that some children beyond that one have also been laid out.
bool inLayoutRange = true;
RenderBox child = earliestUsefulChild;
int index = indexOf(child);
double endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
bool advance() { // returns true if we advanced, false if we have no more children
// This function is used in two different places below, to avoid code duplication.
assert(child != null);
if (child == trailingChildWithLayout)
inLayoutRange = false;
child = childAfter(child);
if (child == null)
inLayoutRange = false;
index += 1;
if (!inLayoutRange) {
if (child == null || indexOf(child) != index) {
// We are missing a child. Insert it (and lay it out) if possible.
child = insertAndLayoutChild(childConstraints,
after: trailingChildWithLayout,
parentUsesSize: true,
);
if (child == null) {
// We have run out of children.
return false;
}
} else {
// Lay out the child.
child.layout(childConstraints, parentUsesSize: true);
}
trailingChildWithLayout = child;
}
assert(child != null);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = endScrollOffset;
assert(childParentData.index == index);
endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
return true;
}
// Find the first child that ends after the scroll offset.
while (endScrollOffset < scrollOffset) {
leadingGarbage += 1;
if (!advance()) {
assert(leadingGarbage == childCount);
assert(child == null);
// we want to make sure we keep the last child around so we know the end scroll offset
collectGarbage(leadingGarbage - 1, 0);
assert(firstChild == lastChild);
final double extent = childScrollOffset(lastChild) + paintExtentOf(lastChild);
geometry = SliverGeometry(
scrollExtent: extent,
paintExtent: 0.0,
maxPaintExtent: extent,
);
return;
}
}
// Now find the first child that ends after our end.
while (endScrollOffset < targetEndScrollOffset) {
if (!advance()) {
reachedEnd = true;
break;
}
}
// Finally count up all the remaining children and label them as garbage.
if (child != null) {
child = childAfter(child);
while (child != null) {
trailingGarbage += 1;
child = childAfter(child);
}
}
// At this point everything should be good to go, we just have to clean up
// the garbage and report the geometry.
collectGarbage(leadingGarbage, trailingGarbage);
assert(debugAssertChildListIsNonEmptyAndContiguous());
double estimatedMaxScrollOffset;
if (reachedEnd) {
estimatedMaxScrollOffset = endScrollOffset;
} else {
estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
constraints,
firstIndex: indexOf(firstChild),
lastIndex: indexOf(lastChild),
leadingScrollOffset: childScrollOffset(firstChild),
trailingScrollOffset: endScrollOffset,
);
assert(estimatedMaxScrollOffset >= endScrollOffset - childScrollOffset(firstChild));
}
final double paintExtent = calculatePaintOffset(
constraints,
from: childScrollOffset(firstChild),
to: endScrollOffset,
);
final double cacheExtent = calculateCacheOffset(
constraints,
from: childScrollOffset(firstChild),
to: endScrollOffset,
);
final double targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent;
geometry = SliverGeometry(
scrollExtent: estimatedMaxScrollOffset,
paintExtent: paintExtent,
cacheExtent: cacheExtent,
maxPaintExtent: estimatedMaxScrollOffset,
// Conservative to avoid flickering away the clip during scroll.
hasVisualOverflow: endScrollOffset > targetEndScrollOffsetForPaint || constraints.scrollOffset > 0.0,
);
// We may have started the layout while scrolled to the end, which would not
// expose a new child.
if (estimatedMaxScrollOffset == endScrollOffset)
childManager.setDidUnderflow(true);
childManager.didFinishLayout();
}
虽然你看着感觉这章里面我所有的东西都是在贴源码,但是为了读懂这个过程我用了整整2个小时才弄明白这个里面的逻辑
我学习flutter的整个过程都记录在里面了
https://www.jianshu.com/c/36554cb4c804
最后附上demo 地址
网友评论