Forked from slightfoot/animated_partial_height_sheet.dart
Created
November 19, 2023 14:04
-
-
Save JohanScheepers/8b760eb62b86c0e79f2cf6f903d30e65 to your computer and use it in GitHub Desktop.
Animated Partial Height Sheet - by Simon Lightfoot - Humpday Q&A - 01/11/2023 - https://www.youtube.com/watch?v=S9C496aj1cA
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MIT License | |
// | |
// Copyright (c) 2023 Simon Lightfoot | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in all | |
// copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
// SOFTWARE. | |
// | |
import 'dart:ui'; | |
import 'package:flutter/gestures.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/rendering.dart'; | |
void main() { | |
runApp( | |
MaterialApp( | |
debugShowCheckedModeBanner: false, | |
theme: ThemeData.dark(), | |
home: const Home(), | |
), | |
); | |
} | |
class Home extends StatelessWidget { | |
const Home({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return Material( | |
child: Stack( | |
children: [ | |
ListView.builder( | |
itemBuilder: (BuildContext context, int index) { | |
return ListTile( | |
title: Text('Item #$index'), | |
); | |
}, | |
), | |
const Positioned( | |
left: 0.0, | |
right: 0.0, | |
bottom: 0.0, | |
child: AnimatedBottomSheet( | |
children: [ | |
SizedBox( | |
height: 200.0, | |
child: ColoredBox( | |
color: Colors.tealAccent, | |
child: Placeholder(), | |
), | |
), | |
SizedBox( | |
height: 400.0, | |
child: ColoredBox( | |
color: Colors.deepOrangeAccent, | |
child: Placeholder(), | |
), | |
), | |
SizedBox( | |
height: 100.0, | |
child: ColoredBox( | |
color: Colors.purpleAccent, | |
child: Placeholder(), | |
), | |
), | |
], | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
class AnimatedBottomSheet extends StatefulWidget { | |
const AnimatedBottomSheet({ | |
super.key, | |
this.initialPage = 0, | |
this.alignment = Alignment.topCenter, | |
required this.children, | |
}); | |
final int initialPage; | |
final Alignment alignment; | |
final List<Widget> children; | |
@override | |
State<AnimatedBottomSheet> createState() => _AnimatedBottomSheetState(); | |
} | |
class _AnimatedBottomSheetState extends State<AnimatedBottomSheet> { | |
late final PageController _controller; | |
@override | |
void initState() { | |
super.initState(); | |
_controller = PageController( | |
initialPage: widget.initialPage, | |
); | |
} | |
@override | |
void dispose() { | |
_controller.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Material( | |
color: Theme.of(context).cardColor, | |
elevation: 8.0, | |
shape: const RoundedRectangleBorder( | |
borderRadius: BorderRadius.only( | |
topLeft: Radius.circular(12.0), | |
topRight: Radius.circular(12.0), | |
), | |
), | |
clipBehavior: Clip.antiAlias, | |
child: Scrollable( | |
axisDirection: AxisDirection.right, | |
physics: const PageScrollPhysics(), | |
dragStartBehavior: DragStartBehavior.down, | |
controller: _controller, | |
viewportBuilder: (BuildContext context, ViewportOffset position) { | |
return PartialHeightLayout( | |
offset: position, | |
alignment: widget.alignment, | |
children: widget.children, | |
); | |
}, | |
), | |
); | |
} | |
} | |
class PartialHeightLayout extends MultiChildRenderObjectWidget { | |
const PartialHeightLayout({ | |
super.key, | |
required this.alignment, | |
required this.offset, | |
required super.children, | |
}); | |
final Alignment alignment; | |
final ViewportOffset offset; | |
@override | |
RenderObject createRenderObject(BuildContext context) { | |
return RenderPartialHeightLayout( | |
alignment: alignment, | |
offset: offset, | |
); | |
} | |
@override | |
void updateRenderObject( | |
BuildContext context, RenderPartialHeightLayout renderObject) { | |
renderObject // | |
..alignment = alignment | |
..offset = offset; | |
} | |
} | |
class _PartialHeightLayoutParentData | |
extends ContainerBoxParentData<RenderBox> {} | |
class RenderPartialHeightLayout extends RenderBox | |
with | |
ContainerRenderObjectMixin<RenderBox, _PartialHeightLayoutParentData>, | |
RenderBoxContainerDefaultsMixin<RenderBox, | |
_PartialHeightLayoutParentData> { | |
RenderPartialHeightLayout({ | |
required Alignment alignment, | |
required ViewportOffset offset, | |
}) : _alignment = alignment, | |
_offset = offset; | |
Alignment _alignment; | |
Alignment get alignment => _alignment; | |
set alignment(Alignment value) { | |
if (_alignment == value) { | |
return; | |
} | |
_alignment = value; | |
markNeedsLayout(); | |
} | |
ViewportOffset _offset; | |
ViewportOffset get offset => _offset; | |
set offset(ViewportOffset value) { | |
if (_offset == value) { | |
return; | |
} | |
// When updating the viewport offset, we might already | |
// be attached and listening, stop listening to the old | |
// offset and listen to the new one instead. | |
if (attached) { | |
_offset.removeListener(markNeedsLayout); | |
} | |
_offset = value; | |
if (attached) { | |
_offset.addListener(markNeedsLayout); | |
} | |
markNeedsLayout(); | |
} | |
@override | |
void attach(PipelineOwner owner) { | |
super.attach(owner); | |
// Only listen to Scrollable position when this layout is | |
// attached to the render tree. | |
_offset.addListener(markNeedsLayout); | |
} | |
@override | |
void detach() { | |
// Stop listening when we are detached from the render tree. | |
_offset.removeListener(markNeedsLayout); | |
super.detach(); | |
} | |
@override | |
void setupParentData(RenderBox child) { | |
if (child.parentData is! _PartialHeightLayoutParentData) { | |
child.parentData = _PartialHeightLayoutParentData(); | |
} | |
} | |
@override | |
void performLayout() { | |
final children = getChildrenAsList(); | |
// Determine which page of content we are within. | |
final viewportWidth = constraints.maxWidth; | |
final fractionalOffset = | |
offset.hasPixels ? (offset.pixels / viewportWidth) : 0.0; | |
// Layout all children from left to right and position them | |
// based on the current scroll offset. | |
double x = offset.hasPixels ? -offset.pixels : 0.0; | |
for (int i = 0; i < children.length; i++) { | |
final child = children[i]; | |
child.layout(constraints, parentUsesSize: true); | |
final parentData = child.parentData as _PartialHeightLayoutParentData; | |
parentData.offset = Offset(x, 0.0); | |
x += child.size.width; | |
} | |
// Calculate the height of the layout based on what amount | |
// of each child page is visible. | |
int index = fractionalOffset.floor(); | |
double height; | |
if (index >= 0 && index + 1 < children.length) { | |
height = lerpDouble( | |
children[index].size.height, | |
children[index + 1].size.height, | |
fractionalOffset - index, | |
)!; | |
} else { | |
height = children[index].size.height; | |
} | |
// Align each child vertically within the layout height | |
final pageRect = Rect.fromLTWH(0, 0, viewportWidth, height); | |
for (final child in children) { | |
final parentData = child.parentData as _PartialHeightLayoutParentData; | |
final aligned = alignment.inscribe(child.size, pageRect); | |
parentData.offset = Offset(parentData.offset.dx, aligned.top); | |
} | |
// Report layout size to framework | |
size = Size( | |
viewportWidth, | |
constraints.constrainHeight(height), | |
); | |
// Report content dimensions to Scrollable. | |
offset.applyViewportDimension(viewportWidth); | |
offset.applyContentDimensions( | |
0.0, | |
(x + offset.pixels) - viewportWidth, | |
); | |
} | |
@override | |
void paint(PaintingContext context, Offset offset) { | |
// Paint all children in their expected positions | |
defaultPaint(context, offset); | |
} | |
@override | |
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { | |
// Hit-test all children based on their positions | |
return defaultHitTestChildren(result, position: position); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment