用Flutter实现一个类似于轮播图的效果

效果图:

过程脑洞自补!!!

类似于上图的效果,我们应该能想到用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的使用。
再补充一点: 设置 PageControllerviewportFraction 参数小于 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入口函数下MaterialApphome参数为IndexPage

有问题可以提出来,大家共同进步。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值