import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(const MaterialApp(home: HomePage()));
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('CustomScrollView 全方位知识点')),
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Center(
child: Container(
width: 120,
height: 120,
color: Colors.amber,
alignment: Alignment.center,
child: PageView.builder(
itemBuilder: (BuildContext context, int index) {
return Container(
color: Colors.red,
alignment: Alignment.center,
child: Text("第${index + 1} 个"),
);
},
itemCount: 10,
),
),
),
),
SliverPersistentHeader(delegate: _GridDelegate()),
],
),
);
}
}
class _GridDelegate extends SliverPersistentHeaderDelegate {
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return Container(
color: Colors.black,
height: 100,
child: ListView.builder(
itemCount: 30,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) => Container(
margin: index == 29
? EdgeInsets.fromLTRB(0, 0, 0, 0)
: EdgeInsets.fromLTRB(0, 0, 10, 0),
color: Colors.green,
height: 80,
width: 100,
alignment: Alignment.center,
child: Text(
"分类 $index",
style: TextStyle(color: Colors.white, fontSize: 25),
),
),
),
);
}
@override
// TODO: implement maxExtent
double get maxExtent => 200;
@override
// TODO: implement minExtent
double get minExtent => 60;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
// TODO: implement shouldRebuild
return false;
}
}
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(const MaterialApp(home: HomePage()));
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('CustomScrollView 全方位知识点')),
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Center(
child: Container(
width: 120,
height: 120,
color: Colors.amber,
alignment: Alignment.center,
child: PageView.builder(
itemBuilder: (BuildContext context, int index) {
return Container(
color: Colors.red,
alignment: Alignment.center,
child: Text("第${index + 1} 个"),
);
},
itemCount: 10,
),
),
),
),
SliverPersistentHeader(delegate: _GridDelegate()),
],
),
);
}
}
class _GridDelegate extends SliverPersistentHeaderDelegate {
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return Container(
color: Colors.black,
height: 100,
child: ListView.builder(
itemCount: 30,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) => Container(
margin: index == 29
? EdgeInsets.fromLTRB(0, 0, 0, 0)
: EdgeInsets.fromLTRB(0, 0, 10, 0),
color: Colors.green,
height: 80,
width: 100,
alignment: Alignment.center,
child: Text(
"分类 $index",
style: TextStyle(color: Colors.white, fontSize: 25),
),
),
),
);
}
@override
// TODO: implement maxExtent
double get maxExtent => 200;
@override
// TODO: implement minExtent
double get minExtent => 60;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
// TODO: implement shouldRebuild
return false;
}
}
import 'dart:async';
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(const MaterialApp(home: HomePage()));
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PageView 知识点')),
body: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 10),
_Section1_Basic(),
_Section2_Indicator(),
_Section3_AutoLoop(),
_Section4_Advanced(),
],
),
),
);
}
}
// ────────────────────────────────────────────────────────────────
// ① 基础 PageView
// ────────────────────────────────────────────────────────────────
class _Section1_Basic extends StatelessWidget {
const _Section1_Basic();
@override
Widget build(BuildContext context) {
return _Card(
title: '① 基础 PageView',
subtitle: '三种构造函数',
children: const [
Text(
'PageView({ children }) 一次性构建\n'
'PageView.builder 懒加载 ✅\n'
'PageView.custom 自定义 Sliver\n\n'
'// 默认水平滚动,每页占满屏幕\n'
'// 99% 场景用 PageView.builder',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ② 指示器点点
// ────────────────────────────────────────────────────────────────
class _Section2_Indicator extends StatefulWidget {
const _Section2_Indicator();
@override
State<_Section2_Indicator> createState() => _Section2_IndicatorState();
}
class _Section2_IndicatorState extends State<_Section2_Indicator> {
final _controller = PageController();
int _current = 0;
final _count = 5;
@override
void initState() {
super.initState();
_controller.addListener(() {
final next = _controller.page?.round() ?? 0;
if (next != _current && mounted) {
setState(() => _current = next);
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _Card(
title: '② 指示器点点',
subtitle: 'Stack 叠加,底部圆点根据当前页高亮',
children: [
SizedBox(
height: 140,
child: Stack(
children: [
// 底层:PageView
PageView.builder(
controller: _controller,
itemCount: _count,
itemBuilder: (_, i) => Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
color: i.isEven ? Colors.amber : Colors.blue,
alignment: Alignment.center,
child: Text('第 ${i + 1} 页',
style: const TextStyle(fontSize: 18, color: Colors.white)),
),
),
// 上层:底部圆点指示器
Positioned(
left: 0,
right: 0,
bottom: 8,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(_count, (i) {
final isActive = i == _current;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.symmetric(horizontal: 3),
width: isActive ? 18 : 6,
height: 6,
decoration: BoxDecoration(
color: isActive ? Colors.white : Colors.white54,
borderRadius: BorderRadius.circular(3),
),
);
}),
),
),
],
),
),
const SizedBox(height: 6),
const Text('滑动上方 PageView → 底部圆点会跟着变',
style: TextStyle(fontSize: 11, color: Colors.blue)),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.all(8),
color: Colors.amber.shade50,
child: const Text(
'// 指示器核心代码:\n\n'
'// 1. PageController 监听\n'
'controller.addListener(() {\n'
' final page = controller.page?.round() ?? 0;\n'
' setState(() => current = page);\n'
'});\n\n'
'// 2. Stack 叠加\n'
'Stack(\n'
' children: [\n'
' PageView(...),\n'
' Positioned(\n'
' bottom: 8,\n'
' child: Row( // 圆点行\n'
' children: List.generate(count, (i) {\n'
' final active = i == current;\n'
' return AnimatedContainer(\n'
' width: active ? 18 : 6, // 选中的变长\n'
' height: 6,\n'
' decoration: BoxDecoration(\n'
' color: active ? Colors.white : Colors.white54,\n'
' borderRadius: BorderRadius.circular(3),\n'
' ),\n'
' );\n'
' }),\n'
' ),\n'
' ),\n'
' ],\n'
')',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ③ 无限循环 + 自动轮播
// ────────────────────────────────────────────────────────────────
class _Section3_AutoLoop extends StatefulWidget {
const _Section3_AutoLoop();
@override
State<_Section3_AutoLoop> createState() => _Section3_AutoLoopState();
}
class _Section3_AutoLoopState extends State<_Section3_AutoLoop> {
// 真实页数
final _realCount = 5;
// 虚拟页数:用一个很大的数模拟无限
// 初始 page 设在中间,往前/往后都能翻很久
final _virtualCount = 10000;
late final PageController _controller;
int _current = 0;
Timer? _timer;
bool _isUserDragging = false;
@override
void initState() {
super.initState();
// 初始跳到中间附近,这样前后都有很多页可翻
_controller = PageController(initialPage: _virtualCount ~/ 2);
_current = _virtualCount ~/ 2;
_controller.addListener(_onPageChanged);
_startAutoPlay();
}
void _onPageChanged() {
final next = _controller.page?.round() ?? _current;
if (next != _current && mounted) {
setState(() => _current = next);
}
}
void _startAutoPlay() {
_timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 2), (_) {
if (!_isUserDragging && _controller.hasClients) {
_controller.nextPage(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
}
});
}
void _pauseAutoPlay() {
_isUserDragging = true;
_timer?.cancel();
}
void _resumeAutoPlay() {
_isUserDragging = false;
_startAutoPlay();
}
@override
void dispose() {
_timer?.cancel();
_controller
..removeListener(_onPageChanged)
..dispose();
super.dispose();
}
/// 虚拟 index → 真实 index
int get _realIndex => _current % _realCount;
@override
Widget build(BuildContext context) {
return _Card(
title: '③ 无限循环 + 自动轮播',
subtitle: '虚拟大页数模拟无限 + Timer.periodic 自动翻页',
children: [
SizedBox(
height: 160,
child: Stack(
children: [
// 底层:PageView(虚拟 10000 页)
NotificationListener<ScrollStartNotification>(
onNotification: (_) {
_pauseAutoPlay();
return false;
},
child: NotificationListener<ScrollEndNotification>(
onNotification: (_) {
_resumeAutoPlay();
return false;
},
child: PageView.builder(
controller: _controller,
itemCount: _virtualCount,
itemBuilder: (_, index) {
final realIndex = index % _realCount;
final colors = [
Colors.red,
Colors.amber,
Colors.blue,
Colors.green,
Colors.purple,
];
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
color: colors[realIndex],
alignment: Alignment.center,
child: Text('真实第 ${realIndex + 1} 页',
style: const TextStyle(
fontSize: 18, color: Colors.white)),
);
},
),
),
),
// 上层:圆点指示器(只显示真实页数)
Positioned(
left: 0,
right: 0,
bottom: 10,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(_realCount, (i) {
final isActive = i == _realIndex;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.symmetric(horizontal: 3),
width: isActive ? 18 : 6,
height: 6,
decoration: BoxDecoration(
color: isActive ? Colors.white : Colors.white54,
borderRadius: BorderRadius.circular(3),
),
);
}),
),
),
],
),
),
const SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('自动轮播中(2 秒/页)',
style: TextStyle(fontSize: 11, color: Colors.green)),
Text('当前:$_current (真实 $_realIndex)',
style: const TextStyle(fontSize: 10, fontFamily: 'monospace')),
],
),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.all(8),
color: Colors.amber.shade50,
child: const Text(
'// 无限循环核心思路:\n'
'// 不用真的循环,而是用虚拟大页数\n'
'// 初始跳到中间 → 前后都能翻很多页\n'
'\n'
'final realCount = 5;\n'
'final virtualCount = 10000; // 虚拟 10000 页\n'
'\n'
'PageController(initialPage: virtualCount ~/ 2);\n'
'\n'
'// itemBuilder 里取模映射到真实数据:\n'
'itemBuilder: (_, index) {\n'
' final realIndex = index % realCount;\n'
' return MyItem(data[realIndex]);\n'
'},\n'
'\n'
'// 自动轮播:\n'
'Timer.periodic(Duration(seconds: 2), (_) {\n'
' controller.nextPage(duration: 400ms, curve: easeInOut);\n'
'});\n\n'
'// 用户手动拖拽时暂停,松手后恢复\n'
'NotificationListener<ScrollStartNotification>\n'
'NotificationListener<ScrollEndNotification>',
style: TextStyle(fontSize: 10.5, fontFamily: 'monospace'),
),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
// ④ 进阶属性总结
// ────────────────────────────────────────────────────────────────
class _Section4_Advanced extends StatelessWidget {
const _Section4_Advanced();
@override
Widget build(BuildContext context) {
return _Card(
title: '④ PageView 进阶属性',
subtitle: 'viewportFraction / padEnds / pageSnapping',
children: const [
Text(
'// 视口比例(一页显示一部分,露出前后页)\n'
'PageView(\n'
' viewportFraction: 0.8, // 每页占 80% 宽度\n'
' ...\n'
')\n\n'
'// 两端是否自动填充空白(默认 true)\n'
'padEnds: true // 首页前末页后有空白\n'
'padEnds: false // 首页贴左末页贴右\n\n'
'// 是否按页吸附(默认 true)\n'
'pageSnapping: true // 停在整页位置\n'
'pageSnapping: false // 停在任意位置(像普通 ListView)\n\n'
'// 滚动方向\n'
'scrollDirection: Axis.horizontal // 默认\n'
'scrollDirection: Axis.vertical // 垂直翻页',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
SizedBox(height: 6),
Text('⚠️ dispose 必释放:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11)),
Text(
'@override\n'
'void dispose() {\n'
' timer?.cancel(); // Timer 必须 cancel\n'
' controller.dispose(); // PageController 必须 dispose\n'
' super.dispose();\n'
'}',
style: TextStyle(fontSize: 11, fontFamily: 'monospace'),
),
],
);
}
}
// ────────────────────────────────────────────────────────────────
class _Card extends StatelessWidget {
final String title;
final String subtitle;
final List<Widget> children;
const _Card({required this.title, this.subtitle = '', required this.children});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey.shade300),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
if (subtitle.isNotEmpty) ...[
const SizedBox(height: 2),
Text(subtitle,
style: TextStyle(fontSize: 11, color: Colors.grey.shade600, fontStyle: FontStyle.italic)),
],
const SizedBox(height: 10),
...children,
],
),
);
}
}