效果
😒😒😒
一、先实现下拉图片放大,
仅copy了[ flukit (Flutter UI Kit)是一个Flutter Widget库 ]中的一些代码。在本示例中无需引用模块。flukit/sliver_flexible_header.dart at main · flutterchina/flukit · GitHub
写组件
extra_info_constraints.dart (别管,复制就行)
import 'package:flutter/widgets.dart';
/// A box constraints with extra information.
///
/// See also:
/// * [SliverFlexibleHeader], which use [ExtraInfoBoxConstraints].
/// * [SliverPersistentHeaderToBox], which use [ExtraInfoBoxConstraints].
class ExtraInfoBoxConstraints<T> extends BoxConstraints {
ExtraInfoBoxConstraints(
this.extra,
BoxConstraints constraints,
) : super(
minWidth: constraints.minWidth,
minHeight: constraints.minHeight,
maxWidth: constraints.maxWidth,
maxHeight: constraints.maxHeight,
);
/// extra information
final T extra;
BoxConstraints asBoxConstraints() => copyWith();
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ExtraInfoBoxConstraints &&
super == other &&
other.extra == extra;
}
@override
int get hashCode {
return hashValues(super.hashCode, extra);
}
}
sliver_flexible_header.dart (别管,复制就行)
import 'dart:math';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:Shura/utils/extra_info_constraints.dart';
typedef SliverFlexibleHeaderBuilder = Widget Function(
BuildContext context,
double maxExtent,
ScrollDirection direction,
);
/// A sliver to provide a flexible header that its height can expand when user continue
/// dragging over scroll . Typically as the first child of [CustomScrollView].
class SliverFlexibleHeader extends StatelessWidget {
const SliverFlexibleHeader({
Key? key,
this.visibleExtent = 0,
required this.builder,
}) : super(key: key);
final SliverFlexibleHeaderBuilder builder;
final double visibleExtent;
@override
Widget build(BuildContext context) {
return _SliverFlexibleHeader(
visibleExtent: visibleExtent,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return builder(
context,
constraints.maxHeight,
// 获取滑动方向
(constraints as ExtraInfoBoxConstraints<ScrollDirection>).extra,
);
},
),
);
}
}
class _SliverFlexibleHeader extends SingleChildRenderObjectWidget {
const _SliverFlexibleHeader({
Key? key,
required Widget child,
this.visibleExtent = 0,
}) : super(key: key, child: child);
final double visibleExtent;
@override
RenderObject createRenderObject(BuildContext context) {
return _FlexibleHeaderRenderSliver(visibleExtent);
}
@override
void updateRenderObject(context, _FlexibleHeaderRenderSliver renderObject) {
renderObject.visibleExtent = visibleExtent;
}
}
class _FlexibleHeaderRenderSliver extends RenderSliverSingleBoxAdapter {
_FlexibleHeaderRenderSliver(double visibleExtent)
: _visibleExtent = visibleExtent;
double _lastOverScroll = 0;
double _lastScrollOffset = 0;
double _visibleExtent = 0;
ScrollDirection _direction = ScrollDirection.idle;
// 该变量用来确保Sliver完全离开屏幕时会通知child且只通知一次.
bool _reported = false;
// 是否需要修正scrollOffset. _visibleExtent 值更新后,
// 为了防止突然的跳动,要先修正 scrollOffset。
double? _scrollOffsetCorrection;
set visibleExtent(double value) {
// 可视长度发生变化,更新状态并重新布局
if (_visibleExtent != value) {
_lastOverScroll = 0;
_reported = false;
// 计算修正值
_scrollOffsetCorrection = value - _visibleExtent;
_visibleExtent = value;
markNeedsLayout();
}
}
@override
void performLayout() {
// _visibleExtent 值更新后,为了防止突然的跳动,先修正 scrollOffset
if (_scrollOffsetCorrection != null) {
geometry = SliverGeometry(
//修正
scrollOffsetCorrection: _scrollOffsetCorrection,
);
_scrollOffsetCorrection = null;
return;
}
if (child == null) {
geometry = SliverGeometry(scrollExtent: _visibleExtent);
return;
}
//当已经完全滑出屏幕时
if (constraints.scrollOffset > _visibleExtent) {
geometry = SliverGeometry(scrollExtent: _visibleExtent);
// 通知 child 重新布局,注意,通知一次即可,如果不通知,滑出屏幕后,child 在最后
// 一次构建时拿到的可用高度可能不为 0。因为使用者在构建子节点的时候,可能会依赖
// "当前的可用高度是否为0" 来做一些特殊处理,比如记录是否子节点已经离开了屏幕,
// 因此,我们需要在离开屏幕时确保LayoutBuilder的builder会被调用一次(构建子组件)。
if (!_reported) {
_reported = true;
child!.layout(
ExtraInfoBoxConstraints(
_direction, //传递滑动方向
constraints.asBoxConstraints(maxExtent: 0),
),
//我们不会使用自节点的 Size, 关于此参数更详细的内容见本书后面关于layout原理的介绍
parentUsesSize: false,
);
}
return;
}
//子组件回到了屏幕中,重置通知状态
_reported = false;
// 下拉过程中overlap会一直变化.
double overScroll = constraints.overlap < 0 ? constraints.overlap.abs() : 0;
var scrollOffset = constraints.scrollOffset;
_direction = ScrollDirection.idle;
// 根据前后的overScroll值之差确定列表滑动方向。注意,不能直接使用 constraints.userScrollDirection,
// 这是因为该参数只表示用户滑动操作的方向。比如当我们下拉超出边界时,然后松手,此时列表会弹回,即列表滚动
// 方向是向上,而此时用户操作已经结束,ScrollDirection 的方向是上一次的用户滑动方向(向下),这时便有问题。
var distance = overScroll > 0
? overScroll - _lastOverScroll
: _lastScrollOffset - scrollOffset;
_lastOverScroll = overScroll;
_lastScrollOffset = scrollOffset;
if (constraints.userScrollDirection == ScrollDirection.idle) {
_direction = ScrollDirection.idle;
_lastOverScroll = 0;
} else if (distance > 0) {
_direction = ScrollDirection.forward;
} else if (distance < 0) {
_direction = ScrollDirection.reverse;
}
// 在Viewport中顶部的可视空间为该 Sliver 可绘制的最大区域。
// 1. 如果Sliver已经滑出可视区域则 constraints.scrollOffset 会大于 _visibleExtent,
// 这种情况我们在一开始就判断过了。
// 2. 如果我们下拉超出了边界,此时 overScroll>0,scrollOffset 值为0,所以最终的绘制区域为
// _visibleExtent + overScroll.
double paintExtent = _visibleExtent + overScroll - constraints.scrollOffset;
// 绘制高度不超过最大可绘制空间
paintExtent = min(paintExtent, constraints.remainingPaintExtent);
//对子组件进行布局,子组件通过 LayoutBuilder可以拿到这里我们传递的约束对象(ExtraInfoBoxConstraints)
child!.layout(
ExtraInfoBoxConstraints(
_direction, //传递滑动方向
constraints.asBoxConstraints(maxExtent: paintExtent),
),
parentUsesSize: false,
);
//最大为_visibleExtent,最小为 0
double layoutExtent = min(_visibleExtent, paintExtent);
//设置geometry,Viewport 在布局时会用到
geometry = SliverGeometry(
scrollExtent: _visibleExtent,
paintOrigin: -overScroll,
paintExtent: paintExtent,
maxPaintExtent: paintExtent,
layoutExtent: layoutExtent,
);
}
}
然后使用
这里已经实现了下拉图像放大的功能,但在CustomScrollView这个Widget中怎么都没弄出来标题栏的效果。所以只好用NotificationListener包裹,重新设计。
// 这个 Widget 已经是 下拉图像的最简版
CustomScrollView(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
slivers: [
SliverFlexibleHeader(
visibleExtent: 200.0,
builder: (context, availableHeight, direction) {
return GestureDetector(
onTap: () => debugPrint('tap'),
child: LayoutBuilder(builder: (context, cons) {
return Image(
image: const NetworkImage(
"https://w.wallhaven.cc/full/zy/wallhaven-zygeko.jpg"),
height: availableHeight,
fit: BoxFit.cover,
);
}),
);
},
),
],
),
二、上滑显示标题
先解释要点:
- 用 Stack 的特性来避免层级问题,否则标题会在底下看不到
-
NotificationListener 监听滑动
-
Positioned 定位组件
-
AnimatedOpacity动画组件
看完整代码
直接复制去编译看看理解更深
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:logger/logger.dart';
import '../components/sliver_flexible_header.dart';
class HomeMyScreen extends StatefulWidget {
const HomeMyScreen({Key? key}) : super(key: key);
@override
State<HomeMyScreen> createState() => _HomeMyScreen();
}
class _HomeMyScreen extends State<HomeMyScreen> {
int trans = 0;
final double headerImgHeight = 400.0;
void setTrans(marginTop) {
setState(() {
trans = marginTop;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
NotificationListener(
onNotification: (ScrollNotification note) {
setTrans(note.metrics.pixels.toInt());
return true;
},
child: CustomScrollView(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
slivers: [
SliverFlexibleHeader(
visibleExtent: (headerImgHeight + 36.0),
builder: (context, availableHeight, direction) {
return GestureDetector(
child: LayoutBuilder(builder: (context, cons) {
return Stack(
children: [
Container(
padding: const EdgeInsets.only(bottom: 36),
child: Image(
image: const NetworkImage(
"https://w.wallhaven.cc/full/zy/wallhaven-zygeko.jpg"),
height: availableHeight,
width: MediaQuery.of(context).size.width,
fit: BoxFit.cover,
),
),
Positioned(
left: 0,
bottom: 36,
child: Container(
width: MediaQuery.of(context).size.width,
height: 48,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: [
0.0,
1.0
], //[渐变起始点, 渐变结束点]
//渐变颜色[始点颜色, 结束颜色]
colors: [
Color.fromRGBO(15, 15, 15, 0),
Color.fromRGBO(15, 15, 15, 0.8)
])),
),
),
Positioned(
left: 12,
bottom: 0,
child: Row(
children: [
Container(
width: 72,
height: 72,
//设置了 decoration 就不能设置color,两者只能存在一个
decoration: const BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black26,
offset: Offset(0, 0.0),
//阴影y轴偏移量
blurRadius: 2,
//阴影模糊程度
spreadRadius: 1 //阴影扩散程度
)
],
image: DecorationImage(
image: AssetImage(
"assets/images/cha4.jpg")),
//设置图片
borderRadius: BorderRadius.all(
Radius.circular(6)))),
Padding(
padding: const EdgeInsets.only(left: 12),
child: SizedBox(
height: 72,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisAlignment:
MainAxisAlignment.spaceAround,
children: const [
Text("吸鼠霸王",
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
color: Colors.white)),
Text("UID:hahaha_666",
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 12,
color: Colors.orange))
],
),
),
)
],
)),
],
);
}),
);
},
),
SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return Container(
child: Padding(
padding: const EdgeInsets.only(left: 10.0, right: 10),
child: Row(
children: [
CircleAvatar(
radius: (index + 1) * 10,
backgroundImage: NetworkImage("https://w.wallhaven.cc/full/zy/wallhaven-zygeko.jpg"),
),
],
),
),
);
}, childCount: 12)),
],
),
),
Positioned(
top: 0,
child: AnimatedOpacity(
opacity: trans > headerImgHeight
? ((trans - headerImgHeight) / 40 > 1
? 1
: (trans - headerImgHeight) / 40)
: 0,
duration: const Duration(milliseconds: 400),
child: ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
///整体模糊度
child: Container(
padding: EdgeInsets.only(
top: MediaQueryData.fromWindow(window).padding.top),
height: 80,
width: MediaQuery.of(context).size.width,
alignment: Alignment.center,
decoration: const BoxDecoration(
color: Color.fromRGBO(15, 15, 15, 0.88),
///背景透明
),
child: Row(
children: [
Container(
padding: const EdgeInsets.only(left: 12, right: 12),
child: const CircleAvatar(
backgroundImage:
AssetImage('assets/images/cha4.jpg'),
),
),
const Text(
'吸鼠霸王',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500),
)
],
),
),
),
))),
Positioned(
right: 16,
top: (MediaQueryData.fromWindow(window).padding.top)+10,
child: CircleAvatar(
radius: 16,
backgroundColor: Colors.black54,
child: IconButton(
onPressed: () {debugPrint('setting');},
color: Colors.white,
iconSize: 16,
icon: Icon(Icons.settings_rounded))))
],
));
}
}
现在的效果💃,也弄了蛮久,气死我了
本地图片用网络图片代替或者用你的本地图像
顶部appbar
Positioned(
top: 0,
child: AnimatedOpacity(
opacity: trans > headerImgHeight
? ((trans - headerImgHeight) / 40 > 1
? 1
: (trans - headerImgHeight) / 40)
: 0,
duration: const Duration(milliseconds: 400),
child: ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
///整体模糊度
child: Container(
padding: EdgeInsets.only(
top: MediaQueryData.fromWindow(window).padding.top),
height: 80,
width: MediaQuery.of(context).size.width,
alignment: Alignment.center,
decoration: const BoxDecoration(
color: Color.fromRGBO(15, 15, 15, 0.88),
///背景透明
),
child: Row(
children: [
Container(
padding: const EdgeInsets.only(left: 12, right: 12),
child: const CircleAvatar(
backgroundImage:
AssetImage('assets/images/cha4.jpg'),
),
),
const Text(
'吸鼠霸王',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500),
)
],
),
),
),
))),
图像下拉
SliverFlexibleHeader(
visibleExtent: (headerImgHeight + 36.0),
builder: (context, availableHeight, direction) {
return GestureDetector(
child: LayoutBuilder(builder: (context, cons) {
return Stack(
children: [
Container(
padding: const EdgeInsets.only(bottom: 36),
child: Image(
image: const NetworkImage(
"https://w.wallhaven.cc/full/zy/wallhaven-zygeko.jpg"),
height: availableHeight,
width: MediaQuery.of(context).size.width,
fit: BoxFit.cover,
),
),
Positioned(
left: 0,
bottom: 36,
child: Container(
width: MediaQuery.of(context).size.width,
height: 48,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: [
0.0,
1.0
], //[渐变起始点, 渐变结束点]
//渐变颜色[始点颜色, 结束颜色]
colors: [
Color.fromRGBO(15, 15, 15, 0),
Color.fromRGBO(15, 15, 15, 0.8)
])),
),
),
Positioned(
left: 12,
bottom: 0,
child: Row(
children: [
Container(
width: 72,
height: 72,
//设置了 decoration 就不能设置color,两者只能存在一个
decoration: const BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black26,
offset: Offset(0, 0.0),
//阴影y轴偏移量
blurRadius: 2,
//阴影模糊程度
spreadRadius: 1 //阴影扩散程度
)
],
image: DecorationImage(
image: AssetImage(
"assets/images/cha4.jpg")),
//设置图片
borderRadius: BorderRadius.all(
Radius.circular(6)))),
Padding(
padding: const EdgeInsets.only(left: 12),
child: SizedBox(
height: 72,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisAlignment:
MainAxisAlignment.spaceAround,
children: const [
Text("吸鼠霸王",
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
color: Colors.white)),
Text("UID:hahaha_666",
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 12,
color: Colors.orange))
],
),
),
)
],
)),
],
);
}),
);
},
),