Flutter中实现轮播图的方式有很多种,比如使用三方flutter_swiper,card_swiper等等,使用这些三方,可以很快很方便的实现一个轮播图展示,基本上也能满足我们日常的开发需求,如果说,想要一些定制化的操作,那么就不得不去更改源码或者自己自定义一个,自己定义的话,Flutter中提供了原生组件PageView,可以使用它很方便的来实现一个轮播图。
PageView类似于Android中的ViewPager,可以实现页面的横向或者纵向滑动,具体的使用方式可以直接PageView(),或者使用PageView.builder(),这两种方式都可以实现,区别就是前者会把所有页面一次性初始化出来,而后者则不会,为了便于大家了解这个组件,我们会简单的举一个小案例。
按照以往惯例,我们先看下本篇文章的大纲,大概如下:
1、最终的实现效果一览
2、PageView组件的属性和具体使用
3、轮播图封装注意事项
4、案例源码刨析
5、封装后的源码及使用方式
6、总结
利用PageView,封装了一些特定的效果,比如文字指示器,圆角指示器,以及指示器的位置,轮播图片的缩进展示等等,录制了一个Gif效果图,如下:
毕竟是使用PageView来实现一个轮播图,那么针对这个组件,我们需要简单的做个介绍:
先看一下基本的常见属性:
属性 |
类型 |
概述 |
scrollDirection |
Axis |
滚动方向,水平或者垂直,默认水平。 水平:Axis.horizontal 垂直:Axis.vertical |
controller |
PageController |
滚动控制器,可以定位页面,获取页面等信息 |
onPageChanged |
ValueChanged |
页面发生改变时的回调 |
physics |
ScrollPhysics |
滑动效果,不设置,会根据不同平台有不同的滚动效果 NeverScrollableScrollPhysics 设置后,页面就不可滚动 BouncingScrollPhysics 表示滚动到底了会有弹回的效果,就是iOS的默认交互 ClampingScrollPhysics 表示滚动到底了就给一个效果,就是Android的默认交互 FixedExtentScrollPhysics 就是iOS经典选择时间组件UIDatePicker那种交互 |
pageSnapping |
bool |
是否是整页滑动,默认为true |
在实际的开发中,PageView.builder()方式使用是居多的,也建议大家以这种方式作为使用,很简单,只需要在itemBuilder里返回页面视图即可,代码如下:
PageView.builder(
itemCount: 6,
onPageChanged: (position) {
print("当前索引为:$position");
},
itemBuilder: (context, index) {
return Container(
color: Colors.amber,
alignment: Alignment.center,
child: Text("我是第$index个页面"));
})
基本效果如下:
基本掌握了PageView的用法之后,我们就开始着手封装一个轮播图,先分析一下,构成轮播图的几个要素,第一,满足自动轮播的要求,而且可以动态设置轮播时长,第二,要能满足多种指示器要求,而且位置可以动态设置,第三,要满足手动轮播和自动轮播要求,并且要处理好手势和定时直接的冲突,第四,最主要的就是使用起来要简单。
简单的确定要素之后,我们就可以动手书写了,自动轮播很简单,我们只需要开启一个定时器即可,但是定时器需要注意开启和暂停,也就是什么时候开始,什么时候暂停,否则很容易造成轮播混乱现象。
轮播图开始,其一,也就是主动设置了自动轮播属性,进入到页面,我们就需要开启定时,如果页面退入后台,再重新回到前台,我们也是需要开启轮播的,其二就是暂停,除了退入后台暂停之外,还有就是手势滑动的时候也需要暂停,否则就会和定时造成冲突。
关于手势,如果我们直接监听页面组件的手势,发现是和PageView有冲突的,为了解决这个手势问题,我们可以采用原始指针事件Listener来监听手势滑动。
部分代码如下,手指按下后,取消定时,手指抬起后,开启定时,当然了如果只有按下和抬起,那么则是一个点击事件,我们可以把这个事件回调给用户。
Listener(
onPointerDown: (event) {
//手指按下,定时取消
_pauseTimer();
_isClick = true;
},
onPointerMove: (event) {
_isClick = false;
},
onPointerUp: (event) {
//手指抬起,定时开启
_startTimer();
//作为点击事件
if (_isClick && widget.bannerClick != null) {
widget.bannerClick!(_currentPage);
}
},
child: PageView.builder()t
)
指示器需要注意,如果说自己用,一种指示器无可厚非,如果是给他人用,那么就要丰富多彩,尽量满足多的需求。
定时器使用的是Timer,定义了两个方法,便于开启和暂停,当轮播时间到时,就可以执行页面切换操作,使用PageController的animateToPage来切换。
/*
* 开启定时
* */
void _startTimer() {
if (!_isRunning) {
_isRunning = true;
_timer = Timer.periodic(Duration(seconds: widget.delay!), (timer) {
_controller.animateToPage(_pagePosition + 1,
duration: const Duration(milliseconds: 800),
curve: Curves.easeInOut);
});
}
}
/*
* 暂停定时
* */
void _pauseTimer() {
if (_isRunning) {
_isRunning = false;
_timer?.cancel(); //取消计时器
}
}
@override
void dispose() {
_controller.dispose();
_timer?.cancel();
super.dispose();
}
当页面退入后台和回到前台,我们需要做暂停和开启定时,那么就需要针对页面做监听操作,添加监听后,记得当前类with WidgetsBindingObserver。
// 添加监听
WidgetsBinding.instance.addObserver(this);
/*
* 感知生命周期变化
* */
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed && widget.autoPlay!) {
_startTimer(); //页面可见,开启定时
} else if (state == AppLifecycleState.paused && _isRunning) {
_pauseTimer(); //页面不可见,关闭定时
}
}
图片的圆角实现就比较多了,比如Container的装饰器,或者使用组件ClipRRect都可以的。
ClipRRect(
//设置图片圆角
borderRadius: BorderRadius.circular(widget.radius!),
child: getBannerImage(imageUrl)))
指示器类型,可以根据业务需求,进行专项定制,目前源码中的类型,有以下几种,分别是,圆形,圆角,矩形,文字,其位置,可以放到中间,左右两边以及轮播图的下方。
/*
* 指示器
* */
Widget _buildIndicators(mainAxisAlignment) {
if (widget.indicatorType == IndicatorType.text) {
//文字
return Container(
alignment: widget.textIndicatorAlignment,
child: VipText(
"${_currentPage + 1}/${widget.imageList!.length}",
style: widget.textIndicatorStyle,
backgroundColor: widget.textIndicatorBgColor,
padding: widget.textIndicatorPadding,
paddingLeft: widget.textIndicatorPaddingLeft,
paddingTop: widget.textIndicatorPaddingTop,
paddingRight: widget.textIndicatorPaddingRight,
paddingBottom: widget.textIndicatorPaddingBottom,
),
);
}
return Row(
mainAxisAlignment: mainAxisAlignment,
children: List.generate(widget.imageList!.length, (index) {
return Container(
width: _currentPage == index
? widget.indicatorWidth
: widget.indicatorUnWidth ?? widget.indicatorWidth,
height: _currentPage == index
? widget.indicatorHeight
: widget.indicatorUnHeight ?? widget.indicatorHeight,
margin: EdgeInsets.symmetric(horizontal: widget.indicatorMargin!),
decoration: BoxDecoration(
shape: widget.indicatorType == IndicatorType.circle
? BoxShape.circle
: BoxShape.rectangle,
borderRadius: widget.indicatorType == IndicatorType.rectangle
? BorderRadius.all(Radius.circular(widget.indicatorRadius!))
: null,
color: _currentPage == index
? widget.indicatorSelectColor
: widget.indicatorUnSelectColor,
),
);
}),
);
}
缩进的话,有两种,一种除了当前图片,左右图片会变小,当滑动到当前图片之后才会放大,一种就是很简单的缩进。
viewportFraction 可以理解为一页内容占据屏幕的比例,铺满就是1,小于1就是不铺满。
PageController(viewportFraction: widget.viewportFraction!)
如果说,在滑动的时候,想要图片实现放大和缩小动画,那么我们需要执行一个放大和缩小动画Transform.scale。
return Transform.scale(
scale: endScale,
child: Container(
margin: widget.imageMargin != null
? EdgeInsets.all(widget.imageMargin!)
: EdgeInsets.only(
left: widget.imageMarginLeft!,
top: widget.imageMarginTop!,
right: widget.imageMarginRight!,
bottom: widget.imageMarginBottom!),
child: ClipRRect(
//设置图片圆角
borderRadius: BorderRadius.circular(widget.radius!),
child: getBannerImage(imageUrl))))
目前源码已经上传至了Github,大家需要的话,可以查看,由于篇幅有限,就不全部粘贴了,地址:
https://github.com/AbnerMing888/flutter_widget/blob/master/lib/ui/widget/vip_banner.dart
属性 |
类型 |
概述 |
imageList |
List |
图片地址集合 |
titleList |
List |
标题集合 |
radius |
double |
图片圆角 |
height |
double |
图片高度 |
delay |
int |
多少时间轮播一次 |
autoPlay |
bool |
是否自动轮播 |
bannerClick |
Function(int) |
条目点击事件 |
showIndicators |
bool |
是否展示指示器 |
imageMarginLeft |
double |
图片距离左边的距离 |
imageMarginTop |
double |
图片距离上边的距离 |
imageMarginRight |
double |
图片距离右边的距离 |
imageMarginBottom |
double |
图片距离下边的距离 |
imageMargin |
double |
图片距离左上右下的距离,统一设置 |
marginLeft |
double |
轮播图整体距离左边的距离 |
marginTop |
double |
轮播图整体距离上边的距离 |
marginRight |
double |
轮播图整体距离右边的距离 |
marginBottom |
double |
轮播图整体距离下边的距离 |
margin |
double |
轮播图整体距离左上右下的距离 |
indicatorMarginLeft |
double |
指示器距离左边的距离 |
indicatorMarginRight |
double |
指示器距离右边的距离 |
indicatorMarginBottom |
double |
指示器距离底部的距离 |
indicatorSelectColor |
Color |
指示器选中的颜色 |
indicatorUnSelectColor |
Color |
指示器未选中的颜色 |
indicatorWidth |
double |
指示器宽 |
indicatorHeight |
double |
指示器高 |
indicatorUnWidth |
double |
指示器未选中宽 |
indicatorUnHeight |
double |
指示器未选中高 |
indicatorMargin |
double |
指示器边距 |
indicatorType |
IndicatorType |
指示器类型 circle, rectangle, text |
indicatorRadius |
double |
指示器圆角度数 |
indicatorBannerBottom |
bool |
指示器位置,是在banner上还是Banner下 |
indicatorBottomColor |
Color |
指示器在Banner下的背景,默认是透明 |
indicatorBottomHeight |
double |
指示器在Banner下的高度 |
indicatorBottomMarginRight |
double |
指示器在Banner下的 距离右边 |
indicatorBottomMarginLeft |
double |
指示器在Banner下的 距离左边 |
indicatorBottomMainAxisAlignment |
MainAxisAlignment |
指示器在Banner下的位置 左,中,右 |
viewportFraction |
double |
banner缩进 |
textIndicatorAlignment |
Alignment |
文字的位置 |
textIndicatorStyle |
TextStyle |
文字样式 |
textIndicatorBgColor |
Color |
文字指示器背景 |
textIndicatorPadding |
double |
文字指示器内边距 |
textIndicatorPaddingLeft |
double |
文字指示器内边距左 |
textIndicatorPaddingTop |
double |
文字指示器内边距上 |
textIndicatorPaddingRight |
double |
文字指示器内边距右 |
textIndicatorPaddingBottom |
double |
文字指示器内边距下 |
titleBgColor |
Color |
文字Title背景 |
titleHeight |
double |
文字Title高度 |
titleAlignment |
Alignment |
文字Title的位置 |
titleStyle |
TextStyle |
文字Title样式 |
titleMarginBottom |
double |
文字Title距离底部 |
bannerOtherScale |
double |
除中间外的其他图片缩放比例 |
placeholderImage |
String |
Banner 占位图 |
errorImage |
String |
Banner 错误图 |
imageBoxFit |
BoxFit |
图片伸缩模式 |
普通加载
VipBanner(
imageList: const [
"https://www.vipandroid.cn/ming/image/gan.png",
"https://www.vipandroid.cn/ming/image/zao.png"
],
bannerClick: (position) {
//条目点击
Toast.toast(context, msg: position.toString());
})
文字指示器
VipBanner(
imageList: const [
"https://www.vipandroid.cn/ming/image/gan.png",
"https://www.vipandroid.cn/ming/image/zao.png"
],
indicatorType: IndicatorType.text,
bannerClick: (position) {
Toast.toast(context, msg: position.toString());
})
圆角指示器
VipBanner(
imageList: const [
"https://www.vipandroid.cn/ming/image/gan.png",
"https://www.vipandroid.cn/ming/image/zao.png"
],
indicatorType: IndicatorType.rectangle,
indicatorRadius: 5,
indicatorWidth: 20,
indicatorHeight: 5,
bannerClick: (position) {
Toast.toast(context, msg: position.toString());
})
使用方式呢,有很多的类型,就不一一举例了,大家可以看源码中的页面,地址是:
https://github.com/AbnerMing888/flutter_widget/blob/master/lib/ui/page/view/banner/banner_page.dart
在封装的时候,务必要确定的有以下几个要素,一是定时轮播,二是手势和定时冲突解决,三是无限轮播,四是指示器的设置,五是图片轮播的效果,搞定这些潜在的要素,一个简简单单的轮播图封装起来并不难。