flutter开发实战-下拉刷新继续下拉路由进入活动页面实现
很多应用都有首页通过下拉刷新,继续下拉进入新的活动会场进入方式。在Flutter中,也可以通过pull_to_refresh来实现控制刷新页,继续下拉进入新的活动会场页面
一、引入pull_to_refresh
在工程的pubspec.yaml中引入插件
pull_to_refresh: ^2.0.0
在pull_to_refresh中有enableTwoLevel、enablePullDown、enablePullUp属性。
onRefresh:当调用下拉刷新的回调onRefresh,当拉取数据完成后可以调用_refreshController1.refreshCompleted()。
onLoading:当调用上拉加载数据的时候的回调onLoading,获取数据完成后调用_refreshController1.loadComplete();
header:下拉刷新显示的headerWidget
footer:上拉加载显示的footerWidget
插件example示例如下
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'
hide RefreshIndicator, RefreshIndicatorState;
import 'package:flutter/widgets.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
/*
there two example implements two level,
the first is common,when twoRefreshing,header will follow the list to scrollDown,when closing,still follow
list move up,
the second example use Navigator and keep offset when twoLevel trigger,
header can use ClassicalHeader to implments twoLevel,it provide outerBuilder(1.4.7)
important point:
1. open enableTwiceRefresh bool ,default is false
2. _refreshController.twiceRefreshComplete() can closing the two level
*/
class TwoLevelExample extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return _TwoLevelExampleState();
}
}
class _TwoLevelExampleState extends State<TwoLevelExample> {
RefreshController _refreshController1 = RefreshController();
RefreshController _refreshController2 = RefreshController();
int _tabIndex = 0;
@override
void initState() {
// TODO: implement initState
_refreshController1.headerMode?.addListener(() {
setState(() {});
});
WidgetsBinding.instance.addPostFrameCallback((_) {
_refreshController1.position?.jumpTo(0);
setState(() {});
});
super.initState();
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return RefreshConfiguration.copyAncestor(
context: context,
enableScrollWhenTwoLevel: true,
maxOverScrollExtent: 120,
child: Scaffold(
bottomNavigationBar: !_refreshController1.isTwoLevel
? BottomNavigationBar(
currentIndex: _tabIndex,
onTap: (index) {
_tabIndex = index;
if (mounted) setState(() {});
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.add), label: "二级刷新例子1"),
BottomNavigationBarItem(
icon: Icon(Icons.border_clear), label: "二级刷新例子2")
],
)
: null,
body: Stack(
children: <Widget>[
Offstage(
offstage: _tabIndex != 0,
child: LayoutBuilder(
builder: (_, c) {
return SmartRefresher(
header: TwoLevelHeader(
textStyle: TextStyle(color: Colors.white),
displayAlignment: TwoLevelDisplayAlignment.fromTop,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("images/secondfloor.jpg"),
fit: BoxFit.cover,
// 很重要的属性,这会影响你打开二楼和关闭二楼的动画效果
alignment: Alignment.topCenter),
),
twoLevelWidget: TwoLevelWidget(),
),
child: CustomScrollView(
physics: ClampingScrollPhysics(),
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
child: Scaffold(
appBar: AppBar(),
body: Column(
children: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text("点击这里返回上一页!"),
),
TextButton(
onPressed: () {
_refreshController1.requestTwoLevel();
},
child: Text("点击这里打开二楼!"),
)
],
),
),
height: 500.0,
),
)
],
),
controller: _refreshController1,
enableTwoLevel: true,
enablePullDown: true,
enablePullUp: true,
onLoading: () async {
await Future.delayed(Duration(milliseconds: 2000));
_refreshController1.loadComplete();
},
onRefresh: () async {
await Future.delayed(Duration(milliseconds: 2000));
_refreshController1.refreshCompleted();
},
onTwoLevel: (bool isOpen) {
print("twoLevel opening:" + isOpen.toString());
},
);
},
),
),
Offstage(
offstage: _tabIndex != 1,
child: SmartRefresher(
header: ClassicHeader(),
child: CustomScrollView(
physics: ClampingScrollPhysics(),
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
child: TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text("点击这里返回上一页!"),
),
color: Colors.red,
height: 680.0,
),
)
],
),
controller: _refreshController2,
enableTwoLevel: true,
onRefresh: () async {
await Future.delayed(Duration(milliseconds: 2000));
_refreshController2.refreshCompleted();
},
onTwoLevel: (bool isOpen) {
if (isOpen) {
print("Asd");
_refreshController2.position?.hold(() {});
Navigator.of(context)
.push(MaterialPageRoute(
builder: (c) => Scaffold(
appBar: AppBar(),
body: Text("二楼刷新"),
)))
.whenComplete(() {
_refreshController2.twoLevelComplete(
duration: Duration(microseconds: 1));
});
}
},
),
)
],
),
),
);
}
}
class TwoLevelWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("images/secondfloor.jpg"),
// 很重要的属性,这会影响你打开二楼和关闭二楼的动画效果,关联到TwoLevelHeader,如果背景一致的情况,请设置相同
alignment: Alignment.topCenter,
fit: BoxFit.cover),
),
child: Stack(
children: <Widget>[
Center(
child: Wrap(
children: <Widget>[
TextButton(
onPressed: () {},
child: Text("登陆"),
),
],
),
),
Container(
height: 60.0,
child: GestureDetector(
child: Icon(
Icons.arrow_back_ios,
color: Colors.white,
),
onTap: () {
SmartRefresher.of(context)?.controller.twoLevelComplete();
},
),
alignment: Alignment.bottomLeft,
),
],
),
);
}
}
如果在下拉进入活动页面需要路由跳转,可以在onTwoLevel回调中调用路由跳转
if (isOpen) {
print("Asd");
_refreshController2.position?.hold(() {});
Navigator.of(context)
.push(MaterialPageRoute(
builder: (c) => Scaffold(
appBar: AppBar(),
body: Text("二楼刷新"),
)))
.whenComplete(() {
_refreshController2.twoLevelComplete(
duration: Duration(microseconds: 1));
});
}
二、下拉进入活动页面实现路由跳转
下拉一定距离后,通过路由跳转进入相应的活动页面,这里是onTwoLevel回调中push到指定页面
onTwoLevel: (bool isOpen) async {
print("aaa twoLevel opening:" + isOpen.toString());
if (isOpen) {
Navigator.of(context)
.push(MaterialPageRoute(builder: (BuildContext context) {
return TopFloorPage();
})).then((value){
print("onTwoLevel push then");
}).whenComplete(() {
print("onTwoLevel push whenComplete");
refreshController.twoLevelComplete(
duration: Duration(microseconds: 1));
});
} else {
refreshController.position?.jumpTo(0);
}
},
下拉进入活动页面实现路由跳转完整代码如下
second_floor_page.dart
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_app_demolab/display/pull_refresh_helper.dart';
import 'package:flutter_app_demolab/display/top_floor_page.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
class SlideTopRouteBuilder extends PageRouteBuilder {
final Widget page;
SlideTopRouteBuilder(this.page)
: super(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionDuration: Duration(milliseconds: 800),
transitionsBuilder:
(context, animation, secondaryAnimation, child) =>
SlideTransition(
position: Tween<Offset>(
begin: Offset(0.0, -1.0), end: Offset(0.0, 0.0))
.animate(CurvedAnimation(
parent: animation, curve: Curves.fastOutSlowIn)),
child: child,
));
}
class SecondFloorPage extends StatefulWidget {
const SecondFloorPage({super.key});
@override
State<SecondFloorPage> createState() => _SecondFloorPageState();
}
class _SecondFloorPageState extends State<SecondFloorPage> {
final RefreshController _refreshController =
RefreshController(initialRefresh: false);
RefreshController get refreshController => _refreshController;
final ScrollController _scrollController = ScrollController();
double _height = 200;
bool _showTopBtn = false;
ScrollController get scrollController => _scrollController;
bool get showTopBtn => _showTopBtn;
scrollToTop() {
_scrollController.animateTo(0,
duration: Duration(milliseconds: 300), curve: Curves.easeOutCubic);
}
@override
void initState() {
// TODO: implement initState
super.initState();
// _scrollController.addListener(() {
// if (_scrollController.offset > _height && !_showTopBtn) {
// _showTopBtn = true;
// setState(() {});
// } else if (_scrollController.offset < _height && _showTopBtn) {
// _showTopBtn = false;
// setState(() {});
// }
// });
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('SecondFloor'),
),
body: MediaQuery.removePadding(
context: context,
removeTop: false,
child: Builder(
builder: (_) {
return RefreshConfiguration.copyAncestor(
context: context,
twiceTriggerDistance: kHomeRefreshHeight - 15,
maxOverScrollExtent: kHomeRefreshHeight,
headerTriggerDistance:
80 + MediaQuery.of(context).padding.top / 3,
child: SmartRefresher(
controller: _refreshController,
header: HomeRefreshHeader(),
enableTwoLevel: true,
onTwoLevel: (bool isOpen) async {
print("aaa twoLevel opening:" + isOpen.toString());
if (isOpen) {
Navigator.of(context)
.push(MaterialPageRoute(builder: (BuildContext context) {
return TopFloorPage();
})).then((value){
print("onTwoLevel push then");
}).whenComplete(() {
print("onTwoLevel push whenComplete");
refreshController.twoLevelComplete(
duration: Duration(microseconds: 1));
});
} else {
refreshController.position?.jumpTo(0);
}
},
footer: RefresherFooter(),
enablePullDown: true,
onRefresh: () async {
await Future.delayed(Duration(milliseconds: 300));
refreshController.refreshCompleted();
},
onLoading: () async {
await Future.delayed(Duration(milliseconds: 300));
refreshController.loadComplete();
},
enablePullUp: true,
child: ListView.builder(
controller: _scrollController,
itemBuilder: (context, index) {
return Card(
child: Container(
alignment: Alignment.center,
height: 80,
child: Text('${index + 1}'),
),
);
},
itemCount: 80,
),
),
);
},
),
),
);
}
}
top_floor_page.dart
import 'package:flutter/material.dart';
class TopFloorPage extends StatefulWidget {
const TopFloorPage({super.key});
@override
State<TopFloorPage> createState() => _TopFloorPageState();
}
class _TopFloorPageState extends State<TopFloorPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('二楼页面'),
),
body: Center(
child: Text('二楼页面详情'),
),
);
}
}
pull_refresh_helper.dart
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_demolab/display/top_floor_page.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
const double kHomeRefreshHeight = 180.0;
class HomeSecondFloorOuter extends StatelessWidget {
final Widget child;
HomeSecondFloorOuter(this.child, {super.key});
@override
Widget build(BuildContext context) {
return Container(
height: kHomeRefreshHeight + MediaQuery.of(context).padding.top + 20,
width: MediaQuery.of(context).size.width,
child: Stack(
children: <Widget>[
CachedNetworkImage(
imageUrl: "https://c-ssl.dtstatic.com/uploads/blog/202404/28/LySGQjOntqjzaBW.thumb.1000_0.jpeg",
fit: BoxFit.cover,
height: kHomeRefreshHeight + MediaQuery.of(context).padding.top + 20,
width: MediaQuery.of(context).size.width,
),
Align(
alignment: Alignment.bottomCenter,
child: Text('跌跌撞撞中,依旧热爱这个世界.',
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
)),
),
Align(alignment: Alignment(0, 0.85), child: child),
],
),
alignment: Alignment.bottomCenter,
);
}
}
/// 由于app不管明暗模式,都是有底色
/// 所以将indicator颜色为亮色
class ActivityIndicator extends StatelessWidget {
final double? radius;
final Brightness? brightness;
ActivityIndicator({this.radius, this.brightness});
@override
Widget build(BuildContext context) {
return Theme(
data: ThemeData(
cupertinoOverrideTheme: CupertinoThemeData(brightness: brightness),
),
child: CupertinoActivityIndicator(radius: radius ?? 10));
}
}
/// 首页列表的header
class HomeRefreshHeader extends StatelessWidget {
const HomeRefreshHeader({super.key});
@override
Widget build(BuildContext context) {
var strings = RefreshLocalizations.of(context)?.currentLocalization ??
EnRefreshString();
return ClassicHeader(
canTwoLevelText: "松开进入二楼",
textStyle: TextStyle(color: Colors.white),
outerBuilder: (child) => HomeSecondFloorOuter(child),
twoLevelView: Container(),
height: 70 + MediaQuery.of(context).padding.top / 3,
refreshingIcon: ActivityIndicator(brightness: Brightness.dark),
releaseText: strings.canRefreshText,
);
}
}
/// 通用的footer
///
/// 由于国际化需要context的原因,所以无法在[RefreshConfiguration]配置
class RefresherFooter extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ClassicFooter(
// failedText: S.of(context).loadMoreFailed,
// idleText: S.of(context).loadMoreIdle,
// loadingText: S.of(context).loadMoreLoading,
// noDataText: S.of(context).loadMoreNoData,
);
}
}
最后需要在MaterialApp中配置RefreshConfiguration
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return RefreshConfiguration(
hideFooterWhenNotFull: true, //列表数据不满一页,不触发加载更多
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
),
);
}
}
效果图如下
三、小结
flutter开发实战-下拉刷新继续下拉路由进入活动页面实现
学习记录,每天不停进步。