Skip to content

Instantly share code, notes, and snippets.

@imaNNeo
Created October 29, 2020 08:08
Show Gist options
  • Select an option

  • Save imaNNeo/bd9bb1e8dd4592f3a9c8e895fd46a220 to your computer and use it in GitHub Desktop.

Select an option

Save imaNNeo/bd9bb1e8dd4592f3a9c8e895fd46a220 to your computer and use it in GitHub Desktop.
import 'package:flutter/material.dart';
import 'dart:math' as math;
main() => runApp(MaterialApp(home: MyHomePage()));
final game1 = GameModel(
image: 'https://img.techpowerup.org/201029/game1.png',
title: 'Road Fight',
subtitle: 'Shooting Cars',
);
final game2 = GameModel(
image: 'https://img.techpowerup.org/201029/game2.png',
title: 'Vikings',
subtitle: 'Sons of Ragnar',
);
List<GameModel> games = [
game1,
game2,
game1,
game2,
game1,
game2,
game1,
game2,
game1,
game2,
game1,
game2,
game1,
game2,
game1,
game2,
];
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter 4 Fun'),
),
body: Center(
child: HorizontalSnappingList(
itemWidth: 188.0,
itemHorizontalMargin: 0,
itemCount: games.length,
itemBuilder: (context, i) => GameItemWidget(gameModel: games[i]),
),
),
);
}
}
class GameModel {
final String image;
final String title;
final String subtitle;
GameModel({
this.image,
this.title,
this.subtitle,
});
}
class GameItemWidget extends StatelessWidget {
final double itemWidth;
final double itemHeight;
final GameModel gameModel;
const GameItemWidget({
Key key,
this.itemWidth = 188.0,
this.itemHeight = 200.0,
@required this.gameModel,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
width: itemWidth,
height: itemHeight,
child: Stack(
fit: StackFit.expand,
children: [
Image.network(
gameModel.image,
fit: BoxFit.fitHeight,
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
gameModel.title,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
SizedBox(
height: 4,
),
Text(
gameModel.subtitle,
style: TextStyle(
color: Colors.white,
fontSize: 14,
),
),
],
),
)
],
),
);
}
}
class HorizontalSnappingList extends StatefulWidget {
final int itemCount;
final IndexedWidgetBuilder itemBuilder;
final double itemWidth;
final double itemHorizontalMargin;
final double listHeight;
final double itemsConsumedWidth;
HorizontalSnappingList({
@required this.itemBuilder,
this.itemCount,
this.itemWidth = 120.0,
this.itemHorizontalMargin = 4.0,
this.listHeight = 200.0,
}) : itemsConsumedWidth = itemWidth + (itemHorizontalMargin * 2);
@override
_HorizontalSnappingListState createState() => _HorizontalSnappingListState();
}
class _HorizontalSnappingListState extends State<HorizontalSnappingList> {
ScrollController _scrollController;
double scrollOffset = 0;
@override
void initState() {
_scrollController = new ScrollController();
_scrollController.addListener(() {
setState(() {
scrollOffset = _scrollController.position.pixels / widget.itemsConsumedWidth;
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: widget.listHeight,
child: ListView.builder(
padding: EdgeInsets.only(left: 16),
controller: _scrollController,
scrollDirection: Axis.horizontal,
physics: SnappingListScrollPhysic(
itemWidth: widget.itemWidth + (widget.itemHorizontalMargin * 2)),
itemCount: widget.itemCount,
itemBuilder: (context, i) {
double scale = ((1 - math.min((i - scrollOffset).abs(), 1.0)) * (1 - 0.8)) + 0.8;
return Transform.scale(
scale: scale,
child: widget.itemBuilder(context, i),
);
},
),
);
}
}
class SnappingListScrollPhysic extends ScrollPhysics {
final double itemWidth;
const SnappingListScrollPhysic({
@required this.itemWidth,
ScrollPhysics parent,
}) : super(parent: parent);
@override
SnappingListScrollPhysic applyTo(ScrollPhysics ancestor) => SnappingListScrollPhysic(
parent: buildParent(ancestor),
itemWidth: itemWidth,
);
double _getItem(ScrollPosition position) => (position.pixels) / itemWidth;
double _getPixels(ScrollPosition position, double item) =>
math.min(item * itemWidth, position.maxScrollExtent);
double _getTargetPixels(ScrollPosition position, Tolerance tolerance, double velocity) {
double item = _getItem(position);
if (velocity < -tolerance.velocity) {
item -= 0.5;
} else if (velocity > tolerance.velocity) {
item += 0.5;
}
return _getPixels(position, item.roundToDouble());
}
@override
Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
// If we're out of range and not headed back in range, defer to the parent
// ballistics, which should put us back in range at a page boundary.
if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
(velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) {
return super.createBallisticSimulation(position, velocity);
}
final Tolerance tolerance = this.tolerance;
final double target = _getTargetPixels(position, tolerance, velocity);
if (target != position.pixels) {
return ScrollSpringSimulation(spring, position.pixels, target, velocity,
tolerance: tolerance);
}
return null;
}
@override
bool get allowImplicitScrolling => false;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment