Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

After deprecation period, remove this recipe from website #8204

Closed
Yang-Xijie opened this issue Feb 3, 2023 · 8 comments · Fixed by #11465
Closed

After deprecation period, remove this recipe from website #8204

Yang-Xijie opened this issue Feb 3, 2023 · 8 comments · Fixed by #11465
Assignees
Labels
a.cookbook Relates to a cookbook recipe or guide d.enhancement Improves docs with specific ask e2-days Effort: < 5 days fix.code-sample Needs new or updated code sample from.page-issue Reported in a reader-filed concern p2-medium Necessary but not urgent concern. Resolve when possible. review.tech Awaiting Technical Review

Comments

@Yang-Xijie
Copy link

Page URL

https://docs.flutter.dev/cookbook/effects/photo-filter-carousel/

Page source

https://github.com/flutter/website/tree/main/src/cookbook/effects/photo-filter-carousel.md

Describe the problem

This cookbook has several places to improve:

  • codes and explanation are different from each other, as mentioned in Update copy to match example on 'Create a photo filter carousel' page #8202
  • Material widget is unnessary.
  • In _buildShadowGradient, height: itemSize + ... will be better than height: itemSize * 2 + ... because the Scrollable will take less space on the screen. If height: itemSize + ... used, it will be better to modify LinearGradient’s Colors.black to Colors.black87 to make the UI more comfortable.
  • The Ring has the with 6.0 while the FilterItem has padding set to 8.0. It will be better if they are the same.
  • In FilterSelctor, current widget hierarchy is Scrollable > LayoutBuilder > Stack. It will be better to change it to LayoutBuilder > Stack > Scrollable because Scrollable is not related with the Ring and Shadow.
  • IgnorePointer is useless because Scrollable has a higher position (upper in the widget hierarchy) than SelctionRing.

Expected fix

No response

Additional context

No response

@Yang-Xijie
Copy link
Author

I tried writing a new version of codes that I am satisfied with (copy it to a newly created Flutter project and replace lib/main.dart), with app functions not changed. From my point of view, the new codes will be better for readers to study.

If I have some spare time, I will add the text explanation for the codes and create a PR. I will check CONTRIBUTION.md before I do that.

import 'dart:math' show min, max;

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'
    show debugPaintSizeEnabled, ViewportOffset;

void main() {
  // debugPaintSizeEnabled = true;

  runApp(
    const MaterialApp(
      home: PhotoWithFilterPage(),
      debugShowCheckedModeBanner: false,
    ),
  );
}

class PhotoWithFilterPage extends StatefulWidget {
  const PhotoWithFilterPage({super.key});

  @override
  State<PhotoWithFilterPage> createState() => _PhotoWithFilterPageState();
}

class _PhotoWithFilterPageState extends State<PhotoWithFilterPage> {
  Color selectedColor = Colors.white;

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.bottomCenter,
      children: [
        Positioned.fill(
          child: PhotoWithFilterView(filterColor: selectedColor),
        ),
        ColorSelectorView(
          onColorSelected: (Color color) {
            setState(() {
              selectedColor = color;
            });
          },
        )
      ],
    );
  }
}

class ColorSelectorView extends StatefulWidget {
  const ColorSelectorView({
    super.key,
    required this.onColorSelected,
    this.colorCountOnScreen = 5,
    this.ringWidth = 8.0,
    this.verticlePaddingSize = 24.0,
  });

  final void Function(Color selectedColor) onColorSelected;
  final int colorCountOnScreen;
  final double ringWidth;
  final double verticlePaddingSize;

  @override
  State<ColorSelectorView> createState() => _ColorSelectorViewState();
}

class _ColorSelectorViewState extends State<ColorSelectorView> {
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, constraints) {
      final double itemSize =
          constraints.maxWidth * 1.0 / widget.colorCountOnScreen;

      return Stack(
        alignment: Alignment.bottomCenter,
        children: [
          ShadowView(
            height: itemSize + widget.verticlePaddingSize * 2,
          ),
          ColorsView(
            colors: [
              Colors.white,
              ...List.generate(
                Colors.primaries.length,
                (index) =>
                    Colors.primaries[(index * 4) % Colors.primaries.length],
              )
            ],
            onColorSelected: widget.onColorSelected,
            fullWidth: constraints.maxWidth,
            colorCountOnScreen: widget.colorCountOnScreen,
            itemSize: itemSize,
            verticlePaddingSize: widget.verticlePaddingSize,
            ringWidth: widget.ringWidth,
          ),
          IgnorePointer(
            // `RingView` with `Padding` is on `ColorsView` (in `Stack`).
            // Without `IgnorePointer`, user cannot slide the `ColorSelectorView`
            // when mouse on or finger tapped at the most center `ColorView`.
            child: Padding(
              padding: EdgeInsets.only(bottom: widget.verticlePaddingSize),
              child: RingView(
                size: itemSize,
                borderWidth: widget.ringWidth,
              ),
            ),
          )
        ],
      );
    });
  }
}

class ColorsView extends StatefulWidget {
  const ColorsView({
    super.key,
    required this.colors,
    required this.onColorSelected,
    required this.itemSize,
    required this.fullWidth,
    required this.verticlePaddingSize,
    required this.ringWidth,
    required this.colorCountOnScreen,
  });

  final List<Color> colors;
  final void Function(Color selectedColor) onColorSelected;
  final double itemSize;
  final double fullWidth;
  final double verticlePaddingSize;
  final double ringWidth;
  final int colorCountOnScreen;

  @override
  State<ColorsView> createState() => _ColorsViewState();
}

class _ColorsViewState extends State<ColorsView> {
  late final PageController _pageController;
  late int _currentPage;

  int get colorCount => widget.colors.length;

  Color itemColor(int index) => widget.colors[index % colorCount];

  @override
  void initState() {
    super.initState();
    _currentPage = 0;
    _pageController = PageController(
      initialPage: _currentPage,
      viewportFraction: 1.0 / widget.colorCountOnScreen,
    );
    _pageController.addListener(_onPageChanged);
  }

  void _onPageChanged() {
    final newPage = (_pageController.page ?? 0.0).round();
    if (newPage != _currentPage) {
      _currentPage = newPage;
      widget.onColorSelected(widget.colors[_currentPage]);
    }
  }

  void _onColorSelected(int index) {
    _pageController.animateToPage(
      index,
      duration: const Duration(milliseconds: 450),
      curve: Curves.ease,
    );
  }

  @override
  void dispose() {
    _pageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scrollable(
      controller: _pageController,
      axisDirection: AxisDirection.right,
      physics: const PageScrollPhysics(),
      viewportBuilder: (context, viewportOffset) {
        viewportOffset.applyViewportDimension(widget.fullWidth);
        viewportOffset.applyContentDimensions(
            0.0, widget.itemSize * (colorCount - 1));

        return Padding(
          padding: EdgeInsets.symmetric(vertical: widget.verticlePaddingSize),
          child: SizedBox(
            height: widget.itemSize,
            child: Flow(
              delegate: ColorsViewFlowDelegate(
                viewportOffset: viewportOffset,
                colorCountOnScreen: widget.colorCountOnScreen,
              ),
              children: [
                for (int i = 0; i < colorCount; i++)
                  Padding(
                    padding: EdgeInsets.all(widget.ringWidth),
                    child: ColorView(
                      onTap: () => _onColorSelected(i),
                      color: itemColor(i),
                    ),
                  ),
              ],
            ),
          ),
        );
      },
    );
  }
}

class ColorsViewFlowDelegate extends FlowDelegate {
  ColorsViewFlowDelegate({
    required this.viewportOffset,
    required this.colorCountOnScreen,
  }) : super(repaint: viewportOffset);

  final ViewportOffset viewportOffset;
  final int colorCountOnScreen;

  @override
  void paintChildren(FlowPaintingContext context) {
    final count = context.childCount;

    // All available painting width
    final size = context.size.width;

    // The distance that a single item "newPage" takes up from the perspective
    // of the scroll paging system. We also use this size for the width and
    // height of a single item.
    final itemExtent = size / colorCountOnScreen;

    // The current scroll position expressed as an item fraction, e.g., 0.0,
    // or 1.0, or 1.3, or 2.9, etc. A value of 1.3 indicates that item at
    // index 1 is active, and the user has scrolled 30% towards the item at
    // index 2.
    final active = viewportOffset.pixels / itemExtent;

    // Index of the first item we need to paint at this moment.
    // At most, we paint 3 items to the left of the active item.
    final minimum = max(0, active.floor() - 3).toInt();

    // Index of the last item we need to paint at this moment.
    // At most, we paint 3 items to the right of the active item.
    final maximum = min(count - 1, active.ceil() + 3).toInt();

    // Generate transforms for the visible items and sort by distance.
    for (var index = minimum; index <= maximum; index++) {
      final itemXFromCenter = itemExtent * index - viewportOffset.pixels;
      final percentFromCenter = 1.0 - (itemXFromCenter / (size / 2)).abs();
      final itemScale = 0.5 + (percentFromCenter * 0.5);
      final opacity = 0.25 + (percentFromCenter * 0.75);

      final itemTransform = Matrix4.identity()
        ..translate((size - itemExtent) / 2)
        ..translate(itemXFromCenter)
        ..translate(itemExtent / 2, itemExtent / 2)
        ..multiply(Matrix4.diagonal3Values(itemScale, itemScale, 1.0))
        ..translate(-itemExtent / 2, -itemExtent / 2);

      context.paintChild(
        index,
        transform: itemTransform,
        opacity: opacity,
      );
    }
  }

  @override
  bool shouldRepaint(covariant ColorsViewFlowDelegate oldDelegate) {
    return oldDelegate.viewportOffset != viewportOffset;
  }
}

class ColorView extends StatelessWidget {
  const ColorView({
    super.key,
    required this.color,
    required this.onTap,
  });

  final Color color;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: AspectRatio(
        aspectRatio: 1.0,
        child: ClipOval(
            child: Image(
                image: const AssetImage("assets/texture.jpg"),
                color: color.withOpacity(0.5),
                colorBlendMode: BlendMode.hardLight)),
      ),
    );
  }
}

class PhotoWithFilterView extends StatelessWidget {
  const PhotoWithFilterView({super.key, required this.filterColor});

  final Color filterColor;

  @override
  Widget build(BuildContext context) {
    return Image(
      image: const AssetImage("assets/photo.jpg"),
      color: filterColor.withOpacity(0.5),
      colorBlendMode: BlendMode.color,
      fit: BoxFit.cover,
    );
  }
}

class ShadowView extends StatelessWidget {
  final double height;

  const ShadowView({super.key, required this.height});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: height,
      child: const DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [
              Colors.transparent,
              Colors.black87,
            ],
          ),
        ),
        child: SizedBox.expand(),
      ),
    );
  }
}

class RingView extends StatelessWidget {
  const RingView({super.key, required this.size, required this.borderWidth});

  final double size;
  final double borderWidth;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: size,
      height: size,
      child: DecoratedBox(
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          border: Border.fromBorderSide(
            BorderSide(width: borderWidth, color: Colors.white),
          ),
        ),
      ),
    );
  }
}

@Yang-Xijie
Copy link
Author

Oops, I use local assets... Just replace

Image(
  image: const AssetImage("assets/texture.jpg"),
  ...
);

Image(
  image: const AssetImage("assets/photo.jpg"),
  ...
);

with

Image.network(
  'https://docs.flutter.dev/cookbook/img-files/effects/instagram-buttons/millenial-texture.jpg',
  ...
)

Image.network(
  'https://docs.flutter.dev/cookbook/img-files/effects/instagram-buttons/millenial-dude.jpg',
  ...
);

to make the codes run.

@Yang-Xijie
Copy link
Author

New codes with ColorSelectorView converted to a StatelessWidget:

import 'dart:math' show min, max;

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'
    show debugPaintSizeEnabled, ViewportOffset;

void main() {
  // debugPaintSizeEnabled = true;

  runApp(
    const MaterialApp(
      home: PhotoWithFilterPage(),
      debugShowCheckedModeBanner: false,
    ),
  );
}

class PhotoWithFilterPage extends StatefulWidget {
  const PhotoWithFilterPage({super.key});

  @override
  State<PhotoWithFilterPage> createState() => _PhotoWithFilterPageState();
}

class _PhotoWithFilterPageState extends State<PhotoWithFilterPage> {
  Color selectedColor = Colors.white;

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.bottomCenter,
      children: [
        Positioned.fill(
          child: PhotoWithFilterView(filterColor: selectedColor),
        ),
        ColorSelectorView(
          onColorSelected: (Color color) {
            setState(() {
              selectedColor = color;
            });
          },
        )
      ],
    );
  }
}

class ColorSelectorView extends StatelessWidget {
  const ColorSelectorView({
    super.key,
    required this.onColorSelected,
    this.colorCountOnScreen = 5,
    this.ringWidth = 8.0,
    this.verticlePaddingSize = 24.0,
  });

  final void Function(Color selectedColor) onColorSelected;
  final int colorCountOnScreen;
  final double ringWidth;
  final double verticlePaddingSize;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, constraints) {
      final double itemSize = constraints.maxWidth * 1.0 / colorCountOnScreen;

      return Stack(
        alignment: Alignment.bottomCenter,
        children: [
          ShadowView(
            height: itemSize + verticlePaddingSize * 2,
          ),
          ColorsView(
            colors: [
              Colors.white,
              ...List.generate(
                Colors.primaries.length,
                (index) =>
                    Colors.primaries[(index * 4) % Colors.primaries.length],
              )
            ],
            onColorSelected: onColorSelected,
            fullWidth: constraints.maxWidth,
            colorCountOnScreen: colorCountOnScreen,
            itemSize: itemSize,
            verticlePaddingSize: verticlePaddingSize,
            ringWidth: ringWidth,
          ),
          IgnorePointer(
            // `RingView` with `Padding` is on `ColorsView` (in `Stack`).
            // Without `IgnorePointer`, user cannot slide the `ColorSelectorView`
            // when mouse on or finger tapped at the most center `ColorView`.
            child: Padding(
              padding: EdgeInsets.symmetric(vertical: verticlePaddingSize),
              child: RingView(
                size: itemSize,
                borderWidth: ringWidth,
              ),
            ),
          )
        ],
      );
    });
  }
}

class ColorsView extends StatefulWidget {
  const ColorsView({
    super.key,
    required this.colors,
    required this.onColorSelected,
    required this.itemSize,
    required this.fullWidth,
    required this.verticlePaddingSize,
    required this.ringWidth,
    required this.colorCountOnScreen,
  });

  final List<Color> colors;
  final void Function(Color selectedColor) onColorSelected;
  final double itemSize;
  final double fullWidth;
  final double verticlePaddingSize;
  final double ringWidth;
  final int colorCountOnScreen;

  @override
  State<ColorsView> createState() => _ColorsViewState();
}

class _ColorsViewState extends State<ColorsView> {
  late final PageController _pageController;
  late int _currentPage;

  int get colorCount => widget.colors.length;
  Color itemColor(int index) => widget.colors[index % colorCount];

  @override
  void initState() {
    super.initState();
    _currentPage = 0;
    _pageController = PageController(
      initialPage: _currentPage,
      viewportFraction: 1.0 / widget.colorCountOnScreen,
    );
    _pageController.addListener(_onPageChanged);
  }

  void _onPageChanged() {
    final newPage = (_pageController.page ?? 0.0).round();
    if (newPage != _currentPage) {
      _currentPage = newPage;
      widget.onColorSelected(widget.colors[_currentPage]);
    }
  }

  void _onColorSelected(int index) {
    _pageController.animateToPage(
      index,
      duration: const Duration(milliseconds: 450),
      curve: Curves.ease,
    );
  }

  @override
  void dispose() {
    _pageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scrollable(
      controller: _pageController,
      axisDirection: AxisDirection.right,
      physics: const PageScrollPhysics(),
      viewportBuilder: (context, viewportOffset) {
        viewportOffset.applyViewportDimension(widget.fullWidth);
        viewportOffset.applyContentDimensions(
            0.0, widget.itemSize * (colorCount - 1));

        return Padding(
          padding: EdgeInsets.symmetric(vertical: widget.verticlePaddingSize),
          child: SizedBox(
            height: widget.itemSize,
            child: Flow(
              delegate: ColorsViewFlowDelegate(
                viewportOffset: viewportOffset,
                colorCountOnScreen: widget.colorCountOnScreen,
              ),
              children: [
                for (int i = 0; i < colorCount; i++)
                  Padding(
                    padding: EdgeInsets.all(widget.ringWidth),
                    child: ColorView(
                      onTap: () => _onColorSelected(i),
                      color: itemColor(i),
                    ),
                  ),
              ],
            ),
          ),
        );
      },
    );
  }
}

class ColorsViewFlowDelegate extends FlowDelegate {
  ColorsViewFlowDelegate({
    required this.viewportOffset,
    required this.colorCountOnScreen,
  }) : super(repaint: viewportOffset);

  final ViewportOffset viewportOffset;
  final int colorCountOnScreen;

  @override
  void paintChildren(FlowPaintingContext context) {
    final count = context.childCount;

    // All available painting width
    final size = context.size.width;

    // The distance that a single item "newPage" takes up from the perspective
    // of the scroll paging system. We also use this size for the width and
    // height of a single item.
    final itemExtent = size / colorCountOnScreen;

    // The current scroll position expressed as an item fraction, e.g., 0.0,
    // or 1.0, or 1.3, or 2.9, etc. A value of 1.3 indicates that item at
    // index 1 is active, and the user has scrolled 30% towards the item at
    // index 2.
    final active = viewportOffset.pixels / itemExtent;

    // Index of the first item we need to paint at this moment.
    // At most, we paint 3 items to the left of the active item.
    final minimum = max(0, active.floor() - 3).toInt();

    // Index of the last item we need to paint at this moment.
    // At most, we paint 3 items to the right of the active item.
    final maximum = min(count - 1, active.ceil() + 3).toInt();

    // Generate transforms for the visible items and sort by distance.
    for (var index = minimum; index <= maximum; index++) {
      final itemXFromCenter = itemExtent * index - viewportOffset.pixels;
      final percentFromCenter = 1.0 - (itemXFromCenter / (size / 2)).abs();
      final itemScale = 0.5 + (percentFromCenter * 0.5);
      final opacity = 0.25 + (percentFromCenter * 0.75);

      final itemTransform = Matrix4.identity()
        ..translate((size - itemExtent) / 2)
        ..translate(itemXFromCenter)
        ..translate(itemExtent / 2, itemExtent / 2)
        ..multiply(Matrix4.diagonal3Values(itemScale, itemScale, 1.0))
        ..translate(-itemExtent / 2, -itemExtent / 2);

      context.paintChild(
        index,
        transform: itemTransform,
        opacity: opacity,
      );
    }
  }

  @override
  bool shouldRepaint(covariant ColorsViewFlowDelegate oldDelegate) {
    return oldDelegate.viewportOffset != viewportOffset;
  }
}

class ColorView extends StatelessWidget {
  const ColorView({
    super.key,
    required this.color,
    required this.onTap,
  });

  final Color color;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: AspectRatio(
        aspectRatio: 1.0,
        child: ClipOval(
            child: Image(
                image: const AssetImage("assets/texture.jpg"),
                color: color.withOpacity(0.5),
                colorBlendMode: BlendMode.hardLight)),
      ),
    );
  }
}

class PhotoWithFilterView extends StatelessWidget {
  const PhotoWithFilterView({super.key, required this.filterColor});

  final Color filterColor;

  @override
  Widget build(BuildContext context) {
    return Image(
      image: const AssetImage("assets/photo.jpg"),
      color: filterColor.withOpacity(0.5),
      colorBlendMode: BlendMode.color,
      fit: BoxFit.cover,
    );
  }
}

class ShadowView extends StatelessWidget {
  final double height;

  const ShadowView({super.key, required this.height});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: height,
      child: const DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [
              Colors.transparent,
              Colors.black87,
            ],
          ),
        ),
        child: SizedBox.expand(),
      ),
    );
  }
}

class RingView extends StatelessWidget {
  const RingView({super.key, required this.size, required this.borderWidth});

  final double size;
  final double borderWidth;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: size,
      height: size,
      child: DecoratedBox(
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          border: Border.fromBorderSide(
            BorderSide(width: borderWidth, color: Colors.white),
          ),
        ),
      ),
    );
  }
}

@Yang-Xijie
Copy link
Author

A detailed text explanation in Chinese:

整体结构

应用整体为单页,使用 MaterialApp 包裹 PhotoWithFilterPage

PhotoWithFilterPage 使用 Stack,呈现出背景有颜色滤镜的图片 PhotoWithFilterView(目前使用项目中的资源图片),下方的选色器 ColorSelectorView 为一个整体。可以看到,PhotoWithFilterPage 是一个 StatefulWidget,其中状态为 selectedColorColorSelectorView 通过回调函数 onColorSelected 对状态进行修改,PhotoWithFilterView 对状态进行使用。

ColorSelectorView

分析下方的选色器 ColorSelectorView,主要由三层构成:中间一层是一个可以左右滑动的 ColorsView,主要使用 Scrollable,使用与 PageView 类似的逻辑进行控制,从而实现每次滑动时的吸附动画效果。前面一层 RingView 画一个圈,表示当前选中的颜色,始终处于最下方正中间。最后一层是一个从上到下的从透明到黑色的颜色梯度 ShadowView,使得选取的背景图片和 ColorsView 在视觉上不冲突。

开启 debugPaintSizeEnabled = true; 截图如下:

debug

手势冲突

ColorSelectorViewStack 中,RingView 添加了 IgnorePointer。这是因为从层级关系上来说,RingView 遮挡(拦截)了 ColorsView 的滑动手势。使用 IgnorePointer 可以使得包裹的 Widget 不接受手势。

使用约束确定大小

这里需要注意一点,我们需要确定下方 ColorSelectorView 的高度。代码的逻辑是,通过 ColorSelectorView.colorCountOnScreen 来决定 ColorsView 在屏幕上呈现多少个 ColorView,这样的话一个 ColorView(含 Padding)的宽度应该是屏幕宽度除以 ColorSelectorView。我们使得 ColorView(含 Padding)的高度和宽度一致即可。为了使得整个 ColorsView 有通用性,我们使用 LayoutBuilder 拿到 ColorSelectorView 的约束 constraints,使用 constraints.maxWidth(上层 Widget 传给 ColorSelectorView 的最大宽度) 计算得到一个 ColorView 的高度和宽度。

关于 Padding

ColorsViewRingView 都添加了上下高度为 verticlePaddingSizePaddingShadowView 则将 ColorsView 的背景填满。

ColorView 因为要添加 RingView,所以添加了和 RingView 的圆环宽度大小一样的 Padding

ColorsView

接下来我们讲解比较核心的 ColorsView,主要是由 Scrollable 构成 UI,Scrollable 保证了全平台统一的滑动体验。

Scrollable 的参数

我们先来查看 Scrollable 的参数:

  • controller
    • ScrollControllerScrollController 可以用来设置一个 Scrollable 的初始滚动位置 initialScrollOffset、读取当前的滚动位置 offset、或者用 animateTo() 来改变当前的滚动位置。
    • 这里我们使用 ScrollController 的子类 PageController,来方便的添加 viewportFraction
    • viewport 可以理解为“视野”,我们希望 Scrollable 在屏幕中的部分呈现出 colorCountOnScreenColorView,将 viewportFraction 设置为 1.0 / colorCountOnScreen
  • axisDirection 表示滑动的主轴为向右的轴。
  • physics 使用 PageScrollPhysics 使得在滑动的时候有着类似一页一页滑动的吸附效果。
  • viewportBuilder
    • viewportOffset.applyViewportDimension() 设置 Scrollable 在屏幕上显示的长度。
    • viewportOffset.applyContentDimensions() 设置可滑动的范围(可以通过这个去隐藏一些边缘的内容),差为内容的总长度。
    • 根据滑动的位置(偏移量) viewportOffset 来确定 Flow 中的布局。

Flow

Flow sizes and positions children efficiently, according to the logic in a FlowDelegate.

简单来说,Flow 可以对 children 实现自定义程度很高的布局,使用者需要对 FlowDelegate 中的 paintChildren() 进行重载。在 YouTube | Flow (Flutter Widget of the Week) 中讲的比较直观,配合矩阵可以做出很不错的动画效果。

ColorsViewFlowDelegatepaintChildren 的最后,在 for 循环中调用 context.paintChild() 实现对各个子 Widget 的绘制。具体是一些数学运算,代码中也有英文注释,感兴趣的同学可以自行查看。

交互逻辑

ColorsView 中,有两套交互逻辑:

  • PageController 检测到用户翻页(左右滑动),需要对当前位置做四舍五入然后更新 int _currentPageselectedColor 的值。
    • 实现在 _ColorsViewState._onPageChanged() 中。
    • _ColorsViewState.initState()_pageController.addListener(_onPageChanged); 表示每次 _pageControllerdouble page 值发生改变都会调用 _onPageChanged()
  • 用户点击 ColorView 进一步调用 onTap,从而改变 int _currentPageselectedColor 的值。
    • 实现在 _ColorsViewState._onColorSelected() 中。
    • _pageController.animateToPage() 中使用动画呈现滑动效果。

@danagbemava-nc danagbemava-nc added st.triage.triage-team Triage team reviewing and categorizing the issue d.enhancement Improves docs with specific ask p2-medium Necessary but not urgent concern. Resolve when possible. a.cookbook Relates to a cookbook recipe or guide e2-days Effort: < 5 days and removed st.triage.triage-team Triage team reviewing and categorizing the issue labels Feb 6, 2023
@atsansone atsansone added the from.page-issue Reported in a reader-filed concern label May 22, 2023
@atsansone atsansone changed the title [PAGE ISSUE]: improve 'Create a photo filter carousel' Update code for 'Create a photo filter carousel' page May 30, 2023
@atsansone
Copy link
Contributor

@domesticmouse : Could you review this issue and make sure we can make the changes outlined?

@atsansone atsansone added review.tech Awaiting Technical Review ltw-triage labels May 30, 2023
@domesticmouse
Copy link
Contributor

Hey @khanhnwin it looks like the linked cookbook page contains code that isn't in a Flutter project? Any ideas on when you plan to move it into a project?

@atsansone this issue is blocked until the linked page is converted to a snippet based page. Even if we update this code now, there is nothing to stop it falling out of sync again the next time we update stable.

@atsansone atsansone added the fix.code-sample Needs new or updated code sample label Jun 1, 2023
@sfshaza2
Copy link
Contributor

As per #10774, I have added a deprecation notice to this recipe and will delete it eventually.

@sfshaza2 sfshaza2 changed the title Update code for 'Create a photo filter carousel' page After deprecation period, remove this recipe from website Jun 21, 2024
@antfitch antfitch assigned antfitch and unassigned khanhnwin Dec 2, 2024
@antfitch
Copy link
Contributor

antfitch commented Dec 2, 2024

@Yang-Xijie this recipe is being turned down this week, but your notes are so helpful! I wonder if you could repost in the new Flutter forum? https://forum.itsallwidgets.com/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a.cookbook Relates to a cookbook recipe or guide d.enhancement Improves docs with specific ask e2-days Effort: < 5 days fix.code-sample Needs new or updated code sample from.page-issue Reported in a reader-filed concern p2-medium Necessary but not urgent concern. Resolve when possible. review.tech Awaiting Technical Review
Projects
None yet
8 participants