flutter 效果实现 —— 下拉滑动面板
效果:
注:如果是类似于 BottomSheet,效果为:由底部往上拖动展开,由上往下拖动收缩。那么,可直接使用 showBottomSheet,并且如果子组件是是滚动组件的话,可使用 DraggableScrollableSheet(snap 属性设为 true)。
代码:
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sliding Box"),
bottom: PreferredSize(
preferredSize: Size.fromHeight(75),
child: Container(
height: 75,
alignment: Alignment.center,
width: double.infinity,
color: Colors.blueGrey,
child: Text("Fixed header"),
),
),
),
body: Stack(
children: [
Padding(
padding: const EdgeInsets.only(top: 26.0),
child: ListView(
children: [
Container(
alignment: Alignment.center,
color: Colors.green,
height: 600,
child: Text("hello"),
)
],
),
),
Material(
elevation: 2,
child: SlidingPanel(
maxHeight: 300,
builder: (BuildContext context, AnimationController controller) {
return Container(
color: Colors.red,
height: 300,
alignment: Alignment.center,
child: Text("sliding body"),
);
},
),
),
],
),
);
}
}
typedef SlidingBodyBuilder = Widget Function(BuildContext context, AnimationController controller);
class SlidingPanel extends StatefulWidget {
const SlidingPanel({Key? key, this.maxHeight = 200, this.minHeight = 0, required this.builder})
: super(key: key);
final SlidingBodyBuilder builder;
final double maxHeight;
final double minHeight;
final double indicatorHeight = 26;
final double flingSpeed = 365;
@override
State<SlidingPanel> createState() => _SlidingPanelState();
}
class _SlidingPanelState extends State<SlidingPanel> with SingleTickerProviderStateMixin {
late AnimationController _ac;
@override
void initState() {
super.initState();
_ac = AnimationController(vsync: this, duration: Duration(milliseconds: 500));
}
@override
void dispose() {
_ac.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: (details) {
_onGestureSlide(details.delta.dy);
},
onPanEnd: (details) {
_onGestureEnd(details.velocity);
},
child: AnimatedBuilder(
animation: _ac,
builder: (BuildContext context, Widget? child) {
final currHeight = (widget.maxHeight - widget.minHeight) * _ac.value + widget.minHeight;
final deltaY = -(widget.maxHeight - currHeight) / 2;
return Container(
height: currHeight + widget.indicatorHeight,
child: Stack(
children: [
Positioned(
top: deltaY,
left: 0,
right: 0,
child: Opacity(
opacity: _ac.value,
child: widget.builder(context, _ac),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: child!,
),
],
),
);
},
child: _buildSlideIndicator(),
),
);
}
void _onGestureSlide(double dy) {
//向下滚动 dy > 0; 向上滚动 dy < 0
print('slide: $dy');
_ac.value += dy / (widget.maxHeight - widget.minHeight);
}
void _onGestureEnd(Velocity v) {
print('onGestureEnd: ${v.pixelsPerSecond.dy}');
//如果动画已经运行,则直接返回
if (_ac.isAnimating) return;
double visualVelocity = v.pixelsPerSecond.dy / (widget.maxHeight - widget.minHeight);
print('pixelsPerSecond: ${v.pixelsPerSecond.dy}, visualVelocity: $visualVelocity');
//若当前下拉速度超过阈值,则进行 fling
if (v.pixelsPerSecond.dy.abs() >= widget.flingSpeed) {
//velocity 等于 -1 是关闭,1 是打开
//fling,按一秒的时间计算滑过的距离
_ac.fling(velocity: visualVelocity);
return;
}
// 若当前下拉速度未超过阈值
if (_ac.value >= 0.5) {
_ac.fling(velocity: 1);
} else {
_ac.fling(velocity: -1);
}
}
Widget _buildSlideIndicator() {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
_ac.fling(velocity: 1 - 2 * _ac.value);
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
width: double.infinity,
color: Colors.white,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 30,
height: 5,
decoration: BoxDecoration(
color: Colors.grey[500],
borderRadius: BorderRadius.all(Radius.circular(12.0)),
),
),
],
),
),
);
}
}