Skip to content

Instantly share code, notes, and snippets.

@slightfoot
Created January 10, 2024 21:35
Show Gist options
  • Save slightfoot/dcf820868494c4daa23f1147764e49ac to your computer and use it in GitHub Desktop.
Save slightfoot/dcf820868494c4daa23f1147764e49ac to your computer and use it in GitHub Desktop.
Section Scrolling - by Simon Lightfoot - Humpday Q&A :: 10th January 2024 #Flutter #Dart https://www.youtube.com/watch?v=4YQG41-cLu4
// MIT License
//
// Copyright (c) 2023 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
/// Madhan Kumar 06:05 PM
/// Q: I have 5 chip Widgets that represents 5 different section
/// header and below that I have that section body then how to
/// change colour of the chip based on current section is visible
/// by scrolling?
void main() {
runApp(const MaterialApp(
debugShowCheckedModeBanner: false,
home: Home(),
));
}
class Home extends StatefulWidget {
const Home({super.key});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
return Material(
child: SafeArea(
bottom: false,
child: SectionHost(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const AllSectionsHeader(),
Expanded(
child: Builder(
builder: (BuildContext context) {
final state =
context.findAncestorStateOfType<_SectionHostState>()!;
return ListView.builder(
controller: state._scrollController,
itemCount: state.sections.length,
itemBuilder: (BuildContext context, int index) {
final section = state.sections[index];
return ContentSection(
key: Key('section-$section'),
section: section,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(state.sections[index]),
const SizedBox(
width: double.infinity,
height: 700.0,
child: Placeholder(),
),
],
),
),
);
},
);
},
),
),
],
),
),
),
);
}
}
class SectionHost extends StatefulWidget {
const SectionHost({
super.key,
required this.child,
});
final Widget child;
@override
State<SectionHost> createState() => _SectionHostState();
}
class _SectionHostState extends State<SectionHost> {
final _scrollController = ScrollController();
final sections = <String>[
'Section 1',
'Section 2',
'Section 3',
'Section 4',
'Section 5',
];
late final _active = ValueNotifier<String>(sections[0]);
final _activeSectionElements = <_ContentSectionElement>[];
String? _lookingForSection;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScrollChanged);
}
void _onScrollChanged() {
if (!_scrollController.hasClients) {
return;
}
for (final element in _activeSectionElements) {
if (element.containsOffset(Offset.zero)) {
_active.value = element.widget.section;
break;
}
}
if (_lookingForSection != null && _active.value == _lookingForSection) {
final position = _scrollController //
.position as ScrollPositionWithSingleContext;
position.goIdle();
_lookingForSection = null;
}
}
void gotoSection(String section) {
if (!_scrollController.hasClients) {
return;
}
if (_active.value == section) {
return;
}
final src = sections.indexOf(_active.value);
final dst = sections.indexOf(section);
final position = _scrollController //
.position as ScrollPositionWithSingleContext;
_lookingForSection = section;
position.beginActivity(BallisticScrollActivity(
position,
GravitySimulation(
src > dst ? -500.0 : 500.0,
position.pixels,
src > dst ? position.minScrollExtent : position.maxScrollExtent,
src > dst ? -500.0 : 500.0,
),
position.context.vsync,
true,
));
}
void addSectionElement(_ContentSectionElement element) {
_activeSectionElements.add(element);
print('added ${element.widget.section}');
}
void removeSectionElement(_ContentSectionElement element) {
print('removed ${element.widget.section}');
_activeSectionElements.remove(element);
}
@override
void dispose() {
_scrollController.removeListener(_onScrollChanged);
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}
class AllSectionsHeader extends StatelessWidget {
const AllSectionsHeader({super.key});
@override
Widget build(BuildContext context) {
final state = context.findAncestorStateOfType<_SectionHostState>()!;
return ValueListenableBuilder(
valueListenable: state._active,
builder: (BuildContext context, String active, Widget? child) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Wrap(
alignment: WrapAlignment.center,
spacing: 8.0,
runSpacing: 8.0,
children: [
for (final section in state.sections) //
Material(
color: section == active ? Colors.blueAccent : Colors.white,
shape: const StadiumBorder(
side: BorderSide(color: Colors.black, width: 1.0),
),
child: InkWell(
onTap: () => state.gotoSection(section),
customBorder: const StadiumBorder(),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
child: Text(section),
),
),
),
],
),
);
},
);
}
}
class ContentSection extends ProxyWidget {
const ContentSection({
super.key,
required this.section,
required super.child,
});
final String section;
@override
Element createElement() => _ContentSectionElement(this);
}
class _ContentSectionElement extends ProxyElement {
_ContentSectionElement(ContentSection super.widget);
@override
ContentSection get widget => super.widget as ContentSection;
_SectionHostState? _sectionHostState;
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
_sectionHostState = findAncestorStateOfType<_SectionHostState>();
_sectionHostState!.addSectionElement(this);
}
@override
void unmount() {
_sectionHostState!.removeSectionElement(this);
_sectionHostState = null;
super.unmount();
}
bool containsOffset(Offset offset) {
final scrollBox =
Scrollable.of(this).context.findRenderObject() as RenderBox;
final box = renderObject as RenderBox;
final rect = box.localToGlobal(Offset.zero, ancestor: scrollBox) & box.size;
return rect.contains(offset);
}
void showOnScreen(EdgeInsets scrollPadding) {
final box = renderObject as RenderBox;
box.showOnScreen(
rect: scrollPadding.inflateRect(Offset.zero & box.size),
duration: const Duration(milliseconds: 150),
);
}
@override
void notifyClients(ProxyWidget oldWidget) {
//
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment