使用Flutter实现一个走马灯布局的示例代码
作者:ColdStone 发布时间:2023-06-19 03:50:03
走马灯是一种常见的效果,本文讲一下如何用 PageView
在 Flutter
里实现一个走马灯, 效果如下,当前页面的高度比其它页面高,切换页面的时候有一个高度变化的动画。实现这样的效果主要用到的是 PageView.builder
部件。
开发
创建首页
首先创建一个 IndexPage
部件,这个部件用来放 PageView
,因为需要使用 setState
方法更新 UI,所以它是 stateful 的。
import 'package:flutter/material.dart';
class IndexPage extends StatefulWidget {
@override
_IndexPageState createState() => _IndexPageState();
}
class _IndexPageState extends State<IndexPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0.0,
backgroundColor: Colors.white,
),
body: Column(
children: <Widget>[],
),
);
}
}
然后在部件内申明一个 _pageIndex
变量用来保存当前显示的页面的 index,在 initState
生命周期里面初始化一个 PageController
用来配置 PageView
部件。
在 body
的 Column
里面创建一个 PageView.builder
,使用一个 SizedBox
部件指定 PageView
的高度,将 controller
设置为 _pageController
,在 onPageChanged
事件里将当前显示页面的 index
值赋值给 _pageIndex
变量。
int _pageIndex = 0;
PageController _pageController;
@override
void initState() {
super.initState();
_pageController = PageController(
initialPage: 0,
viewportFraction: 0.8,
);
}
body: Column(
children: <Widget>[
SizedBox(
height: 580.0,
child: PageView.builder(
itemCount: 3,
pageSnapping: true,
controller: _pageController,
onPageChanged: (int index) {
setState(() {
_pageIndex = index;
});
},
itemBuilder: (BuildContext ctx, int index) {
return _buildItem(_pageIndex, index);
},
),
),
],
),
关键点: 设置 PageController
的 viewportFraction
参数小于 1,这个值是用来设置每个页面在屏幕上显示的比例,小于 1 的话,就可以在当前页面同时显示其它页面的内容了。
/// The fraction of the viewport that each page should occupy.
/// Defaults to 1.0, which means each page fills the viewport in the scrolling direction.
final double viewportFraction;
实现 _buildItem
接着实现 _buildItem
方法,这个方法就是返回 PageView.builder
里每一个页面渲染的内容,第一个参数 activeIndex
是当前显示在屏幕上页面的 index
,第二个参数 index
是每一项自己的 index
。
使用一个 Center
部件让内容居中显示,然后用一个 AnimatedContainer
添加页面切换时的高度变化的动画效果,切换页面的时候使用了 setState
方法改变了 _pageIndex
, Flutter
重新绘制每一项。关键点在于判断当前页面是否为正在显示的页面,是的话它的高度就是 500 不是的话就是 450。
_buildItem(activeIndex, index) {
return Center(
child: AnimatedContainer(
curve: Curves.easeInOut,
duration: Duration(milliseconds: 300),
height: activeIndex == index ? 500.0 : 450.0,
margin: EdgeInsets.symmetric(vertical: 20.0, horizontal: 10.0),
decoration: BoxDecoration(
color: heroes[index].color,
borderRadius: BorderRadius.all(Radius.circular(12.0)),
),
child: Stack(),
),
);
}
添加内容
然后给 AnimatedContainer
添加每一项的内容
child: Stack(
fit: StackFit.expand,
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.all(
Radius.circular(12.0),
),
child: Image.network(
heroes[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(
heroes[index].title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
)
],
),
),
],
),
实现指示器
然后实现页面的指示器,创建一个 PageIndicator
部件,需要传入 pageCount
表示总页数,以及 currentIndex
表示当前显示的页数索引。把所有指示器放在一个 Row
部件里,判断当前指示器的 index
是否为正在显示页面的 index
,是的话显示较深的颜色。
class PageIndicator extends StatelessWidget {
final int pageCount;
final int currentIndex;
const PageIndicator(this.currentIndex, this.pageCount);
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;
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _buildIndicators(),
);
}
}
添加 PageIndicator
到 SizedBox
下面
封装 Carousel
最后的最后优化一下代码,把部件封装一下,让它成为一个单独的部件,创建一个 Carousel
部件,对外暴露 items
和 height
两个属性,分别配置数据和高度。
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;
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: 20.0, horizontal: 10.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,
),
),
),
)
],
),
),
],
),
),
);
}
@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 ctx, int index) {
return _buildItem(_pageIndex, index);
},
),
),
PageIndicator(_pageIndex, widget.items.length),
],
);
}
}
之后在 IndexPage
部件里就只用实例化一个 Carousel
了,同时由于 IndexPage
不用管理部件状态了,可以将它变成 StatelessWidget
。
完整代码
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(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: '九尾妖狐-阿狸',
),
];
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;
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: 20.0, horizontal: 10.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,
),
),
),
)
],
),
),
],
),
),
);
}
@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 ctx, int index) {
return _buildItem(_pageIndex, index);
},
),
),
PageIndicator(_pageIndex, widget.items.length),
],
);
}
}
class PageIndicator extends StatelessWidget {
final int currentIndex;
final int pageCount;
const PageIndicator(this.currentIndex, this.pageCount);
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;
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _buildIndicators(),
);
}
}
class IndexPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0.0,
backgroundColor: Colors.white,
),
body: Carousel(
height: 540,
items: heroes,
),
backgroundColor: Colors.white,
);
}
}
至此,整个布局就完成了! :sunglasses:
来源:https://juejin.im/post/5dd964eaf265da7dee070874
猜你喜欢
- 进阶JavaSE-三大接口:Comparator、Comparable和Cloneable。Comparable和Comparator这两个
- 需求当游戏显示3d场景及其UI的时候。玩家左右晃动手机的时候,UI界面会随之左右偏移。上下晃动的时候,3D场景会随之上下偏移。手机停止晃动的
- 这篇文章主要介绍了Java多态中动态绑定原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以
- import java.io.File;import java.io.FileNotFoundException;import java.i
- 前言不积跬步无以至千里,不积小流,无以成江海在公司一般来说,都只会接触一些CRUD的业务,很多时候可能你想设计很多的代码结构,但是时间不允许
- 本文实例为大家分享了Unity快速生成常用文件夹的具体代码,供大家参考,具体内容如下前言每次打开新工程创建文件夹都很麻烦,写了一个小工具代码
- ConstantConstant 和 ConstantPool 是用于表示常量的一种机制。Constant 接口定义了常量的基本属性和方法,
- Java IO 转化流乱码引起转换流读取乱码读取电脑磁盘上的Java.txt文件内容,文件路径: e:\Java\Java.txt
- 由于工作的需要,最近要对Android客户端软件进行测试,便学习了一下Android客户端测试的方法,现在与大家分享一下。1.在Androi
- 使用 fragmentLayout 实现,可以把小红点添加到任意 view 上。效果 添加小红点到 textview 上添加小红点到 ima
- 在java中我们常常使用加锁机制来确保线程安全,但是如果过度使用加锁,则可能导致锁顺序死锁。同样,我们使用线程池和信号量来限制对资源的使用,
- mybatis oracle 自增序列的代码如下所示:<insert id=" insert " useGener
- 一、建数据库和表1.数据库demo1放一张user表SET FOREIGN_KEY_CHECKS=0;-- ----------------
- 目前常用的ORM框架有 Mybatis(batis)、MybatisPlus,Hibernate、Jpa等几个框架,今天就简单介绍一下搭建M
- 第一步:在d盘新建android文件夹,在此文件夹中再建三个子文件夹,Android Studio 、 Android_SDK、Androi
- 效果如下:鄙人虽然开发WPF有些时间,但之前一直是一些简单Template和Style改改之类的工作,并没有深入研究过。此次为了完成工作,首
- 前言:在看这个变更之前,我们需要回忆下 Android 12 的一个安全性变更, 即声明了 <intent-filter&g
- 环境准备项目结构与前文相同:我们添加新的静态资源:因为添加了静态资源,SpringMVC会拦截,所有需要在SpringConfig的配置类中
- 设计模式要进行共性与可变性的分析,对共性进行抽象,同时对可变性进行封装,没有完美的设计模式,作为一名开发者要懂得取舍,触类旁通,开发出高内聚
- 本文实例讲述了Android数据持久化之Preferences机制。分享给大家供大家参考,具体如下:在Android中,实现数据持久化有五种