IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    使用 Flutter 实现一个走马灯布局

    冷石发表于 2022-08-29 09:01:22
    love 0

    走马灯是一种常见的效果,本文讲一下如何用 PageView 在 Flutter 里实现一个走马灯

    效果

    实现的效果如下,当前页面的高度比其它页面高,切换页面的时候有一个高度变化的动画。实现这样的效果主要用到的是 PageView.builder 部件。

    开发

    创建首页

    首先创建一个 IndexPage 部件,这个部件用来放 PageView,因为需要使用 setState 方法更新 UI,所以它是 stateful 的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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 变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    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 的话,就可以在当前页面同时显示其它页面的内容了。

    1
    2
    3
    /// 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。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    _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 添加每一项的内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    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,是的话显示较深的颜色。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    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 两个属性,分别配置数据和高度。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    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。

    完整代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    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,
    );
    }
    }

    至此,整个布局就完成了! 😎



沪ICP备19023445号-2号
友情链接