这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
废话开篇:大家都知道在苹果手机设置里面有“辅助触控”这样的一个开关,它其实就相当于一个屏幕软键盘,可以拖动到屏幕内的任意位置。闲置的时候为半透明状,用户操作的时候为不透明,在iOS原生上面可以将自定义view加载到keyWindow上,这样就view就处在所有的view最上面,那么,下面来具体在flutter上实现这样的功能。
效果展示:
步骤一、封装自定义全屏浮动组件,将app内全部组件以child形式展示
其实,这里写的有点浮夸了,大致的意思就是封装一个组件,内部采用帧布局的形式进行排列。将app全部的内容作为主要组件进行展示,这时再简单的封装一下悬浮框组件作为帧布局上层组件即可,再利用 GestureDetector 进行拖拽手势监听,随即移动悬浮组件,那么,大体上的布局就完成了。
下面展示一下主要布局代码:
GeneralFloatOnScreenView 代码:
class GeneralFloatOnScreenView extends StatefulWidget {
Widget child;
GeneralFloatOnScreenView({required this.child});
@override
State<StatefulWidget> createState() {
return new GeneralFloatOnScreenViewState();
}
}
GeneralFloatOnScreenViewState 代码
属性声明
//帧布局顶部距离
double _top = 0;
//帧布局左侧距离
double _left = 0;
//悬浮组件宽度,这里设置为宽高一直,因此height并没有声明
double _width = 50;
//记录屏幕或者父类组件宽度,用来判断拖拽听指挥后回归左右边缘判断
double _parentWidth = 0;
bool _isInitData = false;
//悬浮组件透明度
double _opacity = 0.3;
//动画控制器
late AnimationController _controller;
//动画
late Animation<double> _animation;
//这里的浮动组件声明成了属性,目的就是防止多次刷新当此组件内部有一些单独的逻辑的情况下。
late Widget _contentWidget;
初始化,在widget初始化里面去创建组件,这样只要当前组件不在屏幕上消失,那么即使不进行 wantKeepAlive 设置也不会重新初始。在悬浮组件上嵌套了一层 GestureDetector 手势来进行拖拽的移动。
@override
void initState() {
// TODO: implement initState
super.initState();
_contentWidget = new GestureDetector(
onPanUpdate: (DragUpdateDetails details){
_left += details.delta.dx;
_top += details.delta.dy;
_changePosition();
},
onPanEnd: (DragEndDetails details){
_changePosition();
//判断悬浮组件左右回归操作
_animateMoveBackAction();
},
onPanCancel: (){
//当取消手势时进行边缘判断
_changePosition();
},
onPanStart: (DragStartDetails details){
//开始拖拽时将悬浮框透明度设置为1.0
setState(() {
_opacity = 1.0;
});
},
child: new Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(_width / 2.0)),
color: Colors.red,
),
width: _width,
height: _width,
),
);
//这里初始化动画类
_controller =
AnimationController(duration: Duration(milliseconds: 0), vsync: this);
_animation =
Tween(begin: _left, end: _left).animate(_controller);
}
build 方法,这里没有什么特别要注意的,里面只是添加了帧布局进行包裹。
@override
Widget build(BuildContext context) {
//对于必要属性只进行一次计算
if(_isInitData == false) {
_top = MediaQuery.of(context).size.height - 200;
_left = 15;
_parentWidth = MediaQuery.of(context).size.width;
_isInitData = true;
}
return new Stack(
fit: StackFit.passthrough,
children: <Widget>[
this.widget.child,
Positioned(
top: _top,
left: _left,
child: new Opacity(
child: _contentWidget,
opacity: _opacity
),
)
],
);
}
步骤二,如何进行手势的拖拽移动及边缘判断?
当 GestureDetector 手势监听 onPanUpdate 方法里进行悬浮组件的位置移动,直接修改属性 _left、_top值即可,这里注意进行一下屏幕边缘判断。
//位置边界判断
void _changePosition(){
if(_left < 0) {
_left = 0;
}
if(_left >= MediaQuery.of(context).size.width - _width){
_left = MediaQuery.of(context).size.width - _width;
}
if(_top < 0) {
_top = 0;
}
if(_top >= MediaQuery.of(context).size.height - _width) {
_top = MediaQuery.of(context).size.height - _width;
}
//刷新界面
setState(() {
});
}
步骤三、如何进行拖拽手势结束悬浮组件回归边缘动画?
当GestureDetector 手势监听 onPanEnd 方法执行的时候判断当前悬浮组件中轴线停留的位置与屏幕(或者父组件中轴线)的位置关系,偏左向左边缘靠,偏右向有边缘靠。这里的动画并没有添加到某个动画组件上,而是直接执行,监听animation的值的变化进行悬浮组件的回归移动。当动画直接状态为结束后再将悬浮组件的透明度设置为半透明状态。
//中轴线回弹动画
void _animateMoveBackAction(){
double centerX = _left + _width / 2.0;
double toPositionX = 0;
double needMoveLength = 0;
if(centerX <= _parentWidth / 2.0) {
needMoveLength = _left;
} else {
needMoveLength = (_parentWidth - _left - _width);
}
double precent = (needMoveLength / (_parentWidth / 2.0));
int time = (600 * precent).ceil();
if(centerX <= _parentWidth / 2.0){
//回到左边缘
toPositionX = 0;
} else {
//回到右边缘
toPositionX = _parentWidth - _width;
}
//这里由于根据需要偏移的距离需要重新设置动画执行时长,那么之前的动画控制器就先销毁再创建。
_controller.dispose();
_controller =
AnimationController(duration: Duration(milliseconds: time), vsync: this);
//这里对监听 animation 执行过程进行监听,重新绘制悬浮组件位置
_animation =
Tween(begin: _left, end: toPositionX * 1.0).animate(_controller);
_animation.addListener(() {
_left = _animation.value.toDouble();
setState(() {
});
});
_animation.addStatusListener((status) {
if(status == AnimationStatus.completed){
Future.delayed(Duration(microseconds: 200),(){
setState(() {
_opacity = 0.3;
});
});
}
});
_controller.forward();
}
注意组件销毁时进行动画控制器的销毁操作,注意先执行自身的相关操作后执行super.dispos()
@override
void dispose() {
// TODO: implement dispose
_controller.dispose();
super.dispose();
}
步骤四、如何用这个封装好的组件?
将之前 app 的全部组件用 GeneralFloatOnScreenView 包裹起来,将它作为child传给自定义组件即可
home: new Scaffold(
//这里的 new BottomTabbarWidget() 是 app 的全部组件,将它作为child传给自定义组件即可
body: new GeneralFloatOnScreenView(child: new BottomTabbarWidget()),
)
好了,简单的仿iOS全屏悬浮窗就实现好了,代码拙劣,大神勿喷,如果对大家有帮助,更是深感欣慰。