feat: Windows增加分辨率适应,切换分辨率按钮修改
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
|
||||
import 'config/ice_servers.dart';
|
||||
@@ -33,6 +35,12 @@ class ControllerHome extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ControllerHomeState extends State<ControllerHome> {
|
||||
/// 与 Windows 原生窗口通信的通道(用于按视频比例调整窗口高度)。
|
||||
static const MethodChannel _windowChannel = MethodChannel('app/window');
|
||||
|
||||
/// 记录上一次已应用的视频宽高比,避免重复调整窗口。
|
||||
double _lastResizedAspect = 0;
|
||||
|
||||
final _serverUrlController = TextEditingController(
|
||||
text: kDefaultSignalServer,
|
||||
);
|
||||
@@ -217,10 +225,22 @@ class _ControllerHomeState extends State<ControllerHome> {
|
||||
final w = _renderer?.value.width ?? 0;
|
||||
final h = _renderer?.value.height ?? 0;
|
||||
if (w > 0 && h > 0 && mounted) {
|
||||
setState(() => _videoAspect = w / h);
|
||||
final aspect = w / h;
|
||||
setState(() => _videoAspect = aspect);
|
||||
_resizeWindowToAspect(aspect);
|
||||
}
|
||||
}
|
||||
|
||||
/// Windows 端:保持窗口宽度不变,按视频宽高比调整窗口高度;
|
||||
/// 若高度超出屏幕,由原生端改为合适的窗口大小。
|
||||
void _resizeWindowToAspect(double aspect) {
|
||||
if (kIsWeb || defaultTargetPlatform != TargetPlatform.windows) return;
|
||||
if (aspect <= 0) return;
|
||||
if ((aspect - _lastResizedAspect).abs() < 0.001) return;
|
||||
_lastResizedAspect = aspect;
|
||||
_windowChannel.invokeMethod('resizeToVideoAspect', aspect);
|
||||
}
|
||||
|
||||
/// ICE 断开:提示用户并自动返回连接设置面板。
|
||||
Future<void> _onIceDisconnected(String message) async {
|
||||
_showAlert(message);
|
||||
@@ -239,10 +259,52 @@ class _ControllerHomeState extends State<ControllerHome> {
|
||||
setState(() => _connected = false);
|
||||
}
|
||||
|
||||
/// 弹出分辨率选择菜单(iOS 风格 ActionSheet)。
|
||||
void _showResolutionMenu() {
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (ctx) => CupertinoActionSheet(
|
||||
title: const Text('切换分辨率'),
|
||||
actions: [
|
||||
for (int i = 0; i < _resolutionOptions.length; i++)
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
setState(() => _selectedResolution = i);
|
||||
final o = _resolutionOptions[i];
|
||||
_controller?.sendResolutionChange(
|
||||
o['width'] as int,
|
||||
o['height'] as int,
|
||||
o['fps'] as int,
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (i == _selectedResolution) ...[
|
||||
const Icon(CupertinoIcons.check_mark,
|
||||
size: 18, color: CupertinoColors.activeBlue),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Text(_resolutionOptions[i]['label'] as String),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
isDefaultAction: true,
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _disconnect() async {
|
||||
await _controller?.disconnect();
|
||||
_controller = null;
|
||||
_renderer?.removeListener(_onRendererUpdate);
|
||||
_lastResizedAspect = 0;
|
||||
setState(() {
|
||||
_connected = false;
|
||||
_renderer = null;
|
||||
@@ -319,6 +381,58 @@ class _ControllerHomeState extends State<ControllerHome> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 右上角浮动顶部菜单栏:整合「分辨率切换」与「断开连接」。
|
||||
Widget _buildTopMenuBar() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 分辨率菜单按钮:显示当前分辨率标签
|
||||
CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
onPressed: _showResolutionMenu,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(CupertinoIcons.slider_horizontal_3,
|
||||
color: CupertinoColors.white, size: 20),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
_resolutionOptions[_selectedResolution]['label'] as String,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 分隔线
|
||||
Container(
|
||||
width: 1,
|
||||
height: 22,
|
||||
color: CupertinoColors.white.withValues(alpha: 0.3),
|
||||
),
|
||||
// 断开连接按钮
|
||||
CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
onPressed: _disconnect,
|
||||
child: const Icon(
|
||||
CupertinoIcons.xmark_circle_fill,
|
||||
color: CupertinoColors.destructiveRed,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControlPanel() {
|
||||
return SafeArea(
|
||||
// 仅保留顶部安全区(避开系统状态栏),底部/左右保持全屏,
|
||||
@@ -378,48 +492,9 @@ class _ControllerHomeState extends State<ControllerHome> {
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
child: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: _disconnect,
|
||||
child: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.destructiveRed,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(
|
||||
CupertinoIcons.clear,
|
||||
color: CupertinoColors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
child: CupertinoSegmentedControl<int>(
|
||||
groupValue: _selectedResolution,
|
||||
children: <int, Widget>{
|
||||
for (int i = 0; i < _resolutionOptions.length; i++)
|
||||
i: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
child: Text(_resolutionOptions[i]['label'] as String),
|
||||
),
|
||||
},
|
||||
onValueChanged: (int idx) {
|
||||
setState(() => _selectedResolution = idx);
|
||||
final o = _resolutionOptions[idx];
|
||||
_controller?.sendResolutionChange(
|
||||
o['width'] as int,
|
||||
o['height'] as int,
|
||||
o['fps'] as int,
|
||||
);
|
||||
},
|
||||
),
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: _buildTopMenuBar(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "flutter_window.h"
|
||||
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
@@ -27,6 +28,29 @@ bool FlutterWindow::OnCreate() {
|
||||
RegisterPlugins(flutter_controller_->engine());
|
||||
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||
|
||||
// 注册窗口控制通道:Dart 端可请求按视频比例调整窗口尺寸。
|
||||
window_channel_ =
|
||||
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
|
||||
flutter_controller_->engine()->messenger(), "app/window",
|
||||
&flutter::StandardMethodCodec::GetInstance());
|
||||
window_channel_->SetMethodCallHandler(
|
||||
[this](const flutter::MethodCall<flutter::EncodableValue>& call,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>
|
||||
result) {
|
||||
if (call.method_name() == "resizeToVideoAspect") {
|
||||
double aspect = 0.0;
|
||||
if (const auto* value = std::get_if<double>(call.arguments())) {
|
||||
aspect = *value;
|
||||
} else if (const auto* ivalue = std::get_if<int>(call.arguments())) {
|
||||
aspect = static_cast<double>(*ivalue);
|
||||
}
|
||||
ResizeToVideoAspect(aspect);
|
||||
result->Success();
|
||||
} else {
|
||||
result->NotImplemented();
|
||||
}
|
||||
});
|
||||
|
||||
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
||||
this->Show();
|
||||
});
|
||||
@@ -47,6 +71,69 @@ void FlutterWindow::OnDestroy() {
|
||||
Win32Window::OnDestroy();
|
||||
}
|
||||
|
||||
void FlutterWindow::ResizeToVideoAspect(double aspect) {
|
||||
if (aspect <= 0.0) {
|
||||
return;
|
||||
}
|
||||
HWND hwnd = GetHandle();
|
||||
if (!hwnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
RECT window_rect;
|
||||
RECT client_rect;
|
||||
GetWindowRect(hwnd, &window_rect);
|
||||
GetClientRect(hwnd, &client_rect);
|
||||
|
||||
const int window_w = window_rect.right - window_rect.left;
|
||||
const int window_h = window_rect.bottom - window_rect.top;
|
||||
const int client_w = client_rect.right - client_rect.left;
|
||||
const int client_h = client_rect.bottom - client_rect.top;
|
||||
// 边框(含标题栏)占用的额外像素。
|
||||
const int border_w = window_w - client_w;
|
||||
const int border_h = window_h - client_h;
|
||||
|
||||
// 保持窗口宽度不变,按比例计算客户区高度。
|
||||
int target_client_w = client_w;
|
||||
int target_client_h = static_cast<int>(target_client_w / aspect + 0.5);
|
||||
int target_window_w = window_w;
|
||||
int target_window_h = target_client_h + border_h;
|
||||
|
||||
// 获取窗口所在显示器的可用工作区。
|
||||
HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
|
||||
MONITORINFO monitor_info;
|
||||
monitor_info.cbSize = sizeof(MONITORINFO);
|
||||
GetMonitorInfo(monitor, &monitor_info);
|
||||
const int work_h = monitor_info.rcWork.bottom - monitor_info.rcWork.top;
|
||||
|
||||
// 高度超出屏幕:改为以屏幕高度为准的合适窗口大小。
|
||||
if (target_window_h > work_h) {
|
||||
target_window_h = work_h;
|
||||
target_client_h = target_window_h - border_h;
|
||||
target_client_w = static_cast<int>(target_client_h * aspect + 0.5);
|
||||
target_window_w = target_client_w + border_w;
|
||||
}
|
||||
|
||||
// 计算位置:尽量保持当前左上角,同时确保窗口落在工作区内。
|
||||
int new_left = window_rect.left;
|
||||
int new_top = window_rect.top;
|
||||
if (new_left + target_window_w > monitor_info.rcWork.right) {
|
||||
new_left = monitor_info.rcWork.right - target_window_w;
|
||||
}
|
||||
if (new_top + target_window_h > monitor_info.rcWork.bottom) {
|
||||
new_top = monitor_info.rcWork.bottom - target_window_h;
|
||||
}
|
||||
if (new_left < monitor_info.rcWork.left) {
|
||||
new_left = monitor_info.rcWork.left;
|
||||
}
|
||||
if (new_top < monitor_info.rcWork.top) {
|
||||
new_top = monitor_info.rcWork.top;
|
||||
}
|
||||
|
||||
SetWindowPos(hwnd, nullptr, new_left, new_top, target_window_w,
|
||||
target_window_h, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
|
||||
LRESULT
|
||||
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
|
||||
WPARAM const wparam,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include <flutter/dart_project.h>
|
||||
#include <flutter/flutter_view_controller.h>
|
||||
#include <flutter/method_channel.h>
|
||||
#include <flutter/standard_method_codec.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
@@ -23,11 +25,20 @@ class FlutterWindow : public Win32Window {
|
||||
LPARAM const lparam) noexcept override;
|
||||
|
||||
private:
|
||||
// 按给定的视频宽高比(宽/高)调整窗口:保持窗口宽度不变,
|
||||
// 高度按比例计算;若高度超出屏幕工作区,则以屏幕高度为准
|
||||
// 反推出合适的窗口宽高。
|
||||
void ResizeToVideoAspect(double aspect);
|
||||
|
||||
// The project to run.
|
||||
flutter::DartProject project_;
|
||||
|
||||
// The Flutter instance hosted by this window.
|
||||
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
|
||||
|
||||
// 与 Dart 层通信的窗口控制通道。
|
||||
std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>>
|
||||
window_channel_;
|
||||
};
|
||||
|
||||
#endif // RUNNER_FLUTTER_WINDOW_H_
|
||||
|
||||
@@ -26,7 +26,8 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
|
||||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Size size(1280, 720);
|
||||
// 竖屏 16:9 窗口(宽:高 = 9:16),适配控制端竖屏画面。
|
||||
Win32Window::Size size(450, 800);
|
||||
if (!window.Create(L"webrtc_controller_flutter", origin, size)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user