简体   繁体   中英

Prevent Small Hops in Drag Upon Touch Up/End in Flutter

I have built a custom slider and have been using GestureDetector with onHorizontalDragUpdate to report drag changes, update the UI and value .

However, when a user lifts their finger, there can sometimes be a small, unintentional hop/drag, enough to adjust the value on the slider and reduce accuracy. How can I stop this occuring?

I have considered adding a small delay to prevent updates if the drag hasn't moved for a tiny period and assessing the primaryDelta , but unsure if this would be fit for purpose or of there is a more routine common practive to prevent this.

--

Example of existing drag logic I am using. The initial drag data is from onHorizontalDragUpdate in _buildThumb . When the slider is rebuilt, the track size and thumb position is calculated in the LayoutBuilder and then the value is calculated based on the thumb position.

    double valueForPosition({required double min, required double max}) {
    double posIncrements = ((max) / (_divisions));
    double posIncrement = (_thumbPosX / (posIncrements));
    double incrementVal =
        (increment) * (posIncrement + widget.minimumValue).round() +
            (widget.minimumValue - widget.minimumValue.truncate());
    return incrementVal.clamp(widget.minimumValue, widget.maximumValue);
  }

  double thumbPositionForValue({required double min, required double max}) {
    return (max / (widget.maximumValue - widget.minimumValue - 1)) *
        (value - widget.minimumValue - 1);
  }

  double trackWidthForValue({
    required double min,
    required double max,
    required double thumbPosition,
  }) {
    return (thumbPosition + (_thumbTouchZoneWidth / 2))
        .clamp(min, max)
        .toDouble();
  }

  bool isDragging = false;
  bool isSnapping = false;
  Widget _buildSlider() {
    return SizedBox(
      height: _contentHeight,
      child: LayoutBuilder(
        builder: (context, constraints) {
          double minThumbPosX = -(_thumbTouchZoneWidth - _thumbWidth) / 2;
          double maxThumbPosX =
              constraints.maxWidth - (_thumbTouchZoneWidth / 2);
          if (isDragging) {
            _thumbPosX = _thumbPosX.clamp(minThumbPosX, maxThumbPosX);
            value = valueForPosition(min: minThumbPosX, max: maxThumbPosX);
            WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
              widget.onChanged(value);
            });
          } else {
            _thumbPosX = thumbPositionForValue(
              min: minThumbPosX,
              max: maxThumbPosX,
            );
          }

          double minTrackWidth = 0;
          double maxTrackWidth = constraints.maxWidth;
          double trackWidth = 0;
          if (isDragging) {
            trackWidth = (_thumbPosX + (_thumbTouchZoneWidth / 2))
                .clamp(_thumbWidth, constraints.maxWidth);
          } else {
            trackWidth = trackWidthForValue(
              min: minTrackWidth,
              max: maxTrackWidth,
              thumbPosition: _thumbPosX,
            );
          }

          return Stack(
            alignment: Alignment.centerLeft,
            clipBehavior: Clip.none,
            children: [
              _buildLabels(),
              _buildInactiveTrack(),
              Positioned(
                width: trackWidth,
                child: _buildActiveTrack(),
              ),
              Positioned(
                left: _thumbPosX,
                child: _buildThumb(),
              ),
            ],
          );
        },
      ),
    );
  }

    Widget _buildThumb() {
        return GestureDetector(
          behavior: HitTestBehavior.opaque,
          dragStartBehavior: DragStartBehavior.down,
          onHorizontalDragUpdate: (details) {
            setState(() {
              _thumbPosX += details.delta.dx;
              isDragging = true;
            });
          },
          child: // Thumb UI
        );
      }

Updated : I make a little adjustment by adding a delay state and lastChangedTime.

  • If the user stops dragging for a short period (3 sec), the slider will be locked until the next new value is updated + a short delay (1.5 sec)

I follow your train of thought and make a simple example from Slider widget.

Is the result act like your expected? (You can adjust the Duration to any number)

DartPad: https://dartpad.dev/?id=95f2bd6d004604b3c37f27dd2852cb31

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

  @override
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  double _currentSliderValue = 20;
  DateTime lastChangedTime = DateTime.now();
  bool isDalying = false;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(_currentSliderValue.toString()),
        const SizedBox(height: 30),
        Slider(
          value: _currentSliderValue,
          max: 100,
          label: _currentSliderValue.round().toString(),
          onChanged: (double value) async {
            if (isDalying) {
              await Future.delayed(
                Duration(milliseconds: 1500), 
                () => isDalying = false,
              );
            } else {
              if (DateTime.now().difference(lastChangedTime) >
                  Duration(seconds: 3)) {
                isDalying = true;
              } else {
                setState(() {
                  _currentSliderValue = value;
                });
              }
            }
            lastChangedTime = DateTime.now();
          },
        ),
      ],
    );
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM