使用Flutter實(shí)現(xiàn)一個走馬燈布局的示例代碼
走馬燈是一種常見的效果,本文講一下如何用 PageView 在 Flutter 里實(shí)現(xiàn)一個走馬燈, 效果如下,當(dāng)前頁面的高度比其它頁面高,切換頁面的時候有一個高度變化的動畫。實(shí)現(xiàn)這樣的效果主要用到的是 PageView.builder 部件。
開發(fā)
創(chuàng)建首頁
首先創(chuàng)建一個 IndexPage 部件,這個部件用來放 PageView ,因?yàn)樾枰褂?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>[],
),
);
}
}
然后在部件內(nèi)申明一個 _pageIndex 變量用來保存當(dāng)前顯示的頁面的 index,在 initState 生命周期里面初始化一個 PageController 用來配置 PageView 部件。
在 body 的 Column 里面創(chuàng)建一個 PageView.builder ,使用一個 SizedBox 部件指定 PageView 的高度,將 controller 設(shè)置為 _pageController ,在 onPageChanged 事件里將當(dāng)前顯示頁面的 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);
},
),
),
],
),
關(guān)鍵點(diǎn): 設(shè)置 PageController 的 viewportFraction 參數(shù)小于 1,這個值是用來設(shè)置每個頁面在屏幕上顯示的比例,小于 1 的話,就可以在當(dāng)前頁面同時顯示其它頁面的內(nèi)容了。
/// 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;
實(shí)現(xiàn) _buildItem
接著實(shí)現(xiàn) _buildItem 方法,這個方法就是返回 PageView.builder 里每一個頁面渲染的內(nèi)容,第一個參數(shù) activeIndex 是當(dāng)前顯示在屏幕上頁面的 index ,第二個參數(shù) index 是每一項(xiàng)自己的 index 。
使用一個 Center 部件讓內(nèi)容居中顯示,然后用一個 AnimatedContainer 添加頁面切換時的高度變化的動畫效果,切換頁面的時候使用了 setState 方法改變了 _pageIndex , Flutter 重新繪制每一項(xiàng)。關(guān)鍵點(diǎn)在于判斷當(dāng)前頁面是否為正在顯示的頁面,是的話它的高度就是 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(),
),
);
}
添加內(nèi)容
然后給 AnimatedContainer 添加每一項(xiàng)的內(nèi)容
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, ), ), ), ) ], ), ), ], ),
實(shí)現(xiàn)指示器
然后實(shí)現(xiàn)頁面的指示器,創(chuàng)建一個 PageIndicator 部件,需要傳入 pageCount 表示總頁數(shù),以及 currentIndex 表示當(dāng)前顯示的頁數(shù)索引。把所有指示器放在一個 Row 部件里,判斷當(dāng)前指示器的 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
最后的最后優(yōu)化一下代碼,把部件封裝一下,讓它成為一個單獨(dú)的部件,創(chuàng)建一個 Carousel 部件,對外暴露 items 和 height 兩個屬性,分別配置數(shù)據(jù)和高度。
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 部件里就只用實(shí)例化一個 Carousel 了,同時由于 IndexPage 不用管理部件狀態(tài)了,可以將它變成 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:
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android圖片處理:識別圖像方向并顯示實(shí)例教程
在Android中使用ImageView顯示圖片的時候發(fā)現(xiàn)圖片顯示不正,方向偏了或者倒過來了,下面與大家分享下具體的解決方法,感性的朋友可以參考下2013-06-06
Android遞歸方式刪除某文件夾下的所有文件(.mp3文件等等)
以刪除為例,當(dāng)然,對于遍歷某文件夾下的所有文件均可用這個方法。如搜索.mp3文件等,具體實(shí)現(xiàn)如下,感興趣的朋友可以參考下哈2013-06-06
Android編程實(shí)現(xiàn)禁止系統(tǒng)鎖屏與解鎖亮屏的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)禁止系統(tǒng)鎖屏與解鎖亮屏的方法,實(shí)例分析了Android關(guān)閉屏幕、鎖屏及解鎖屏幕的相關(guān)技巧,需要的朋友可以參考下2015-12-12
android實(shí)現(xiàn)自動發(fā)送郵件
這篇文章主要為大家詳細(xì)介紹了android實(shí)現(xiàn)自動發(fā)送郵件,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-07-07
Android Studio 3.6安裝全過程及AVD安裝運(yùn)行步驟詳解
這篇文章主要介紹了Android Studio 3.6安裝全過程及AVD安裝運(yùn)行步驟詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03
android實(shí)現(xiàn)條目倒計(jì)時功能
這篇文章主要為大家詳細(xì)介紹了android實(shí)現(xiàn)條目倒計(jì)時功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-09-09
Android自定義ToolBar并實(shí)現(xiàn)沉浸式的方法
這篇文章主要給大家介紹了關(guān)于Android自定義ToolBar并實(shí)現(xiàn)沉浸式的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對各位Android開發(fā)者們具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05

