[搬运]flutter如何在Widget上叠加其他overlay widget
作者简介:Jose,刚大学毕业,现带领团队负责维护Flutter的Material库,
Material是一个帮助团队建设高质量用户体验的设计体系。
假设你的ui里有一个widget,并且您希望在该widget的顶部覆盖一个浮动widget。
可能该widget被旋转或应用了其他转换,如何将widget的位置和转换信息传递到浮动widget呢?
你可以使用CompositedTransformTarget, CompositedTransformWidget、LayerLink、Overlay和OverlayEntry来完成上述操作。
在Flutter中,overlay允许您将视觉元素插入到overlay的堆栈中,从而将它们显示在其他widget的顶部。使用OverlayEntry将widget插入到overlay中,同时可以使用Positioned和AnimatedPositioned来定位你的widget。当你需要把一个widget浮动在另一个widget上面时,就可以使用这种方法,并且这些widget都可以复用。
在这一篇文章中,开发者想要对现有的TextField增加自动建议功能,我们当然可以使用Stack来实现这个功能,但这种方法并不友好,具有很强的侵入性,还需要将整个屏幕设计为Stack。如本文所述,建议使用Overlay来实现这种效果,这种场景非常常见。
直接使用使用Overlay表现的很直观,但在Flutter中实现起来却有限困难:使用一个builder回调函数来创建OverlayEntry实例,并把它插入到Overlay的堆栈中,同时你还需要持有该OverlayEntry实例的引用,可以方便的对该实例进行更新、删除操作。OverlayEntry的position、transformation依赖的widget必须也要存在于overlay中,否则就会发生冲突。因为overlay的MediaQuery的context不同于常规的contenxt。调用Overlay之前,在widget中使用padding或margin的时候就会发生类似问题。幸运的是,flutter已经为你处理了这些问题。(这一段还需要润色,要衔接上下文中心思想)
如果你的overlay entry所依赖的“target”不在overlay堆栈中,那么我们需要使用CompositedTransformTarget、CompositedTransformFollower和LayerLink将它们粘合在一起:
-
用CompositedTransformFollower包装需要浮动的widget
-
CompositedTransformFollower必须是CompositedTransformTarget的子孙接点
-
用CompositedTransformTarget包装你要跟随依赖的“target”
-
使用LayerLink将CompositedTransformTarget和CompositedTransformFollower粘在一起
下图Gif中,蓝色的Container不在overlay中,而绿色的在overlay中。蓝色的Container作为CompositedTransformTarget的child节点,绿色的作为CompostedTransformFollower的child节点,他们通过LayerLink实例连接到一起。注意绿色overlay widget是如何知道蓝色widget的bounds数据,因为蓝色widget并不在overlay中:
建议你使用DartPad亲自尝试下。
下面是实例代码(作者Hans Muller):
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: Slide()));
}
class Indicator extends StatelessWidget {
Indicator({ Key key, this.link, this.offset }) : super(key: key);
final LayerLink link;
final Offset offset;
@override
Widget build(BuildContext context) {
return CompositedTransformFollower(
offset: offset,
link: link,
child: Container(
color: Colors.green,
),
);
}
}
class Slide extends StatefulWidget {
Slide({ Key key }) : super(key: key);
@override
_SlideState createState() => _SlideState();
}
class _SlideState extends State<Slide> {
final double indicatorWidth = 24.0;
final double indicatorHeight = 300.0;
final double slideHeight = 200.0;
final double slideWidth = 400.0;
final LayerLink layerLink = LayerLink();
OverlayEntry overlayEntry;
Offset indicatorOffset;
Offset getIndicatorOffset(Offset dragOffset) {
final double x = (dragOffset.dx - (indicatorWidth / 2.0)).clamp(0.0, slideWidth - indicatorWidth);
final double y = (slideHeight - indicatorHeight) / 2.0;
return Offset(x, y);
}
void showIndicator(DragStartDetails details) {
indicatorOffset = getIndicatorOffset(details.localPosition);
overlayEntry = OverlayEntry(
builder: (BuildContext context) {
return Positioned(
top: 0.0,
left: 0.0,
child: SizedBox(
width: indicatorWidth,
height: indicatorHeight,
child: Indicator(
offset: indicatorOffset,
link: layerLink
),
),
);
},
);
Overlay.of(context).insert(overlayEntry);
}
void updateIndicator(DragUpdateDetails details) {
indicatorOffset = getIndicatorOffset(details.localPosition);
overlayEntry.markNeedsBuild();
}
void hideIndicator(DragEndDetails details) {
overlayEntry.remove();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Overlay Indicator')),
body: Center(
child: CompositedTransformTarget(
link: layerLink,
child: Container(
width: slideWidth,
height: slideHeight,
color: Colors.blue.withOpacity(0.2),
child: GestureDetector(
onPanStart: showIndicator,
onPanUpdate: updateIndicator,
onPanEnd: hideIndicator,
),
),
),
),
);
}
}
上面例子展示了如何将浮动widget和它依赖的“target”粘合到一起使用。接下来我们要对CompositedTransformTarget做一些Transformation来展示这些widget的真正强大之处。你会注意到绿色的overlay widget也会自动被施加这些Transformation,这一切都得益于LayerLink。
下面的gif图中,蓝色的container依然是不在overlay,绿色在。这一次我们对CompositedTransformTarget(即蓝色container)做了一个旋转的Transformation,如你所见,尽管CompositeTransformFollower位于overlay中,它依然感知得到“target”的位置和被施加的transformations。
建议你使用DartPad亲自尝试下。
下面是实例代码(作者Hans Muller):
import 'dart:math' as math;
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: Slide()));
}
class Indicator extends StatelessWidget {
Indicator({ Key key, this.link, this.offset }) : super(key: key);
final LayerLink link;
final Offset offset;
@override
Widget build(BuildContext context) {
return CompositedTransformFollower(
offset: offset,
link: link,
child: Container(
color: Colors.green,
),
);
}
}
class Slide extends StatefulWidget {
Slide({ Key key }) : super(key: key);
@override
_SlideState createState() => _SlideState();
}
class _SlideState extends State<Slide> {
final double indicatorWidth = 24.0;
final double indicatorHeight = 300.0;
final double slideHeight = 200.0;
final double slideWidth = 400.0;
final LayerLink layerLink = LayerLink();
OverlayEntry overlayEntry;
Offset indicatorOffset;
Offset getIndicatorOffset(Offset dragOffset) {
final double x = (dragOffset.dx - (indicatorWidth / 2.0)).clamp(0.0, slideWidth - indicatorWidth);
final double y = (slideHeight - indicatorHeight) / 2.0;
return Offset(x, y);
}
void showIndicator(DragStartDetails details) {
indicatorOffset = getIndicatorOffset(details.localPosition);
overlayEntry = OverlayEntry(
builder: (BuildContext context) {
return Positioned(
top: 0.0,
left: 0.0,
child: SizedBox(
width: indicatorWidth,
height: indicatorHeight,
child: Indicator(
offset: indicatorOffset,
link: layerLink
),
),
);
},
);
Overlay.of(context).insert(overlayEntry);
}
void updateIndicator(DragUpdateDetails details) {
indicatorOffset = getIndicatorOffset(details.localPosition);
overlayEntry.markNeedsBuild();
}
void hideIndicator(DragEndDetails details) {
overlayEntry.remove();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Overlay Indicator')),
body: Transform.rotate(
angle: -math.pi / 12.0,
child: Center(
child: CompositedTransformTarget(
link: layerLink,
child: Container(
width: slideWidth,
height: slideHeight,
color: Colors.blue.withOpacity(0.2),
child: GestureDetector(
onPanStart: showIndicator,
onPanUpdate: updateIndicator,
onPanEnd: hideIndicator,
),
),
),
),
),
);
}
}
总结
当你要实现一个widget浮动在另一个widget之上时,使用overlay,然后连接这些widget,非常简单易用。本文中,你学会了如何使用LayerLink来粘合这些widget。现在该你自己动手了。