timePickerTheme static method

TimePickerThemeData timePickerTheme({
  1. required ColorScheme colorScheme,
  2. Color? backgroundColor,
  3. SchemeColor? backgroundSchemeColor,
  4. double? elevation,
  5. double? radius,
  6. double? elementRadius,
  7. InputDecorationTheme? inputDecorationTheme,
  8. bool? useInputDecoratorTheme,
  9. TextStyle? dayPeriodTextStyle,
  10. TextStyle? dialTextStyle,
  11. TextStyle? helpTextStyle,
  12. TextStyle? hourMinuteTextStyle,
  13. bool? useMaterial3,
})

An opinionated TimePickerThemeData with custom corner radius.

Corner radius defaults to kDialogRadius 28dp. The internal shapes in the picker also have rounding, their corner radii are defined by elementRadius that defaults to kCardRadius 12.

In the InputDecorator, if you pass it an input decoration style that matches the main input decoration style and corner rounding it will be used on the data entry elements in the picker.

Implementation

static TimePickerThemeData timePickerTheme({
  /// Typically the same `ColorScheme` that is also used for your `ThemeData`.
  required final ColorScheme colorScheme,

  /// Dialog background color.
  ///
  /// If null and [backgroundSchemeColor] is also null, then it
  /// gets default via Dialog's default null theme behavior.
  ///
  /// If [backgroundSchemeColor] is defined, it will override any color
  /// passed in here.
  ///
  /// Can be used to make a custom themed dialog with own background color,
  /// even after the [ThemeData.dialogBackgroundColor] property is
  /// is deprecated in Flutter SDK. See
  /// https://github.com/flutter/flutter/issues/91772).
  final Color? backgroundColor,

  /// Selects which color from the passed in colorScheme to use as the dialog
  /// background color.
  ///
  /// If not defined, then the passed in [backgroundColor] will be used,
  /// which may be null too and dialog then falls back to Flutter SDK default
  /// value for TimePickerDialog, which is [colorScheme.surface].
  ///
  /// FlexColorScheme sub-theming uses this property to match the background
  /// color of this dialog to other standard dialogs. It sets it via
  /// [FlexSubThemesData] to [SchemeColor.surface].
  final SchemeColor? backgroundSchemeColor,

  /// Dialog elevation.
  ///
  /// If not defined, defaults to [kDialogElevation] = 6.
  final double? elevation,

  /// Outer corner radius.
  ///
  /// If not defined, defaults to [kDialogRadius] 28dp,
  /// based on M3 Specification
  /// https://m3.material.io/components/dialogs/specs
  final double? radius,

  /// Elements corner radius.
  ///
  /// Defaults to [kTimeElementRadius] = 8.
  final double? elementRadius,

  /// An input decoration theme, for the time picker.
  ///
  /// You would typically pass in one that matches the main used input
  /// decoration theme in order to get same input style with possible
  /// rounding used in the app otherwise on the input fields in the picker.
  ///
  /// It adds the custom overrides to the passed in decorator, that the widget
  /// does internally to the default null InputDecorationTheme. There is
  /// no need to add those in the passed in InputDecorationTheme. Just pass
  /// in your overall used app InputDecorationTheme.
  final InputDecorationTheme? inputDecorationTheme,

  /// Set to true to not use the provided [inputDecorationTheme].
  ///
  /// If this flag is false, the provided [inputDecorationTheme] is not used,
  /// additionally the theme fix this theme helper does internally is
  /// not applied and pure null value is passed. This enables getting the
  /// default widget behavior input decorator, or opting in on getting the
  /// provided inputDecorationTheme with the internal style fix for issue
  /// https://github.com/flutter/flutter/issues/54104 applied automatically
  /// to the provided inputDecorationTheme.
  ///
  /// If not defined, defaults to false.
  final bool? useInputDecoratorTheme,

  /// Used to configure the [TextStyle]s for the day period control.
  ///
  /// If this is null, the time picker defaults to the overall theme's
  /// [TextTheme.titleMedium].
  final TextStyle? dayPeriodTextStyle,

  /// The [TextStyle] for the numbers on the time selection dial.
  ///
  /// If [dialTextStyle]'s [TextStyle.color] is a [WidgetStateColor], then
  /// the effective text color can depend on the [WidgetState.selected]
  /// state, i.e. if the text is selected or not.
  ///
  /// If this style is null then the dial's text style is based on the theme's
  /// [ThemeData.textTheme].
  final TextStyle? dialTextStyle,

  /// Used to configure the [TextStyle]s for the helper text in the header.
  ///
  /// If this is null, the time picker defaults to the overall theme's
  /// [TextTheme.labelSmall].
  final TextStyle? helpTextStyle,

  /// Used to configure the [TextStyle]s for the hour/minute controls.
  ///
  /// If this is null, the time picker defaults to the overall theme's
  /// [TextTheme.headline3].
  final TextStyle? hourMinuteTextStyle,

  /// A temporary flag used to disable Material-3 design and use legacy
  /// Material-2 design instead. Material-3 design is the default.
  /// Material-2 will be deprecated in Flutter.
  ///
  /// If set to true, the theme will use Material3 default styles when
  /// properties are undefined, if false defaults will use FlexColorScheme's
  /// own opinionated default values.
  ///
  /// The M2/M3 defaults will only be used for properties that are not
  /// defined, if defined they keep their defined values.
  ///
  /// If undefined, defaults to true.
  final bool? useMaterial3,
}) {
  final bool useM3 = useMaterial3 ?? true;
  final bool useDecorator = useInputDecoratorTheme ?? false;

  final Color? background = backgroundSchemeColor == null
      ? backgroundColor // might be null, then SDK theme defaults.
      : schemeColor(backgroundSchemeColor, colorScheme);

  Color defaultHourMinuteColor() {
    return WidgetStateColor.resolveWith((Set<WidgetState> states) {
      // These styles are copied for M# default, we are not going to test
      // them again.
      // coverage:ignore-start
      if (states.contains(WidgetState.selected)) {
        Color overlayColor = colorScheme.primaryContainer;
        if (states.contains(WidgetState.pressed)) {
          overlayColor = colorScheme.onPrimaryContainer;
        } else if (states.contains(WidgetState.hovered)) {
          const double hoverOpacity = 0.08;
          overlayColor =
              colorScheme.onPrimaryContainer.withOpacity(hoverOpacity);
        } else if (states.contains(WidgetState.focused)) {
          const double focusOpacity = 0.12;
          overlayColor =
              colorScheme.onPrimaryContainer.withOpacity(focusOpacity);
        }
        return Color.alphaBlend(overlayColor, colorScheme.primaryContainer);
      } else {
        Color overlayColor = colorScheme.surfaceContainerHighest;
        if (states.contains(WidgetState.pressed)) {
          overlayColor = colorScheme.onSurface;
        } else if (states.contains(WidgetState.hovered)) {
          const double hoverOpacity = 0.08;
          overlayColor = colorScheme.onSurface.withOpacity(hoverOpacity);
        } else if (states.contains(WidgetState.focused)) {
          const double focusOpacity = 0.12;
          overlayColor = colorScheme.onSurface.withOpacity(focusOpacity);
        }
        return Color.alphaBlend(
            overlayColor, colorScheme.surfaceContainerHighest);
      }
      // coverage:ignore-end
    });
  }

  InputDecorationTheme timePickerDefaultInputDecorationTheme() {
    const BorderRadius defaultRadius = BorderRadius.all(Radius.circular(8.0));
    return InputDecorationTheme(
      contentPadding: EdgeInsets.zero,
      filled: true,
      hoverColor: colorScheme.brightness == Brightness.dark
          ? Colors.white.withOpacity(0.04)
          : Colors.black.withOpacity(0.04),
      fillColor: defaultHourMinuteColor(),
      focusColor: colorScheme.primaryContainer,
      enabledBorder: const OutlineInputBorder(
        borderRadius: defaultRadius,
        borderSide: BorderSide(color: Colors.transparent),
      ),
      errorBorder: OutlineInputBorder(
        borderRadius: defaultRadius,
        borderSide: BorderSide(color: colorScheme.error, width: 2),
      ),
      focusedBorder: OutlineInputBorder(
        borderRadius: defaultRadius,
        borderSide: BorderSide(color: colorScheme.primary, width: 2),
      ),
      focusedErrorBorder: OutlineInputBorder(
        borderRadius: defaultRadius,
        borderSide: BorderSide(color: colorScheme.error, width: 2),
      ),
      disabledBorder: OutlineInputBorder(
        borderRadius: defaultRadius,
        borderSide: BorderSide(
            color: colorScheme.onSurface.withOpacity(0.12), width: 1),
      ),
      errorStyle: const TextStyle(fontSize: 0, height: 0),
    );
  }

  WidgetStateProperty<Color> dayPeriodForegroundColor() {
    return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
      Color? textColor;
      if (states.contains(WidgetState.selected)) {
        if (states.contains(WidgetState.pressed)) {
          textColor = colorScheme.onTertiaryContainer;
        } else {
          // not pressed
          if (states.contains(WidgetState.focused)) {
            textColor = colorScheme.onTertiaryContainer;
          } else {
            // not focused
            if (states.contains(WidgetState.hovered)) {
              textColor = colorScheme.onTertiaryContainer;
            }
          }
        }
      } else {
        // unselected
        if (states.contains(WidgetState.pressed)) {
          textColor = colorScheme.onSurfaceVariant;
        } else {
          // not pressed
          if (states.contains(WidgetState.focused)) {
            textColor = colorScheme.onSurfaceVariant;
          } else {
            // not focused
            if (states.contains(WidgetState.hovered)) {
              textColor = colorScheme.onSurfaceVariant;
            }
          }
        }
      }
      return textColor ?? colorScheme.onTertiaryContainer;
    });
  }

  return TimePickerThemeData(
    // Optional text styles
    dayPeriodTextStyle: dayPeriodTextStyle,
    dialTextStyle: dialTextStyle,
    helpTextStyle: helpTextStyle,
    hourMinuteTextStyle: hourMinuteTextStyle,
    //
    backgroundColor: background,
    elevation: elevation ?? kDialogElevation,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.all(
        Radius.circular(radius ?? kDialogRadius),
      ),
    ),
    hourMinuteShape: RoundedRectangleBorder(
      borderRadius: BorderRadius.all(
        Radius.circular(elementRadius ?? kTimeElementRadius),
      ),
    ),
    dayPeriodShape: RoundedRectangleBorder(
      borderRadius: BorderRadius.all(
        Radius.circular(elementRadius ?? kTimeElementRadius),
      ),
    ),
    //
    // TODO(rydmike): Fixes for clock dial background color issue in M3.
    // TODO(rydmike): Remove in V8? It has been fixed in Flutter too.
    // https://github.com/flutter/flutter/issues/118657
    dialBackgroundColor: useM3 ? colorScheme.surfaceContainerHighest : null,
    dayPeriodColor: useM3
        ? WidgetStateColor.resolveWith((Set<WidgetState> states) {
            if (states.contains(WidgetState.selected)) {
              return colorScheme.tertiaryContainer;
            }
            return Colors.transparent;
          })
        : null,
    dayPeriodTextColor: useM3
        ? WidgetStateColor.resolveWith((Set<WidgetState> states) {
            return dayPeriodForegroundColor().resolve(states);
          })
        : null,
    // M3 styling Flutter 3.7 does not do this yet, but we can du in M3 mode.
    hourMinuteColor: useM3
        ? WidgetStateColor.resolveWith((Set<WidgetState> states) {
            if (states.contains(WidgetState.selected)) {
              Color overlayColor = colorScheme.primaryContainer;
              if (states.contains(WidgetState.pressed)) {
                overlayColor = colorScheme.onPrimaryContainer;
              } else if (states.contains(WidgetState.focused)) {
                overlayColor =
                    colorScheme.onPrimaryContainer.withAlpha(kAlphaFocused);
              } else if (states.contains(WidgetState.hovered)) {
                overlayColor =
                    colorScheme.onPrimaryContainer.withAlpha(kAlphaHovered);
              }
              return Color.alphaBlend(
                  overlayColor, colorScheme.primaryContainer);
            } else {
              Color overlayColor = colorScheme.surfaceContainerHighest;
              if (states.contains(WidgetState.pressed)) {
                overlayColor = colorScheme.onSurface;
              } else if (states.contains(WidgetState.focused)) {
                overlayColor = colorScheme.onSurface.withAlpha(kAlphaFocused);
              } else if (states.contains(WidgetState.hovered)) {
                overlayColor = colorScheme.onSurface.withAlpha(kAlphaHovered);
              }
              return Color.alphaBlend(
                  overlayColor, colorScheme.surfaceContainerHighest);
            }
          })
        : null,
    //
    // Custom additions the Widget does internally, but for some reason
    // only does to null default theme. If you use a custom decorator
    // you are supposed to know that you have to add these shenanigans
    // for it to work and look right.
    inputDecorationTheme: useDecorator
        ? inputDecorationTheme?.copyWith(
              contentPadding: EdgeInsets.zero,
              // Prevent the error text from appearing.
              // See https://github.com/flutter/flutter/issues/54104
              errorStyle: const TextStyle(fontSize: 0, height: 0),
            ) ??
            const InputDecorationTheme().copyWith(
              contentPadding: EdgeInsets.zero,
              // Prevent the error text from appearing.
              // See https://github.com/flutter/flutter/issues/54104
              errorStyle: const TextStyle(fontSize: 0, height: 0),
            )
        // To get back to a default style, we have to provide an explicit
        // default matching style, very tedious. Read more about this here:
        // https://github.com/flutter/flutter/pull/128950#issuecomment-1657177393
        : timePickerDefaultInputDecorationTheme(),
  );
}