Skip to content

Instantly share code, notes, and snippets.

@tomialagbe
Forked from slightfoot/scrolling_panel.dart
Created January 16, 2021 07:53
Show Gist options
  • Save tomialagbe/bdceb15aa29c46dea3c9c55b89fe19ff to your computer and use it in GitHub Desktop.
Save tomialagbe/bdceb15aa29c46dea3c9c55b89fe19ff to your computer and use it in GitHub Desktop.
Scrolling Panel - by Simon Lightfoot - 20/10/2020
// MIT License
//
// Copyright (c) 2020 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 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
void main() {
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
home: Material(
color: Colors.black,
child: ScrollingPanel(
imageUrls: imageThumbUrls,
),
),
));
}
///
/// This are PicasaWeb thumbnail URLs and could potentially change. Ideally the PicasaWeb API
/// should be used to fetch the URLs.
///
/// Credit to Romain Guy for the photos:
/// http://www.curious-creature.org/
/// https://plus.google.com/109538161516040592207/about
/// http://www.flickr.com/photos/romainguy
///
List<String> imageThumbUrls = [
"https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s240-c/A%252520Photographer.jpg",
"https://lh4.googleusercontent.com/--dq8niRp7W4/URquVgmXvgI/AAAAAAAAAbs/-gnuLQfNnBA/s240-c/A%252520Song%252520of%252520Ice%252520and%252520Fire.jpg",
"https://lh5.googleusercontent.com/-7qZeDtRKFKc/URquWZT1gOI/AAAAAAAAAbs/hqWgteyNXsg/s240-c/Another%252520Rockaway%252520Sunset.jpg",
"https://lh3.googleusercontent.com/--L0Km39l5J8/URquXHGcdNI/AAAAAAAAAbs/3ZrSJNrSomQ/s240-c/Antelope%252520Butte.jpg",
"https://lh6.googleusercontent.com/-8HO-4vIFnlw/URquZnsFgtI/AAAAAAAAAbs/WT8jViTF7vw/s240-c/Antelope%252520Hallway.jpg",
"https://lh4.googleusercontent.com/-WIuWgVcU3Qw/URqubRVcj4I/AAAAAAAAAbs/YvbwgGjwdIQ/s240-c/Antelope%252520Walls.jpg",
"https://lh6.googleusercontent.com/-UBmLbPELvoQ/URqucCdv0kI/AAAAAAAAAbs/IdNhr2VQoQs/s240-c/Apre%2525CC%252580s%252520la%252520Pluie.jpg",
"https://lh3.googleusercontent.com/-s-AFpvgSeew/URquc6dF-JI/AAAAAAAAAbs/Mt3xNGRUd68/s240-c/Backlit%252520Cloud.jpg",
"https://lh5.googleusercontent.com/-bvmif9a9YOQ/URquea3heHI/AAAAAAAAAbs/rcr6wyeQtAo/s240-c/Bee%252520and%252520Flower.jpg",
"https://lh5.googleusercontent.com/-n7mdm7I7FGs/URqueT_BT-I/AAAAAAAAAbs/9MYmXlmpSAo/s240-c/Bonzai%252520Rock%252520Sunset.jpg",
"https://lh6.googleusercontent.com/-4CN4X4t0M1k/URqufPozWzI/AAAAAAAAAbs/8wK41lg1KPs/s240-c/Caterpillar.jpg",
"https://lh3.googleusercontent.com/-rrFnVC8xQEg/URqufdrLBaI/AAAAAAAAAbs/s69WYy_fl1E/s240-c/Chess.jpg",
"https://lh5.googleusercontent.com/-WVpRptWH8Yw/URqugh-QmDI/AAAAAAAAAbs/E-MgBgtlUWU/s240-c/Chihuly.jpg",
"https://lh5.googleusercontent.com/-0BDXkYmckbo/URquhKFW84I/AAAAAAAAAbs/ogQtHCTk2JQ/s240-c/Closed%252520Door.jpg",
"https://lh3.googleusercontent.com/-PyggXXZRykM/URquh-kVvoI/AAAAAAAAAbs/hFtDwhtrHHQ/s240-c/Colorado%252520River%252520Sunset.jpg",
"https://lh3.googleusercontent.com/-ZAs4dNZtALc/URquikvOCWI/AAAAAAAAAbs/DXz4h3dll1Y/s240-c/Colors%252520of%252520Autumn.jpg",
"https://lh4.googleusercontent.com/-GztnWEIiMz8/URqukVCU7bI/AAAAAAAAAbs/jo2Hjv6MZ6M/s240-c/Countryside.jpg",
"https://lh4.googleusercontent.com/-bEg9EZ9QoiM/URquklz3FGI/AAAAAAAAAbs/UUuv8Ac2BaE/s240-c/Death%252520Valley%252520-%252520Dunes.jpg",
"https://lh6.googleusercontent.com/-ijQJ8W68tEE/URqulGkvFEI/AAAAAAAAAbs/zPXvIwi_rFw/s240-c/Delicate%252520Arch.jpg",
"https://lh5.googleusercontent.com/-Oh8mMy2ieng/URqullDwehI/AAAAAAAAAbs/TbdeEfsaIZY/s240-c/Despair.jpg",
"https://lh5.googleusercontent.com/-gl0y4UiAOlk/URqumC_KjBI/AAAAAAAAAbs/PM1eT7dn4oo/s240-c/Eagle%252520Fall%252520Sunrise.jpg",
"https://lh3.googleusercontent.com/-hYYHd2_vXPQ/URqumtJa9eI/AAAAAAAAAbs/wAalXVkbSh0/s240-c/Electric%252520Storm.jpg",
"https://lh5.googleusercontent.com/-PyY_yiyjPTo/URqunUOhHFI/AAAAAAAAAbs/azZoULNuJXc/s240-c/False%252520Kiva.jpg",
"https://lh6.googleusercontent.com/-PYvLVdvXywk/URqunwd8hfI/AAAAAAAAAbs/qiMwgkFvf6I/s240-c/Fitzgerald%252520Streaks.jpg",
"https://lh4.googleusercontent.com/-KIR_UobIIqY/URquoCZ9SlI/AAAAAAAAAbs/Y4d4q8sXu4c/s240-c/Foggy%252520Sunset.jpg",
"https://lh6.googleusercontent.com/-9lzOk_OWZH0/URquoo4xYoI/AAAAAAAAAbs/AwgzHtNVCwU/s240-c/Frantic.jpg",
"https://lh3.googleusercontent.com/-0X3JNaKaz48/URqupH78wpI/AAAAAAAAAbs/lHXxu_zbH8s/s240-c/Golden%252520Gate%252520Afternoon.jpg",
"https://lh6.googleusercontent.com/-95sb5ag7ABc/URqupl95RDI/AAAAAAAAAbs/g73R20iVTRA/s240-c/Golden%252520Gate%252520Fog.jpg",
"https://lh3.googleusercontent.com/-JB9v6rtgHhk/URqup21F-zI/AAAAAAAAAbs/64Fb8qMZWXk/s240-c/Golden%252520Grass.jpg",
"https://lh4.googleusercontent.com/-EIBGfnuLtII/URquqVHwaRI/AAAAAAAAAbs/FA4McV2u8VE/s240-c/Grand%252520Teton.jpg",
"https://lh4.googleusercontent.com/-WoMxZvmN9nY/URquq1v2AoI/AAAAAAAAAbs/grj5uMhL6NA/s240-c/Grass%252520Closeup.jpg",
"https://lh3.googleusercontent.com/-6hZiEHXx64Q/URqurxvNdqI/AAAAAAAAAbs/kWMXM3o5OVI/s240-c/Green%252520Grass.jpg",
"https://lh5.googleusercontent.com/-6LVb9OXtQ60/URquteBFuKI/AAAAAAAAAbs/4F4kRgecwFs/s240-c/Hanging%252520Leaf.jpg",
"https://lh4.googleusercontent.com/-zAvf__52ONk/URqutT_IuxI/AAAAAAAAAbs/D_bcuc0thoU/s240-c/Highway%2525201.jpg",
"https://lh6.googleusercontent.com/-H4SrUg615rA/URquuL27fXI/AAAAAAAAAbs/4aEqJfiMsOU/s240-c/Horseshoe%252520Bend%252520Sunset.jpg",
];
// ------------------------------------------------
@immutable
class ScrollingPanel extends StatefulWidget {
const ScrollingPanel({
Key key,
this.speed = 25.0,
@required this.imageUrls,
}) : super(key: key);
final double speed;
final List<String> imageUrls;
@override
_ScrollingPanelState createState() => _ScrollingPanelState();
}
class _ScrollingPanelState extends State<ScrollingPanel> {
Future<void> _preload;
double _opacity = 0.0;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_preload ??= Future.wait([
for (int i = 0; i < widget.imageUrls.length; i++)
precacheImage(NetworkImage(widget.imageUrls[i]), context),
]).then((_) {
if (mounted) {
setState(() => _opacity = 1.0);
}
});
}
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final size = mediaQuery.size * 1.1; // zoom level
return AnimatedOpacity(
opacity: _opacity,
duration: const Duration(seconds: 3),
curve: Curves.decelerate,
child: FutureBuilder(
future: _preload,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return SizedBox();
}
return OverflowBox(
minWidth: size.width,
maxWidth: size.width,
minHeight: size.height,
maxHeight: size.height,
child: _ScrollingPanelLayout(
speed: widget.speed,
children: [
for (final url in widget.imageUrls)
Image.network(
url,
fit: BoxFit.cover,
),
],
),
);
},
),
);
}
}
@immutable
class _ScrollingPanelLayout extends MultiChildRenderObjectWidget {
_ScrollingPanelLayout({
Key key,
this.speed,
List<Widget> children,
}) : assert(speed != null),
super(key: key, children: children);
final double speed;
@override
_RenderScrollingPanel createRenderObject(BuildContext context) {
return _RenderScrollingPanel(speed: speed);
}
@override
void updateRenderObject(BuildContext context, _RenderScrollingPanel renderObject) {
renderObject..speed = speed;
}
}
class _ScrollingPanelParentData extends ContainerBoxParentData<RenderBox> {}
class _RenderScrollingPanel extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, _ScrollingPanelParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, _ScrollingPanelParentData> {
_RenderScrollingPanel({
@required double speed,
}) : _speed = speed;
Ticker _ticker;
double _dy = 0.0;
double _segmentHeight = 0.0;
double _speed;
double get speed => _speed;
set speed(double value) {
if (_speed != value) {
_speed = value;
markNeedsPaint();
}
}
@override
bool get alwaysNeedsCompositing => true;
@override
void setupParentData(RenderBox child) {
if (child.parentData is! _ScrollingPanelParentData) {
child.parentData = _ScrollingPanelParentData();
}
}
_ScrollingPanelParentData _childParentData(RenderBox child) =>
child.parentData as _ScrollingPanelParentData;
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_ticker = Ticker(_onTick);
_ticker.start();
}
void _onTick(Duration elapsed) {
_dy = speed * (elapsed.inMicroseconds / Duration.microsecondsPerSecond);
markNeedsPaint();
}
@override
void detach() {
_ticker.dispose();
_ticker = null;
super.detach();
}
@override
void performLayout() {
assert(constraints.hasBoundedWidth);
final childWidthConstraint = constraints.maxWidth / 5.0;
final childConstraints = BoxConstraints(
minWidth: childWidthConstraint,
maxWidth: childWidthConstraint,
minHeight: childWidthConstraint * 1.5,
maxHeight: childWidthConstraint * 1.5,
);
final children = getChildrenAsList();
for (int i = 0; i < children.length; i++) {
final child = children[i];
child.layout(childConstraints, parentUsesSize: true);
}
final childSize = firstChild.size;
for (int i = 0; i < children.length; i++) {
final x = (i % 5) * childSize.width;
final y = (i ~/ 5) * childSize.height;
_childParentData(children[i]).offset = Offset(x, y);
}
_segmentHeight = (children.length / 5) * childSize.height;
size = constraints.biggest;
}
@override
void paint(PaintingContext context, Offset offset) {
final _top = (_dy % _segmentHeight);
for (int i = 0; i < 2; i++) {
context.pushLayer(
OffsetLayer(offset: Offset(0.0, -_top + (i * _segmentHeight))),
defaultPaint,
offset,
);
}
}
@override
bool hitTestChildren(BoxHitTestResult result, {Offset position}) {
return defaultHitTestChildren(result, position: position);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment