Как заставить SliverPersistentHeader «зарасти» - PullRequest
3 голосов
/ 06 мая 2019

Я использую SliverPersistentHeader в моем CustomScrollView, чтобы иметь постоянный заголовок, который сжимается и увеличивается при прокрутке пользователя, но когда он достигает своего максимального размера, он чувствует себя немного жестким, так как он не «зарастает» .

Вот видео, которое я хочу (из приложения Spotify) и какое у меня поведение:

Video of behaviour.

Ответы [ 2 ]

2 голосов
/ 10 мая 2019

В поисках решения этой проблемы я натолкнулся на три различных способа ее решения:

  1. Создание Stack, содержащего CustomScrollView и виджет заголовка (с наложением сверхупредставления прокрутки), укажите ScrollController для CustomScrollView и передайте контроллер виджету заголовка, чтобы настроить его размер
  2. . Используйте ScrollController, передайте его CustomScrollView и используйтезначение контроллера для настройки maxExtent из SliverPersistentHeader (это то, что Евгений рекомендовал ).
  3. Напишите свой собственный Щепка, чтобы он делал именно то, что я хочу.

У меня возникли проблемы с решением 1 & 2:

  1. Это решение показалось мне немного "хакерским".У меня также была проблема, что «перетаскивание» заголовка больше не прокручивалось, поскольку заголовок больше не был внутри CustomScrollView.
  2. Настройка размера ленты во время прокруткиприводит к странным побочным эффектам.Примечательно, что расстояние между заголовком и полосками внизу увеличивается во время прокрутки.

Вот почему я выбрал решение 3. Я уверен, что способ, которым я его реализовал, не самый лучший, но он работаетименно так, как я хочу:

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

/// The delegate that is provided to [ElSliverPersistentHeader].
abstract class ElSliverPersistentHeaderDelegate {
  double get maxExtent;
  double get minExtent;

  /// This acts exactly like `SliverPersistentHeaderDelegate.build()` but with
  /// the difference that `shrinkOffset` might be negative, in which case,
  /// this widget exceeds `maxExtent`.
  Widget build(BuildContext context, double shrinkOffset);
}

/// Pretty much the same as `SliverPersistentHeader` but when the user
/// continues to drag down, the header grows in size, exceeding `maxExtent`.
class ElSliverPersistentHeader extends SingleChildRenderObjectWidget {
  final ElSliverPersistentHeaderDelegate delegate;
  ElSliverPersistentHeader({
    Key key,
    ElSliverPersistentHeaderDelegate delegate,
  })  : this.delegate = delegate,
        super(
            key: key,
            child:
                _ElSliverPersistentHeaderDelegateWrapper(delegate: delegate));

  @override
  _ElPersistentHeaderRenderSliver createRenderObject(BuildContext context) {
    return _ElPersistentHeaderRenderSliver(
        delegate.maxExtent, delegate.minExtent);
  }
}

class _ElSliverPersistentHeaderDelegateWrapper extends StatelessWidget {
  final ElSliverPersistentHeaderDelegate delegate;

  _ElSliverPersistentHeaderDelegateWrapper({Key key, this.delegate})
      : super(key: key);

  @override
  Widget build(BuildContext context) =>
      LayoutBuilder(builder: (context, constraints) {
        final height = constraints.maxHeight;
        return delegate.build(context, delegate.maxExtent - height);
      });
}

class _ElPersistentHeaderRenderSliver extends RenderSliver
    with RenderObjectWithChildMixin<RenderBox> {
  final double maxExtent;
  final double minExtent;

  _ElPersistentHeaderRenderSliver(this.maxExtent, this.minExtent);

  @override
  bool hitTestChildren(HitTestResult result,
      {@required double mainAxisPosition, @required double crossAxisPosition}) {
    if (child != null) {
      return child.hitTest(result,
          position: Offset(crossAxisPosition, mainAxisPosition));
    }
    return false;
  }

  @override
  void performLayout() {
    /// The amount of scroll that extends the theoretical limit.
    /// I.e.: when the user drags down the list, although it already hit the
    /// top.
    ///
    /// This seems to be a bit of a hack, but I haven't found a way to get this
    /// information in another way.
    final overScroll =
        constraints.viewportMainAxisExtent - constraints.remainingPaintExtent;

    /// The actual Size of the widget is the [maxExtent] minus the amount the
    /// user scrolled, but capped at the [minExtent] (we don't want the widget
    /// to become smaller than that).
    /// Additionally, we add the [overScroll] here, since if there *is*
    /// "over scroll", we want the widget to grow in size and exceed
    /// [maxExtent].
    final actualSize =
        math.max(maxExtent - constraints.scrollOffset + overScroll, minExtent);

    /// Now layout the child with the [actualSize] as `maxExtent`.
    child.layout(constraints.asBoxConstraints(maxExtent: actualSize));

    /// We "clip" the `paintExtent` to the `maxExtent`, otherwise the list
    /// below stops moving when reaching the border.
    ///
    /// Tbh, I'm not entirely sure why that is.
    final paintExtent = math.min(actualSize, maxExtent);

    /// For the layout to work properly (i.e.: the following slivers to
    /// scroll behind this sliver), the `layoutExtent` must not be capped
    /// at [minExtent], otherwise the next sliver will "stop" scrolling when
    /// [minExtent] is reached,
    final layoutExtent = math.max(maxExtent - constraints.scrollOffset, 0.0);

    geometry = SliverGeometry(
      scrollExtent: maxExtent,
      paintExtent: paintExtent,
      layoutExtent: layoutExtent,
      maxPaintExtent: maxExtent,
    );
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null) {
      /// This sliver is always displayed at the top.
      context.paintChild(child, Offset(0.0, 0.0));
    }
  }
}
1 голос
/ 06 мая 2019

как вариант, вы можете просто скопировать и вставить этот код, чтобы увидеть, как он работает

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Page(),
      ),
    );
  }
}

class Page extends StatefulWidget {
  @override
  _PageState createState() => _PageState();
}

class _PageState extends State<Page> {
  static final double _initialToolbarHeight = 300;
  static final double _maxSizeFactor = 1.3; // image max size will 130%
  static final double _transformSpeed = 0.001; // 0.1 very fast,   0.001 slow

  ScrollController _controller;
  double _factor = 1;
  double _expandedToolbarHeight = _initialToolbarHeight;

  @override
  void initState() {
    _controller = ScrollController();
    _controller.addListener(_scrollListener);
    super.initState();
  }

  _scrollListener() {
    if (_controller.offset < 0) {
      _factor = 1 + _controller.offset.abs() * _transformSpeed;
      _factor = _factor.clamp(1, _maxSizeFactor);
      _expandedToolbarHeight = _initialToolbarHeight + _controller.offset.abs(); //
    } else {
      _factor = 1;
      _expandedToolbarHeight = _initialToolbarHeight; //
    }
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      physics: const BouncingScrollPhysics(),
      controller: _controller,
      slivers: [
        SliverPersistentHeader(
          floating: false,
          pinned: true,
          delegate: _SliverAppBarDelegate(
            minHeight: 300,
            maxHeight: 300,
            child: Transform.scale(
              scale: _factor,
              child: Image.network('https://picsum.photos/id/1025/990/660', fit: BoxFit.cover),
            ),
          ),
        ),
        SliverList(
          delegate: SliverChildListDelegate(getItems()),
        )
      ],
    );
  }

  getItems() {
    return List.generate(50, (pos) {
      return Container(
        height: 64,
        child: Text('item $pos'),
      );
    });
  }
}

class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  _SliverAppBarDelegate({
    @required this.minHeight,
    @required this.maxHeight,
    @required this.child,
  });

  final double minHeight;
  final double maxHeight;
  final Widget child;

  @override
  double get minExtent => minHeight;

  @override
  double get maxExtent => maxHeight;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return new SizedBox.expand(child: child);
  }

  @override
  bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
    return maxHeight != oldDelegate.maxHeight || minHeight != oldDelegate.minHeight || child != oldDelegate.child;
  }
}

Video of behaviour.

...