Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slider has a slight a lag when drag #51715

Open
Tracked by #125329
tomasbaran opened this issue Feb 29, 2020 · 10 comments
Open
Tracked by #125329

Slider has a slight a lag when drag #51715

tomasbaran opened this issue Feb 29, 2020 · 10 comments
Labels
a: fidelity Matching the OEM platforms better f: material design flutter/packages/flutter/material repository. found in release: 3.3 Found to occur in 3.3 found in release: 3.7 Found to occur in 3.7 framework flutter/packages/flutter repository. See also f: labels. has reproducible steps The issue has been confirmed reproducible and is ready to work on P2 Important issues not at the top of the work list team-design Owned by Design Languages team triaged-design Triaged by Design Languages team

Comments

@tomasbaran
Copy link

tomasbaran commented Feb 29, 2020

  1. It looks like the default slider widget in Flutter has a slight lag when being moved. It's definitely nothing terrible BUT slow enough to notice it doesn't feel native on iOS. Just check how iOS slider should behave by going e.g. to iPhone Settings->Screen and Brightness -> Change Brightness by sliding.

Sliding a native iOS slider is instant whereas sliding a Flutter slider is with a (slight) lag.

2. Code Sample
import 'package:flutter/material.dart';


void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);


  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int height = 170;
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Slider(
          min: 150.0,
          max: 180.0,
          value: height.toDouble(),
          divisions: 30,
          onChanged: (double newValue) {
            setState(() {
              height = newValue.toInt();
            });
          },
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
  1. https://photos.app.goo.gl/mcSbDNyoPJYwYrns8

Details

Target Platform:
iOS
Target OS version/browser:
13.3.1
Devices:
iPhone XR

Logs

Analyzing flutter_app_slider_experiment...
No issues found! (ran in 2.2s)

[✓] Flutter (Channel master, v1.15.4-pre.241, on Mac OS X 10.15.3 19D76, locale es-ES)
• Flutter version 1.15.4-pre.241 at /Users/tomas/Developer/flutter
• Framework revision 1d3fcfd (5 hours ago), 2020-02-29 10:01:18 -0800
• Engine revision 755e2b5
• Dart version 2.8.0 (build 2.8.0-dev.10.0 09bbd3cca5)

[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.3)
• Android SDK at /Users/tomas/Library/Android/sdk
• Android NDK location not configured (optional; useful for native profiling support)
• Platform android-29, build-tools 29.0.3
• Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
• All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 11.3.1)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Xcode 11.3.1, Build version 11C504
• CocoaPods version 1.8.4

[✓] Android Studio (version 3.5)
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin version 43.0.1
• Dart plugin version 191.8593
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)

[✓] Connected device (1 available)
• iPhone • 00008020-000415840E50003A • ios • iOS 13.3.1

• No issues found!

@tomasbaran tomasbaran added the from: performance template Issues created via a performance issue template label Feb 29, 2020
@HansMuller HansMuller added a: fidelity Matching the OEM platforms better f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. labels Mar 2, 2020
@HansMuller
Copy link
Contributor

CC @clocksmith

@TahaTesser TahaTesser removed the from: performance template Issues created via a performance issue template label Mar 9, 2020
@TahaTesser
Copy link
Member

Code Sample
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int height = 170;
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Slider(
          min: 150.0,
          max: 180.0,
          value: height.toDouble(),
          divisions: 30,
          onChanged: (double newValue) {
            setState(() {
              height = newValue.toInt();
            });
          },
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

flutter doctor -v
[✓] Flutter (Channel dev, v1.18.0, on Mac OS X 10.15.4 19E266, locale en-GB)
    • Flutter version 1.18.0 at /Users/taha/Code/flutter_dev
    • Framework revision 8f7327f83a (2 days ago), 2020-04-06 22:11:01 -0400
    • Engine revision 49891e0653
    • Dart version 2.8.0 (build 2.8.0-dev.20.0 1210d27678)
 
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.3)
    • Android SDK at /Users/taha/Code/sdk
    • Platform android-29, build-tools 29.0.3
    • ANDROID_HOME = /Users/taha/Code/sdk
    • Java binary at: /Applications/Android
      Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build
      1.8.0_212-release-1586-b4-5784211)
    • All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 11.4)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 11.4, Build version 11E146
    • CocoaPods version 1.9.1
[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Android Studio (version 3.6)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin version 45.0.1
    • Dart plugin version 192.7761
    • Java version OpenJDK Runtime Environment (build
      1.8.0_212-release-1586-b4-5784211)
[✓] VS Code (version 1.44.0)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.9.1
[✓] Connected device (3 available)
    • macOS      • macOS      • darwin-x64     • Mac OS X 10.15.4 19E266
    • Chrome     • chrome     • web-javascript • Google Chrome 81.0.4044.92
    • Web Server • web-server • web-javascript • Flutter Tools
• No issues found!
taha@Tahas-MacBook-Pro AndroidStudioProjects % flutterd doctor -v
[✓] Flutter (Channel dev, v1.18.0, on Mac OS X 10.15.4 19E266, locale en-GB)
    • Flutter version 1.18.0 at /Users/taha/Code/flutter_dev
    • Framework revision 8f7327f83a (2 days ago), 2020-04-06 22:11:01 -0400
    • Engine revision 49891e0653
    • Dart version 2.8.0 (build 2.8.0-dev.20.0 1210d27678)
 
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.3)
    • Android SDK at /Users/taha/Code/sdk
    • Platform android-29, build-tools 29.0.3
    • ANDROID_HOME = /Users/taha/Code/sdk
    • Java binary at: /Applications/Android
      Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build
      1.8.0_212-release-1586-b4-5784211)
    • All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 11.4)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 11.4, Build version 11E146
    • CocoaPods version 1.9.1
[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Android Studio (version 3.6)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin version 45.0.1
    • Dart plugin version 192.7761
    • Java version OpenJDK Runtime Environment (build
      1.8.0_212-release-1586-b4-5784211)
[✓] VS Code (version 1.44.0)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.9.1
[✓] Connected device (5 available)
    • SM M305F      • 32003c30dc19668f          • android-arm64  • Android 10
      (API 29)
    • Taha’s iPhone • 00008020-001059882212002E • ios            • iOS 13.4.1
    • macOS         • macOS                     • darwin-x64     • Mac OS X
      10.15.4 19E266
    • Chrome        • chrome                    • web-javascript • Google Chrome
      81.0.4044.92
    • Web Server    • web-server                • web-javascript • Flutter Tools
• No issues found!

@TahaTesser TahaTesser added the has reproducible steps The issue has been confirmed reproducible and is ready to work on label Apr 9, 2020
@bashennekam
Copy link

Strangely enough if you remove the "divisions" the performance is much better.

@TahaTesser TahaTesser added the found in release: 1.18 Occurs in 1.18 label Jul 30, 2020
@pedromassangocode
Copy link

Reproducible on latest master channel.

flutter doctor
[✓] Flutter (Channel master, 1.26.0-2.0.pre.82, on Mac OS X 10.15.7 19H2 darwin-x64, locale en)
    • Flutter version 1.26.0-2.0.pre.82 at /Users/pedromassango/dev/SDKs/flutter_master
    • Framework revision b84d8ee2df (6 hours ago), 2020-12-18 11:59:01 +0530
    • Engine revision fd6b409f95
    • Dart version 2.12.0 (build 2.12.0-157.0.dev)

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
    • Android SDK at /Users/pedromassango/Library/Android/sdk
    • Platform android-30, build-tools 30.0.2
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)
    • All Android licenses accepted.

[!] Xcode - develop for iOS and macOS (Xcode 12.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 12.1, Build version 12A7403
    ! CocoaPods 1.9.3 out of date (1.10.0 is recommended).
        CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that responds to your plugin usage on the Dart side.
        Without CocoaPods, plugins will not work on iOS or macOS.
        For more info, see https://flutter.dev/platform-plugins
      To upgrade see https://guides.cocoapods.org/using/getting-started.html#installation for instructions.

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 4.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)

[✓] IntelliJ IDEA Community Edition (version 2020.3)
    • IntelliJ at /Applications/IntelliJ IDEA CE.app
    • Flutter plugin version 52.0.5
    • Dart plugin version 203.5981.152

[✓] VS Code (version 1.51.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.16.0

[✓] Connected device (2 available)
    • macOS (desktop) • macos  • darwin-x64     • Mac OS X 10.15.7 19H2 darwin-x64
    • Chrome (web)    • chrome • web-javascript • Google Chrome 87.0.4280.88

! Doctor found issues in 1 category.
Process finished with exit code 0

@pedromassangocode pedromassangocode added found in release: 1.26 Found to occur in 1.26 platform-ios iOS applications specifically and removed found in release: 1.18 Occurs in 1.18 labels Dec 18, 2020
@ousvat
Copy link

ousvat commented Sep 14, 2021

Reproducible on last stable version for both android and iOS:

[✓] Flutter (Channel stable, 2.5.0, on macOS 11.5.2 20G95 darwin-x64, locale
    en-RO)
    • Flutter version 2.5.0 at /Users/ovidiu/dev_tools/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 4cc385b4b8 (6 days ago), 2021-09-07 23:01:49 -0700
    • Engine revision f0826da7ef
    • Dart version 2.14.0

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
    • Android SDK at /Users/ovidiu/Library/Android/sdk
    • Platform android-30, build-tools 30.0.3
    • ANDROID_HOME = /Users/ovidiu/Library/Android/sdk
    • Java binary at: /Applications/Android
      Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 12.5.1, Build version 12E507
    • CocoaPods version 1.10.1

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2020.3)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165)

[✓] VS Code (version 1.56.2)
    • VS Code at /Users/ovidiu/Downloads/Visual Studio Code.app/Contents
    • Flutter extension version 3.23.0

[✓] Connected device (3 available)
    • SM M205F (mobile)        • 32012e74bd0c4671                         • android-arm64  • Android 10 (API 29)
    • Ovidiu’s iPhone (mobile) • 9e8fe704f67edabb67c62d36a6e63a09fcf26e26 • ios            • iOS 14.7.1 18G82
    • Chrome (web)             • chrome                                   • web-javascript • Google Chrome 93.0.4577.63

@VadimMalykhin
Copy link

The animation duration is set to 75 milliseconds and isn't configurable. That's why you feel the input lag.

static const Duration enableAnimationDuration = Duration(milliseconds: 75);

@maheshmnj
Copy link
Member

Reproducible as of stable 3.3 and master 3.7

code sample
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      showPerformanceOverlay: true,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int height = 170;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Slider(
          min: 150.0,
          max: 180.0,
          value: height.toDouble(),
          divisions: 30,
          onChanged: (double newValue) {
            setState(() {
              height = newValue.toInt();
            });
          },
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
flutter doctor -v (mac)
[✓] Flutter (Channel master, 3.7.0-13.0.pre.131, on macOS 13.1 22C65 darwin-arm64, locale en-IN)
    • Flutter version 3.7.0-13.0.pre.131 on channel master at /Users/mahesh/Development/flutter_master
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 0196e6050b (2 days ago), 2022-12-31 11:10:23 -0500
    • Engine revision 932591ec04
    • Dart version 3.0.0 (build 3.0.0-76.0.dev)
    • DevTools version 2.20.0
    • If those were intentional, you can disregard the above warnings; however it is recommended to use "git" directly to perform update checks
      and upgrades.

[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0-rc4)
    • Android SDK at /Users/mahesh/Library/Android/sdk
    • Platform android-33, build-tools 33.0.0-rc4
    • ANDROID_HOME = /Users/mahesh/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 14.0.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 14A400
    • CocoaPods version 1.11.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2021.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)

[✓] IntelliJ IDEA Community Edition (version 2021.2.1)
    • IntelliJ at /Applications/IntelliJ IDEA CE.app
    • Flutter plugin version 61.2.4
    • Dart plugin version 212.5080.8

[✓] VS Code (version 1.74.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.56.0

[✓] Connected device (3 available)
    • iPhone 12 Pro (mobile) • 026D5789-9E78-4AD5-B1B2-3F8D4E7F65E4 • ios            • com.apple.CoreSimulator.SimRuntime.iOS-14-5 (simulator)
    • macOS (desktop)        • macos                                • darwin-arm64   • macOS 13.1 22C65 darwin-arm64
    • Chrome (web)           • chrome                               • web-javascript • Google Chrome 108.0.5359.124

[✓] HTTP Host Availability
    • All required HTTP hosts are available
    
• No issues found!
[✓] Flutter (Channel stable, 3.3.9, on macOS 12.6 21G115 darwin-arm, locale en-IN)
    • Flutter version 3.3.9 on channel stable at /Users/mahesh/Development/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision b8f7f1f986 (24 hours ago), 2022-11-23 06:43:51 +0900
    • Engine revision 8f2221fbef
    • Dart version 2.18.5
    • DevTools version 2.15.0

[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0-rc4)
    • Android SDK at /Users/mahesh/Library/Android/sdk
    • Platform android-33, build-tools 33.0.0-rc4
    • ANDROID_HOME = /Users/mahesh/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 14.0.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 14A400
    • CocoaPods version 1.11.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2021.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)

[✓] IntelliJ IDEA Community Edition (version 2021.2.1)
    • IntelliJ at /Applications/IntelliJ IDEA CE.app
    • Flutter plugin version 61.2.4
    • Dart plugin version 212.5080.8

[✓] VS Code (version 1.70.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.53.20221101

[✓] Connected device (3 available)
    • iPhone 12 Pro (mobile) • 026D5789-9E78-4AD5-B1B2-3F8D4E7F65E4 • ios            •
      com.apple.CoreSimulator.SimRuntime.iOS-14-5 (simulator)
    • macOS (desktop)        • macos                                • darwin-arm64   • macOS 12.6 21G115 darwin-arm
    • Chrome (web)           • chrome                               • web-javascript • Google Chrome 107.0.5304.110

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!

@maheshmnj maheshmnj added found in release: 3.3 Found to occur in 3.3 found in release: 3.7 Found to occur in 3.7 and removed found in release: 1.26 Found to occur in 1.26 labels Jan 4, 2023
@Piinks Piinks added the P2 Important issues not at the top of the work list label Jan 5, 2023
@TahaTesser
Copy link
Member

TahaTesser commented Apr 20, 2023

@tomasbaran
The stutter or lag seems to be related to value, where you're converting int to double, and also in onChanged you're converting double to int.
It seems like a negligible operation but not doing so gets rid of the stutter even without divisions

code sample
import 'package:flutter/material.dart';


void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({super.key, required this.title});


  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  double height = 170.0;
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Slider(
          min: 150.0,
          max: 180.0,
          value: height,
          onChanged: (double newValue) {
            setState(() {
              height = newValue;
            });
          },
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
With conversion Without conversion

Note:
I can see the same behavior on all platforms

@TahaTesser TahaTesser removed the platform-ios iOS applications specifically label Apr 20, 2023
@TahaTesser TahaTesser changed the title Flutter Slider widget has a slight a lag (when being moved) which doesn't feel native on iOS. Slider has a slight a lag when drag Apr 20, 2023
@rydmike
Copy link
Contributor

rydmike commented Apr 20, 2023

Yet another analysis of the issue

Since I am well aware of this issue and have seen it a lot, here is my analysis of its causes. As far as I can see, there are potentially two issues at play in this report.

  1. Stutter when using a continuous Slider based on int value
  2. Delay on discrete Slider with both int and double based values.

Let's tackle them both.

1) Stutter when using continuous Slider based on int value

This case is pretty logical. If your source value is based on an int value, and the Slider is continuous, you get a double value back and Slider has moved/operated a bit, this is reflected in the returned value. When you convert this value to an int and assign it back to the Slider's value, it will "stutter" when it reverts back to the integer value. This will happen until returned new double value reaches the next integer value, and it snaps to new integer value.

Using integer as source value for a Slider is fine, but it is only "visually" compatible with using a discrete Slider where its discrete steps are also integer values.

The conversion between int and double is negligible and not behind the stutter or any visually observed delay.

We can demonstrate this. Sliders below share values, so that we can observe their behavior differences.

Moving the continuous INT Slider, it sets int value and uses an int value back from returned new value via double conversion. Moving it slowly, we can se it tries to move, but it is forced back to int the value, causing the stutter.

We can even see the same effect on the Continuous INT Slider when we operate the Continuous DOUBLE Slider, since they share double control value.

Screen.Recording.2023-04-20.at.22.44.02.mov

Conclusion: To avoid the stutter, only use int input control values with discrete Sliders, that can match its selectable and returned discrete Slider double values. Some documentation to explain this might be helpful.

2) Delay on discrete Slider with both int and double based values

Regarding the delay when operating Sliders, we can see and observe that the thumb LAGS considerably behind when operating discrete Sliders, this does not happen with continuous Sliders. The delay does not depend at all on if source value is double or int based (other than the unrelated and logical int stutter above for continuous Slider).

We can see this below, where when operating the discrete Sliders, the Slider thumb lags a lot behind the mouse (or finger touch), when it is moved rapidly:

Screen.Recording.2023-04-20.at.22.45.41.mov

Concerning the cause of this delay, @VadimMalykhin has the right idea, the lag is caused by a hard coded animation delay. The referenced value is, however, incorrect:

static const Duration enableAnimationDuration = Duration(milliseconds: 75);

Alternatively, the Slider has since he made his observation (it was a while ago) been changed. Changing the suggested constant to zero, does not remove the observed delay.

The actual delay is deeper, it is in the _RenderSlider that extends RenderBox here:

static const Duration _positionAnimationDuration = Duration(milliseconds: 75);

If we set this constant duration to zero, we can get a much nicer discrete Sliders that tracks rapid mouse or finger operations much better. As shown below where the above constant was set to zero:

Screen.Recording.2023-04-20.at.22.47.44.mov

I don't know what the rationale behind this delay is. My guess is, if you have a discrete Slider with only a few discrete values, the animation makes sense. It allows the thumb to nicely slide animate to the next value. However, if your discrete Slider has a large number of discrete values, this animation no longer makes any sense. Just jumping to the next value would then be better, since it is getting closer to the continuous use case.

I agree that this constant should be exposed so users can modify it to fit their use case. And please, via a theme property as well, not just on widget level. 😄

Example code

Code sample
// MIT License
//
// Copyright (c) 2023 Mike Rydstrom
//
// 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 NON-INFRINGEMENT. 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/foundation.dart';
import 'package:flutter/material.dart';

// This issue reported here: https://github.com/flutter/flutter/issues/51715

// A seed color for the M3 ColorScheme.
const Color seedColor = Color(0xFF6750A4);
// Make M3 ColorSchemes from a seed color.
final ColorScheme schemeLight = ColorScheme.fromSeed(
  brightness: Brightness.light,
  seedColor: seedColor,
);
final ColorScheme schemeDark = ColorScheme.fromSeed(
  brightness: Brightness.dark,
  seedColor: seedColor,
);

// Example theme
ThemeData theme(ThemeMode mode, ThemeSettings settings) {
  final ColorScheme colorScheme =
  mode == ThemeMode.light ? schemeLight : schemeDark;

  return ThemeData(
      colorScheme: colorScheme,
      useMaterial3: settings.useMaterial3,
      visualDensity: VisualDensity.standard,
      sliderTheme: const SliderThemeData(
        showValueIndicator: ShowValueIndicator.always,
      ));
}

void main() {
  runApp(const IssueDemoApp());
}

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

  @override
  State<IssueDemoApp> createState() => _IssueDemoAppState();
}

class _IssueDemoAppState extends State<IssueDemoApp> {
  ThemeMode themeMode = ThemeMode.light;
  bool longLabel = false;
  TextDirection textDirection = TextDirection.ltr;
  ThemeSettings settings = const ThemeSettings(
    useMaterial3: true,
    useCustomTheme: false,
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      themeMode: themeMode,
      theme: theme(ThemeMode.light, settings),
      darkTheme: theme(ThemeMode.dark, settings),
      home: Directionality(
        textDirection: textDirection,
        child: Scaffold(
          appBar: AppBar(
            title: settings.useMaterial3
                ? const Text("INT Slider Issue (Material 3)")
                : const Text("INT Slider Issue (Material 2)"),
            actions: [
              IconButton(
                icon: settings.useMaterial3
                    ? const Icon(Icons.filter_3)
                    : const Icon(Icons.filter_2),
                onPressed: () {
                  setState(() {
                    settings =
                        settings.copyWith(useMaterial3: !settings.useMaterial3);
                  });
                },
                tooltip: "Switch to Material ${settings.useMaterial3 ? 2 : 3}",
              ),
              IconButton(
                icon: themeMode == ThemeMode.dark
                    ? const Icon(Icons.wb_sunny_outlined)
                    : const Icon(Icons.wb_sunny),
                onPressed: () {
                  setState(() {
                    if (themeMode == ThemeMode.light) {
                      themeMode = ThemeMode.dark;
                    } else {
                      themeMode = ThemeMode.light;
                    }
                  });
                },
                tooltip: "Toggle brightness",
              ),
            ],
          ),
          body: HomePage(
            settings: settings,
            onSettings: (ThemeSettings value) {
              setState(() {
                settings = value;
              });
            },
            longLabel: longLabel,
            onLongLabel: (bool value) {
              setState(() {
                longLabel = value;
              });
            },
            textDirection: textDirection,
            onTextDirection: (TextDirection value) {
              setState(() {
                textDirection = value;
              });
            },
          ),
        ),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({
    super.key,
    required this.settings,
    required this.onSettings,
    required this.longLabel,
    required this.onLongLabel,
    required this.textDirection,
    required this.onTextDirection,
  });
  final ThemeSettings settings;
  final ValueChanged<ThemeSettings> onSettings;
  final bool longLabel;
  final ValueChanged<bool> onLongLabel;
  final TextDirection textDirection;
  final ValueChanged<TextDirection> onTextDirection;

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      children: const [
        SizedBox(height: 8),
        Text('Example of stutter and delay when using INT and discrete versus'
            'continuous Slider with comparison to using DOUBLE and '
            'discrete versus continuous Slider.'),
        SizedBox(height: 16),
        SliderShowCase(),
        SizedBox(height: 16),
        ShowColorSchemeColors(),
      ],
    );
  }
}

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

  @override
  State<SliderShowCase> createState() => _SliderShowCaseState();
}

class _SliderShowCaseState extends State<SliderShowCase> {
  int intHeight = 170;
  late double doubleHeight;

  @override
  void initState() {
    super.initState();
    doubleHeight = intHeight.toDouble();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        children: <Widget>[
          ListTile(
            title: const Text('Discrete INT slider'),
            subtitle: Slider(
              min: 150.0,
              max: 180.0,
              value: intHeight.toDouble(),
              label: doubleHeight.toStringAsFixed(3),
              divisions: 30,
              onChanged: (double newValue) {
                setState(() {
                  doubleHeight = newValue;
                  intHeight = newValue.toInt();
                });
              },
            ),
            trailing: Text(doubleHeight.toStringAsFixed(3)),
          ),
          ListTile(
            title: const Text('Continuous INT slider'),
            subtitle: Slider(
              min: 150.0,
              max: 180.0,
              value: intHeight.toDouble(),
              label: doubleHeight.toStringAsFixed(3),
              onChanged: (double newValue) {
                setState(() {
                  doubleHeight = newValue;
                  intHeight = newValue.toInt();
                });
              },
            ),
            trailing: Text(doubleHeight.toStringAsFixed(3)),
          ),
          ListTile(
            title: const Text('Discrete DOUBLE slider'),
            subtitle: Slider(
              min: 150.0,
              max: 180.0,
              value: doubleHeight,
              label: doubleHeight.toStringAsFixed(3),
              divisions: 30,
              onChanged: (double newValue) {
                setState(() {
                  doubleHeight = newValue;
                  intHeight = newValue.toInt();
                });
              },
            ),
            trailing: Text(doubleHeight.toStringAsFixed(3)),
          ),
          ListTile(
            title: const Text('Continuous DOUBLE slider'),
            subtitle: Slider(
              min: 150.0,
              max: 180.0,
              value: doubleHeight,
              label: doubleHeight.toStringAsFixed(3),
              onChanged: (double newValue) {
                setState(() {
                  doubleHeight = newValue;
                  intHeight = newValue.toInt();
                });
              },
            ),
            trailing: Text(doubleHeight.toStringAsFixed(3)),
          ),
        ],
      ),
    );
  }
}

/// A Theme Settings class to bundle properties we want to modify on our
/// theme interactively.
@immutable
class ThemeSettings with Diagnosticable {
  final bool useMaterial3;
  final bool useCustomTheme;

  const ThemeSettings({
    required this.useMaterial3,
    required this.useCustomTheme,
  });

  /// Flutter debug properties override, includes toString.
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<bool>('useMaterial3', useMaterial3));
    properties.add(DiagnosticsProperty<bool>('useCustomTheme', useCustomTheme));
  }

  /// Copy the object with one or more provided properties changed.
  ThemeSettings copyWith({
    bool? useMaterial3,
    bool? useCustomTheme,
    bool? useIndicatorWidth,
    bool? useTileHeight,
  }) {
    return ThemeSettings(
      useMaterial3: useMaterial3 ?? this.useMaterial3,
      useCustomTheme: useCustomTheme ?? this.useCustomTheme,
    );
  }

  /// Override the equality operator.
  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    if (other.runtimeType != runtimeType) return false;
    return other is ThemeSettings &&
        other.useMaterial3 == useMaterial3 &&
        other.useCustomTheme == useCustomTheme;
  }

  /// Override for hashcode, dart.ui Jenkins based.
  @override
  int get hashCode => Object.hashAll(<Object?>[
    useMaterial3.hashCode,
    useCustomTheme.hashCode,
  ]);
}

/// Draw a number of boxes showing the colors of key theme color properties
/// in the ColorScheme of the inherited ThemeData and its color properties.
class ShowColorSchemeColors extends StatelessWidget {
  const ShowColorSchemeColors({super.key, this.onBackgroundColor});

  /// The color of the background the color widget are being drawn on.
  ///
  /// Some of the theme colors may have semi transparent fill color. To compute
  /// a legible text color for the sum when it shown on a background color, we
  /// need to alpha merge it with background and we need the exact background
  /// color it is drawn on for that. If not passed in from parent, it is
  /// assumed to be drawn on card color, which usually is close enough.
  final Color? onBackgroundColor;

  // Return true if the color is light, meaning it needs dark text for contrast.
  static bool _isLight(final Color color) =>
      ThemeData.estimateBrightnessForColor(color) == Brightness.light;

  // On color used when a theme color property does not have a theme onColor.
  static Color _onColor(final Color color, final Color bg) =>
      _isLight(Color.alphaBlend(color, bg)) ? Colors.black : Colors.white;

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final ColorScheme colorScheme = theme.colorScheme;
    final bool useMaterial3 = theme.useMaterial3;
    const double spacing = 4;

    // Grab the card border from the theme card shape
    ShapeBorder? border = theme.cardTheme.shape;
    // If we had one, copy in a border side to it.
    if (border is RoundedRectangleBorder) {
      border = border.copyWith(
        side: BorderSide(
          color: colorScheme.outlineVariant,
          width: 1,
        ),
      );
      // If
    } else {
      // If border was null, make one matching Card default, but with border
      // side, if it was not null, we leave it as it was.
      border ??= RoundedRectangleBorder(
        borderRadius: BorderRadius.all(Radius.circular(useMaterial3 ? 12 : 4)),
        side: BorderSide(
          color: colorScheme.outlineVariant,
          width: 1,
        ),
      );
    }

    // Get effective background color.
    final Color background =
        onBackgroundColor ?? theme.cardTheme.color ?? theme.cardColor;

    // Wrap this widget branch in a custom theme where card has a border outline
    // if it did not have one, but retains its ambient themed border radius.
    return Theme(
      data: Theme.of(context).copyWith(
        cardTheme: CardTheme.of(context).copyWith(
          elevation: 0,
          surfaceTintColor: Colors.transparent,
          shape: border,
        ),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 8),
            child: Text(
              'ColorScheme Colors',
              style: theme.textTheme.titleMedium,
            ),
          ),
          Wrap(
            alignment: WrapAlignment.start,
            crossAxisAlignment: WrapCrossAlignment.center,
            spacing: spacing,
            runSpacing: spacing,
            children: <Widget>[
              ColorCard(
                label: 'Primary',
                color: colorScheme.primary,
                textColor: colorScheme.onPrimary,
              ),
              ColorCard(
                label: 'on\nPrimary',
                color: colorScheme.onPrimary,
                textColor: colorScheme.primary,
              ),
              ColorCard(
                label: 'Primary\nContainer',
                color: colorScheme.primaryContainer,
                textColor: colorScheme.onPrimaryContainer,
              ),
              ColorCard(
                label: 'onPrimary\nContainer',
                color: colorScheme.onPrimaryContainer,
                textColor: colorScheme.primaryContainer,
              ),
              ColorCard(
                label: 'Secondary',
                color: colorScheme.secondary,
                textColor: colorScheme.onSecondary,
              ),
              ColorCard(
                label: 'on\nSecondary',
                color: colorScheme.onSecondary,
                textColor: colorScheme.secondary,
              ),
              ColorCard(
                label: 'Secondary\nContainer',
                color: colorScheme.secondaryContainer,
                textColor: colorScheme.onSecondaryContainer,
              ),
              ColorCard(
                label: 'on\nSecondary\nContainer',
                color: colorScheme.onSecondaryContainer,
                textColor: colorScheme.secondaryContainer,
              ),
              ColorCard(
                label: 'Tertiary',
                color: colorScheme.tertiary,
                textColor: colorScheme.onTertiary,
              ),
              ColorCard(
                label: 'on\nTertiary',
                color: colorScheme.onTertiary,
                textColor: colorScheme.tertiary,
              ),
              ColorCard(
                label: 'Tertiary\nContainer',
                color: colorScheme.tertiaryContainer,
                textColor: colorScheme.onTertiaryContainer,
              ),
              ColorCard(
                label: 'on\nTertiary\nContainer',
                color: colorScheme.onTertiaryContainer,
                textColor: colorScheme.tertiaryContainer,
              ),
              ColorCard(
                label: 'Error',
                color: colorScheme.error,
                textColor: colorScheme.onError,
              ),
              ColorCard(
                label: 'on\nError',
                color: colorScheme.onError,
                textColor: colorScheme.error,
              ),
              ColorCard(
                label: 'Error\nContainer',
                color: colorScheme.errorContainer,
                textColor: colorScheme.onErrorContainer,
              ),
              ColorCard(
                label: 'onError\nContainer',
                color: colorScheme.onErrorContainer,
                textColor: colorScheme.errorContainer,
              ),
              ColorCard(
                label: 'Background',
                color: colorScheme.background,
                textColor: colorScheme.onBackground,
              ),
              ColorCard(
                label: 'on\nBackground',
                color: colorScheme.onBackground,
                textColor: colorScheme.background,
              ),
              ColorCard(
                label: 'Surface',
                color: colorScheme.surface,
                textColor: colorScheme.onSurface,
              ),
              ColorCard(
                label: 'on\nSurface',
                color: colorScheme.onSurface,
                textColor: colorScheme.surface,
              ),
              ColorCard(
                label: 'Surface\nVariant',
                color: colorScheme.surfaceVariant,
                textColor: colorScheme.onSurfaceVariant,
              ),
              ColorCard(
                label: 'onSurface\nVariant',
                color: colorScheme.onSurfaceVariant,
                textColor: colorScheme.surfaceVariant,
              ),
              ColorCard(
                label: 'Outline',
                color: colorScheme.outline,
                textColor: colorScheme.background,
              ),
              ColorCard(
                label: 'Outline\nVariant',
                color: colorScheme.outlineVariant,
                textColor: colorScheme.onBackground,
              ),
              ColorCard(
                label: 'Shadow',
                color: colorScheme.shadow,
                textColor: _onColor(colorScheme.shadow, background),
              ),
              ColorCard(
                label: 'Scrim',
                color: colorScheme.scrim,
                textColor: _onColor(colorScheme.scrim, background),
              ),
              ColorCard(
                label: 'Inverse\nSurface',
                color: colorScheme.inverseSurface,
                textColor: colorScheme.onInverseSurface,
              ),
              ColorCard(
                label: 'onInverse\nSurface',
                color: colorScheme.onInverseSurface,
                textColor: colorScheme.inverseSurface,
              ),
              ColorCard(
                label: 'Inverse\nPrimary',
                color: colorScheme.inversePrimary,
                textColor: colorScheme.inverseSurface,
              ),
              ColorCard(
                label: 'Surface\nTint',
                color: colorScheme.surfaceTint,
                textColor: colorScheme.onPrimary,
              ),
            ],
          ),
        ],
      ),
    );
  }
}

/// A [SizedBox] with a [Card] and string text in it. Used in this demo to
/// display theme color boxes.
///
/// Can specify label text color and background color.
class ColorCard extends StatelessWidget {
  const ColorCard({
    super.key,
    required this.label,
    required this.color,
    required this.textColor,
    this.size,
  });

  final String label;
  final Color color;
  final Color textColor;
  final Size? size;

  @override
  Widget build(BuildContext context) {
    const double fontSize = 11;
    const Size effectiveSize = Size(86, 58);

    return SizedBox(
      width: effectiveSize.width,
      height: effectiveSize.height,
      child: Card(
        margin: EdgeInsets.zero,
        clipBehavior: Clip.antiAlias,
        color: color,
        child: Center(
          child: Text(
            label,
            style: TextStyle(color: textColor, fontSize: fontSize),
            textAlign: TextAlign.center,
          ),
        ),
      ),
    );
  }
}

@TahaTesser
Copy link
Member

@rydmike
Incredible analysis. I really appreciate it, Slider and RangeSlider are well within my area of focus and I'm working on improving them significantly to my best abilities. This analysis, no doubt will help me.

I agree with your points, it's not just valued conversions but hard-coded animation as well.

@flutter-triage-bot flutter-triage-bot bot added team-design Owned by Design Languages team triaged-design Triaged by Design Languages team labels Jul 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: fidelity Matching the OEM platforms better f: material design flutter/packages/flutter/material repository. found in release: 3.3 Found to occur in 3.3 found in release: 3.7 Found to occur in 3.7 framework flutter/packages/flutter repository. See also f: labels. has reproducible steps The issue has been confirmed reproducible and is ready to work on P2 Important issues not at the top of the work list team-design Owned by Design Languages team triaged-design Triaged by Design Languages team
Projects
Status: Todo
Development

No branches or pull requests