time_ring_picker 0.0.2
time_ring_picker: ^0.0.2 copied to clipboard
A highly customizable circular clock-style time range picker for selecting start/end times (e.g. sleep schedules) with overnight range support.
import 'package:flutter/material.dart';
import 'package:time_ring_picker/time_ring_picker.dart';
void main() => runApp(const ExampleApp());
/// Demo app for `time_ring_picker` — shows a default-styled picker and a
/// fully customized one side by side.
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'time_ring_picker example',
theme: ThemeData.dark(useMaterial3: true),
home: const ExampleHomePage(),
);
}
}
class ExampleHomePage extends StatefulWidget {
const ExampleHomePage({super.key});
@override
State<ExampleHomePage> createState() => _ExampleHomePageState();
}
class _ExampleHomePageState extends State<ExampleHomePage> {
TimeOfDay _start = const TimeOfDay(hour: 21, minute: 0);
TimeOfDay _end = const TimeOfDay(hour: 6, minute: 0);
Duration _duration = const Duration(hours: 9);
// Which half the "Day & Night style" ring below is zoomed into.
// start=21:00 (PM) and end=06:00 (AM) each only show up -- and can
// only be dragged -- while their own half is selected here.
DayHalf _dayNightHalf = DayHalf.pm;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('time_ring_picker')),
body: SafeArea(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
'Default style',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Center(
child: TimeRingPicker(
initialStartTime: _start,
initialEndTime: _end,
onTimeChanged: (start, end, duration) {
setState(() {
_start = start;
_end = end;
_duration = duration;
});
},
),
),
const SizedBox(height: 16),
Text(
'Selected: ${_start.format(context)} - ${_end.format(context)} '
'(${_duration.inHours}h ${_duration.inMinutes % 60}m)',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 32),
Text(
'Fully customized style, 24h format',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Center(
child: TimeRingPicker(
initialStartTime: const TimeOfDay(hour: 22, minute: 30),
initialEndTime: const TimeOfDay(hour: 5, minute: 45),
timeFormat: TimeFormat.h24,
minuteInterval: 15,
style: const TimeRingPickerStyle(
size: 220,
borderWidth: 10,
backgroundColor: Color(0xFF1B1D28),
// Gradient on the selected arc only -- the unselected
// track stays a plain flat backgroundColor, so which
// part is actually selected reads clearly at a
// glance instead of the whole ring being one
// indistinguishable bright gradient.
gradientColors: [Color(0xFFFF7E5F), Color(0xFFFEB47B)],
handleColor: Colors.white,
handleSize: 20,
handleBorderColor: Color(0xFFFF7E5F),
durationTextStyle: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
startEndTimeTextStyle: TextStyle(
fontSize: 13,
color: Colors.white70,
),
showTicks: false,
numberPosition: NumberPosition.inside,
startHandleIcon: Icons.bedtime,
endHandleIcon: Icons.wb_sunny,
handleIconColor: Color(0xFFFF7E5F),
arcGlowBlurRadius: 10,
arcGlowSpread: 1,
arcGlowOpacity: 0.55,
),
onTimeChanged: (start, end, duration) {},
),
),
const SizedBox(height: 32),
Text(
'Day & Night style',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
_AmPmHeaderTabs(
isAm: _dayNightHalf == DayHalf.am,
onChanged: (isAm) => setState(
() => _dayNightHalf = isAm ? DayHalf.am : DayHalf.pm,
),
),
const SizedBox(height: 16),
Center(
child: TimeRingPicker(
initialStartTime: const TimeOfDay(hour: 21, minute: 0),
initialEndTime: const TimeOfDay(hour: 6, minute: 0),
visibleHalf: _dayNightHalf,
style: const TimeRingPickerStyle(
backgroundColor: Color(0xFF141A2E),
// gradientColors[0] paints at the *start* handle
// (moon/night), [1]/[2] toward the *end* handle
// (sun/day) -- gold fading through orange to a deep
// red-orange, gradient on the selected arc only (the
// unselected track stays a plain flat backgroundColor).
gradientColors: [
Color(0xFFFFCB4D),
Color(0xFFFF8B3D),
Color(0xFFE85B3A),
],
arcGlowBlurRadius: 12,
arcGlowSpread: 2,
arcGlowOpacity: 0.5,
handleColor: Colors.white,
handleBorderColor: Color(0xFFFF8B3D),
numberStyle: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.white70,
),
// The moon/sun handle icons and the gradient sweep
// already show which end is night vs day, so a plain
// classic clock face (no repeated-digit caption, no
// extra half-tint) reads cleanest here.
showNumberPeriodCaption: false,
showHalfDayShading: false,
showTicks: false,
startHandleIcon: Icons.nightlight_round,
endHandleIcon: Icons.wb_sunny_rounded,
handleIconColor: Color(0xFF1B2A4A),
durationTextStyle: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
numberPosition: NumberPosition.inside,
startEndTimeTextStyle: TextStyle(
fontSize: 14,
color: Colors.white70,
),
),
onTimeChanged: (start, end, duration) {},
),
),
],
),
),
);
}
}
/// An `AM` / `PM` segmented header, styled to match the "Day & Night
/// style" ring below it, that drives which half that ring's
/// [TimeRingPicker.visibleHalf] is zoomed into.
class _AmPmHeaderTabs extends StatelessWidget {
const _AmPmHeaderTabs({required this.isAm, required this.onChanged});
final bool isAm;
final ValueChanged<bool> onChanged;
static const _activeColor = Color(0xFFFFC371);
static const _inactiveColor = Colors.white38;
@override
Widget build(BuildContext context) {
return Container(
width: 220,
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFF1B2233),
borderRadius: BorderRadius.circular(14),
),
child: Row(
children: [
Expanded(child: _tab('AM', isAm, () => onChanged(true))),
Container(width: 1, height: 20, color: Colors.white24),
Expanded(child: _tab('PM', !isAm, () => onChanged(false))),
],
),
);
}
Widget _tab(String label, bool selected, VoidCallback onTap) {
return InkWell(
onTap: onTap,
child: Text(
label,
textAlign: TextAlign.center,
style: TextStyle(
color: selected ? _activeColor : _inactiveColor,
fontWeight: selected ? FontWeight.bold : FontWeight.w500,
),
),
);
}
}