Created
February 12, 2025 12:03
-
-
Save JohanScheepers/56b5694bd43781abbfcc3276563a7e85 to your computer and use it in GitHub Desktop.
Page scroller - List Scroller
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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