flutter_magnetic_scroll 0.3.0 copy "flutter_magnetic_scroll: ^0.3.0" to clipboard
flutter_magnetic_scroll: ^0.3.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> {
  int _tabIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Magnetic Scroll Features')),
      body: IndexedStack(
        index: _tabIndex,
        children: [
          const VerticalFeedExample(),
          const HorizontalCarouselExample(),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _tabIndex,
        onTap: (index) => setState(() => _tabIndex = index),
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.list),
            label: 'Vertical Feed',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.view_carousel),
            label: 'Horizontal Carousel',
          ),
        ],
      ),
    );
  }
}

class VerticalFeedExample extends StatefulWidget {
  const VerticalFeedExample({super.key});

  @override
  State<VerticalFeedExample> createState() => _VerticalFeedExampleState();
}

class _VerticalFeedExampleState extends State<VerticalFeedExample> {
  late MagneticScrollController _controller;
  int _currentIndex = 0;
  late List<double> _itemSizes;

  @override
  void initState() {
    super.initState();
    _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);

    _controller.currentIndex.addListener(() {
      setState(() {
        _currentIndex = _controller.currentIndex.value;
      });
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return 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: MagneticScrollView(
            controller: _controller,
            itemSizes: _itemSizes,
            itemCount: _itemSizes.length,
            maxFlingItems: 4,
            onItemFocused: (index) {
              debugPrint('Physics targeted index: $index');
            },
            itemBuilder: (context, index) {
              final height = _itemSizes[index];
              final isFocused = _currentIndex == index;
              const verticalMargin = 8.0;

              return AnimatedContainer(
                duration: const Duration(milliseconds: 300),
                height: height - (verticalMargin * 2),
                margin: const 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,
                    ),
                  ),
                ),
              );
            },
          ),
        ),
      ],
    );
  }
}

class HorizontalCarouselExample extends StatefulWidget {
  const HorizontalCarouselExample({super.key});

  @override
  State<HorizontalCarouselExample> createState() =>
      _HorizontalCarouselExampleState();
}

class _HorizontalCarouselExampleState extends State<HorizontalCarouselExample> {
  late MagneticScrollController _controller;
  int _currentIndex = 0;

  final double _itemWidth = 250.0;

  @override
  void initState() {
    super.initState();
    // Use the MagneticScrollController to easily track the center-aligned item
    _controller = MagneticScrollController(
      itemSize: _itemWidth,
      alignment: MagneticSnapAlignment.center,
    );

    _controller.currentIndex.addListener(() {
      setState(() {
        _currentIndex = _controller.currentIndex.value;
      });
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text(
            'Center Aligned Item: $_currentIndex',
            style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
        ),
        const SizedBox(height: 20),
        SizedBox(
          height: 300,
          child: MagneticScrollView(
            strictSingleItemSnapping: false,
            controller: _controller,
            scrollDirection: Axis.horizontal,
            itemSize: _itemWidth,
            alignment: MagneticSnapAlignment.center,
            itemCount: 20,
            itemBuilder: (context, index) {
              final isFocused = _currentIndex == index;
              const horizontalMargin = 12.0;

              final scale = isFocused ? 1.0 : 0.9;
              return AnimatedContainer(
                duration: const Duration(milliseconds: 300),
                width: _itemWidth - (horizontalMargin * 2),
                margin: const EdgeInsets.symmetric(
                  horizontal: horizontalMargin,
                  vertical: 16,
                ),
                decoration: BoxDecoration(
                  color: Colors.primaries[index % Colors.primaries.length],
                  borderRadius: BorderRadius.circular(24),
                  // Make the focused item pop out slightly!
                  boxShadow: isFocused
                      ? [
                          const BoxShadow(
                            color: Colors.black45,
                            blurRadius: 15,
                            spreadRadius: 2,
                          ),
                        ]
                      : [],
                ),
                // Scale effect for the focused item
                transform: Matrix4.diagonal3Values(scale, scale, 1.0),
                transformAlignment: Alignment.center,
                child: Center(
                  child: Text(
                    'Card $index',
                    style: const TextStyle(
                      fontSize: 32,
                      fontWeight: FontWeight.bold,
                      color: Colors.white,
                    ),
                  ),
                ),
              );
            },
          ),
        ),
        const SizedBox(height: 40),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton.icon(
              icon: const Icon(Icons.arrow_back),
              label: const Text('Previous'),
              onPressed: () {
                if (_currentIndex > 0) {
                  _controller.animateToItem(index: _currentIndex - 1);
                }
              },
            ),
            const SizedBox(width: 16),
            ElevatedButton.icon(
              icon: const Icon(Icons.arrow_forward),
              label: const Text('Next'),
              onPressed: () {
                if (_currentIndex < 19) {
                  _controller.animateToItem(index: _currentIndex + 1);
                }
              },
            ),
          ],
        ),
      ],
    );
  }
}
0
likes
160
points
158
downloads

Documentation

API reference

Publisher

verified publisherpinz.dev

Weekly Downloads

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.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_magnetic_scroll