如何使用 PageController 的当前值更新子部件(加载指示器)?

How to update child widget (loading indicator) with current value of PageController?

仅当 PageController(或 PageView)的当前值与其在 Row 中的索引相同时,我才想为 LinearProgressIndicator 设置动画。

如您在屏幕截图中所见,我有一个 PageView,其中有一排 LinearProgressIndicatorPageView 由自己的 PageController 控制,当 AnimationController 结束时切换页面。

我想实现类似于 this idea 的效果,即只有当前页面的 LoadingIndicator 应该是动画的。

代码

您可以在下面找到顶部小部件的代码,即 PageView 和底行 LoadingIndicators。

import 'package:flutter/material.dart';

class PageViewWithLoadingIndicators extends StatefulWidget {
  final List<String> imageUrls;
  final List<Widget> images;
  final Duration totalDuration;
  final Duration transitionDuration;
  final Curve animationCurve;

  const PageViewWithLoadingIndicators(
      {Key key,
      this.imageUrls,
      this.images,
      this.totalDuration = const Duration(seconds: 10),
      this.transitionDuration = const Duration(milliseconds: 700),
      this.animationCurve = Curves.easeInOut})
      : super(key: key);

  @override
  PageViewWithLoadingIndicatorsState createState() => PageViewWithLoadingIndicatorsState();
}

class PageViewWithLoadingIndicatorsState extends State<PageViewWithLoadingIndicators>
    with SingleTickerProviderStateMixin {
  Animation<double> loadingBarAnimation;
  AnimationController controller;
  PageController pageController;
  int count;
  int index;

  @override
  void initState() {
    super.initState();
    assert(widget.imageUrls.isNotEmpty || widget.images.isNotEmpty);

    count = widget.imageUrls.length;
    index = 0;
    controller = AnimationController(duration: widget.totalDuration ~/ count, vsync: this);
    pageController = PageController(initialPage: 0);

    loadingBarAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(controller)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reset();
          setState(() {
            index++;
          });
          if (index == count) {
            setState(() {
              index = 0;
            });
          }
          pageController.animateToPage(index,
              curve: widget.animationCurve, duration: widget.transitionDuration);
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }
      });

    controller.forward();
  }

  @override
  void dispose() {
    controller.dispose();
    pageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Container(
          child: PageView(
            children: widget.images != null
                ? widget.images
                : widget.imageUrls
                    .map<Widget>((f) => Image.network(
                          f,
                          fit: BoxFit.cover,
                        ))
                    .toList(),
            physics: NeverScrollableScrollPhysics(),
            controller: pageController,
          ),
        ),
        Positioned(
          bottom: 0,
          left: 0,
          right: 0,
          child: PageLoader(animation: loadingBarAnimation, count: count),
        )
      ],
    );
  }
}

class PageLoader extends AnimatedWidget {
  final int count;

  PageLoader({Key key, @required this.count, Animation<double> animation})
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;

    return Row(
        children: List.generate(
      count,
      (index) => SmallLoadingBar(animation, index),
    ));
  }
}

class SmallLoadingBar extends StatelessWidget {
  final int index;
  final Animation<double> value;

  SmallLoadingBar(this.value, this.index);

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Container(
          margin: EdgeInsets.only(top: 10, bottom: 10, left: 5, right: 5),
          height: 5,
          width: double.infinity,
          decoration: BoxDecoration(color: Colors.white24),
          child: LinearProgressIndicator(
              backgroundColor: Colors.transparent,
              valueColor: AlwaysStoppedAnimation<Color>(Colors.white70),
              value: value.value)),
    );
  }
}

你可以简单地使用它:

  final List<String> images = [
    'https://i.imgur.com/F8PBP7P.jpg',
    'https://i.imgur.com/DtWaRhg.jpg',
    'https://i.imgur.com/GsRLPXM.jpg',
    'https://i.imgur.com/BMnhHaH.jpg',
    'https://i.imgur.com/qXvgwpw.jpg',
  ];

PageViewWithLoadingIndicators(imageUrls: images)

最好的方法是什么?使用 InheritedWidget 或 Stream 检查 LoadingIndicator 容器中的当前页面索引?

I would like to animate LinearProgressIndicator only if the current value of PageController (or PageView) is the same as its index in Row.

使用您当前的代码很容易做到这一点。您所要做的就是传递您在动画侦听器中更新的索引。首先,您可以稍微简化一下动画侦听器代码:

setState(() {
    index++;
    if (index == count) {
       index = 0;
    }
});

然后通过小部件层次结构将索引值传递给您将在其中使用它的 LinearProgressIndicator:

// in PageViewWithLoadingIndicatorsState's build() method
child: PageLoader(animation: loadingBarAnimation, count: count, current: index),

在 SmallLoadingBar 中,您可以使用传递的值仅对当前加载指示器进行动画处理:

class SmallLoadingBar extends StatelessWidget {
  final int index;
  final Animation<double> value;
  final int current;

  SmallLoadingBar(this.value, this.index, this.current);

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Container(
          margin: EdgeInsets.only(top: 10, bottom: 10, left: 5, right: 5),
          height: 5,
          width: double.infinity,
          decoration: BoxDecoration(color: Colors.white24),
          child: LinearProgressIndicator(
              backgroundColor: Colors.transparent,
              valueColor: AlwaysStoppedAnimation<Color>(Colors.white70),
              value: _calculateState(index, current))),
    );
  }

  double _calculateState(int index, int current) {
    // this is the page at which we are so it needs to be animated
    if (index == current) {
      return value.value;
    } else if (index < current) {
      // this is behind the current indicator so was already animated
      return 1;
    } else {
      // this is after the current indicator so it wasn't yet animated
      return 0;
    }
  }
}

当然,要真正做到这一点,您需要将传递给指示器的动画与实际图像加载时间联系起来。如何执行此操作取决于您计划如何处理图像的加载。