下拉放大背景在个人主页的设计中比较常见,但是我没在Flutter的文章中找到实现方式。
结合了几个文章的方案后,我尝试修改实现方式,最终结合CustomScrollView 和Listener的方法实现了以下的方案
CustomScrollView
可以说列表视图想要自定义滚动效果,绕不过CustomScrollView
详细的定义可以参见其他博客https://juejin.im/post/5bceb534e51d457aa4596f9a
这里不详细赘述
SliverPersistentHeader 可高度自定制的头部伸缩视图
很多文章里推荐大家用SliverAppBar来实现头部,这种方案并不是很灵活
SliverPersistentHeader 可以说对下拉放大背景图的支持更好一些
SliverPersistentHeader(
delegate: _SVPersonalAppBarDelegate(
minHeight: defaultHeight,
maxHeight: maxHeight,
child: _createImageWidget()),
)
_createImageWidget() {
return Container(
child: Image.network(
'https://pic4.zhimg.com/80/v2-b02e601349241df0e3f25fd1ec622155_1440w.jpg',
fit: BoxFit.cover,
),
);
}
SliverPersistentHeader 需要我们传入一个SliverPersistentHeaderDelegate对象,SliverPersistentHeaderDelegate是一个抽象类,所以需要我们自己去继承实现一个子类
class _SVPersonalAppBarDelegate extends SliverPersistentHeaderDelegate {
_SVPersonalAppBarDelegate({
@required this.minHeight,
@required this.maxHeight,
@required this.child,
});
final double minHeight;
final double maxHeight;
final Widget child;
@override
double get minExtent => minHeight;
@override
double get maxExtent => max(maxHeight, minHeight);
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return new SizedBox.expand(child: child);
}
@override
bool shouldRebuild(_SVPersonalAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}
完成头部视图的定制后,我们需要关心的几个问题
- 放大后,解除触摸,如何让他自动重置到正常状态?
- 从顶部快速滑下后,如何制造弹性放大回置的动画?
我通过引入Listener 和状态监听来完成了这两部份的实现
完整代码如下
import 'dart:math';
import 'package:flutter/material.dart';
enum SVDragState {
SVDragStateIdle,
SVDragStateBegin,
SVDrageStateEnd,
}
class SVPersonalInfoPage extends StatefulWidget {
@override
_SVPersonalInfoPageState createState() => _SVPersonalInfoPageState();
}
class _SVPersonalInfoPageState extends State {
double startOffsetY;
static double defaultHeight = 250;
static double maxHeight = 350.0;
static double offsetY = 0;
static double distance = maxHeight - defaultHeight;
SVDragState dragState = SVDragState.SVDragStateIdle;
final ScrollController controller =
ScrollController(initialScrollOffset: distance);
@override
void initState() {
controller.addListener(() {
offsetY = controller.offset;
if (offsetY <= 0) {
controller.jumpTo(0);
_resetWithAnimation(true);
}
});
super.initState();
}
_resetWithAnimation(bool delay) {
if (!delay) {
if (controller.offset < distance && dragState == SVDragState.SVDrageStateEnd) {
dragState = SVDragState.SVDragStateIdle;
controller.animateTo(distance,
duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
return;
}
}
Future.delayed(Duration(milliseconds: 200)).then((value) {
if (controller.offset < distance && dragState == SVDragState.SVDrageStateEnd) {
dragState = SVDragState.SVDragStateIdle;
controller.animateTo(distance,
duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
return;
}
});
}
@override
Widget build(BuildContext context) {
return Listener(
onPointerDown: (e) { dragState = SVDragState.SVDragStateBegin; },
onPointerUp: (e) {
dragState = SVDragState.SVDrageStateEnd;
_resetWithAnimation(false);
},
child: CustomScrollView(
controller: controller,
slivers: [
SliverPersistentHeader(
delegate: _SVPersonalAppBarDelegate(
minHeight: defaultHeight,
maxHeight: maxHeight,
child: _createImageWidget()),
),
SliverList(delegate: SliverChildBuilderDelegate(
(context, index) {
return Container(
child: Text(
"This is item $index",
style: TextStyle(fontSize: 20),
),
color: Colors.redAccent,
);
},
))
],
));
}
_createImageWidget() {
return Container(
child: Image.network(
'https://pic4.zhimg.com/80/v2-b02e601349241df0e3f25fd1ec622155_1440w.jpg',
fit: BoxFit.cover,
),
);
}
}
class _SVPersonalAppBarDelegate extends SliverPersistentHeaderDelegate {
_SVPersonalAppBarDelegate({
@required this.minHeight,
@required this.maxHeight,
@required this.child,
});
final double minHeight;
final double maxHeight;
final Widget child;
@override
double get minExtent => minHeight;
@override
double get maxExtent => max(maxHeight, minHeight);
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return new SizedBox.expand(child: child);
}
@override
bool shouldRebuild(_SVPersonalAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}