前言
Flutter是一种强大的跨渠道移动使用开发框架,答应开发者构建美观且高性能的移动使用。在某些情况下,你可能需要在使用内完成小窗口功用,以改善用户体会或供给一些特定的功用。无论是用于显示告诉、音乐播放器控制、或其他构思功用,使用内小窗口都是进步用户体会的有力工具。
本文将介绍如何在Flutter使用内创建小窗口,并展示一个简略的示例。
一般我们对小窗有如下要求:
- 跨页面显示
- 可以跟从手指拖动
- 铺开主动侧边吸附
代码完成
完好代码
小窗的显示与躲藏
使用OverlayEntry
进行跨页面显示
首要界说一个窗口管理器,进行窗口的显示和躲藏
class SmallWindowManager {
static final SmallWindowManager _instance = SmallWindowManager._();
factory SmallWindowManager() => _instance;
SmallWindowManager._();
///浮窗
OverlayEntry? overlayEntry;
show(BuildContext context) {
if (overlayEntry == null) {
overlayEntry = OverlayEntry(builder: (BuildContext context) {
return SmallWindowWidget(
top: 160,
left: 279,
child: GestureDetector(
onTap: () {
hide();
},
child: Material(
child: Container(
color: Colors.red,
width: 100,
height: 200,
child: const Center(
child: Text("small"),
),
),
),
),
);
});
Overlay.of(context).insert(overlayEntry!);
}
}
///封闭小窗
void hide() {
overlayEntry?.remove();
overlayEntry = null;
}
}
小窗跟从与吸附
- 使用
Positioned
的left
和top
特点进行定位,经过key
确定小窗和窗口的尺寸,判定小窗移动的鸿沟 - 使用
Gestedrector
进行手势操作,手指移动时回调onPanUpdate
,经过手指的偏移量计算定位,小窗即可跟从手指移动 - 松开手指时回调
onPanEnd
,计算开始方位和结束方位,执行吸附动画
class SmallWindowWidget extends StatefulWidget {
final Duration duration;
final Widget child;
final double top;
final double left;
const SmallWindowWidget({
super.key,
this.duration = const Duration(milliseconds: 100),
required this.child,
required this.top,
required this.left,
});
@override
State<SmallWindowWidget> createState() => _SmallWindowWidgetState();
}
class _SmallWindowWidgetState extends State<SmallWindowWidget> with TickerProviderStateMixin {
AnimationController? _controller;
double left = 0;
double top = 0;
double maxX = 0;
double maxY = 0;
var parentKey = GlobalKey();
var childKey = GlobalKey();
var parentSize = const Size(0, 0);
var childSize = const Size(0, 0);
@override
void initState() {
left = widget.left;
top = widget.top;
WidgetsBinding.instance.addPostFrameCallback((d) {
parentSize = getWidgetSize(parentKey);
childSize = getWidgetSize(childKey);
maxX = parentSize.width - childSize.width;
maxY = parentSize.height - childSize.height;
});
super.initState();
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
key: parentKey,
fit: StackFit.expand,
children: [
Positioned(
key: childKey,
left: left,
top: top,
child: GestureDetector(
onPanUpdate: (d) {
var delta = d.delta;
left += delta.dx;
top += delta.dy;
setState(() {});
},
onPanEnd: (d) {
left = getValue(left, maxX);
top = getValue(top, maxY);
adsorb();
},
child: widget.child,
),
)
],
);
}
///约束鸿沟
double getValue(double value, double max) {
if (value < 0) {
return 0;
} else if (value > max) {
return max;
} else {
return value;
}
}
///吸附
void adsorb() {
bool isLeft = (left + childSize.width / 2) < parentSize.width / 2;
_controller = AnimationController(vsync: this)..duration = widget.duration;
var animation = Tween<double>(begin: left, end: isLeft ? 0 : maxX).animate(_controller!);
animation.addListener(() {
left = animation.value;
setState(() {});
});
_controller!.forward();
}
Size getWidgetSize(GlobalKey key) {
final RenderBox renderBox = key.currentContext?.findRenderObject() as RenderBox;
return renderBox.size;
}
}