Эффект
Вращающийся фонарь — распространенный эффект, о том, как его использовать, поговорим в этой статье.PageView
существуетFlutter
Реализовать marquee lantern, эффект следующий, высота текущей страницы выше остальных страниц, и есть анимация изменения высоты при переключении страниц. Для достижения этого эффекта в основном используетсяPageView.builder
часть.
развивать
Создать домашнюю страницу
Сначала создайтеIndexPage
компонент, который используется для размещенияPageView
, потому что вам нужно использоватьsetState
метод обновляет пользовательский интерфейс, поэтому он сохраняет состояние.
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
Переменная используется для сохранения индекса текущей отображаемой страницы, в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,
);
}
}
На этом весь макет готов! 😎