The effect

This article describes how to use PageView to create a Flutter effect. The current page height is higher than other pages, and there is an animation of the height change when the page is changed. The pageView. Builder widget is used to achieve this effect.

The development of

Create a page

We started creating an IndexPage widget, which was used to put PageView, which was stateful because the UI was updated using the setState method.

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>[], ), ); }}Copy the code

Then declare a _pageIndex variable in the widget to hold the index of the currently displayed page, and initialize a PageController in the initState lifecycle to configure the PageView widget.

Create a PageView.builder inside the body Column, use a SizedBox widget to specify the height of the PageView, set controller to _pageController, Inside the onPageChanged event, assign the index value of the currently displayed page to the _pageIndex variable.

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); },),),],Copy the code

Key point: Set the viewportFraction parameter of the PageController to less than 1. This value is used to set the percentage of each page displayed on the screen.

/// The fraction of the viewport that each page should occupy.
// Defaults to 1.0, which means each page fillsthe viewport in the scrolling direction.
final double viewportFraction;
Copy the code

implementation_buildItem

We then implement the _buildItem method, which returns the content of each page rendered in PageView. Builder. The first parameter activeIndex is the index of the page currently displayed on the screen, and the second parameter index is the index of each item.

Use a Center widget to Center the content, then use an AnimatedContainer to animate the height changes when the page is changed. The setState method is used to change the _pageIndex when the page is changed, and the Flutter redraws each item. The key is to determine whether the current page is the one being displayed, if it is, its height is 500, if not 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(),
    ),
  );
}
Copy the code

Add content

Then add the content for each item to the 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,
                ),
              ),
            ),
          )
        ],
      ),
    ),
  ],
),
Copy the code

Implementation indicator

Then implement a PageIndicator and create a PageIndicator, passing in pageCount to indicate the total number of pages and currentIndex to indicate the number of pages currently displayed. Place all indicators in a Row widget and determine if the index of the current indicator is the index of the page being displayed, and if so, display a darker color.

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) {
    returnRow( mainAxisAlignment: MainAxisAlignment.center, children: _buildIndicators(), ); }}Copy the code

Add PageIndicator under SizedBox

encapsulationCarousel

Finally, I optimized the code to encapsulate the widget and make it a separate widget. I created a Carousel widget and exposed the items and height properties to configure the data and height respectively.

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), ], ); }}Copy the code

We then only instantiate a Carousel in the IndexPage widget, and since the IndexPage does not manage widget state, we can turn it into a StatelessWidget.

The complete code

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: 'Ice Shooter - Ash',
  ),
  Hero(
    color: Color(0xFF7D6588),
    image: "https://game.gtimg.cn/images/lol/act/img/skin/big39006.jpg",
    title: 'Blade Dancer - Erelia',
  ),
  Hero(
    color: Color(0xFF4C314D),
    image: "https://game.gtimg.cn/images/lol/act/img/skin/big103015.jpg",
    title: 'Nine-tailed Fox - Ali',)];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) {
    returnRow( 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, ); }}Copy the code

At this point, the entire layout is complete! 😎

Blog address