原文在這裡 作者簡介:Jose,剛大學畢業,現帶領團隊負責維護Flutter的Material庫, Material是一個幫助團隊建設高質量用戶體驗的設計體系。 假設你的ui里有一個widget,並且您希望在該widget的頂部覆蓋一個浮動widget。 可能該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。現在該你自己動手了。
想瞭解更多關於Jose,可以訪問他的GitHub、LinkedIn
、YouTube、Instagram