
需求
- 进入自动定位到当前最新时间
- 时间刻针位置保持不变,只能拖拽时间轴改变时间
- 时间轴任意时间都支持拉到时间刻针下
- 拉去到时间轴两边顶端自动回弹
- 支持自动播放(此处是间隔一分钟时间轴动一下)
- 根据不同的时间片段展示不同的颜色
- 停止滑动的时候返回当前的时间
import 'dart:async'
import 'package:camera/utils/log_utils.dart'
import 'package:flutter/material.dart'
class VideoSegment {
final String type
final DateTime startTime
final DateTime endTime
VideoSegment({required this.type, required this.startTime, required this.endTime})
}
class CustomTimeSlider extends StatefulWidget {
final int startHour
final int? startMinute
final List<VideoSegment> videoSegments
final Function(String data) selectTimeCallback
const CustomTimeSlider({
Key? key,
required this.videoSegments,
required this.selectTimeCallback,
required this.startHour,
this.startMinute,
}) : super(key: key)
@override
State<CustomTimeSlider> createState() => _CustomTimeSliderState()
}
class _CustomTimeSliderState extends State<CustomTimeSlider> {
final String tag = "TimeRuler"
// 存储选择的小时
double selectedHour = 0
final ScrollController _scrollController = ScrollController()
double timelineWidth = 2400.0
double timelineHeight = 100.0
late double oneHourWidth
late double oneMinuteWidth
String formattedTime = ''
Timer? _autoScrollTimer
@override
void initState() {
// TODO: implement initState
super.initState()
oneHourWidth = timelineWidth / 24
oneMinuteWidth = timelineWidth / 24 / 60
setOffset(widget.startHour, widget.startMinute ?? 00)
// 添加滚动监听
_scrollController.addListener(_onScroll)
}
//设置初始偏移
setOffset(int setHour, int setMinute) {
Log.d(tag, "setOffset setHour:$setHour setMinute:$setMinute")
Future.delayed(const Duration(milliseconds: 500), () {
// 初始化 selectedHour 为当前时间的小时部分
selectedHour = setHour.toDouble()
Log.d(tag, "setOffset selectedHour:$selectedHour")
// 计算当前时间对应的小时偏移量
double hourOffset = selectedHour * oneHourWidth
// 分钟部分,可以添加分钟的偏移量
hourOffset = hourOffset + setMinute * oneMinuteWidth
Log.d(tag, "setOffset hourOffset:$hourOffset")
// 将滚动控制器的偏移量设置为计算的偏移量
_scrollController.jumpTo(hourOffset)
})
}
//时间轴监听
void _onScroll() {
// 获取当前滚动的位置
double currentPosition = _scrollController.offset
// 计算所选小时和分钟
int newSelectedHour = currentPosition ~/ oneHourWidth
double remainingPixels = currentPosition % oneHourWidth
int selectedMinute = (remainingPixels / oneMinuteWidth).round()
// 更新所选小时
setState(() {
selectedHour = newSelectedHour + selectedMinute / 60
})
// 在此处可以执行其他与滚动相关的操作
setFormatHourMinute(selectedHour)
// 停止之前的自动滚动计时器
_autoScrollTimer?.cancel()
// 获取当前时间的秒数
int currentSecond = DateTime.now().second
// 计算距离下一分钟的秒数
int secondsUntilNextMinute = 60 - currentSecond
// 开始自动滚动计时器,首次延迟到下一分钟的0秒触发,然后每分钟触发一次
_autoScrollTimer = Timer(Duration(seconds: secondsUntilNextMinute), () {
double targetOffset = _scrollController.offset + oneMinuteWidth
Log.d(tag, "_onScroll targetOffset:$targetOffset")
_scrollController.animateTo(
targetOffset,
duration: const Duration(milliseconds: 500),
curve: Curves.linear,
)
// 再次设置计时器,以在下一分钟触发滚动
_autoScrollTimer = Timer.periodic(const Duration(minutes: 1), (timer) {
double targetOffset = _scrollController.offset + oneMinuteWidth
_scrollController.animateTo(
targetOffset,
duration: const Duration(milliseconds: 500),
curve: Curves.linear,
)
})
})
}
//将时间转变为hh:mm
setFormatHourMinute(double hour) {
int hourInt = hour.toInt()
int minute = ((hour - hourInt) * 60).toInt()
String hourString = hourInt.toString().padLeft(2, '0')
String minuteString = minute.toString().padLeft(2, '0')
if (hourString == "24" || hourString == "00") {
//时间轴被滑动到两端尽头
minuteString = "00"
}
formattedTime = '$hourString:$minuteString'
// Log.d(tag, "setFormatHourMinute formattedTime:$formattedTime")
}
@override
void dispose() {
_autoScrollTimer?.cancel()
// 移除滚动监听
_scrollController.removeListener(_onScroll)
_scrollController.dispose()
super.dispose()
}
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
// ScrollStartNotification:当滚动开始时触发的通知。
// ScrollUpdateNotification:当滚动位置更新时触发的通知。
// OverscrollNotification:当滚动超出边界时触发的通知。
// ScrollCancelNotification:当滚动被取消时触发的通知
// ScrollEndNotification:当滚动停止时触发的通知
if (notification is ScrollEndNotification) {
Log.d(tag, "notification dragDetails:${notification.dragDetails}")
if (notification.dragDetails != null) {
// 用户停止滑动时间轴,可以在这里执行您的操作 例如,获取所选时间并触发回调函数
//notification.dragDetails 自动滑动时为null
widget.selectTimeCallback(formattedTime)
}
}
return false
},
child: Column(
children: [
// Text("选择的时间是:$formattedTime"),
SizedBox(
height: 50,
child: Stack(
children: [
SizedBox(
width: MediaQuery.of(context).size.width,
height: 50,
child: ListView.builder(
scrollDirection: Axis.horizontal,
controller: _scrollController,
itemBuilder: (context, index) {
return Row(
children: [
SizedBox(width: MediaQuery.of(context).size.width / 2, height: timelineHeight),
CustomPaint(
size: Size(timelineWidth, timelineHeight), // 调整大小以适应您的需求
painter: RulerPainter(videoSegments: widget.videoSegments),
),
SizedBox(width: MediaQuery.of(context).size.width / 2, height: timelineHeight),
],
)
},
//回弹效果滚动
physics: const BouncingScrollPhysics(),
itemCount: 1,
),
),
Positioned(
top: 0,
left: MediaQuery.of(context).size.width / 2,
child: GestureDetector(
child: Container(
width: 1, // 时间刻针的宽度
height: 50, // 时间刻针的高度,与时间标尺高度一致
color: Colors.red, // 时间刻针的颜色
),
),
),
],
),
),
],
),
)
}
}
class RulerPainter extends CustomPainter {
final List<VideoSegment> videoSegments
RulerPainter({required this.videoSegments})
@override
void paint(Canvas canvas, Size size) {
final double width = size.width
final double height = size.height
const int numberPieces = 24
final double hourWidth = width / numberPieces
final double minuteWidth = hourWidth / 60
final Paint linePaint = Paint()
..color = Colors.black
..strokeWidth = 1.0
final Paint backgroundPaint = Paint()..color = Colors.grey
final TextPainter textPainter = TextPainter(textDirection: TextDirection.ltr)
TextStyle textStyle = const TextStyle(color: Colors.black, fontSize: 12)
//绘制背景颜色
canvas.drawRect(
Rect.fromPoints(const Offset(0, 0), Offset(size.width, size.height)),
backgroundPaint,
)
// 绘制刻度线和时间文本
for (int hour = 0
final double x = hour * hourWidth
for (int minuteData = 0
// 计算当前分钟对应的总分钟数
int currentMinute = hour * 60 + minuteData
final double minuteX = x + minuteData * minuteWidth
// 根据视频片段设置颜色
for (VideoSegment segment in videoSegments) {
// 计算视频段的分钟范围
int startMinute = segment.startTime.hour * 60 + segment.startTime.minute
int endMinute = segment.endTime.hour * 60 + segment.endTime.minute
if (currentMinute >= startMinute && currentMinute <= endMinute) {
if (segment.type == 'normal') {
backgroundPaint.color = Colors.green
} else if (segment.type == 'ai') {
backgroundPaint.color = Colors.greenAccent
}
// 添加其他类型的颜色设置
canvas.drawRect(
Rect.fromPoints(Offset(x + segment.startTime.minute * minuteWidth, 0), Offset(minuteX, size.height)),
backgroundPaint,
)
}
}
}
// 绘制刻度线
canvas.drawLine(Offset(x, 0), Offset(x, height * 0.7), linePaint)
// 显示时间文本
final String timeText = '$hour:00'
textPainter.text = TextSpan(text: timeText, style: textStyle)
textPainter.layout()
textPainter.paint(canvas, Offset(x - textPainter.width / 2, height * 0.7))
if (hour < 24) {
// 绘制小刻度线
for (int minute = 10
final double minuteX = x + minute * minuteWidth
if (minute % 30 == 0) {
canvas.drawLine(Offset(minuteX, height * 0.3), Offset(minuteX, height * 0.7), linePaint)
// 显示分钟文本
final String minuteText = '$hour:${minute.toString().padLeft(2, '0')}'
textPainter.text = TextSpan(text: minuteText, style: textStyle)
textPainter.layout()
textPainter.paint(canvas, Offset(minuteX - textPainter.width / 2, height * 0.7))
} else {
canvas.drawLine(Offset(minuteX, height * 0.5), Offset(minuteX, height * 0.7), linePaint)
}
}
}
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
//禁止其父级小部件的状态更改时重新绘制。
// return false
if (oldDelegate is RulerPainter) {
//videoSegments值需要回执的数据
// 比较旧的和新的videoSegments值,以确定是否需要重绘。
return !listEquals(videoSegments, oldDelegate.videoSegments)
}
return true
}
}