在颤动中实现像香奈儿应用程序一样的自定义滚动?

Posted

技术标签:

【中文标题】在颤动中实现像香奈儿应用程序一样的自定义滚动?【英文标题】:implementing custom scrolling like Chanel app in flutter? 【发布时间】:2021-01-17 20:18:05 【问题描述】:

最近我安装了一个名为Chanel Fashion的新应用程序,在它的主页上有一种非常奇怪的滚动方式,你可以从下面的GIF中看到它,我非常怀疑它是任何类型的自定义滚动条,我认为它是一个pageview,关于如何在颤振中实现这样的东西的任何提示?

P.s 这个blog 试图在 android 中制作类似的东西,但它在很多方面都不同。

P.s 2 这个 SO question 试图在 ios 上实现它。

【问题讨论】:

使用 slivers 可以完成类似的事情。这是一个很棒的条子指南medium.com/flutter/slivers-demystified-6ff68ab0296f @ASADHAMEED Slivers 的小部件种类繁多,请问您能更精确一点吗? 查看我分享的链接上的最后一个示例。 不错,但这不适用于随机图像:必须对其进行编辑,以便在顶部裁剪时看起来不错。 你能用一个不那么快和生涩显示效果的 gif 吗?很难准确理解示例中发生了什么。 【参考方案1】:

这是我的演示

demo chanel scroll

演示中的库:插值:^1.0.2+2

main.dart

import 'package:chanel_scroll_animation/chanel1/chanel1_page.dart';
import 'package:flutter/material.dart';
void main() 
  runApp(MyApp());


class MyApp extends StatelessWidget 
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) 
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: Chanel1Page(),
    );
  

chanel1_page.dart

import 'package:chanel_scroll_animation/chanel1/item.dart';
import 'package:chanel_scroll_animation/chanel1/snapping_list_view.dart';
import 'package:chanel_scroll_animation/models/model.dart';
import 'package:flutter/material.dart';


class Chanel1Page extends StatefulWidget 
  @override
  _Chanel1PageState createState() => _Chanel1PageState();


class _Chanel1PageState extends State<Chanel1Page> 
  ScrollController _scrollController;
  double y=0;
  double maxHeight=0;
  @override
  void initState() 
    // TODO: implement initState
    super.initState();
    _scrollController=new ScrollController();
    _scrollController.addListener(() 
      print("_scrollController.offset.toString() "+_scrollController.offset.toString());


      setState(() 
        y=_scrollController.offset;
      );

    );
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) 
      final Size size=MediaQuery.of(context).size;
      setState(() 
        maxHeight=size.height/2;
      );

    );

  


  @override
  Widget build(BuildContext context) 

    return Scaffold(
      body: SafeArea(
        child: maxHeight!=0?SnappingListView(
          controller: _scrollController,
            snapToInterval: maxHeight,
            scrollDirection: Axis.vertical,
          children: [

            Container(
              height:  ( models.length +1) * maxHeight,

              child: Column(
                children: [
                  for (int i = 0; i < models.length; i++)
                    Item(item: models[i],index: i,y: y,)
                ],
              ),
            )

          ],
        ):Container(),
      ),

    );
  

item.dart

import 'package:chanel_scroll_animation/models/model.dart';
import 'package:flutter/material.dart';
import 'package:interpolate/interpolate.dart';

const double MIN_HEIGHT = 128;
class Item extends StatefulWidget 
  final Model item;
  final int index;
  final double y;
  Item(this.item,this.index,this.y);

  @override
  _ItemState createState() => _ItemState();


class _ItemState extends State<Item> 

  Interpolate ipHeight;
  double maxHeight=0;
  @override
  void initState() 
    // TODO: implement initState
    super.initState();
   WidgetsBinding.instance.addPostFrameCallback((timeStamp) 
      final Size size=MediaQuery.of(context).size;
     maxHeight=size.height/2;
     initInterpolate();
   );
  

  initInterpolate()
  
    ipHeight=Interpolate(
      inputRange: [(widget.index-1)*maxHeight,widget.index*maxHeight],
      outputRange: [MIN_HEIGHT,maxHeight],
      extrapolate: Extrapolate.clamp,
    );
  
  @override
  Widget build(BuildContext context) 
    final Size size=MediaQuery.of(context).size;
    double height=ipHeight!=null? ipHeight.eval(widget.y):MIN_HEIGHT;
    print("height "+height.toString());

    return Container(
      height: height,
      child: Stack(
        children: [
          Positioned.fill(
            child: Image.asset(
              widget.item.picture,
              fit: BoxFit.cover,
            ),
          ),
          Positioned(
            bottom:40,
            left: 30,
            right: 30,
            child: Column(
              children: [
                Text(
                  widget.item.subtitle,
                  style: TextStyle(fontSize: 16, color: Colors.white),
                ),
                SizedBox(
                  height: 10,
                ),
                Text(
                  widget.item.title.toUpperCase(),
                  style: TextStyle(fontSize: 24, color: Colors.white),
                  textAlign: TextAlign.center,
                ),
              ],
            ),
          )
        ],
      ),
    );
  

snapping_list_view.dart

import "package:flutter/widgets.dart";
import "dart:math";

class SnappingListView extends StatefulWidget 
  final Axis scrollDirection;
  final ScrollController controller;

  final IndexedWidgetBuilder itemBuilder;
  final List<Widget> children;
  final int itemCount;

  final double snapToInterval;
  final ValueChanged<int> onItemChanged;

  final EdgeInsets padding;

  SnappingListView(
      this.scrollDirection,
        this.controller,
        @required this.children,
        @required this.snapToInterval,
        this.onItemChanged,
        this.padding = const EdgeInsets.all(0.0))
      : assert(snapToInterval > 0),
        itemCount = null,
        itemBuilder = null;

  SnappingListView.builder(
      this.scrollDirection,
        this.controller,
        @required this.itemBuilder,
        this.itemCount,
        @required this.snapToInterval,
        this.onItemChanged,
        this.padding = const EdgeInsets.all(0.0))
      : assert(snapToInterval > 0),
        children = null;

  @override
  createState() => _SnappingListViewState();


class _SnappingListViewState extends State<SnappingListView> 
  int _lastItem = 0;

  @override
  Widget build(BuildContext context) 
    final startPadding = widget.scrollDirection == Axis.horizontal
        ? widget.padding.left
        : widget.padding.top;
    final scrollPhysics = SnappingListScrollPhysics(
        mainAxisStartPadding: startPadding, itemExtent: widget.snapToInterval);
    final listView = widget.children != null
        ? ListView(
        scrollDirection: widget.scrollDirection,
        controller: widget.controller,
        children: widget.children,

        physics: scrollPhysics,
        padding: widget.padding)
        : ListView.builder(
        scrollDirection: widget.scrollDirection,
        controller: widget.controller,
        itemBuilder: widget.itemBuilder,
        itemCount: widget.itemCount,

        physics: scrollPhysics,
        padding: widget.padding);
    return NotificationListener<ScrollNotification>(
        child: listView,
        onNotification: (notif) 
          if (notif.depth == 0 &&
              widget.onItemChanged != null &&
              notif is ScrollUpdateNotification) 
            final currItem =
                (notif.metrics.pixels - startPadding) ~/ widget.snapToInterval;
            if (currItem != _lastItem) 
              _lastItem = currItem;
              widget.onItemChanged(currItem);
            
          
          return false;
        );
  


class SnappingListScrollPhysics extends ScrollPhysics 
  final double mainAxisStartPadding;
  final double itemExtent;

  const SnappingListScrollPhysics(
      ScrollPhysics parent,
        this.mainAxisStartPadding = 0.0,
        @required this.itemExtent)
      : super(parent: parent);

  @override
  SnappingListScrollPhysics applyTo(ScrollPhysics ancestor) 
    return SnappingListScrollPhysics(
        parent: buildParent(ancestor),
        mainAxisStartPadding: mainAxisStartPadding,
        itemExtent: itemExtent);
  

  double _getItem(ScrollPosition position) 
    return (position.pixels - mainAxisStartPadding) / itemExtent;
  

  double _getPixels(ScrollPosition position, double item) 
    return min(item * itemExtent, position.maxScrollExtent);
  

  double _getTargetPixels(
      ScrollPosition position, Tolerance tolerance, double velocity) 
    double item = _getItem(position);
    if (velocity < -tolerance.velocity)
      item -= 0.5;
    else if (velocity > tolerance.velocity) item += 0.5;
    return _getPixels(position, item.roundToDouble());
  

  @override
  Simulation createBallisticSimulation(
      ScrollMetrics position, double velocity) 
    // If we're out of range and not headed back in range, defer to the parent
    // ballistics, which should put us back in range at a page boundary.
    if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
        (velocity >= 0.0 && position.pixels >= position.maxScrollExtent))
      return super.createBallisticSimulation(position, velocity);
    final Tolerance tolerance = this.tolerance;
    final double target = _getTargetPixels(position, tolerance, velocity);
    if (target != position.pixels)
      return ScrollSpringSimulation(spring, position.pixels, target, velocity,
          tolerance: tolerance);
    return null;
  

  @override
  bool get allowImplicitScrolling => false;

【讨论】:

内插库已停产【参考方案2】:

将 a 与 SingleChildScrollView 一起使用,并将列作为子列。为了使图片在标题时变小,请使用FittedBox。用SizedBox 包裹FittedBox 以控制内部小部件的大小。使用滚动通知器在滚动时进行更新并跟踪用户滚动的距离。将滚动量除以您想要的最大高度,以便了解需要调整大小的当前小部件。通过找到余数并将其除以最大高度并乘以最小和最大尺寸的差值来调整该小部件的大小,然后添加最小尺寸。这将确保平稳过渡。然后将列中的任何小部件设置为最大尺寸并低于最小尺寸,以确保延迟不会破坏滚动条。

使用AnimatedOpacity 允许标题的描述淡入和淡出,或制作您认为应该如何显示的自定义动画。

以下代码应该可以使用您想要的样式自定义文本小部件。输入要在列表中的自定义TitleWithImage(包含小部件和两个字符串)项目,将 maxHeight 和 minHeight 输入自定义小部件。虽然我修复了一些,但它可能没有完全优化并且可能有很多错误:

import 'package:flutter/material.dart';

class CoolListView extends StatefulWidget 
  final List<TitleWithImage> items;
  final double minHeight;
  final double maxHeight;
  const CoolListView(Key key, this.items, this.minHeight, this.maxHeight) : super(key: key);

  @override
  _CoolListViewState createState() => _CoolListViewState();


class _CoolListViewState extends State<CoolListView> 
  List<Widget> widgets=[];
  ScrollController _scrollController = new ScrollController();
  @override
  Widget build(BuildContext context) 
    if(widgets.length == 0)
      for(int i = 0; i<widget.items.length; i++)
        if(i==0)
          widgets.add(ListItem(height: widget.maxHeight, item: widget.items[0],descriptionTransparent: false));
        
        else
          widgets.add(
            ListItem(height: widget.minHeight, item: widget.items[i], descriptionTransparent: true,)
          );
        
      
    
    return new NotificationListener<ScrollUpdateNotification>(
      child: SingleChildScrollView(
        controller: _scrollController,
        child: Column(
          children: widgets,
        )
      ),
      onNotification: (t) 
        if (t!= null && t is ScrollUpdateNotification) 
          int currentWidget = (_scrollController.position.pixels/widget.maxHeight).ceil();
          currentWidget = currentWidget==-1?0:currentWidget;
          setState(() 
            if(currentWidget != widgets.length-1)//makes higher index min
              for(int i = currentWidget+1; i<=widgets.length-1; i++)
                print(i);
                widgets[i] = ListItem(height: widget.minHeight, item: widget.items[i],descriptionTransparent: true,);
              
            
            if(currentWidget!=0)
              widgets[currentWidget] = ListItem(
                height: _scrollController.position.pixels%widget.maxHeight/widget.maxHeight*(widget.maxHeight-widget.minHeight)+widget.minHeight,
                item: widget.items[currentWidget],
                descriptionTransparent: true,
              );
              for(int i = currentWidget-1; i>=0; i--)
                widgets[i] = ListItem(height: widget.maxHeight,
                  item: widget.items[i],
                  descriptionTransparent: false,
                );
              
            
            else
              widgets[0] = ListItem(
                height: widget.maxHeight,
                item: widget.items[0],
                descriptionTransparent: false
              );
            
          );
        
      ,
    );
  

  

class TitleWithImage

  final Widget image;
  final String title;
  final String description;
  TitleWithImage(this.image, this.title, this.description);

class ListItem extends StatelessWidget 
  final double height;
  final TitleWithImage item;
  final bool descriptionTransparent;
  const ListItem(Key key, this.height, this.item, this.descriptionTransparent) : super(key: key);
  @override
  Widget build(BuildContext context) 
    return Container(
      child:Stack(
        children: [
          SizedBox(
            height: height,
            width: MediaQuery.of(context).size.width,
            child: FittedBox(
            fit: BoxFit.none,
            child:Align(
              alignment: Alignment.center,
              child: item.image
            )
            ),
          ),
          SizedBox(
            height: height,
            width: MediaQuery.of(context).size.width,
            child: Column(
              children: [
                Spacer(),
                Text(item.title,),
                AnimatedOpacity(
                  child: Text(
                    item.description,
                    style: TextStyle(
                      color: Colors.black
                    ),
                  ),
                  opacity: descriptionTransparent? 0.0 : 1.0,
                  duration: Duration(milliseconds: 500),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  

编辑这是我的 main.dart:

import 'package:cool_list_view/CoolListView.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Collapsing List Demo')),
        body: CoolListView(
          items: [
            new TitleWithImage(
              Container(
                height: 1000,
                width:1000,
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topLeft,
                    end:
                        Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
                    colors: [
                      const Color(0xffee0000),
                      const Color(0xffeeee00)
                    ], // red to yellow
                    tileMode: TileMode.repeated, // repeats the gradient over the canvas
                  ),
                ),
              ),
              'title',
              'description',
            ),
            new TitleWithImage(
              Container(
                height: 1000,
                width:1000,
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topLeft,
                    end:
                        Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
                    colors: [
                      Colors.orange,
                      Colors.blue,
                    ], // red to yellow
                    tileMode: TileMode.repeated, // repeats the gradient over the canvas
                  ),
                ),
              ),
              'title',
              'description',
            ),
            new TitleWithImage(
              Container(
                height: 1000,
                width:1000,
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topLeft,
                    end:
                        Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
                    colors: [
                      const Color(0xffee0000),
                      const Color(0xffeeee00)
                    ], // red to yellow
                    tileMode: TileMode.repeated, // repeats the gradient over the canvas
                  ),
                ),
              ),
              'title',
              'description',
            ),
            new TitleWithImage(
              Container(
                height: 1000,
                width:1000,
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topLeft,
                    end:
                        Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
                    colors: [
                      const Color(0xffee0000),
                      const Color(0xffeeee00)
                    ], // red to yellow
                    tileMode: TileMode.repeated, // repeats the gradient over the canvas
                  ),
                ),
              ),
              'title',
              'description',
            ),
            new TitleWithImage(
              Container(
                height: 1000,
                width:1000,
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topLeft,
                    end:
                        Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
                    colors: [
                      const Color(0xffee0000),
                      const Color(0xffeeee00)
                    ], // red to yellow
                    tileMode: TileMode.repeated, // repeats the gradient over the canvas
                  ),
                ),
              ),
              'title',
              'description',
            ),
            new TitleWithImage(
              Container(
                height: 1000,
                width:1000,
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topLeft,
                    end:
                        Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
                    colors: [
                      const Color(0xffee0000),
                      const Color(0xffeeee00)
                    ], // red to yellow
                    tileMode: TileMode.repeated, // repeats the gradient over the canvas
                  ),
                ),
              ),
              'title',
              'description',
            ),
            new TitleWithImage(
              Container(
                height: 1000,
                width:1000,
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topLeft,
                    end:
                        Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
                    colors: [
                      const Color(0xffee0000),
                      const Color(0xffeeee00)
                    ], // red to yellow
                    tileMode: TileMode.repeated, // repeats the gradient over the canvas
                  ),
                ),
              ),
              'title',
              'description',
            ),
            new TitleWithImage(Container(height: 1000,width:1000,color: Colors.blue), 'title', 'description'),
            new TitleWithImage(Container(height: 1000,width:1000, color: Colors.orange), 'title', 'description'),
          ],
          minHeight: 50,
          maxHeight: 300,
        ),
      ),
    );
  

【讨论】:

嘿,感谢您的解释,但您能否提供一个工作示例,在上面的示例中为“小部件”或为空。 我将把我测试过的 main.dart 放入 谢谢它太棒了,但不幸的是,这种方法有一些问题,1-它在大屏幕上不起作用 2-它不会比 IDK 的几个项目更进一步,但是滚动会得到取决于屏幕尺寸,在几个项目之后如此缓慢,无论如何这是一个强大的方法,谢谢和upvote。 它不会超过最后几项,因为没有更多的列表项。我还没有完全使用过 Channel 应用程序,所以我不知道最后一个列表项结束时会发生什么。也许我的 main.dart 中的列表示例对于大(高)屏幕来说太短了。也许可以通过使用 ListView 而不是 SingleScrollView 和 Column 来优化它。 Slivers 也可以用来优化它,因为它们能够卸载。这只是一个示例,而不是我创建的优化的无错误小部件。 @Harry 我需要一些帮助来解决我的问题:***.com/questions/69234567/… 我正在尝试淡化多个小部件(我只发布了 2 个,另一个被注释掉了)。您的帮助将不胜感激..提前致谢..【参考方案3】:

您可以使用 ScrollController 值来更改小部件的大小或它的孩子的大小,抱歉我无法编写代码,因为它很耗时并且需要一些计算,但请观看此视频:https://www.youtube.com/watch?v=Cn6VCTaHB-k&t=558s 它会给你基本想法并帮助您继续前进。

【讨论】:

已经看到了,首先,记住我的有视差效果,其次你不能在绘制后调整常规滚动条的子项大小,第三个是找出滚动条中的第一个可见子项本身就是一个很大的挑战 是的,基本上滚动偏移量会在子大小变化时发生变化,我只是尝试通过使用条件(偏移量 % 小部件高度 ==0)和一些滚动方向来获取索​​引,但如果你滚动快这条线不会有任何影响。我认为有一种方法可以做到这一点,但发现它需要一些时间,天气通过滚动偏移和子高度计算索引,同时考虑计算增加的​​小部件大小或其他方式。【参考方案4】:

尝试使用 Sliver。

这是我的意思的一个例子:

body: CustomScrollView(
    slivers: <Widget>[
      SliverAppBar(
        backgroundColor: Color(0xFF0084C9),
        leading: IconButton(
          icon: Icon(
            Icons.blur_on,
            color: Colors.white70,
          ),
          onPressed: () 
            Scaffold.of(context).openDrawer();
          ,
        ),
        expandedHeight: bannerHigh,
        floating: true,
        pinned: true,
        flexibleSpace: FlexibleSpaceBar(
          title: Text("Your title",
              style: TextStyle(
                  fontSize: 18,
                  color: Colors.white,
                  fontWeight: FontWeight.w600)),
          background: Image.network(
            'image url',
            fit: BoxFit.cover,
          ),
        ),
      ),
      SliverList(
        delegate: SliverChildListDelegate(
          <Widget>[

          ],
        ),
      ),
    ],
  ),
);

【讨论】:

不是事件关闭,首先头部和列表没有区别,展开的视图有文字动画,第三当你设置pinned='true'时所有其他没有展开的视图,会粘在屏幕上,当小部件太多时,它会占据整个屏幕,很多其他的东西 很抱歉您没有发现这对您有帮助。我把它作为一个工作演示而不是在你的代码中实现它,我让它工作了。简而言之,SilverList() 应该可以解决问题

以上是关于在颤动中实现像香奈儿应用程序一样的自定义滚动?的主要内容,如果未能解决你的问题,请参考以下文章

自定义滚动谷歌颤动中的水平列表视图

如何在颤动中显示带有动画的自定义对话框?

如何在颤动中创建具有圆形边缘的自定义容器?

如何在颤动中创建这样的自定义 ListView 项目?

颤动中的自定义容器形状

在颤动的谷歌地图的自定义标记中添加数字? [关闭]