Flutter之Sliver组件扩展
Sliver主要用于处理嵌套滑动、合并多个List的情况,像GridView、ListView自身是支持滑动的,这个时候如果再在外层嵌套一个滑动的容器组件的话,如果说GridView、ListView的滑动方向与外层可滑动容器的滑动方向一致的时候,必然会引发滑动冲突的问题,这个时候我们的外层可滑动容器可以采用CustomScrollView,并配合SliverGrid、SliverList实现,查看ListView、GridView的源码可以知道,SliverGrid、SliverList相当于是GridView、ListView去除了滚动特性,即:
SliverGrid + Scrollable = GridView
SliverList + Scrollable = ListView
接下来看一个官方的GridView的实例:
CustomScrollView(
primary: false,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.all(20),
sliver: SliverGrid.count(
crossAxisSpacing: 10,
mainAxisSpacing: 10,
crossAxisCount: 2,
children: <Widget>[
Container(
padding: const EdgeInsets.all(8),
child: const Text("He'd have you all unravel at the"),
color: Colors.green[100],
),
Container(
padding: const EdgeInsets.all(8),
child: const Text('Heed not the rabble'),
color: Colors.green[200],
),
Container(
padding: const EdgeInsets.all(8),
child: const Text('Sound of screams but the'),
color: Colors.green[300],
),
Container(
padding: const EdgeInsets.all(8),
child: const Text('Who scream'),
color: Colors.green[400],
),
Container(
padding: const EdgeInsets.all(8),
child: const Text('Revolution is coming...'),
color: Colors.green[500],
),
Container(
padding: const EdgeInsets.all(8),
child: const Text('Revolution, they...'),
color: Colors.green[600],
),
],
),
),
],
)
现在我们想单独为这个SliverGrid设置圆角和背景怎么办,实现下面的效果怎么办呢:
首先我们会想到给SliverGrid套一个Container,然后为Container指定decoration来实现,这样行不行呢,试试看就知道了
CustomScrollView(
primary: false,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.all(20),
sliver: Container(
decoration: BoxDecoration(
color: Color(0xffff0000),
borderRadius: BorderRadius.all(Radius.circular(20))
),
child: SliverGrid.count(
crossAxisSpacing: 10,
mainAxisSpacing: 10,
crossAxisCount: 2,
children: <Widget>[
Container(
padding: const EdgeInsets.all(8),
child: const Text("He'd have you all unravel at the"),
color: Colors.green[100],
),
Container(
padding: const EdgeInsets.all(8),
child: const Text('Heed not the rabble'),
color: Colors.green[200],
),
Container(
padding: const EdgeInsets.all(8),
child: const Text('Sound of screams but the'),
color: Colors.green[300],
),
Container(
padding: const EdgeInsets.all(8),
child: const Text('Who scream'),
color: Colors.green[400],
),
Container(
padding: const EdgeInsets.all(8),
child: const Text('Revolution is coming...'),
color: Colors.green[500],
),
Container(
padding: const EdgeInsets.all(8),
child: const Text('Revolution, they...'),
color: Colors.green[600],
),
],
),
),
),
],
),
很明显这样是不行的,这是因为一个支持Sliver的组件他的子组件对应的RenderObject必须要是一个RenderSliver对象,而Container内部在处理decoration属性的时候是通过DecoratedBox来处理的,而DecoratedBox本身对应的RenderObject是一个RenderBox对象。
然而google官方并没有为我们提供一个支持Sliver的DecoratedBox组件,而默认支持Sliver的组件包含SliverPadding、SliverOpacity、SliverOffstage并不能满足我们的要求,所以我们可以通过扩展一个Sliver组件,实现我们想要的效果,接下来我们参考DecoratedBox的源码来实现一个SliverDecoratedBox,从而实现图2中的效果:
1.编写SliverDecoratedBox widget
直接拷贝DecoratedBox的源码在他的基础上做改造:
1.接收子元素的成员变量名有child变成了Sliver
2.createRenderObject返回对象由RenderDecoratedBox变成RenderSliverDecoratedBox
3.updateRenderObject参数中的RenderDecoratedBox变成RenderSliverDecoratedBox
class SliverDecoratedBox extends SingleChildRenderObjectWidget {
const SliverDecoratedBox({
Key? key,
required this.decoration,
this.position = DecorationPosition.background,
Widget? sliver,
}) : assert(decoration != null),
assert(position != null),
super(key: key, child: sliver);
final Decoration decoration;
final DecorationPosition position;
@override
RenderSliverDecoratedBox createRenderObject(BuildContext context) {
return RenderSliverDecoratedBox(
decoration: decoration,
position: position,
configuration: createLocalImageConfiguration(context),
);
}
@override
void updateRenderObject(BuildContext context, RenderSliverDecoratedBox renderObject) {
renderObject
..decoration = decoration
..configuration = createLocalImageConfiguration(context)
..position = position;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
final String label;
switch (position) {
case DecorationPosition.background:
label = 'bg';
break;
case DecorationPosition.foreground:
label = 'fg';
break;
}
properties.add(EnumProperty<DecorationPosition>('position', position, level: DiagnosticLevel.hidden));
properties.add(DiagnosticsProperty<Decoration>(label, decoration));
}
}
2.实现RenderSliverDecoratedBox RenderSliver
直接拷贝RenderDecoratedBox的源码在他的基础上做改造:
1.父类由RenderProxyBox变为RenderProxySliver
2.接收子元素的成员变量类型由RenderBox变为RenderSliver
3.获取组件的大小的Size对象,不在通过从RenderBox继承的size属性获取,而是通过从RenderSliver继承的getAbsoluteSize方法获取
class RenderSliverDecoratedBox extends RenderProxySliver {
RenderSliverDecoratedBox({
required Decoration decoration,
DecorationPosition position = DecorationPosition.background,
ImageConfiguration configuration = ImageConfiguration.empty,
RenderSliver? sliver,
}) : assert(decoration != null),
assert(position != null),
assert(configuration != null),
_decoration = decoration,
_position = position,
_configuration = configuration {
child = sliver;
}
BoxPainter? _painter;
Decoration get decoration => _decoration;
Decoration _decoration;
set decoration(Decoration value) {
assert(value != null);
if (value == _decoration)
return;
_painter?.dispose();
_painter = null;
_decoration = value;
markNeedsPaint();
}
DecorationPosition get position => _position;
DecorationPosition _position;
set position(DecorationPosition value) {
assert(value != null);
if (value == _position)
return;
_position = value;
markNeedsPaint();
}
ImageConfiguration get configuration => _configuration;
ImageConfiguration _configuration;
set configuration(ImageConfiguration value) {
assert(value != null);
if (value == _configuration)
return;
_configuration = value;
markNeedsPaint();
}
@override
void detach() {
_painter?.dispose();
_painter = null;
super.detach();
markNeedsPaint();
}
@override
void paint(PaintingContext context, Offset offset) {
Size size = getAbsoluteSize();
assert(size.width != null);
assert(size.height != null);
_painter ??= _decoration.createBoxPainter(markNeedsPaint);
final ImageConfiguration filledConfiguration = configuration.copyWith(size: size);
if (position == DecorationPosition.background) {
int? debugSaveCount;
assert(() {
debugSaveCount = context.canvas.getSaveCount();
return true;
}());
_painter!.paint(context.canvas, offset, filledConfiguration);
assert(() {
if (debugSaveCount != context.canvas.getSaveCount()) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('${_decoration.runtimeType} painter had mismatching save and restore calls.'),
ErrorDescription(
'Before painting the decoration, the canvas save count was $debugSaveCount. '
'After painting it, the canvas save count was ${context.canvas.getSaveCount()}. '
'Every call to save() or saveLayer() must be matched by a call to restore().',
),
DiagnosticsProperty<Decoration>('The decoration was', decoration, style: DiagnosticsTreeStyle.errorProperty),
DiagnosticsProperty<BoxPainter>('The painter was', _painter, style: DiagnosticsTreeStyle.errorProperty),
]);
}
return true;
}());
if (decoration.isComplex)
context.setIsComplexHint();
}
super.paint(context, offset);
if (position == DecorationPosition.foreground) {
_painter!.paint(context.canvas, offset, filledConfiguration);
if (decoration.isComplex)
context.setIsComplexHint();
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(_decoration.toDiagnosticsNode(name: 'decoration'));
properties.add(DiagnosticsProperty<ImageConfiguration>('configuration', configuration));
}
}
4.处理child对应的圆角
class RenderSliverDecoratedBox extends RenderProxySliver {
...
@override
void paint(PaintingContext context, Offset offset) {
...
if(decoration is BoxDecoration) {
BorderRadiusGeometry? borderRadius = (decoration as BoxDecoration).borderRadius;
if(borderRadius != null) {
RRect clipRect = borderRadius.resolve(configuration.textDirection).toRRect(Rect.fromLTRB(
0, 0, constraints.crossAxisExtent, geometry!.maxPaintExtent));
context.pushClipRRect(
needsCompositing,
offset,
clipRect.outerRect,
clipRect,
super.paint,
);
}
}
}
...
}
到此一个支持Sliver的DecoratedBox就编写完了,接下来我们在使用的时候只需要将上面的Container替换成SliverDecoratedBox即可,这里只是以DecoratedBox为例来说明,我们怎么参考系统组件的源码来扩展一个支持Sliver的对应的组件。
CustomScrollView(
primary: false,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.all(20),
sliver: SliverDecoratedBox(
decoration: BoxDecoration(
color: Color(0xffff0000),
borderRadius: BorderRadius.all(Radius.circular(20))
),
sliver: SliverGrid.count(
crossAxisSpacing: 10,
mainAxisSpacing: 10,
crossAxisCount: 2,
children: <Widget>[
Container(
padding: const EdgeInsets.all(8),
child: const Text("He'd have you all unravel at the"),
color: Colors.green[100],
),
Container(
padding: const EdgeInsets.all(8),
child: const Text('Heed not the rabble'),
color: Colors.green[200],
),
Container(
padding: const EdgeInsets.all(8),
child: const Text('Sound of screams but the'),
color: Colors.green[300],
),
Container(
padding: const EdgeInsets.all(8),
child: const Text('Who scream'),
color: Colors.green[400],
),
Container(
padding: const EdgeInsets.all(8),
child: const Text('Revolution is coming...'),
color: Colors.green[500],
),
Container(
padding: const EdgeInsets.all(8),
child: const Text('Revolution, they...'),
color: Colors.green[600],
),
],
),
),
),
],
),
5.完整源码
class SliverDecoratedBox extends SingleChildRenderObjectWidget {
const SliverDecoratedBox({
Key? key,
required this.decoration,
this.position = DecorationPosition.background,
Widget? sliver,
}) : assert(decoration != null),
assert(position != null),
super(key: key, child: sliver);
final Decoration decoration;
final DecorationPosition position;
@override
RenderSliverDecoratedBox createRenderObject(BuildContext context) {
return RenderSliverDecoratedBox(
decoration: decoration,
position: position,
configuration: createLocalImageConfiguration(context),
);
}
@override
void updateRenderObject(BuildContext context, RenderSliverDecoratedBox renderObject) {
renderObject
..decoration = decoration
..configuration = createLocalImageConfiguration(context)
..position = position;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
final String label;
switch (position) {
case DecorationPosition.background:
label = 'bg';
break;
case DecorationPosition.foreground:
label = 'fg';
break;
}
properties.add(EnumProperty<DecorationPosition>('position', position, level: DiagnosticLevel.hidden));
properties.add(DiagnosticsProperty<Decoration>(label, decoration));
}
}
class RenderSliverDecoratedBox extends RenderProxySliver {
RenderSliverDecoratedBox({
required Decoration decoration,
DecorationPosition position = DecorationPosition.background,
ImageConfiguration configuration = ImageConfiguration.empty,
RenderSliver? sliver,
}) : assert(decoration != null),
assert(position != null),
assert(configuration != null),
_decoration = decoration,
_position = position,
_configuration = configuration {
child = sliver;
}
BoxPainter? _painter;
Decoration get decoration => _decoration;
Decoration _decoration;
set decoration(Decoration value) {
assert(value != null);
if (value == _decoration)
return;
_painter?.dispose();
_painter = null;
_decoration = value;
markNeedsPaint();
}
DecorationPosition get position => _position;
DecorationPosition _position;
set position(DecorationPosition value) {
assert(value != null);
if (value == _position)
return;
_position = value;
markNeedsPaint();
}
ImageConfiguration get configuration => _configuration;
ImageConfiguration _configuration;
set configuration(ImageConfiguration value) {
assert(value != null);
if (value == _configuration)
return;
_configuration = value;
markNeedsPaint();
}
@override
void detach() {
_painter?.dispose();
_painter = null;
super.detach();
markNeedsPaint();
}
@override
void paint(PaintingContext context, Offset offset) {
Size size = getAbsoluteSize();
assert(size.width != null);
assert(size.height != null);
_painter ??= _decoration.createBoxPainter(markNeedsPaint);
final ImageConfiguration filledConfiguration = configuration.copyWith(size: size);
if (position == DecorationPosition.background) {
int? debugSaveCount;
assert(() {
debugSaveCount = context.canvas.getSaveCount();
return true;
}());
_painter!.paint(context.canvas, offset, filledConfiguration);
assert(() {
if (debugSaveCount != context.canvas.getSaveCount()) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('${_decoration.runtimeType} painter had mismatching save and restore calls.'),
ErrorDescription(
'Before painting the decoration, the canvas save count was $debugSaveCount. '
'After painting it, the canvas save count was ${context.canvas.getSaveCount()}. '
'Every call to save() or saveLayer() must be matched by a call to restore().',
),
DiagnosticsProperty<Decoration>('The decoration was', decoration, style: DiagnosticsTreeStyle.errorProperty),
DiagnosticsProperty<BoxPainter>('The painter was', _painter, style: DiagnosticsTreeStyle.errorProperty),
]);
}
return true;
}());
if (decoration.isComplex)
context.setIsComplexHint();
}
super.paint(context, offset);
if (position == DecorationPosition.foreground) {
_painter!.paint(context.canvas, offset, filledConfiguration);
if (decoration.isComplex)
context.setIsComplexHint();
}
if(decoration is BoxDecoration) {
BorderRadiusGeometry? borderRadius = (decoration as BoxDecoration).borderRadius;
if(borderRadius != null) {
RRect clipRect = borderRadius.resolve(configuration.textDirection).toRRect(Rect.fromLTRB(
0, 0, constraints.crossAxisExtent, geometry!.maxPaintExtent));
context.pushClipRRect(
needsCompositing,
offset,
clipRect.outerRect,
clipRect,
super.paint,
);
}
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(_decoration.toDiagnosticsNode(name: 'decoration'));
properties.add(DiagnosticsProperty<ImageConfiguration>('configuration', configuration));
}
}