Skip to content

Instantly share code, notes, and snippets.

@gzlock
Last active March 15, 2021 09:24
Show Gist options
  • Save gzlock/5ca8735d43d8183419a0f9efae0af192 to your computer and use it in GitHub Desktop.
Save gzlock/5ca8735d43d8183419a0f9efae0af192 to your computer and use it in GitHub Desktop.
Flutter Stacked Item ListView
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
backgroundColor: Colors.black,
// body: MyList()
body: MyList(),
),
);
}
}
class MyList extends StatefulWidget {
@override
State<StatefulWidget> createState() => _MyList();
}
class _MyList extends State<MyList> {
AnimationController animationController;
final ScrollController controller = ScrollController();
final List<dynamic> data = FOOD_DATA;
final double itemHeight = 400, verticalMarin = 10;
final double opacityLimit = 0.7;
Map<dynamic, dynamic> output = {};
List<Widget> children;
@override
void initState() {
controller.addListener(() {
setState(() {});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Stack(children: [
ListView.builder(
itemExtent: itemHeight,
cacheExtent: 1000000.0,
padding: EdgeInsets.only(left: 20, right: 20, top: 50),
controller: controller,
itemCount: data.length,
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
double scroll =
((controller.offset - index * itemHeight) / itemHeight);
double currentScroll =
((controller.offset - index * itemHeight) / itemHeight)
.clamp(0.0, 1.0);
output['$index'] = 'scroll $scroll $currentScroll';
double opacity = 1.0;
double y = 0;
bool atTop = false;
if (currentScroll > 0 || (index == 0 && controller.offset <= 0)) {
/// 当前的item是否在最顶部
if (currentScroll < 1) {
atTop = true;
}
/// 计算透明度
if (currentScroll > opacityLimit)
opacity = lerpDouble(0.0, 1.0,
((1 - currentScroll) / (1 - opacityLimit)).clamp(0.0, 1.0));
/// 计算偏移
y = currentScroll * itemHeight;
} else {
y = scroll * 30;
}
opacity = opacity.clamp(0.0, 1.0);
output['$index'] += '\n opacity ${1 - currentScroll} $opacity';
return ItemWidget(
atTop: atTop,
index: index,
height: itemHeight,
verticalPadding: 10,
post: data[index],
offsetY: y,
opacity: opacity,
);
}),
// Positioned(
// bottom: 0,
// right: 0,
// child: Container(
// color: Colors.grey[200],
// padding: EdgeInsets.all(6),
// child: outputList(),
// ),
// ),
]);
}
Widget outputList() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: ListTile.divideTiles(
context: context,
tiles: output.keys.map((e) => Text('$e: ${output[e]}')),
color: Colors.black,
).toList(),
);
}
}
class ItemWidget extends StatelessWidget {
final int index;
final bool atTop;
final double height, verticalPadding, offsetY, rotateX, opacity, heightFactor;
final Map post;
const ItemWidget({
Key key,
this.atTop,
this.index,
this.height,
this.verticalPadding,
this.offsetY,
this.rotateX,
this.heightFactor,
this.opacity,
this.post,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Opacity(
opacity: opacity,
child: Transform(
// offset: Offset(0, offsetY),
alignment: Alignment.topCenter,
transform: Matrix4.identity()..translate(0.0, offsetY),
child: item(post),
),
);
}
Widget item(Map post) {
return Container(
height: height,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(20.0)),
color: Colors.white,
boxShadow: [
BoxShadow(color: Colors.black.withAlpha(100), blurRadius: 10),
],
image: DecorationImage(
image: AssetImage('assets/images/${post["image"]}'),
alignment: Alignment.centerRight,
scale: 0.1,
fit: BoxFit.cover,
)),
child: Stack(
children: [
Padding(
padding: EdgeInsets.symmetric(
horizontal: 20,
vertical: verticalPadding,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'$index ${atTop ? 'At Top' : ''}',
style: const TextStyle(
fontSize: 28, fontWeight: FontWeight.bold),
),
Text(
'y: $offsetY\nopacity: $opacity',
style: const TextStyle(fontSize: 17),
),
],
),
),
Align(
alignment: Alignment.topLeft,
child: RuleWidget(split: height),
),
],
),
);
}
}
class RuleWidget extends StatelessWidget {
final double split;
const RuleWidget({Key key, this.split}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
width: 10,
child: Column(
children: List.generate(
split ~/ 10,
(index) => Divider(height: 10, color: Colors.red),
),
),
);
}
}
const FOOD_DATA = [
{"name": "Burger", "brand": "Hawkers", "price": 2.99, "image": "burger.png"},
{
"name": "Cheese Dip",
"brand": "Hawkers",
"price": 4.99,
"image": "cheese_dip.png"
},
{"name": "Cola", "brand": "Mcdonald", "price": 1.49, "image": "cola.png"},
{"name": "Fries", "brand": "Mcdonald", "price": 2.99, "image": "fries.png"},
{
"name": "Ice Cream",
"brand": "Ben & Jerry's",
"price": 9.49,
"image": "ice_cream.png"
},
{
"name": "Noodles",
"brand": "Hawkers",
"price": 4.49,
"image": "noodles.png"
},
{"name": "Pizza", "brand": "Dominos", "price": 17.99, "image": "pizza.png"},
{
"name": "Sandwich",
"brand": "Hawkers",
"price": 2.99,
"image": "sandwich.png"
},
{"name": "Wrap", "brand": "Subway", "price": 6.99, "image": "wrap.png"}
];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment