Skip to content

Instantly share code, notes, and snippets.

@ariedov
Last active October 11, 2018 15:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ariedov/103e6d838ec8b73eca2bea0608db29bc to your computer and use it in GitHub Desktop.
Save ariedov/103e6d838ec8b73eca2bea0608db29bc to your computer and use it in GitHub Desktop.
Scrollable cards
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
const CARD_SIZE = const Size(172.0 * 1.6, 248.0 * 1.6);
const SEPARATOR_WIDTH = 10.0;
const WIDTH_DIFFERENCE = 30.0;
const ITEM_COUNT = 80;
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: RootPage());
}
}
class RootPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
final horizontalOffset = (screenSize.width - CARD_SIZE.width) / 2;
return MyHomePage(
screenSize: screenSize,
horizontalOffset: horizontalOffset,
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.screenSize, this.horizontalOffset})
: super(key: key);
final Size screenSize;
final horizontalOffset;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
ScrollController scrollController;
ViewModel viewModel;
AnimationController snipController;
Tween<double> snipTween;
Tween<double> snipSizeTween;
@override
void initState() {
snipController =
AnimationController(vsync: this, duration: Duration(milliseconds: 200))
..addListener(() {
setState(() {
final curve = CurvedAnimation(
parent: snipController, curve: Curves.fastOutSlowIn);
viewModel.cardWidthDelta = snipSizeTween.evaluate(curve);
final position = snipTween.evaluate(curve);
scrollController.jumpTo(position);
});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
setState(() {
viewModel.centerItemPosition = viewModel.nextItemPosition;
viewModel.nextItemPosition = -1;
viewModel.cardWidthDelta = 0.0;
});
}
});
scrollController =
ScrollController(initialScrollOffset: WIDTH_DIFFERENCE / 2);
viewModel = ViewModel();
super.initState();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onHorizontalDragStart: _horizontalStart,
onHorizontalDragUpdate: _horizontalUpdate,
onHorizontalDragEnd: _horizontalEnd,
child: ListView.separated(
padding: EdgeInsets.only(
left: widget.horizontalOffset, right: widget.horizontalOffset),
scrollDirection: Axis.horizontal,
physics: NeverScrollableScrollPhysics(),
controller: scrollController,
separatorBuilder: (context, index) {
return SizedBox(width: SEPARATOR_WIDTH);
},
itemBuilder: (context, index) {
Size newSize = CARD_SIZE;
if (index == viewModel.centerItemPosition) {
newSize = Size(
CARD_SIZE.width + WIDTH_DIFFERENCE - viewModel.cardWidthDelta,
CARD_SIZE.height +
WIDTH_DIFFERENCE -
viewModel.cardWidthDelta);
}
if (index == viewModel.nextItemPosition) {
newSize = Size(CARD_SIZE.width + viewModel.cardWidthDelta,
CARD_SIZE.height + viewModel.cardWidthDelta);
}
return Align(
alignment: Alignment.center,
child: VideoCard(
imageUrl:
"https://s9.vcdn.biz/static/f/1427270211/image.jpg/pt/r193x272",
size: newSize),
);
},
itemCount: ITEM_COUNT),
);
}
_horizontalStart(DragStartDetails details) {
viewModel.centerItemPosition = currentItemPosition;
viewModel.nextItemPosition = -1;
viewModel.dragStartPosition = details.globalPosition.dx;
viewModel.offset = scrollController.offset;
}
_horizontalUpdate(DragUpdateDetails details) {
if (details.globalPosition.dx < viewModel.dragStartPosition) {
viewModel.nextItemPosition = viewModel.centerItemPosition + 1;
} else {
viewModel.nextItemPosition = viewModel.centerItemPosition - 1;
}
if (viewModel.nextItemPosition < 0 || viewModel.nextItemPosition >= ITEM_COUNT) {
return;
}
setState(() {
final resultOffset = viewModel.offset - details.delta.dx;
viewModel.offset = 0.0;
if (resultOffset >= 0 && resultOffset <= ITEM_COUNT * cardWidth) {
viewModel.offset = resultOffset;
}
scrollController.jumpTo(viewModel.offset);
viewModel.cardWidthDelta =
_calculateWidthDelta(details.globalPosition.dx);
});
}
_calculateWidthDelta(double currentPosition) {
final distance = (viewModel.dragStartPosition - currentPosition).abs();
return ((distance * WIDTH_DIFFERENCE) /
(CARD_SIZE.width + WIDTH_DIFFERENCE))
.clamp(0.0, WIDTH_DIFFERENCE);
}
_horizontalEnd(DragEndDetails details) {
if (viewModel.nextItemPosition >= 0 && viewModel.nextItemPosition < ITEM_COUNT) {
snipTween = Tween(
begin: scrollController.offset,
end: (viewModel.nextItemPosition * cardWidth) +
(WIDTH_DIFFERENCE / 2));
snipSizeTween =
Tween(begin: viewModel.cardWidthDelta, end: WIDTH_DIFFERENCE);
snipController.forward(from: 0.0);
}
}
int get currentItemPosition =>
(scrollController.offset + (WIDTH_DIFFERENCE / 2)) ~/ cardWidth;
double get cardWidth => CARD_SIZE.width + SEPARATOR_WIDTH;
}
class ViewModel {
int centerItemPosition = 0;
int nextItemPosition = -1;
double cardWidthDelta = 0.0;
double dragStartPosition = 0.0;
double offset = 0.0;
}
class VideoCard extends StatelessWidget {
final String imageUrl;
final Size size;
const VideoCard({Key key, this.imageUrl, this.size}) : super(key: key);
@override
Widget build(BuildContext context) {
return ClipPath(
clipper: ShapeBorderClipper(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0),
)),
child: SizedBox(
width: size.width,
height: size.height,
child: Card(
color: Color(0xff1a1a1a),
margin: EdgeInsets.all(0.0),
child: Image.network(imageUrl, fit: BoxFit.cover)),
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment