flutter_magnetic_scroll 0.1.0
flutter_magnetic_scroll: ^0.1.0 copied to clipboard
A Flutter custom ScrollPhysics package for TikTok/Reels style feeds and pickers. Features magnetic snapping to fixed or variable item boundaries with physics-based smoothness.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_magnetic_scroll/flutter_magnetic_scroll.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Magnetic Scroll Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MagneticScrollExample(),
);
}
}
class MagneticScrollExample extends StatefulWidget {
const MagneticScrollExample({super.key});
@override
State<MagneticScrollExample> createState() => _MagneticScrollExampleState();
}
class _MagneticScrollExampleState extends State<MagneticScrollExample> {
late MagneticScrollController _controller;
int _currentIndex = 0;
// We'll showcase variable sizes. Let's create a list of varying sizes.
late List<double> _itemSizes;
@override
void initState() {
super.initState();
// Initialize dummy sizes: some 200, some 300, some 400
_itemSizes = List.generate(20, (index) {
if (index % 3 == 0) return 200.0;
if (index % 3 == 1) return 300.0;
return 400.0;
});
_controller = MagneticScrollController(itemSizes: _itemSizes);
// Listen to changes for UI updates (e.g. playing the video in focus)
_controller.currentIndex.addListener(() {
setState(() {
_currentIndex = _controller.currentIndex.value;
});
debugPrint('Item snapped into focus: $_currentIndex');
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Magnetic Scroll Features'),
actions: [
IconButton(
icon: const Icon(Icons.arrow_downward),
onPressed: () {
// Programmatic navigation demonstration
if (_currentIndex < _itemSizes.length - 1) {
_controller.animateToItem(
index: _currentIndex + 1,
duration: const Duration(milliseconds: 400),
);
}
},
),
IconButton(
icon: const Icon(Icons.arrow_upward),
onPressed: () {
if (_currentIndex > 0) {
_controller.animateToItem(
index: _currentIndex - 1,
duration: const Duration(milliseconds: 400),
);
}
},
),
],
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Current Focused Index: $_currentIndex',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
Expanded(
child: ListView.builder(
controller: _controller,
// Use our custom scroll physics here!
physics: MagneticScrollPhysics(
itemSizes: _itemSizes,
strictSingleItemSnapping: true, // Only snap one item per swipe
onItemFocused: (index) {
// This is another way to listen to focus events directly from physics!
// It fires immediately when the physics starts the snap animation.
debugPrint('Physics targeted index: $index');
},
),
itemCount: _itemSizes.length,
itemBuilder: (context, index) {
// The actual scroll extent for this item is exactly `height`
final height = _itemSizes[index];
final isFocused = _currentIndex == index;
final verticalMargin = 8.0;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
// We subtract the vertical margins from the container height
// so that the total scroll extent exactly matches `height`
height: height - (verticalMargin * 2),
margin: EdgeInsets.symmetric(
vertical: verticalMargin,
horizontal: 16,
),
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isFocused ? Colors.white : Colors.transparent,
width: 4,
),
boxShadow: isFocused
? [
const BoxShadow(
color: Colors.black26,
blurRadius: 10,
),
]
: [],
),
child: Center(
child: Text(
'Item $index \nTotal Extent: $height',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
);
},
),
),
],
),
);
}
}