Flutter,自定义滚动效果

Posted

技术标签:

【中文标题】Flutter,自定义滚动效果【英文标题】:Flutter, custom scroll effects 【发布时间】:2018-06-07 08:32:38 【问题描述】:

我想实现这个视频中的布局(5:50)https://www.youtube.com/watch?v=KYUTQQ1usZE&index=1&list=PL23Revp-82LKxKN9SXqQ5Nxaa1ZpYEQuaadd#t=05m50s

你会如何解决这个问题?我尝试使用 ListView 和 GridLayout,但这似乎仅限于存档。我是否需要使用 CustomMultiChildLayout (https://docs.flutter.io/flutter/widgets/CustomMultiChildLayout-class.html) 或 CustomScrollView (https://docs.flutter.io/flutter/widgets/CustomScrollView-class.html) 之类的东西? 任何建议将不胜感激,谢谢 :)

更新: 据我所知,我需要使用 CustomScrollView (如果我错了,请纠正我)。但我对 Flutter 框架留给我的选项有点不知所措。而且我从文档中不确定我需要扩展哪些类或需要实现哪些接口来归档我的目标。我不知道我需要深入研究框架。当涉及到具有自定义滚动效果的条子和列表时,涉及以下类:

RenderSliver 这确实是实现滚动效果的渲染对象的基础。我想重新实现它会是矫枉过正。但也许将其子类化并从那里开始(也许也有些矫枉过正)? RenderSliverMultiBoxAdaptor 如果我们在层次结构中走得更高,我们会找到抽象类 RenderSliv​​erMultiBoxAdaptor。带有多个盒子孩子的条子。 A RenderSliverBoxChildManager 这为 RenderSliv​​erMultiBoxAdaptor 提供了子进程。这些都是抽象类。那么也许可以从这里开始并扩展这些类? RenderSliverList 这扩展了 RenderSliv​​erMultiBoxAdaptor 并提供沿主轴布置的盒子子项。子节点由实现 RenderSliv​​erBoxChildManager 的类传递。 SliverMultiBoxAdaptorElement 实现 RenderSliv​​erBoxChildManager。所以 RenderSliv​​erList 和 SliverMultiBoxAdaptorElement 是 RenderSliv​​erMultiBoxAdaptor 和 RenderSliv​​erBoxChildManager 的具体实现。我认为我可以扩展这些类。但如果我这样做,我无论如何都必须重新实现 performLayout 方法。那么也许可以重用 SliverMultiBoxAdaptorElement 并扩展 RenderSliv​​erMultiBoxAdaptor? SliverList 这个类最终创建了渲染对象(一个以 SliverMultiBoxAdaptorElement 作为子管理器的 RenderSliv​​erList),并为 SliverMultiBoxAdaptorElement 提供了一个 SliverChildDelegate,而后者又懒惰地为SliverMultiBoxAdaptorWidget 构建子代。 SliverList 将多个盒子子元素沿主轴放置在一个线性数组中。它使用一个扩展 SliverChildDelegate 的类来动态提供孩子。它可以放置在 CustomScrollViews slivers 数组中。这是在 CustomScrollView 中创建列表的最具体的条子。那么我是否也可以简单地归档我的目标,即根据视频进行布局?到目前为止,我尝试为 CustomScrollView 提供一个 ScrollController 来拦截滚动偏移量,然后根据滚动偏移量和带有SliverChildBuilderDelegate 的元素索引构建子元素。但是这样做时,滚动视图不再滚动。只有当所有单元格的总高度超过视口时,它才会滚动。

那么我真的必须扩展 RenderSliv​​erMultiBoxAdaptor 并自己实现 perfromLayout 方法吗?对我来说,这似乎是现在唯一的选择......

【问题讨论】:

为什么不监控 ListViewItem 是否是倒数第二个并更改其与滚动成比例的高度? 我遇到了同样的问题,无法自定义滚动,我正在考虑自己使用 Scrollable 类来做,但不幸的是还没有足够的文档。 你能分享一下你完成的结果吗? 感谢您提出这个问题!您提供的列表对于了解条子的可能性非常有帮助:) 【参考方案1】:

乍一看很难理解 slivers 的逻辑。

但重要的是 SliverGeometry 类

paintOrigin - 将其视为一种 delta y。当你想制作小部件时 固定在屏幕上,您需要从顶部推动它。 constraints.scrollOffset 显示逻辑位置的滚动偏移量 小部件。 scrollExtent 显示小部件的逻辑高度。它帮助小部件 知道您滚动了所有条子。

import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        body: MyHomePage(),
      ),
    );
  


class MyHomePage extends StatefulWidget 
  @override
  _MyHomePageState createState() => _MyHomePageState();


class _MyHomePageState extends State<MyHomePage> 
  final GlobalKey _key = GlobalKey();

  RenderObject ansestor;
  @override
  void initState() 
    WidgetsBinding.instance.addPostFrameCallback(_getPosition);

    super.initState();
  

  _getPosition(_) 
    setState(() 
      ansestor = _key.currentContext.findRenderObject();
    );
  

  @override
  Widget build(BuildContext context) 
    return LayoutBuilder(builder: (context, constraints) 
      return CustomScrollView(
        physics: ClampingScrollPhysics(),
        key: _key,
        slivers: <Widget>[
          CustomSliver(
            isInitiallyExpanded: true,
            ansestor: ansestor,
            child: _Item(
              title: 'first title',
              fileName: 'item_1',
            ),
          ),
          CustomSliver(
            ansestor: ansestor,
            child: _Item(
              title: 'second title',
              fileName: 'item_2',
            ),
          ),
          CustomSliver(
            ansestor: ansestor,
            child: _Item(
              title: 'third title',
              fileName: 'item_3',
            ),
          ),
          CustomSliver(
            ansestor: ansestor,
            child: _Item(
              title: 'fourth title',
              fileName: 'item_4',
            ),
          ),
          CustomSliver(
            ansestor: ansestor,
            child: _Item(
              title: 'fifth title',
              fileName: 'item_5',
            ),
          ),
          CustomSliver(
            ansestor: ansestor,
            child: _Item(
              title: 'first title',
              fileName: 'item_6',
            ),
          ),
          SliverToBoxAdapter(
            child: Container(
              child: Center(
                child: Text('end'),
              ),
              height: 1200,
              color: Colors.green.withOpacity(0.3),
            ),
          ),
        ],
      );
    );
  


class CustomSliver extends SingleChildRenderObjectWidget 
  CustomSliver(
    this.child,
    Key key,
    this.ansestor,
    this.isInitiallyExpanded = false,
  ) : super(key: key);

  final RenderObject ansestor;
  final bool isInitiallyExpanded;

  @override
  RenderObject createRenderObject(BuildContext context) 
    return CustomRenderSliver(
      isInitiallyExpanded: isInitiallyExpanded,
    );
  

  @override
  void updateRenderObject(
    BuildContext context,
    CustomRenderSliver renderObject,
  ) 
    renderObject.ansestor = ansestor;
    renderObject.markNeedsLayout();
  

  final Widget child;


class CustomRenderSliver extends RenderSliverSingleBoxAdapter 
  CustomRenderSliver(
    RenderBox child,
    this.isInitiallyExpanded,
  ) : super(child: child);

  final double max = 250;
  final double min = 100;

  RenderObject ansestor;
  final bool isInitiallyExpanded;
  void performLayout() 
    var constraints = this.constraints;

    double distanceToTop;

    double maxExtent;

    if (ansestor != null) 
      distanceToTop = child.localToGlobal(Offset.zero, ancestor: ansestor).dy;
    

    if (ansestor == null) 
      if (isInitiallyExpanded) 
        maxExtent = max;
       else 
        maxExtent = min;
      
     else 
      if (constraints.scrollOffset > 0) 
        maxExtent = (max - constraints.scrollOffset).clamp(0.0, max);
       else if (distanceToTop < max) 
        maxExtent = min + (3 * (250 - distanceToTop) / 5);
       else 
        maxExtent = min;
      
    

    child.layout(
      constraints.asBoxConstraints(maxExtent: maxExtent),
      parentUsesSize: true,
    );

    var paintExtent = math.min(maxExtent, constraints.remainingPaintExtent);

    geometry = SliverGeometry(
      paintOrigin: maxExtent == 0 ? 0.0 : constraints.scrollOffset,
      scrollExtent: max,
      paintExtent: paintExtent,
      maxPaintExtent: paintExtent,
      hasVisualOverflow: true,
    );

    constraints = constraints.copyWith(remainingPaintExtent: double.infinity);
    setChildParentData(child, constraints, geometry);
  


class _Item extends StatelessWidget 
  const _Item(
    Key key,
    @required this.title,
    @required this.fileName,
  ) : super(key: key);

  final String title;
  final String fileName;

  @override
  Widget build(BuildContext context) 
    return LayoutBuilder(
      builder: (context, constraints) 
        return Container(
          height: 250,
          decoration: BoxDecoration(
            image: DecorationImage(
              image: AssetImage('assets/images/$fileName.png'),
              fit: BoxFit.fitWidth,
            ),
          ),
          child: Padding(
            padding: const EdgeInsets.only(top: 40),
            child: Text(
              title,
              style: Theme.of(context).textTheme.headline4.copyWith(
                    color: Colors.white,
                    fontWeight: FontWeight.bold,
                    fontSize: 60,
                  ),
            ),
          ),
        );
      ,
    );
  

【讨论】:

以上是关于Flutter,自定义滚动效果的主要内容,如果未能解决你的问题,请参考以下文章

Flutter 粘合剂CustomScrollView控件

Flutter 粘合剂CustomScrollView控件

jQuery自定义数字滚动效果

自定义ViewpagerIndicator (仿猫眼,添加边缘回弹滚动效果)

Flutter:为用户滚动增加摩擦力,而不是弹簧

Flutter视频编辑轨道 | 自定义View实现UI交互效果 | 触摸事件处理