在垂直 PageView 中包裹不同高度的项目 - Flutter
Wrap item different height in vertical PageView - Flutter
我有一个垂直的 PageView
具有不同的项目高度 :
但我想根据高度包裹每件物品。
这里我要的最终结果:
我们该怎么做?
首先,您应该使用 SafeArea
以防止您的小部件进入缺口。参见[这个][1]。
那么您应该使用 ListView
而不是 PageView
,因为 PageView
创建的页面大小相同。在 ListView
中创建一个 int
的数组来存储小部件的高度并使用它来创建具有不同大小的小部件。
List<int> heights = [100, 120, 10];// and so on
\then use it as follow:
ListView.builder(
itemCount: 6,
itemBuilder: (context, i){
return Container(
height:heights[i],
width: 200, // or any value you want
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: YourWidget);
},
),
[1]:
PageView
不支持动态垂直子项。使用 ListView
和 Snap 机制。
添加 this 库以启用捕捉:
flutter pub add snap_scroll_physics
想法是为每个元素添加一个 Snap-point 到 ListView:
ListView(
physics: SnapScrollPhysics(snaps: [
Snap(200, 50),
]),
...
完整的工作代码:
import 'package:flutter/material.dart';
import 'package:snap_scroll_physics/snap_scroll_physics.dart';
class DynamicVerticalPageView extends StatefulWidget {
const DynamicVerticalPageView({Key? key}) : super(key: key);
@override
State<DynamicVerticalPageView> createState() =>
_DynamicVerticalPageViewState();
}
class _DynamicVerticalPageViewState extends State<DynamicVerticalPageView> {
late double screenHeight;
final double cardPadding = 10.0;
late ScrollController _scrollController;
List<double> testHeightList = [170, 300, 400, 500, 300, 100, 400, 1000];
List<double> elementPixelPosition =
[]; //stores the pixel position of elements
@override
void initState() {
super.initState();
_scrollController = ScrollController(
initialScrollOffset: (testHeightList[0] / 2) + cardPadding);
}
List<Snap> getSnapList() {
List<Snap> returnList = [];
double lastHeight = 0;
for (double height in testHeightList) {
double totalHeightOfCard = height + cardPadding * 2;
// If the scroll offset is expected to stop on the card it will snap to the middle
returnList.add(Snap(lastHeight + totalHeightOfCard / 2,
distance: totalHeightOfCard / 2));
elementPixelPosition.add(lastHeight + totalHeightOfCard / 2);
lastHeight += totalHeightOfCard;
}
return returnList;
}
@override
Widget build(BuildContext context) {
screenHeight = MediaQuery.of(context).size.height;
return Stack(
children: [
NotificationListener(
onNotification: (t) {
if (t is ScrollEndNotification) {
print(
"At Card: ${elementPixelPosition.indexWhere((element) => element == _scrollController.position.pixels)}");
}
return false;
},
child: ListView(
padding: EdgeInsets.zero,
controller: _scrollController,
physics: SnapScrollPhysics(snaps: getSnapList()),
//here all the snaps will be applied
children: [
Container(
// container to start in the middle
height: screenHeight / 2,
),
for (int i = 0; i < testHeightList.length; i++)
Padding(
padding: EdgeInsets.all(cardPadding), // padding for card
child: Container(
height: testHeightList[i], //height of the card
alignment: Alignment.center,
child: Card(
margin: EdgeInsets.zero,
elevation: 6,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)),
child: Center(
child: Text(
"Card $i",
),
),
),
),
),
],
),
),
//a red line, just so you know where the middle is
Center(
child: Container(
color: Colors.red,
height: 5,
width: MediaQuery.of(context).size.width,
),
),
],
);
}
}
随意更改捕捉机制,如您所愿(例如使用更平滑的过渡或捕捉点 -> 例如附加 Snap.avoidZone
)
使用垂直页面构建器和堆栈中的内容。这样您就可以在拥有自己的布局的同时利用滚动动画。
这是我写的插件。随意使用它并将其变成一个 pub.dev 插件。
用法:
import 'package:flutter/material.dart';
import 'VerticalPageViewer.dart';
class StackTest extends StatefulWidget {
const StackTest({Key? key}) : super(key: key);
@override
State<StackTest> createState() => _StackTestState();
}
class _StackTestState extends State<StackTest> {
final List<Widget> images = [
Container(
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
Container(
decoration: BoxDecoration(
color: Colors.cyan,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
Container(
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
];
List<double> testHeight = [100, 300, 500];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'Dynamic Height PageView',
style: TextStyle(color: Colors.white),
),
centerTitle: true,
),
body: SafeArea(
child: Container(
child: DynamicHeightPageView(
heightList: testHeight,
children: images,
onSelectedItem: (index) {
print("index: $index");
},
),
),
),
);
}
}
DynamicHeightPageView class:
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:collection/collection.dart';
typedef PageChangedCallback = void Function(double? page);
typedef PageSelectedCallback = void Function(int index);
class DynamicHeightPageView extends StatefulWidget {
final List<double> heightList;
final List<Widget> children;
final double cardWidth;
final ScrollPhysics? physics;
final PageChangedCallback? onPageChanged;
final PageSelectedCallback? onSelectedItem;
final int initialPage;
DynamicHeightPageView({
required this.heightList,
required this.children,
this.physics,
this.cardWidth = 300,
this.onPageChanged,
this.initialPage = 0,
this.onSelectedItem,
}) : assert(heightList.length == children.length);
@override
_DynamicHeightPageViewState createState() => _DynamicHeightPageViewState();
}
class _DynamicHeightPageViewState extends State<DynamicHeightPageView> {
double? currentPosition;
PageController? controller;
@override
void initState() {
super.initState();
currentPosition = widget.initialPage.toDouble();
controller = PageController(initialPage: widget.initialPage);
controller!.addListener(() {
setState(() {
currentPosition = controller!.page;
if (widget.onPageChanged != null) {
Future(() => widget.onPageChanged!(currentPosition));
}
if (widget.onSelectedItem != null && (currentPosition! % 1) == 0) {
Future(() => widget.onSelectedItem!(currentPosition!.toInt()));
}
});
});
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return GestureDetector(
onTap: () {
print("Current Element index tab: ${currentPosition!.round()}");
},
child: Stack(
children: [
CardController(
cardWidth: widget.cardWidth,
heightList: widget.heightList,
children: widget.children,
currentPosition: currentPosition,
cardViewPagerHeight: constraints.maxHeight,
cardViewPagerWidth: constraints.maxWidth,
),
Positioned.fill(
child: PageView.builder(
physics: widget.physics,
scrollDirection: Axis.vertical,
itemCount: widget.children.length,
controller: controller,
itemBuilder: (context, index) {
return Container();
},
),
)
],
),
);
});
}
}
class CardController extends StatelessWidget {
final double? currentPosition;
final List<double> heightList;
final double cardWidth;
final double cardViewPagerHeight;
final double? cardViewPagerWidth;
final List<Widget>? children;
CardController({
this.children,
this.cardViewPagerWidth,
required this.cardWidth,
required this.cardViewPagerHeight,
required this.heightList,
this.currentPosition,
});
@override
Widget build(BuildContext context) {
List<Widget> cardList = [];
for (int i = 0; i < children!.length; i++) {
var cardHeight = heightList[i];
var cardTop = getTop(cardHeight, cardViewPagerHeight, i, heightList);
var cardLeft = (cardViewPagerWidth! / 2) - (cardWidth / 2);
Widget card = Positioned(
top: cardTop,
left: cardLeft,
child: Container(
width: cardWidth,
height: cardHeight,
child: children![i],
),
);
cardList.add(card);
}
return Stack(
children: cardList,
);
}
double getTop(
double cardHeight, double viewHeight, int i, List<double> heightList) {
double diff = (currentPosition! - i);
double diffAbs = diff.abs();
double basePosition = (viewHeight / 2) - (cardHeight / 2);
if (diffAbs == 0) {
//element in focus
return basePosition;
}
int intCurrentPosition = currentPosition!.toInt();
double doubleCurrentPosition = currentPosition! - intCurrentPosition;
//calculate distance between to-pull elements
late double pullHeight;
if (heightList.length > intCurrentPosition + 1) {
//check for end of list
pullHeight = heightList[intCurrentPosition] / 2 +
heightList[intCurrentPosition + 1] / 2;
} else {
pullHeight = heightList[intCurrentPosition] / 2;
}
if (diff >= 0) {
//before focus element
double afterListSum = heightList.getRange(i, intCurrentPosition + 1).sum;
return (viewHeight / 2) -
afterListSum +
heightList[intCurrentPosition] / 2 -
pullHeight * doubleCurrentPosition;
} else {
//after focus element
var beforeListSum = heightList.getRange(intCurrentPosition, i).sum;
return (viewHeight / 2) +
beforeListSum -
heightList[intCurrentPosition] / 2 -
pullHeight * doubleCurrentPosition;
}
}
}
我有一个垂直的 PageView
具有不同的项目高度 :
但我想根据高度包裹每件物品。
这里我要的最终结果:
我们该怎么做?
首先,您应该使用 SafeArea
以防止您的小部件进入缺口。参见[这个][1]。
那么您应该使用 ListView
而不是 PageView
,因为 PageView
创建的页面大小相同。在 ListView
中创建一个 int
的数组来存储小部件的高度并使用它来创建具有不同大小的小部件。
List<int> heights = [100, 120, 10];// and so on
\then use it as follow:
ListView.builder(
itemCount: 6,
itemBuilder: (context, i){
return Container(
height:heights[i],
width: 200, // or any value you want
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: YourWidget);
},
),
[1]:
PageView
不支持动态垂直子项。使用 ListView
和 Snap 机制。
添加 this 库以启用捕捉:
flutter pub add snap_scroll_physics
想法是为每个元素添加一个 Snap-point 到 ListView:
ListView(
physics: SnapScrollPhysics(snaps: [
Snap(200, 50),
]),
...
完整的工作代码:
import 'package:flutter/material.dart';
import 'package:snap_scroll_physics/snap_scroll_physics.dart';
class DynamicVerticalPageView extends StatefulWidget {
const DynamicVerticalPageView({Key? key}) : super(key: key);
@override
State<DynamicVerticalPageView> createState() =>
_DynamicVerticalPageViewState();
}
class _DynamicVerticalPageViewState extends State<DynamicVerticalPageView> {
late double screenHeight;
final double cardPadding = 10.0;
late ScrollController _scrollController;
List<double> testHeightList = [170, 300, 400, 500, 300, 100, 400, 1000];
List<double> elementPixelPosition =
[]; //stores the pixel position of elements
@override
void initState() {
super.initState();
_scrollController = ScrollController(
initialScrollOffset: (testHeightList[0] / 2) + cardPadding);
}
List<Snap> getSnapList() {
List<Snap> returnList = [];
double lastHeight = 0;
for (double height in testHeightList) {
double totalHeightOfCard = height + cardPadding * 2;
// If the scroll offset is expected to stop on the card it will snap to the middle
returnList.add(Snap(lastHeight + totalHeightOfCard / 2,
distance: totalHeightOfCard / 2));
elementPixelPosition.add(lastHeight + totalHeightOfCard / 2);
lastHeight += totalHeightOfCard;
}
return returnList;
}
@override
Widget build(BuildContext context) {
screenHeight = MediaQuery.of(context).size.height;
return Stack(
children: [
NotificationListener(
onNotification: (t) {
if (t is ScrollEndNotification) {
print(
"At Card: ${elementPixelPosition.indexWhere((element) => element == _scrollController.position.pixels)}");
}
return false;
},
child: ListView(
padding: EdgeInsets.zero,
controller: _scrollController,
physics: SnapScrollPhysics(snaps: getSnapList()),
//here all the snaps will be applied
children: [
Container(
// container to start in the middle
height: screenHeight / 2,
),
for (int i = 0; i < testHeightList.length; i++)
Padding(
padding: EdgeInsets.all(cardPadding), // padding for card
child: Container(
height: testHeightList[i], //height of the card
alignment: Alignment.center,
child: Card(
margin: EdgeInsets.zero,
elevation: 6,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)),
child: Center(
child: Text(
"Card $i",
),
),
),
),
),
],
),
),
//a red line, just so you know where the middle is
Center(
child: Container(
color: Colors.red,
height: 5,
width: MediaQuery.of(context).size.width,
),
),
],
);
}
}
随意更改捕捉机制,如您所愿(例如使用更平滑的过渡或捕捉点 -> 例如附加 Snap.avoidZone
)
使用垂直页面构建器和堆栈中的内容。这样您就可以在拥有自己的布局的同时利用滚动动画。
这是我写的插件。随意使用它并将其变成一个 pub.dev 插件。
用法:
import 'package:flutter/material.dart';
import 'VerticalPageViewer.dart';
class StackTest extends StatefulWidget {
const StackTest({Key? key}) : super(key: key);
@override
State<StackTest> createState() => _StackTestState();
}
class _StackTestState extends State<StackTest> {
final List<Widget> images = [
Container(
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
Container(
decoration: BoxDecoration(
color: Colors.cyan,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
Container(
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
];
List<double> testHeight = [100, 300, 500];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'Dynamic Height PageView',
style: TextStyle(color: Colors.white),
),
centerTitle: true,
),
body: SafeArea(
child: Container(
child: DynamicHeightPageView(
heightList: testHeight,
children: images,
onSelectedItem: (index) {
print("index: $index");
},
),
),
),
);
}
}
DynamicHeightPageView class:
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:collection/collection.dart';
typedef PageChangedCallback = void Function(double? page);
typedef PageSelectedCallback = void Function(int index);
class DynamicHeightPageView extends StatefulWidget {
final List<double> heightList;
final List<Widget> children;
final double cardWidth;
final ScrollPhysics? physics;
final PageChangedCallback? onPageChanged;
final PageSelectedCallback? onSelectedItem;
final int initialPage;
DynamicHeightPageView({
required this.heightList,
required this.children,
this.physics,
this.cardWidth = 300,
this.onPageChanged,
this.initialPage = 0,
this.onSelectedItem,
}) : assert(heightList.length == children.length);
@override
_DynamicHeightPageViewState createState() => _DynamicHeightPageViewState();
}
class _DynamicHeightPageViewState extends State<DynamicHeightPageView> {
double? currentPosition;
PageController? controller;
@override
void initState() {
super.initState();
currentPosition = widget.initialPage.toDouble();
controller = PageController(initialPage: widget.initialPage);
controller!.addListener(() {
setState(() {
currentPosition = controller!.page;
if (widget.onPageChanged != null) {
Future(() => widget.onPageChanged!(currentPosition));
}
if (widget.onSelectedItem != null && (currentPosition! % 1) == 0) {
Future(() => widget.onSelectedItem!(currentPosition!.toInt()));
}
});
});
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return GestureDetector(
onTap: () {
print("Current Element index tab: ${currentPosition!.round()}");
},
child: Stack(
children: [
CardController(
cardWidth: widget.cardWidth,
heightList: widget.heightList,
children: widget.children,
currentPosition: currentPosition,
cardViewPagerHeight: constraints.maxHeight,
cardViewPagerWidth: constraints.maxWidth,
),
Positioned.fill(
child: PageView.builder(
physics: widget.physics,
scrollDirection: Axis.vertical,
itemCount: widget.children.length,
controller: controller,
itemBuilder: (context, index) {
return Container();
},
),
)
],
),
);
});
}
}
class CardController extends StatelessWidget {
final double? currentPosition;
final List<double> heightList;
final double cardWidth;
final double cardViewPagerHeight;
final double? cardViewPagerWidth;
final List<Widget>? children;
CardController({
this.children,
this.cardViewPagerWidth,
required this.cardWidth,
required this.cardViewPagerHeight,
required this.heightList,
this.currentPosition,
});
@override
Widget build(BuildContext context) {
List<Widget> cardList = [];
for (int i = 0; i < children!.length; i++) {
var cardHeight = heightList[i];
var cardTop = getTop(cardHeight, cardViewPagerHeight, i, heightList);
var cardLeft = (cardViewPagerWidth! / 2) - (cardWidth / 2);
Widget card = Positioned(
top: cardTop,
left: cardLeft,
child: Container(
width: cardWidth,
height: cardHeight,
child: children![i],
),
);
cardList.add(card);
}
return Stack(
children: cardList,
);
}
double getTop(
double cardHeight, double viewHeight, int i, List<double> heightList) {
double diff = (currentPosition! - i);
double diffAbs = diff.abs();
double basePosition = (viewHeight / 2) - (cardHeight / 2);
if (diffAbs == 0) {
//element in focus
return basePosition;
}
int intCurrentPosition = currentPosition!.toInt();
double doubleCurrentPosition = currentPosition! - intCurrentPosition;
//calculate distance between to-pull elements
late double pullHeight;
if (heightList.length > intCurrentPosition + 1) {
//check for end of list
pullHeight = heightList[intCurrentPosition] / 2 +
heightList[intCurrentPosition + 1] / 2;
} else {
pullHeight = heightList[intCurrentPosition] / 2;
}
if (diff >= 0) {
//before focus element
double afterListSum = heightList.getRange(i, intCurrentPosition + 1).sum;
return (viewHeight / 2) -
afterListSum +
heightList[intCurrentPosition] / 2 -
pullHeight * doubleCurrentPosition;
} else {
//after focus element
var beforeListSum = heightList.getRange(intCurrentPosition, i).sum;
return (viewHeight / 2) +
beforeListSum -
heightList[intCurrentPosition] / 2 -
pullHeight * doubleCurrentPosition;
}
}
}