Skip to content

Instantly share code, notes, and snippets.

@JohanScheepers
Created February 12, 2025 12:03
Show Gist options
  • Save JohanScheepers/56b5694bd43781abbfcc3276563a7e85 to your computer and use it in GitHub Desktop.
Save JohanScheepers/56b5694bd43781abbfcc3276563a7e85 to your computer and use it in GitHub Desktop.
Page scroller - List Scroller
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
// Install flutter pub add scroll_to_index
void main() {
runApp(const MainApp());
}
// * Start from index 0
final currentPageIndex = ValueNotifier(0);
final rainbowColors = RainbowColors.generate();
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
// * Needed to enable PageView scrolling on desktop and web
scrollBehavior: AppScrollBehavior(),
home: SplitView(
leftBuilder: (_) => PageViewScreen(
allColors: rainbowColors,
currentPageIndexNotifier: currentPageIndex,
),
rightBuilder: (_) => ListViewScreen(
allColors: rainbowColors,
currentPageIndexNotifier: currentPageIndex,
),
),
);
}
}
class PageViewScreen extends StatefulWidget {
const PageViewScreen({
super.key,
required this.allColors,
required this.currentPageIndexNotifier,
});
final List<ColorInfo> allColors;
final ValueNotifier<int> currentPageIndexNotifier;
@override
State<PageViewScreen> createState() => _PageViewScreenState();
}
class _PageViewScreenState extends State<PageViewScreen> {
final _pageController = PageController();
@override
void initState() {
super.initState();
// * Add listener to sync PageView with index changes from ListView
widget.currentPageIndexNotifier.addListener(_onPageIndexChanged);
}
@override
void dispose() {
widget.currentPageIndexNotifier.removeListener(_onPageIndexChanged);
_pageController.dispose();
super.dispose();
}
void _onPageIndexChanged() {
if (_pageController.page?.round() !=
widget.currentPageIndexNotifier.value) {
_pageController.jumpToPage(widget.currentPageIndexNotifier.value);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: _pageController,
// * Update the notifier index when the page changes
onPageChanged: (index) => widget.currentPageIndexNotifier.value = index,
children: widget.allColors
.map((color) => ColoredBoxItem(colorInfo: color))
.toList(),
),
);
}
}
class ListViewScreen extends StatefulWidget {
const ListViewScreen({
super.key,
required this.allColors,
required this.currentPageIndexNotifier,
});
final List<ColorInfo> allColors;
final ValueNotifier<int> currentPageIndexNotifier;
@override
State<ListViewScreen> createState() => _ListViewScreenState();
}
class _ListViewScreenState extends State<ListViewScreen> {
final _scrollController = AutoScrollController(
suggestedRowHeight: 80, // approximate height of each list item
);
@override
void initState() {
super.initState();
// * Add listener to sync ListView with index changes from PageView
widget.currentPageIndexNotifier.addListener(_scrollToCurrentIndex);
}
@override
void dispose() {
widget.currentPageIndexNotifier.removeListener(_scrollToCurrentIndex);
_scrollController.dispose();
super.dispose();
}
void _scrollToCurrentIndex() {
final index = widget.currentPageIndexNotifier.value;
_scrollController.scrollToIndex(
index,
duration: const Duration(milliseconds: 300),
preferPosition: AutoScrollPosition.middle,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
controller: _scrollController,
itemCount: widget.allColors.length,
itemBuilder: (_, index) {
final colorInfo = widget.allColors[index];
return ValueListenableBuilder<int>(
valueListenable: widget.currentPageIndexNotifier,
builder: (_, currentPageIndex, __) => AutoScrollTag(
key: ValueKey(index),
controller: _scrollController,
index: index,
child: GestureDetector(
// * Update the notifier index when an item is selected
onTap: () => widget.currentPageIndexNotifier.value = index,
child: ColoredBoxItem(
colorInfo: colorInfo,
isSelected: index == currentPageIndex,
),
),
),
);
},
),
);
}
}
class AppScrollBehavior extends MaterialScrollBehavior {
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
PointerDeviceKind.trackpad,
};
}
class ColoredBoxItem extends StatelessWidget {
const ColoredBoxItem({
super.key,
required this.colorInfo,
this.isSelected = false,
});
final ColorInfo colorInfo;
final bool isSelected;
@override
Widget build(BuildContext context) {
// Use the color luminance to determine the border and text color for better contrast
final isDark = colorInfo.color.computeLuminance() < 0.3;
final borderColor = isDark ? Colors.white : Colors.black;
// * When selected, the border is visible
return DecoratedBox(
decoration: BoxDecoration(
color: colorInfo.color,
border: Border.all(
color: isSelected ? borderColor : Colors.transparent,
width: 3,
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
colorInfo.shade.toString(),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 32,
color: isDark ? Colors.white : Colors.black87,
),
),
),
),
);
}
}
class RainbowColors {
static const colors = [
Colors.indigo,
Colors.lightBlue,
Colors.lightGreen,
Colors.lime,
Colors.orange,
Colors.pink,
Colors.purple,
];
static List<ColorInfo> generate() {
final allColors = <ColorInfo>{};
for (var color in colors) {
for (var key in color.keys) {
allColors.add((color: color[key]!, shade: key));
}
}
return allColors.toList();
}
}
class SplitView extends StatelessWidget {
const SplitView({
super.key,
required this.leftBuilder,
required this.rightBuilder,
});
final WidgetBuilder leftBuilder;
final WidgetBuilder rightBuilder;
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
flex: 2,
child: leftBuilder(context),
),
Expanded(
flex: 1,
child: rightBuilder(context),
),
],
);
}
}
typedef ColorInfo = ({Color color, int shade});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment