Flutter: Add friction to user scrolling, not spring

Viewed 1632

I have a list view with a custom scroll physics class that defines how I want the scrolling and spring effects of a list I have. I have managed to set the spring dampening the way I want it but I can't seem to find any settings around the making the drag heavier for the user.

What I mean by that is when the user drags I want the list to feel like it has tension so the user needs to drag further than they normally would I and I will handle the movement to the next item in the list using the custom scroll physics.

I want it to feel like a turn style at a train station. There is lots of tension you receive with your body and once you pass through the turn style resets itself to center.

List:

child: ListView.builder(
  cacheExtent: MediaQuery.of(context).size.height * 2,
  padding: EdgeInsets.only(top: 0),
  itemExtent: constraints.maxHeight -
      SizeConfig.blockSizeVertical * 40,
  itemCount: channelList?.length ?? 0,
  controller: _controller,
  physics: _physics,
  itemBuilder: (BuildContext context, int index) =>
      buildList(index),
),

Custom scroll class:

import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';

class CustomScrollPhysics extends ScrollPhysics {
  final double itemDimension;
  static final SpringDescription customSpring =
      SpringDescription.withDampingRatio(
    mass: 4,
    stiffness: 150.0,
    ratio: 2.0,
  );

  @override
  double get dragStartDistanceMotionThreshold => 40;

  @override
  double get minFlingVelocity => double.infinity;

  @override
  double get maxFlingVelocity => double.infinity;

  @override
  double get minFlingDistance => double.infinity;

  CustomScrollPhysics({this.itemDimension, ScrollPhysics parent})
      : super(parent: parent);

  @override
  CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
    return CustomScrollPhysics(
        itemDimension: itemDimension, parent: buildParent(ancestor));
  }

  double _getPage(ScrollPosition position) {
    return position.pixels / itemDimension;
  }

  double _getPixels(double page) {
    return page * itemDimension;
  }

  double _getTargetPixels(
      ScrollPosition position, Tolerance tolerance, double velocity) {
    double page = _getPage(position);
    if (velocity < -tolerance.velocity) {
      page -= 0.01;
    } else if (velocity > tolerance.velocity) {
      page += 0.01;
    }
    return _getPixels(page.roundToDouble());
  }

  @override
  Simulation createBallisticSimulation(
      ScrollMetrics position, double velocity) {
    // If we're out of range and not headed back in range, defer to the parent
    // ballistics, which should put us back in range at an item boundary.
    if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
        (velocity >= 0.0 && position.pixels >= position.maxScrollExtent))
      return super.createBallisticSimulation(position, (velocity));
    final Tolerance tolerance = this.tolerance;
    final double target = _getTargetPixels(position, tolerance, velocity);
    if (target != position.pixels) {
      return ScrollSpringSimulation(
          customSpring, position.pixels, target, velocity,
          tolerance: tolerance);
    }
    return null;
  }

  @override
  bool get allowImplicitScrolling => false;
}

/// Note: This Widget creates the ballistics model for a snap-to physics in a ListView.
/// Normally you might use the ListViewScrollView, however that widget currently
/// has a bug that prevents onTap detection of child widgets.

The list is centered on the screen and roughly 60% of the view height.

2 Answers

If you only need to display one item in the list at a time and the rendering of an item will fit on the screen, using a PageView widget might give you the effect that you are after.

Try this,

enter image description here

import 'dart:math';
 
import 'package:flutter/material.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          home: MyHomePage(),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      final _listController = ScrollController();
    
      final List<int> _list = List.generate(12, (index) => index);
    
      ScrollPhysics _scrollPhysics;
    
      @override
      void initState() {
        super.initState();
    
        _listController.addListener(() {
          if (_listController.position.haveDimensions && _scrollPhysics == null) {
            setState(() {
              var dimension =
                  _listController.position.maxScrollExtent / (_list.length - 4);
              _scrollPhysics = CustomScrollPhysics(itemDimension: dimension);
            });
          }
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: SafeArea(
            child: ListView.builder(
              scrollDirection: Axis.vertical,
              controller: _listController,
              physics: _scrollPhysics,
              itemCount: _list.length,
              itemBuilder: (context, index) => Container(
                height: 300,
                width: double.infinity,
                color: randomColor,
                child: FlutterLogo(),
                margin: const EdgeInsets.all(2.0),
              ),
            ),
          ),
        );
      }
    
      Color get randomColor =>
          Color((Random().nextDouble() * 0xFFFFFF).toInt() << 0).withOpacity(1.0);
    }
    
    class CustomScrollPhysics extends ScrollPhysics {
      final double itemDimension;
    
      CustomScrollPhysics({this.itemDimension, ScrollPhysics parent})
          : super(parent: parent);
    
      @override
      CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
        return CustomScrollPhysics(
            itemDimension: itemDimension, parent: buildParent(ancestor));
      }
    
      double _getPage(ScrollPosition position) {
        return position.pixels / itemDimension;
      }
    
      double _getPixels(double page) {
        return page * itemDimension;
      }
    
      double _getTargetPixels(
          ScrollPosition position, Tolerance tolerance, double velocity) {
        double page = _getPage(position);
        if (velocity < -tolerance.velocity) {
          page -= 0.1;
        } else if (velocity > tolerance.velocity) {
          page += 0.1;
        }
        return _getPixels(page.roundToDouble());
      }
    
      @override
      Simulation createBallisticSimulation(
          ScrollMetrics position, double velocity) {
        // If we're out of range and not headed back in range, defer to the parent
        // ballistics, which should put us back in range at a page boundary.
        if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
            (velocity >= 0.0 && position.pixels >= position.maxScrollExtent))
          return super.createBallisticSimulation(position, velocity);
        final Tolerance tolerance = this.tolerance;
        final double target = _getTargetPixels(position, tolerance, velocity);
        if (target != position.pixels)
          return ScrollSpringSimulation(spring, position.pixels, target, velocity,
              tolerance: tolerance);
        return null;
      }
    
      @override
      bool get allowImplicitScrolling => false;
    }

now in flutter, there is no pre define method for friction, but this is possible with some custom way.

Related