Skip to content

Commit 4400736

Browse files
committed
Show a slider to adjust focus timer
1 parent 8c89cb3 commit 4400736

File tree

5 files changed

+188
-93
lines changed

5 files changed

+188
-93
lines changed

.vscode/settings.json

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"cSpell.words": [
3+
"audioplayers",
34
"Cupertino"
45
]
56
}

lib/src/utils/constants.dart

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import 'package:flutter/material.dart';
22

3+
// Doubles
4+
const double kSliderHeight = 20;
5+
const double kInactiveSliderWidth = 1;
6+
const double kActiveSliderWidth = 2;
7+
38
// Colors
49
const Color kForegroundColor = Color(0xFFE9E9E9);
510
const Color kBackgroundColor = Color(0xff222529);

lib/src/utils/extensions.dart

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
extension DurationExtension on Duration {
22
/// Method to convert the duration to mm:ss format.
3-
String formatToMMss() {
3+
String formatToHHMMss() {
4+
final int hours = inHours.remainder(60);
45
final int minutes = inMinutes.remainder(60);
56
final int seconds = inSeconds.remainder(60);
7+
8+
final String formattedHours = hours > 9 ? hours.toString().padLeft(2, '0') : hours.toString();
69
final String formattedMinutes = minutes.toString().padLeft(2, '0');
710
final String formattedSeconds = seconds.toString().padLeft(2, '0');
11+
12+
if (hours > 0) {
13+
return '$formattedHours:$formattedMinutes:$formattedSeconds';
14+
}
815
return '$formattedMinutes:$formattedSeconds';
916
}
1017
}

lib/src/views/timer_view.dart

+123-92
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
66
import 'package:velocity_x/velocity_x.dart';
77

88
import '../utils/utils_barrel.dart';
9+
import '../widgets/vertical_time_slider.dart';
910

1011
class TimerView extends StatefulWidget {
1112
const TimerView({super.key});
@@ -19,6 +20,7 @@ class _TimerViewState extends State<TimerView> {
1920
Duration _lastUsedDuration = kDefaultDuration;
2021
Duration _duration = kDefaultDuration;
2122
TimerState _timerState = TimerState.stopped;
23+
int _sliderValue = kDefaultDuration.inMinutes;
2224

2325
final _player = AudioPlayer();
2426
final _playerAssetSource = AssetSource('sounds/wrist-watch-beep.mp3');
@@ -87,104 +89,133 @@ class _TimerViewState extends State<TimerView> {
8789
Widget build(BuildContext context) {
8890
return Material(
8991
color: kBackgroundColor,
90-
child: <Widget>[
91-
SizedBox(
92-
height: 20,
93-
child: Row(
94-
children: List.generate(
95-
240,
96-
(index) {
97-
final isInvisible = index % 2 != 0;
98-
final twentyFiveIndicator = index == 50;
99-
100-
return Container(
101-
width: twentyFiveIndicator ? 1.5 : 1,
102-
height: 20,
103-
color: isInvisible
104-
? Colors.transparent
105-
: twentyFiveIndicator
106-
? kForegroundColor
107-
: kTimeSliderInactiveColor,
108-
);
109-
},
110-
),
111-
),
112-
),
113-
const SizedBox(height: 16),
114-
Row(
115-
children: [
116-
_buildSuggestionButtons(
117-
'5m',
118-
() {
119-
setState(() {
120-
_duration = const Duration(minutes: 5);
121-
_startTimer();
122-
});
123-
},
124-
),
125-
const SizedBox(width: 18),
126-
_buildSuggestionButtons(
127-
'15m',
128-
() {
129-
setState(() {
130-
_duration = const Duration(minutes: 15);
131-
_startTimer();
132-
});
133-
},
134-
),
135-
const SizedBox(width: 18),
136-
_buildSuggestionButtons(
137-
'25m',
138-
() {
139-
setState(() {
140-
_duration = const Duration(minutes: 25);
141-
_startTimer();
142-
});
143-
},
144-
),
145-
const Spacer(),
146-
CupertinoButton(
147-
child: const Icon(
148-
CupertinoIcons.ellipsis,
149-
size: 12,
150-
color: Color(0xFF9DA3A7),
92+
child: Padding(
93+
padding: const EdgeInsets.all(12),
94+
child: Column(
95+
children: <Widget>[
96+
SizedBox(
97+
// For safety only
98+
height: kSliderHeight,
99+
child: LayoutBuilder(
100+
builder: (_, constraints) {
101+
final width = constraints.maxWidth.floor();
102+
103+
return GestureDetector(
104+
behavior: HitTestBehavior.opaque,
105+
onHorizontalDragUpdate: (details) {
106+
if (details.localPosition.dx > width + 20) {
107+
return;
108+
}
109+
setState(() {
110+
_sliderValue = ((details.localPosition.dx).clamp(0, width - 1) / 2).floor();
111+
_duration = Duration(minutes: _sliderValue);
112+
});
113+
},
114+
onTapDown: (details) {
115+
setState(() {
116+
_sliderValue = ((details.localPosition.dx).clamp(0, width - 1) / 2).floor();
117+
_duration = Duration(minutes: _sliderValue);
118+
});
119+
},
120+
child: Stack(
121+
alignment: Alignment.center,
122+
children: [
123+
// Slider Base
124+
const IgnorePointer(
125+
child: VerticalTimeSlider(),
126+
),
127+
// Indicator
128+
Positioned(
129+
left: _sliderValue.toDouble() * 2,
130+
child: Container(
131+
height: kSliderHeight,
132+
width: kActiveSliderWidth,
133+
color: kForegroundColor,
134+
),
135+
),
136+
],
137+
),
138+
);
139+
},
151140
),
152-
minSize: 0,
153-
padding: EdgeInsets.zero,
154-
onPressed: () {},
155141
),
156-
],
157-
),
158-
const Spacer(),
159-
Row(
160-
crossAxisAlignment: CrossAxisAlignment.end,
161-
children: [
162-
CupertinoButton(
163-
child: () {
164-
if (_timerState == TimerState.running) {
165-
return 'stop';
166-
}
167-
return 'start';
168-
}()
169-
.text
170-
.size(13)
171-
.color(kForegroundColor)
172-
.make(),
173-
padding: EdgeInsets.zero,
174-
minSize: 0,
175-
onPressed: () async {
176-
if (_timerState == TimerState.running) {
177-
_cancelTimer(TimerState.stopped);
178-
} else {
179-
_startTimer();
180-
}
181-
},
142+
const SizedBox(height: 16),
143+
Row(
144+
children: [
145+
_buildSuggestionButtons(
146+
'5m',
147+
() {
148+
setState(() {
149+
_duration = const Duration(minutes: 5);
150+
_startTimer();
151+
});
152+
},
153+
),
154+
const SizedBox(width: 16),
155+
_buildSuggestionButtons(
156+
'15m',
157+
() {
158+
setState(() {
159+
_duration = const Duration(minutes: 15);
160+
_startTimer();
161+
});
162+
},
163+
),
164+
const SizedBox(width: 16),
165+
_buildSuggestionButtons(
166+
'25m',
167+
() {
168+
setState(() {
169+
_duration = const Duration(minutes: 25);
170+
_startTimer();
171+
});
172+
},
173+
),
174+
const Spacer(),
175+
CupertinoButton(
176+
child: const Icon(
177+
CupertinoIcons.ellipsis,
178+
size: 12,
179+
color: Color(0xFF9DA3A7),
180+
),
181+
minSize: 0,
182+
padding: EdgeInsets.zero,
183+
onPressed: () {},
184+
),
185+
],
182186
),
183187
const Spacer(),
184-
_duration.formatToMMss().text.size(32).light.color(kForegroundColor).make(),
188+
Row(
189+
crossAxisAlignment: CrossAxisAlignment.end,
190+
children: [
191+
CupertinoButton(
192+
child: () {
193+
if (_timerState == TimerState.running) {
194+
return 'stop';
195+
}
196+
return 'start';
197+
}()
198+
.text
199+
.size(13)
200+
.color(kForegroundColor)
201+
.make(),
202+
padding: EdgeInsets.zero,
203+
minSize: 0,
204+
onPressed: () async {
205+
if (_timerState == TimerState.running) {
206+
_cancelTimer(TimerState.stopped);
207+
} else {
208+
_startTimer();
209+
}
210+
},
211+
),
212+
const Spacer(),
213+
_duration.formatToHHMMss().text.size(32).light.color(kForegroundColor).make(),
214+
],
215+
),
185216
],
186217
),
187-
].vStack().p8(),
218+
),
188219
);
189220
}
190221

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import 'package:flutter/material.dart';
2+
3+
import '../utils/utils_barrel.dart';
4+
5+
class VerticalTimeSlider extends StatelessWidget {
6+
const VerticalTimeSlider({
7+
super.key,
8+
this.color,
9+
this.thickness = 1.0,
10+
});
11+
12+
final Color? color;
13+
final double thickness;
14+
15+
@override
16+
Widget build(BuildContext context) {
17+
return CustomPaint(
18+
size: Size(
19+
double.infinity,
20+
thickness,
21+
),
22+
painter: _VerticalSliderPainter(),
23+
);
24+
}
25+
}
26+
27+
class _VerticalSliderPainter extends CustomPainter {
28+
@override
29+
void paint(Canvas canvas, Size size) {
30+
const double dashWidth = kInactiveSliderWidth; // Width
31+
const double dashSpace = kInactiveSliderWidth;
32+
33+
final Paint paint = Paint()
34+
..color = kTimeSliderInactiveColor
35+
..strokeWidth = kSliderHeight // Height
36+
..style = PaintingStyle.stroke;
37+
38+
double currentX = 0;
39+
while (currentX < size.width) {
40+
canvas.drawLine(
41+
Offset(currentX, 0),
42+
Offset(currentX + dashWidth, 0),
43+
paint,
44+
);
45+
currentX += dashWidth + dashSpace;
46+
}
47+
}
48+
49+
@override
50+
bool shouldRepaint(_) => false;
51+
}

0 commit comments

Comments
 (0)