效果图:
过程脑洞自补!!!
类似于上图的效果,我们应该能想到用PageView来实现,但是思来想去,PageView官方并没有给自定义的参数,就是我们需要的下面的指示器的效果。现在我就来说下一下怎么来实现他。
在Flutter的字典中,有一句话说的好,“万事不决Stack”好了我们就套用Stack布局来实现图片和图片标题的内容嵌套。
首先我们先建立一个存放图片的类,本来想用api来,但是发现还是用代码来实现吧
class Hero {
final Color color;
final String image;
final String title;
Hero({
@required this.color,
@required this.image,
@required this.title,
});
}
这里我们建立了一个Hero类,里面有三个必填参数。然后我们根据这个类来建立我们所需要的轮播图的内容。
为了方便点我直接用List来显示
List heroes = [
Hero(
color: Color(0xFF4C314D),
image: "https://game.gtimg.cn/images/lol/act/img/skin/big38005.jpg",
title: '虚空行者-卡萨丁'
),
Hero(
color: Color(0xFF86F3FB),
image: "https://game.gtimg.cn/images/lol/act/img/skin/big22009.jpg",
title: '寒冰射手-艾希',
),
Hero(
color: Color(0xFF7D6588),
image: "https://game.gtimg.cn/images/lol/act/img/skin/big39006.jpg",
title: '刀锋舞者-艾瑞莉娅',
),
Hero(
color: Color(0xFF4C314D),
image: "https://game.gtimg.cn/images/lol/act/img/skin/big103015.jpg",
title: '九尾妖狐-阿狸',
),
Hero(
color: Color(0xFF4C314D),
image: "https://game.gtimg.cn/images/lol/act/img/skin/big51004.jpg",
title: '皮城女警-凯瑟琳'
),
];
然后进入我们的主要部分,创建一个有状态的组件Carousel,然后他接收连个必填的参数,上面我们定义的List内容列表,高度。然后在他的State里面用PageView和我们自定义的一个**PageIndicator(指示器)**来实现。
class Carousel extends StatefulWidget {
final List items;
final double height;
const Carousel({
@required this.items,
@required this.height,
});
@override
_CarouselState createState() => _CarouselState();
}
class _CarouselState extends State<Carousel> {
int _pageIndex = 0;
PageController _pageController;
@override
void initState() {
super.initState();
_pageController = PageController(
initialPage: 0,
viewportFraction: 0.8,
);
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
height: widget.height,
child: PageView.builder(
pageSnapping: true,
itemCount: heroes.length,
controller: _pageController,
onPageChanged: (int index) {
setState(() {
_pageIndex = index;
});
},
itemBuilder: (BuildContext context, int index) {
return _buildItem(_pageIndex, index);
},
),
),
PageIndicator(_pageIndex, widget.items.length),
],
);
}
Widget _buildItem(activeIndex, index) {
final items = widget.items;
return Center(
child: AnimatedContainer(
curve: Curves.easeInOut,
duration: Duration(milliseconds: 300),
height: activeIndex == index ? 500.0 : 450.0,
margin: EdgeInsets.symmetric(vertical: 10.0, horizontal: 5.0),
decoration: BoxDecoration(
color: items[index].color,
borderRadius: BorderRadius.all(Radius.circular(12.0)),
),
child: Stack(
fit: StackFit.expand,
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.all(
Radius.circular(12.0),
),
child: Image.network(
items[index].image,
fit: BoxFit.cover,
),
),
Align(
alignment: Alignment.bottomCenter,
child: Row(
children: <Widget>[
Expanded(
child: Container(
padding: EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.only(
bottomRight: Radius.circular(12.0),
bottomLeft: Radius.circular(12.0),
),
),
child: Text(
items[index].title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
)
],
),
),
],
),
),
);
}
}
分析代码:我们在组件内部用一个_pageIndex变量来保存当前显示页面的index,很自然的我们要在initState
声明周期里面要初始化一个PageController
用来控制PageView组件。在在里面的内容我就不多说了,这是最简单的PageView.builder的使用。
再补充一点: 设置 PageController 的 viewportFraction 参数小于 1,这个值是用来设置每个页面在屏幕上显示的比例,小于 1 的话,就可以在当前页面同时显示其它页面的内容了。
然后下面是我们自定义封装的指示器组件
class PageIndicator extends StatelessWidget {
final int currentIndex;
final int pageCount;
const PageIndicator(this.currentIndex, this.pageCount);
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _buildIndicators(),
);
}
Widget _indicator(bool isActive) {
return Container(
width: 6.0,
height: 6.0,
margin: EdgeInsets.symmetric(horizontal: 3.0),
decoration: BoxDecoration(
color: isActive ? Color(0xff666a84) : Color(0xffb9bcca),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black12,
offset: Offset(0.0, 3.0),
blurRadius: 3.0,
),
],
),
);
}
List<Widget> _buildIndicators() {
List<Widget> indicators = [];
for (int i = 0; i < pageCount; i++) {
indicators.add(i == currentIndex ? _indicator(true) : _indicator(false));
}
return indicators;
}
}
分析代码: PageIndicator 组件我们接受了两个参数,一个是当前页面的索引,还有一个是页面的总数。多的话我也不说了,这里是很简单的逻辑。
最后我贴一下完整代码
import 'package:flutter/material.dart';
class Hero {
final Color color;
final String image;
final String title;
Hero({
@required this.color,
@required this.image,
@required this.title,
});
}
List heroes = [
Hero(
color: Color(0xFF4C314D),
image: "https://game.gtimg.cn/images/lol/act/img/skin/big38005.jpg",
title: '虚空行者-卡萨丁'
),
Hero(
color: Color(0xFF86F3FB),
image: "https://game.gtimg.cn/images/lol/act/img/skin/big22009.jpg",
title: '寒冰射手-艾希',
),
Hero(
color: Color(0xFF7D6588),
image: "https://game.gtimg.cn/images/lol/act/img/skin/big39006.jpg",
title: '刀锋舞者-艾瑞莉娅',
),
Hero(
color: Color(0xFF4C314D),
image: "https://game.gtimg.cn/images/lol/act/img/skin/big103015.jpg",
title: '九尾妖狐-阿狸',
),
Hero(
color: Color(0xFF4C314D),
image: "https://game.gtimg.cn/images/lol/act/img/skin/big51004.jpg",
title: '皮城女警-凯瑟琳'
),
];
class Carousel extends StatefulWidget {
final List items;
final double height;
const Carousel({
@required this.items,
@required this.height,
});
@override
_CarouselState createState() => _CarouselState();
}
class _CarouselState extends State<Carousel> {
int _pageIndex = 0;
PageController _pageController;
@override
void initState() {
super.initState();
_pageController = PageController(
initialPage: 0,
viewportFraction: 0.8,
);
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
height: widget.height,
child: PageView.builder(
pageSnapping: true,
itemCount: heroes.length,
controller: _pageController,
onPageChanged: (int index) {
setState(() {
_pageIndex = index;
});
},
itemBuilder: (BuildContext context, int index) {
return _buildItem(_pageIndex, index);
},
),
),
PageIndicator(_pageIndex, widget.items.length),
],
);
}
Widget _buildItem(activeIndex, index) {
final items = widget.items;
return Center(
child: AnimatedContainer(
curve: Curves.easeInOut,
duration: Duration(milliseconds: 300),
height: activeIndex == index ? 500.0 : 450.0,
margin: EdgeInsets.symmetric(vertical: 10.0, horizontal: 5.0),
decoration: BoxDecoration(
color: items[index].color,
borderRadius: BorderRadius.all(Radius.circular(12.0)),
),
child: Stack(
fit: StackFit.expand,
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.all(
Radius.circular(12.0),
),
child: Image.network(
items[index].image,
fit: BoxFit.cover,
),
),
Align(
alignment: Alignment.bottomCenter,
child: Row(
children: <Widget>[
Expanded(
child: Container(
padding: EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.only(
bottomRight: Radius.circular(12.0),
bottomLeft: Radius.circular(12.0),
),
),
child: Text(
items[index].title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
)
],
),
),
],
),
),
);
}
}
class PageIndicator extends StatelessWidget {
final int currentIndex;
final int pageCount;
const PageIndicator(this.currentIndex, this.pageCount);
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _buildIndicators(),
);
}
Widget _indicator(bool isActive) {
return Container(
width: 6.0,
height: 6.0,
margin: EdgeInsets.symmetric(horizontal: 3.0),
decoration: BoxDecoration(
color: isActive ? Color(0xff666a84) : Color(0xffb9bcca),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black12,
offset: Offset(0.0, 3.0),
blurRadius: 3.0,
),
],
),
);
}
List<Widget> _buildIndicators() {
List<Widget> indicators = [];
for (int i = 0; i < pageCount; i++) {
indicators.add(i == currentIndex ? _indicator(true) : _indicator(false));
}
return indicators;
}
}
class IndexPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0.0,
backgroundColor: Colors.white,
),
body: Carousel(
height: 510,
items: heroes,
),
backgroundColor: Colors.white,
);
}
}
总结
本文主要是讲解了PageVIew的使用还有Stack在什么情况下适用(当然任何情况都可以,你要是适配做好了,按照设计图的尺寸直接Stack完成就是不太友好,代码的整体可读性不是太好,当然你要是做了全封装那当我没说)。还有我们自定义指示器的使用,其实很简单,就是一个index渲染的过程。最后我要说的是,我没贴上main入口函数的代码,相信只要看完了动动脑子的都知道,在这里我也说了你直接复制就能用,main入口函数下MaterialApp的home参数为IndexPage
。
有问题可以提出来,大家共同进步。