Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/common/widgets/scale_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,19 @@ class ScaledWidgetsFlutterBinding extends WidgetsFlutterBinding {

double get scaleFactor => _scaleFactor;

/// scaleFactor 的可监听镜像。缩放变化只影响 RenderView 配置(画布逻辑
/// 尺寸),根 View 的 MediaQuery 数据(physicalSize/dpr 来自引擎)并不会
/// 变,依赖 MediaQuery 的整棵子树(包括 _builder 里的缩放校正)不会自动
/// 重建——布局仍按旧逻辑尺寸进行,与画布脱节(画面只占 scale 比例、
/// 露出白底)。消费方必须监听它来触发重建。
final ValueNotifier<double> scaleFactorNotifier = ValueNotifier(1.0);

/// Update scaleFactor callback, then rebuild layout
set scaleFactor(double scaleFactor) {
if (_scaleFactor == scaleFactor) return;
_scaleFactor = scaleFactor;
handleMetricsChanged();
scaleFactorNotifier.value = scaleFactor;
}

double devicePixelRatioScaled = 0;
Expand Down
32 changes: 32 additions & 0 deletions lib/harmony_adapt/harmony_channel.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:PiliPlus/common/widgets/scale_app.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:flutter/services.dart';
import 'package:os_type/os_type.dart';

abstract class HarmonyChannel {
static final MethodChannel _channel = const MethodChannel('harmonyChannel')
Expand All @@ -26,6 +27,10 @@ abstract class HarmonyChannel {
static bool _landscape = false;
static bool _miniWindow = false;

/// 当前是否处于系统自由小窗(悬浮窗/全景多窗)。小窗内窗口宽高比不代表
/// 设备方向,基于方向的自动全屏等逻辑应据此跳过。
static bool get isMiniWindow => _miniWindow;

/// 当方向或小窗变化
static Future<void> onLandscapeOrMiniWindowChange(
bool? landscape,
Expand Down Expand Up @@ -53,4 +58,31 @@ abstract class HarmonyChannel {
static void autoRotateLandscape() {
_channel.invokeMethod('autoRotateLandscape');
}

/// 自由多窗装饰栏按钮(全屏/最小化/关闭)的颜色跟随应用颜色模式而非
/// 下方内容:浅色模式下深色按钮叠在播放页黑色顶部上视觉不可见。顶部为
/// 深色内容的页面(视频/直播播放页)在可见期间持有此状态,使按钮切为
/// 浅色风格;无人持有时恢复跟随系统。用持有者集合而非开关,规避
/// 路由切换(如视频页跳视频页)中生命周期回调顺序的不确定性。
static final Set<Object> _darkDecorOwners = <Object>{};

static void holdDecorDark(Object owner) {
if (!OS.isHarmony) return;
final wasEmpty = _darkDecorOwners.isEmpty;
_darkDecorOwners.add(owner);
if (wasEmpty) {
_setDecorButtonDark(true);
}
}

static void releaseDecorDark(Object owner) {
if (!OS.isHarmony) return;
if (_darkDecorOwners.remove(owner) && _darkDecorOwners.isEmpty) {
_setDecorButtonDark(false);
}
}

static void _setDecorButtonDark(bool dark) {
_channel.invokeMethod('setDecorButtonDark', {'dark': dark});
}
}
11 changes: 11 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,17 @@ class MyApp extends StatelessWidget {
}

static Widget _builder(BuildContext context, Widget? child) {
// scaleFactor 变化不会改变根 View 的 MediaQuery 数据(physicalSize/dpr
// 均来自引擎),本方法不会因此重建,下面的缩放校正会失效、页面布局与
// 渲染画布脱节(如平板全景多窗内点全屏后内容只占 75%、右/下露白底)。
// 必须显式监听缩放变化触发重建。
return ListenableBuilder(
listenable: ScaledWidgetsFlutterBinding.instance.scaleFactorNotifier,
builder: (context, _) => _scaledBuilder(context, child),
);
}

static Widget _scaledBuilder(BuildContext context, Widget? child) {
// 鸿蒙小窗横屏临时缩小时需要获取真正的缩放比例
final uiScale = ScaledWidgetsFlutterBinding.instance.scaleFactor;
final mediaQuery = MediaQuery.of(context);
Expand Down
7 changes: 7 additions & 0 deletions lib/pages/live_room/view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recogniz
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/harmony_adapt/harmony_channel.dart';
import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/models/common/live/live_contribution_rank_type.dart';
import 'package:PiliPlus/models_new/live/live_room_info_h5/data.dart';
Expand Down Expand Up @@ -73,6 +74,9 @@ class _LiveRoomPageState extends State<LiveRoomPage>
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// 页面顶部是深色播放器:自由多窗的装饰栏按钮切浅色风格,否则浅色
// 模式下深色按钮不可见
HarmonyChannel.holdDecorDark(this);
_liveRoomController = Get.put(
LiveRoomController(heroTag),
tag: heroTag,
Expand Down Expand Up @@ -104,6 +108,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
@override
Future<void> didPopNext() async {
WidgetsBinding.instance.addObserver(this);
HarmonyChannel.holdDecorDark(this);
plPlayerController
..isLive = true
..danmakuController = _liveRoomController.danmakuController;
Expand Down Expand Up @@ -131,6 +136,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
@override
void didPushNext() {
WidgetsBinding.instance.removeObserver(this);
HarmonyChannel.releaseDecorDark(this);
plPlayerController.removeStatusLister(playerListener);
_liveRoomController
..danmakuController?.clear()
Expand Down Expand Up @@ -159,6 +165,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
void dispose() {
_pipModeWorker?.dispose();
videoPlayerServiceHandler?.onVideoDetailDispose(heroTag);
HarmonyChannel.releaseDecorDark(this);
WidgetsBinding.instance.removeObserver(this);
if ((Platform.isAndroid || OS.isHarmony) && !plPlayerController.setSystemBrightness) {
ScreenBrightnessPlatform.instance.resetApplicationScreenBrightness();
Expand Down
12 changes: 12 additions & 0 deletions lib/pages/video/view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:PiliPlus/common/widgets/image_viewer/hero_dialog_route.dart';
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_dynamic_header.dart';
import 'package:PiliPlus/harmony_adapt/harmony_channel.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/main.dart';
import 'package:PiliPlus/models/common/episode_panel_type.dart';
Expand Down Expand Up @@ -138,6 +139,9 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>

PlPlayerController.setPlayCallBack(playCallBack);
videoDetailController = Get.put(VideoDetailController(), tag: heroTag);
// 页面顶部是黑色播放器:自由多窗的装饰栏按钮切浅色风格,否则浅色
// 模式下深色按钮不可见
HarmonyChannel.holdDecorDark(this);
// 画中画状态翻转时强制重建:PiP 结束时若窗口尺寸恰好没变(如画中画
// 期间从智慧多窗应用栏以小窗打开 app),没有视口变化触发重建,页面
// 会滞留在画中画布局(黑边+播控被状态栏遮挡)。
Expand Down Expand Up @@ -339,6 +343,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
@override
void dispose() {
_pipModeWorker?.dispose();
HarmonyChannel.releaseDecorDark(this);
plPlayerController
?..removeStatusLister(playerListener)
..removePositionListener(positionListener);
Expand Down Expand Up @@ -393,6 +398,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
return;
}

HarmonyChannel.releaseDecorDark(this);
WidgetsBinding.instance.removeObserver(this);

if ((Platform.isAndroid || OS.isHarmony) && !videoDetailController.setSystemBrightness) {
Expand Down Expand Up @@ -429,6 +435,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
return;
}

HarmonyChannel.holdDecorDark(this);
WidgetsBinding.instance.addObserver(this);

plPlayerController?.isLive = false;
Expand Down Expand Up @@ -555,8 +562,13 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
}
}
if (PlatformUtils.isMobile) {
// 鸿蒙自由小窗(悬浮窗/全景多窗)内窗口宽高比不代表设备方向:横屏
// 小窗退出全屏时窗口尚未恢复竖屏尺寸,此处若按"非竖屏"自动重进
// 全屏会形成退不出去的回环。
final aspectIsOrientation = !OS.isHarmony || !HarmonyChannel.isMiniWindow;
if (!isPortrait &&
!isFullScreen &&
aspectIsOrientation &&
plPlayerController != null &&
videoDetailController.autoPlay) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Expand Down
62 changes: 61 additions & 1 deletion ohos/entry/src/main/ets/pages/Index.ets
Original file line number Diff line number Diff line change
@@ -1,24 +1,84 @@

import common from '@ohos.app.ability.common';
import window from '@ohos.window';
import { FlutterPage } from '@ohos/flutter_ohos'

let storage = LocalStorage.getShared()
const EVENT_BACK_PRESS = 'EVENT_BACK_PRESS'
const TAG = 'Index'

@Entry(storage)
@Component
struct Index {
private context = getContext(this) as common.UIAbilityContext
@LocalStorageLink('viewId') viewId: string = "";
// 引擎的视口尺寸只跟随 XComponent surface 的 OnSurfaceChanged,而系统
// 程序化改窗口(如全景多窗全屏走的 enableLandscapeMultiWindow)不会触发
// ArkUI 重布局,surface 停在旧尺寸,Flutter 按旧窗口尺寸布局,右/下出现
// 大片白区(手动拖动窗口会强制重布局所以能恢复)。这里监听窗口尺寸并把
// 它显式绑定到根容器,用状态变化驱动重布局,让 surface 跟上窗口。
@State winWidth: number = 0;
@State winHeight: number = 0;
private win: window.Window | null = null;
private sizeCallback: ((size: window.Size) => void) | null = null;
private rectCallback: ((opts: window.RectChangeOptions) => void) | null = null;

private applySize(widthPx: number, heightPx: number): void {
this.winWidth = px2vp(widthPx);
this.winHeight = px2vp(heightPx);
}

aboutToAppear(): void {
window.getLastWindow(this.context).then((w: window.Window) => {
this.win = w;
const rect = w.getWindowProperties().windowRect;
this.applySize(rect.width, rect.height);
this.sizeCallback = (size: window.Size) => {
this.applySize(size.width, size.height);
};
w.on('windowSizeChange', this.sizeCallback);
// windowSizeChange 对部分程序化窗口调整不回调,windowRectChange
// (@since 12) 覆盖移动/缩放全部来源,双保险。
try {
this.rectCallback = (opts: window.RectChangeOptions) => {
this.applySize(opts.rect.width, opts.rect.height);
};
w.on('windowRectChange', this.rectCallback);
} catch (err) {
console.error(TAG, `windowRectChange listener failed: ${err}`);
}
}).catch((err: Object) => {
console.error(TAG, `getLastWindow failed: ${err}`);
});
}

aboutToDisappear(): void {
if (this.win !== null) {
try {
if (this.sizeCallback !== null) {
this.win.off('windowSizeChange', this.sizeCallback);
}
if (this.rectCallback !== null) {
this.win.off('windowRectChange', this.rectCallback);
}
} catch (err) {
console.error(TAG, `off window listeners failed: ${err}`);
}
this.sizeCallback = null;
this.rectCallback = null;
}
}

build() {
Column() {
FlutterPage({ viewId: this.viewId })
}
.width(this.winWidth > 0 ? this.winWidth : '100%')
.height(this.winHeight > 0 ? this.winHeight : '100%')
}

onBackPress(): boolean {
this.context.eventHub.emit(EVENT_BACK_PRESS)
return true
}
}
}
25 changes: 25 additions & 0 deletions ohos/entry/src/main/ets/plugins/HarmonyChannel.ets
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ConfigurationConstant from "@ohos.app.ability.ConfigurationConstant";
import window from "@ohos.window";
import {
FlutterPlugin,
Expand Down Expand Up @@ -51,6 +52,12 @@ export default class HarmonyChannel implements FlutterPlugin {
await this.autoRotateLandscape();
result.success(true);
break;
case "setDecorButtonDark": {
const dark: boolean = call.argument("dark") ?? false;
await this.setDecorButtonDark(dark);
result.success(true);
break;
}
// case 'setWindowLayoutFullScreen': {
// this.lastWindow ??= await window.getLastWindow(getContext())
// this.lastWindow.setWindowLayoutFullScreen(call.argument('fullScreen'));
Expand Down Expand Up @@ -80,4 +87,22 @@ export default class HarmonyChannel implements FlutterPlugin {
this.lastWindow ??= await window.getLastWindow(getContext());
this.lastWindow.setPreferredOrientation(window.Orientation.AUTO_ROTATION_LANDSCAPE)
}

/**
* 自由多窗装饰栏按钮颜色:dark=true 时按深色模式渲染(浅色按钮,适配
* 深色内容顶部),false 恢复跟随系统颜色模式。仅装饰栏可见(自由窗口)
* 时有视觉效果,其余窗口形态下为无害空操作。
*/
private async setDecorButtonDark(dark: boolean) {
this.lastWindow ??= await window.getLastWindow(getContext());
try {
this.lastWindow.setDecorButtonStyle({
colorMode: dark
? ConfigurationConstant.ColorMode.COLOR_MODE_DARK
: ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET,
});
} catch (err) {
console.error("HarmonyChannel", `setDecorButtonStyle failed: ${err}`);
}
}
}
Loading