aves/lib/labs/sliver_transition_grid_delegate.dart
2020-04-01 10:40:02 +09:00

176 lines
7.1 KiB
Dart

import 'dart:math' as math;
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
class SliverTransitionGridDelegateWithCrossAxisCount extends SliverGridDelegate {
const SliverTransitionGridDelegateWithCrossAxisCount({
@required this.crossAxisCount,
this.mainAxisSpacing = 0.0,
this.crossAxisSpacing = 0.0,
this.childAspectRatio = 1.0,
}) : assert(crossAxisCount != null && crossAxisCount > 0),
assert(mainAxisSpacing != null && mainAxisSpacing >= 0),
assert(crossAxisSpacing != null && crossAxisSpacing >= 0),
assert(childAspectRatio != null && childAspectRatio > 0);
/// The number of children in the cross axis.
final double crossAxisCount;
/// The number of logical pixels between each child along the main axis.
final double mainAxisSpacing;
/// The number of logical pixels between each child along the cross axis.
final double crossAxisSpacing;
/// The ratio of the cross-axis to the main-axis extent of each child.
final double childAspectRatio;
@override
SliverGridLayout getLayout(SliverConstraints constraints) {
final t = crossAxisCount - crossAxisCount.truncateToDouble();
return SliverTransitionGridTileLayout(
current: _buildSettings(constraints, crossAxisCount),
floor: t != 0 ? _buildSettings(constraints, crossAxisCount.floorToDouble()) : null,
ceil: t != 0 ? _buildSettings(constraints, crossAxisCount.ceilToDouble()) : null,
t: t,
reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
);
}
SliverTransitionGridTileLayoutSettings _buildSettings(SliverConstraints constraints, double crossAxisCount) {
final usableCrossAxisExtent = constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1);
final childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
final childMainAxisExtent = childCrossAxisExtent / childAspectRatio;
final current = SliverTransitionGridTileLayoutSettings(
crossAxisCount: crossAxisCount,
mainAxisStride: childMainAxisExtent + mainAxisSpacing,
crossAxisStride: childCrossAxisExtent + crossAxisSpacing,
childMainAxisExtent: childMainAxisExtent,
childCrossAxisExtent: childCrossAxisExtent,
);
return current;
}
@override
bool shouldRelayout(SliverTransitionGridDelegateWithCrossAxisCount oldDelegate) {
return oldDelegate.crossAxisCount != crossAxisCount || oldDelegate.mainAxisSpacing != mainAxisSpacing || oldDelegate.crossAxisSpacing != crossAxisSpacing || oldDelegate.childAspectRatio != childAspectRatio;
}
}
class SliverTransitionGridTileLayoutSettings {
final double crossAxisCount;
/// The number of pixels from the leading edge of one tile to the leading edge
/// of the next tile in the main axis.
final double mainAxisStride;
/// The number of pixels from the leading edge of one tile to the leading edge
/// of the next tile in the cross axis.
final double crossAxisStride;
/// The number of pixels from the leading edge of one tile to the trailing
/// edge of the same tile in the main axis.
final double childMainAxisExtent;
/// The number of pixels from the leading edge of one tile to the trailing
/// edge of the same tile in the cross axis.
final double childCrossAxisExtent;
const SliverTransitionGridTileLayoutSettings({
@required this.crossAxisCount,
@required this.mainAxisStride,
@required this.crossAxisStride,
@required this.childMainAxisExtent,
@required this.childCrossAxisExtent,
}) : assert(crossAxisCount != null && crossAxisCount > 0),
assert(mainAxisStride != null && mainAxisStride >= 0),
assert(crossAxisStride != null && crossAxisStride >= 0),
assert(childMainAxisExtent != null && childMainAxisExtent >= 0),
assert(childCrossAxisExtent != null && childCrossAxisExtent >= 0);
}
class SliverTransitionGridTileLayout extends SliverGridLayout {
/// Creates a layout that uses equally sized and spaced tiles.
///
/// All of the arguments must not be null and must not be negative. The
/// `crossAxisCount` argument must be greater than zero.
const SliverTransitionGridTileLayout({
@required this.current,
this.floor,
this.ceil,
this.t = 0,
@required this.reverseCrossAxis,
}) : assert(reverseCrossAxis != null);
final SliverTransitionGridTileLayoutSettings current, floor, ceil;
final double t;
/// Whether the children should be placed in the opposite order of increasing
/// coordinates in the cross axis.
///
/// For example, if the cross axis is horizontal, the children are placed from
/// left to right when [reverseCrossAxis] is false and from right to left when
/// [reverseCrossAxis] is true.
///
/// Typically set to the return value of [axisDirectionIsReversed] applied to
/// the [SliverConstraints.crossAxisDirection].
final bool reverseCrossAxis;
@override
int getMinChildIndexForScrollOffset(double scrollOffset) {
final settings = t == 0 ? current : floor;
final index = settings.mainAxisStride > 0.0 ? (settings.crossAxisCount * (scrollOffset ~/ settings.mainAxisStride)).floor() : 0;
return index;
}
@override
int getMaxChildIndexForScrollOffset(double scrollOffset) {
final settings = t == 0 ? current : floor;
if (settings.mainAxisStride > 0.0) {
final mainAxisCount = (scrollOffset / settings.mainAxisStride).ceil();
final index = math.max(0, settings.crossAxisCount * mainAxisCount - 1).ceil();
return index;
}
return 0;
}
double _getScrollOffset(int index, SliverTransitionGridTileLayoutSettings settings) {
return (index ~/ settings.crossAxisCount) * settings.mainAxisStride;
}
double _getCrossAxisOffset(int index, SliverTransitionGridTileLayoutSettings settings) {
final crossAxisStart = (index % settings.crossAxisCount) * settings.crossAxisStride;
if (reverseCrossAxis) {
return settings.crossAxisCount * settings.crossAxisStride - crossAxisStart - settings.childCrossAxisExtent - (settings.crossAxisStride - settings.childCrossAxisExtent);
}
return crossAxisStart;
}
@override
SliverGridGeometry getGeometryForChildIndex(int index) {
return SliverGridGeometry(
scrollOffset: t == 0 ? _getScrollOffset(index, current) : lerpDouble(_getScrollOffset(index, floor), _getScrollOffset(index, ceil), t),
crossAxisOffset: t == 0 ? _getCrossAxisOffset(index, current) : lerpDouble(_getCrossAxisOffset(index, floor), _getCrossAxisOffset(index, ceil), t),
mainAxisExtent: current.childMainAxisExtent,
crossAxisExtent: current.childCrossAxisExtent,
);
}
@override
double computeMaxScrollOffset(int childCount) {
assert(childCount != null);
if (t != 0) {
final index = childCount - 1;
final maxScrollOffset = lerpDouble(_getScrollOffset(index, floor), _getScrollOffset(index, ceil), t) + current.mainAxisStride;
return maxScrollOffset;
}
final mainAxisCount = ((childCount - 1) ~/ current.crossAxisCount) + 1;
final mainAxisSpacing = current.mainAxisStride - current.childMainAxisExtent;
final maxScrollOffset = current.mainAxisStride * mainAxisCount - mainAxisSpacing;
return maxScrollOffset;
}
}