Files
tongxunruanjian/mobile-next/openim_common/lib/src/widgets/custom_cupertino_controls.dart
T
编码工程师andmultica-agent 0a2fbac92a feat(mobile-next): 导入官方 openim-flutter-demo 固定基线 3.8.3-patch.3 并完成双构建
- 上游: github.com/OpenIMSDK/openim-flutter-demo tag 3.8.3-patch.3
  commit b3dfdb1e8aaeaaf6f0793e10cadd20d5c184a31f
- 并行目录 mobile-next/ 原样导入(含 LICENSE),不覆盖现网 mobile/
- 锁定 Flutter 3.24.5 / Dart 3.5.4 / JDK 17 / Gradle 7.6.3 / AGP 7.3.1
- 实测 Android debug 与 release 构建均成功(见 docs/mobile-next-baseline-import.md)
- 本提交可单独回退:git revert 1db1229(重写前)

Co-authored-by: multica-agent <github@multica.ai>
2026-08-19 01:10:39 +08:00

829 lines
22 KiB
Dart

import 'dart:async';
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:chewie/src/animated_play_pause.dart';
import 'package:chewie/src/center_play_button.dart';
import 'package:chewie/src/chewie_player.dart';
import 'package:chewie/src/chewie_progress_colors.dart';
import 'package:chewie/src/cupertino/cupertino_progress_bar.dart';
import 'package:chewie/src/cupertino/widgets/cupertino_options_dialog.dart';
import 'package:chewie/src/helpers/utils.dart';
import 'package:chewie/src/models/option_item.dart';
import 'package:chewie/src/models/subtitle_model.dart';
import 'package:chewie/src/notifiers/index.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:video_player/video_player.dart';
class CustomCupertinoControls extends StatefulWidget {
const CustomCupertinoControls({
required this.backgroundColor,
required this.iconColor,
this.showPlayButton = true,
Key? key,
}) : super(key: key);
final Color backgroundColor;
final Color iconColor;
final bool showPlayButton;
@override
State<StatefulWidget> createState() {
return _CustomCupertinoControlsState();
}
}
class _CustomCupertinoControlsState extends State<CustomCupertinoControls> with SingleTickerProviderStateMixin {
late PlayerNotifier notifier;
late VideoPlayerValue _latestValue;
double? _latestVolume;
Timer? _hideTimer;
final marginSize = 5.0;
Timer? _expandCollapseTimer;
Timer? _initTimer;
bool _dragging = false;
Duration? _subtitlesPosition;
bool _subtitleOn = false;
Timer? _bufferingDisplayTimer;
bool _displayBufferingIndicator = false;
late VideoPlayerController controller;
ChewieController get chewieController => _chewieController!;
ChewieController? _chewieController;
@override
void initState() {
super.initState();
notifier = Provider.of<PlayerNotifier>(context, listen: false);
}
@override
Widget build(BuildContext context) {
if (_latestValue.hasError) {
return chewieController.errorBuilder != null
? chewieController.errorBuilder!(
context,
chewieController.videoPlayerController.value.errorDescription!,
)
: const Center(
child: Icon(
CupertinoIcons.exclamationmark_circle,
color: Colors.white,
size: 42,
),
);
}
final backgroundColor = widget.backgroundColor;
final iconColor = widget.iconColor;
final orientation = MediaQuery.of(context).orientation;
final barHeight = orientation == Orientation.portrait ? 30.0 : 47.0;
final buttonPadding = orientation == Orientation.portrait ? 16.0 : 24.0;
return MouseRegion(
onHover: (_) => _cancelAndRestartTimer(),
child: GestureDetector(
onTap: () => _cancelAndRestartTimer(),
child: AbsorbPointer(
absorbing: notifier.hideStuff,
child: Stack(
children: [
if (_displayBufferingIndicator)
const Center(
child: CupertinoActivityIndicator(
color: Colors.white,
radius: 15,
),
)
else
_buildHitArea(),
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
_buildTopBar(
backgroundColor,
iconColor,
barHeight,
buttonPadding,
),
const Spacer(),
if (_subtitleOn)
Transform.translate(
offset: Offset(
0.0,
notifier.hideStuff ? barHeight * 0.8 : 0.0,
),
child: _buildSubtitles(chewieController.subtitle!),
),
_buildBottomBar(backgroundColor, iconColor, barHeight),
],
),
],
),
),
),
);
}
@override
void dispose() {
_dispose();
super.dispose();
}
void _dispose() {
controller.removeListener(_updateState);
_hideTimer?.cancel();
_expandCollapseTimer?.cancel();
_initTimer?.cancel();
}
@override
void didChangeDependencies() {
final oldController = _chewieController;
_chewieController = ChewieController.of(context);
controller = chewieController.videoPlayerController;
if (oldController != chewieController) {
_dispose();
_initialize();
}
super.didChangeDependencies();
}
GestureDetector _buildOptionsButton(
Color iconColor,
double barHeight,
) {
final options = <OptionItem>[];
if (chewieController.additionalOptions != null && chewieController.additionalOptions!(context).isNotEmpty) {
options.addAll(chewieController.additionalOptions!(context));
}
return GestureDetector(
onTap: () async {
_hideTimer?.cancel();
if (chewieController.optionsBuilder != null) {
await chewieController.optionsBuilder!(context, options);
} else {
await showCupertinoModalPopup<OptionItem>(
context: context,
semanticsDismissible: true,
useRootNavigator: chewieController.useRootNavigator,
builder: (context) => CupertinoOptionsDialog(
options: options,
cancelButtonText: chewieController.optionsTranslation?.cancelButtonText,
),
);
if (_latestValue.isPlaying) {
_startHideTimer();
}
}
},
child: Container(
height: barHeight,
color: Colors.transparent,
padding: const EdgeInsets.only(left: 4.0, right: 8.0),
margin: const EdgeInsets.only(right: 6.0),
child: Icon(
Icons.more_vert,
color: iconColor,
size: 18,
),
),
);
}
Widget _buildSubtitles(Subtitles subtitles) {
if (!_subtitleOn) {
return const SizedBox();
}
if (_subtitlesPosition == null) {
return const SizedBox();
}
final currentSubtitle = subtitles.getByPosition(_subtitlesPosition!);
if (currentSubtitle.isEmpty) {
return const SizedBox();
}
if (chewieController.subtitleBuilder != null) {
return chewieController.subtitleBuilder!(
context,
currentSubtitle.first!.text,
);
}
return Padding(
padding: EdgeInsets.only(left: marginSize, right: marginSize),
child: Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: const Color(0x96000000),
borderRadius: BorderRadius.circular(10.0),
),
child: Text(
currentSubtitle.first!.text.toString(),
style: const TextStyle(
fontSize: 18,
),
textAlign: TextAlign.center,
),
),
);
}
Widget _buildBottomBar(
Color backgroundColor,
Color iconColor,
double barHeight,
) {
return SafeArea(
bottom: chewieController.isFullScreen,
minimum: chewieController.controlsSafeAreaMinimum,
child: AnimatedOpacity(
opacity: notifier.hideStuff ? 0.0 : 1.0,
duration: const Duration(milliseconds: 300),
child: Container(
color: Colors.transparent,
alignment: Alignment.bottomCenter,
margin: EdgeInsets.all(marginSize),
child: ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: BackdropFilter(
filter: ui.ImageFilter.blur(
sigmaX: 10.0,
sigmaY: 10.0,
),
child: Container(
height: barHeight,
color: backgroundColor,
child: chewieController.isLive
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
_buildPlayPause(controller, iconColor, barHeight),
_buildLive(iconColor),
],
)
: Row(
children: <Widget>[
_buildPlayPause(controller, iconColor, barHeight),
_buildPosition(iconColor),
_buildProgressBar(),
_buildRemaining(iconColor),
_buildSubtitleToggle(iconColor, barHeight),
if (chewieController.allowPlaybackSpeedChanging)
_buildSpeedButton(controller, iconColor, barHeight),
if (chewieController.additionalOptions != null &&
chewieController.additionalOptions!(context).isNotEmpty)
_buildOptionsButton(iconColor, barHeight),
],
),
),
),
),
),
),
);
}
Widget _buildLive(Color iconColor) {
return Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Text(
'LIVE',
style: TextStyle(color: iconColor, fontSize: 12.0),
),
);
}
GestureDetector _buildExpandButton(
Color backgroundColor,
Color iconColor,
double barHeight,
double buttonPadding,
) {
return GestureDetector(
onTap: _onExpandCollapse,
child: AnimatedOpacity(
opacity: notifier.hideStuff ? 0.0 : 1.0,
duration: const Duration(milliseconds: 300),
child: ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 10.0),
child: Container(
height: barHeight,
padding: EdgeInsets.only(
left: buttonPadding,
right: buttonPadding,
),
color: backgroundColor,
child: Center(
child: Icon(
chewieController.isFullScreen
? CupertinoIcons.arrow_down_right_arrow_up_left
: CupertinoIcons.arrow_up_left_arrow_down_right,
color: iconColor,
size: 16,
),
),
),
),
),
),
);
}
Widget _buildHitArea() {
final bool isFinished = _latestValue.position >= _latestValue.duration;
final bool showPlayButton = widget.showPlayButton && !_latestValue.isPlaying && !_dragging;
return GestureDetector(
onTap: _latestValue.isPlaying
? _cancelAndRestartTimer
: () {
_hideTimer?.cancel();
setState(() {
notifier.hideStuff = false;
});
},
child: CenterPlayButton(
backgroundColor: widget.backgroundColor,
iconColor: widget.iconColor,
isFinished: isFinished,
isPlaying: controller.value.isPlaying,
show: showPlayButton,
onPressed: _playPause,
),
);
}
GestureDetector _buildMuteButton(
VideoPlayerController controller,
Color backgroundColor,
Color iconColor,
double barHeight,
double buttonPadding,
) {
return GestureDetector(
onTap: () {
_cancelAndRestartTimer();
if (_latestValue.volume == 0) {
controller.setVolume(_latestVolume ?? 0.5);
} else {
_latestVolume = controller.value.volume;
controller.setVolume(0.0);
}
},
child: AnimatedOpacity(
opacity: notifier.hideStuff ? 0.0 : 1.0,
duration: const Duration(milliseconds: 300),
child: ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 10.0),
child: ColoredBox(
color: backgroundColor,
child: Container(
height: barHeight,
padding: EdgeInsets.only(
left: buttonPadding,
right: buttonPadding,
),
child: Icon(
_latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off,
color: iconColor,
size: 16,
),
),
),
),
),
),
);
}
GestureDetector _buildPlayPause(
VideoPlayerController controller,
Color iconColor,
double barHeight,
) {
return GestureDetector(
onTap: _playPause,
child: Container(
height: barHeight,
color: Colors.transparent,
padding: const EdgeInsets.only(
left: 6.0,
right: 6.0,
),
child: AnimatedPlayPause(
color: widget.iconColor,
playing: controller.value.isPlaying,
),
),
);
}
Widget _buildPosition(Color iconColor) {
final position = _latestValue.position;
return Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Text(
formatDuration(position),
style: TextStyle(
color: iconColor,
fontSize: 12.0,
),
),
);
}
Widget _buildRemaining(Color iconColor) {
final position = _latestValue.duration - _latestValue.position;
return Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Text(
'-${formatDuration(position)}',
style: TextStyle(color: iconColor, fontSize: 12.0),
),
);
}
Widget _buildSubtitleToggle(Color iconColor, double barHeight) {
if (chewieController.subtitle?.isEmpty ?? true) {
return const SizedBox();
}
return GestureDetector(
onTap: _subtitleToggle,
child: Container(
height: barHeight,
color: Colors.transparent,
margin: const EdgeInsets.only(right: 10.0),
padding: const EdgeInsets.only(
left: 6.0,
right: 6.0,
),
child: Icon(
Icons.subtitles,
color: _subtitleOn ? iconColor : Colors.grey[700],
size: 16.0,
),
),
);
}
void _subtitleToggle() {
setState(() {
_subtitleOn = !_subtitleOn;
});
}
GestureDetector _buildSkipBack(Color iconColor, double barHeight) {
return GestureDetector(
onTap: _skipBack,
child: Container(
height: barHeight,
color: Colors.transparent,
margin: const EdgeInsets.only(left: 10.0),
padding: const EdgeInsets.only(
left: 6.0,
right: 6.0,
),
child: Icon(
CupertinoIcons.gobackward_15,
color: iconColor,
size: 18.0,
),
),
);
}
GestureDetector _buildSkipForward(Color iconColor, double barHeight) {
return GestureDetector(
onTap: _skipForward,
child: Container(
height: barHeight,
color: Colors.transparent,
padding: const EdgeInsets.only(
left: 6.0,
right: 8.0,
),
margin: const EdgeInsets.only(
right: 8.0,
),
child: Icon(
CupertinoIcons.goforward_15,
color: iconColor,
size: 18.0,
),
),
);
}
GestureDetector _buildSpeedButton(
VideoPlayerController controller,
Color iconColor,
double barHeight,
) {
return GestureDetector(
onTap: () async {
_hideTimer?.cancel();
final chosenSpeed = await showCupertinoModalPopup<double>(
context: context,
semanticsDismissible: true,
useRootNavigator: chewieController.useRootNavigator,
builder: (context) => _PlaybackSpeedDialog(
speeds: chewieController.playbackSpeeds,
selected: _latestValue.playbackSpeed,
),
);
if (chosenSpeed != null) {
controller.setPlaybackSpeed(chosenSpeed);
}
if (_latestValue.isPlaying) {
_startHideTimer();
}
},
child: Container(
height: barHeight,
color: Colors.transparent,
padding: const EdgeInsets.only(
left: 6.0,
right: 8.0,
),
margin: const EdgeInsets.only(
right: 8.0,
),
child: Transform(
alignment: Alignment.center,
transform: Matrix4.skewY(0.0)
..rotateX(math.pi)
..rotateZ(math.pi * 0.8),
child: Icon(
Icons.speed,
color: iconColor,
size: 18.0,
),
),
),
);
}
Widget _buildTopBar(
Color backgroundColor,
Color iconColor,
double barHeight,
double buttonPadding,
) {
return Container(
height: barHeight,
margin: EdgeInsets.only(
top: marginSize,
right: marginSize,
left: marginSize,
),
child: Row(
children: <Widget>[
if (chewieController.allowFullScreen)
_buildExpandButton(
backgroundColor,
iconColor,
barHeight,
buttonPadding,
),
],
),
);
}
void _cancelAndRestartTimer() {
_hideTimer?.cancel();
setState(() {
notifier.hideStuff = false;
_startHideTimer();
});
}
Future<void> _initialize() async {
_subtitleOn = chewieController.subtitle?.isNotEmpty ?? false;
controller.addListener(_updateState);
_updateState();
if (controller.value.isPlaying || chewieController.autoPlay) {
_startHideTimer();
}
if (chewieController.showControlsOnInitialize) {
_initTimer = Timer(const Duration(milliseconds: 200), () {
setState(() {
notifier.hideStuff = false;
});
});
}
}
void _onExpandCollapse() {
setState(() {
notifier.hideStuff = true;
chewieController.toggleFullScreen();
_expandCollapseTimer = Timer(const Duration(milliseconds: 300), () {
setState(() {
_cancelAndRestartTimer();
});
});
});
}
Widget _buildProgressBar() {
return Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 12.0),
child: CupertinoVideoProgressBar(
controller,
onDragStart: () {
setState(() {
_dragging = true;
});
_hideTimer?.cancel();
},
onDragUpdate: () {
_hideTimer?.cancel();
},
onDragEnd: () {
setState(() {
_dragging = false;
});
_startHideTimer();
},
colors: chewieController.cupertinoProgressColors ??
ChewieProgressColors(
playedColor: const Color.fromARGB(
120,
255,
255,
255,
),
handleColor: const Color.fromARGB(
255,
255,
255,
255,
),
bufferedColor: const Color.fromARGB(
60,
255,
255,
255,
),
backgroundColor: const Color.fromARGB(
20,
255,
255,
255,
),
),
),
),
);
}
void _playPause() {
final isFinished = _latestValue.position >= _latestValue.duration;
setState(() {
if (controller.value.isPlaying) {
notifier.hideStuff = false;
_hideTimer?.cancel();
controller.pause();
} else {
_cancelAndRestartTimer();
if (!controller.value.isInitialized) {
controller.initialize().then((_) {
controller.play();
});
} else {
if (isFinished) {
controller.seekTo(Duration.zero);
}
controller.play();
}
}
});
}
void _skipBack() {
_cancelAndRestartTimer();
final beginning = Duration.zero.inMilliseconds;
final skip = (_latestValue.position - const Duration(seconds: 15)).inMilliseconds;
controller.seekTo(Duration(milliseconds: math.max(skip, beginning)));
}
void _skipForward() {
_cancelAndRestartTimer();
final end = _latestValue.duration.inMilliseconds;
final skip = (_latestValue.position + const Duration(seconds: 15)).inMilliseconds;
controller.seekTo(Duration(milliseconds: math.min(skip, end)));
}
void _startHideTimer() {
final hideControlsTimer = chewieController.hideControlsTimer.isNegative
? ChewieController.defaultHideControlsTimer
: chewieController.hideControlsTimer;
_hideTimer = Timer(hideControlsTimer, () {
setState(() {
notifier.hideStuff = true;
});
});
}
void _bufferingTimerTimeout() {
_displayBufferingIndicator = true;
if (mounted) {
setState(() {});
}
}
void _updateState() {
if (!mounted) return;
if (chewieController.progressIndicatorDelay != null) {
if (controller.value.isBuffering) {
_bufferingDisplayTimer ??= Timer(
chewieController.progressIndicatorDelay!,
_bufferingTimerTimeout,
);
} else {
_bufferingDisplayTimer?.cancel();
_bufferingDisplayTimer = null;
_displayBufferingIndicator = false;
}
} else {
_displayBufferingIndicator = controller.value.isBuffering;
}
setState(() {
_latestValue = controller.value;
_subtitlesPosition = controller.value.position;
});
}
}
class _PlaybackSpeedDialog extends StatelessWidget {
const _PlaybackSpeedDialog({
Key? key,
required List<double> speeds,
required double selected,
}) : _speeds = speeds,
_selected = selected,
super(key: key);
final List<double> _speeds;
final double _selected;
@override
Widget build(BuildContext context) {
final selectedColor = CupertinoTheme.of(context).primaryColor;
return CupertinoActionSheet(
actions: _speeds
.map(
(e) => CupertinoActionSheetAction(
onPressed: () {
Navigator.of(context).pop(e);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (e == _selected) Icon(Icons.check, size: 20.0, color: selectedColor),
Text(e.toString()),
],
),
),
)
.toList(),
);
}
}