实现像Chanel应用一样的自定义滚动?

问题描述

最近我安装了一个名为Chanel Fashion的新应用,它的主页上有一种非常奇怪的滚动类型,您可以从GIF下面看到它,我非常怀疑这是任何类型的自定义滚动条这是一次网页浏览,关于如何在Flutter中实现这种功能的任何提示

enter image description here

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

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

解决方法

这是我的演示

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,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,@required this.itemBuilder,this.itemCount,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,itemBuilder: widget.itemBuilder,itemCount: widget.itemCount,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;
}
,

将a与带有列的FittedBox一起使用。为了使图片较小,请使用FittedBox。用SizedBox包裹AnimatedOpacity以控制内部小部件的大小。使用滚动通知程序在滚动时引起更新,并跟踪用户滚动多远。将滚动量除以所需的最大高度,以便知道需要调整大小的当前小部件。通过找到剩余部分并将其除以最大高度,再乘以最小和最大大小之差,来调整小部件的大小,然后添加最小大小。这将确保平稳过渡。然后,将列中上方的所有小部件设置为最大尺寸,将其设置为最小尺寸以下,以确保滞后不会破坏滚动条。

使用TitleWithImage可以使标题的描述淡入或淡出,或制作自定义动画以显示其外观。

以下代码应该可以工作,尽管您可以使用所需的样式自定义文本小部件。输入要在列表中的自定义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,); } } if(currentWidget!=0){ widgets[currentWidget] = ListItem( height: _scrollController.position.pixels%widget.maxHeight/widget.maxHeight*(widget.maxHeight-widget.minHeight)+widget.minHeight,item: widget.items[currentWidget],); for(int i = currentWidget-1; i>=0; i--){ widgets[i] = ListItem(height: widget.maxHeight,descriptionTransparent: false,); } } else{ widgets[0] = ListItem( height: widget.maxHeight,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,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),); } } (包含小部件和两个字符串)项,将maxHeight和minHeight放入自定义小部件。尽管我已修复了一些问题,但它可能尚未完全优化,并且可能存在很多错误:

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,so there are ten blinds.
                    colors: [
                      Colors.orange,Colors.blue,new TitleWithImage(Container(height: 1000,color: Colors.blue),'description'),color: Colors.orange),minHeight: 50,maxHeight: 300,);
  }
}

编辑,这是我的main.dart:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp();

  runApp(FlashChat());
}

,

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

,

尝试使用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',SliverList(
        delegate: SliverChildListDelegate(
          <Widget>[

          ],);