/*
* Copyright (c) Huawei Device Co., Ltd. 2024-2025. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
CommonUtils,
LogDomain,
LogHelper,
} from '@ohos/basicutils';
import {
PluginClickType,
PluginComponentInfo,
PluginIconType,
PluginInfo,
PluginLocalInfo,
PluginPosition,
PluginRequestEvent,
PluginSlot,
PluginType,
PluginUpdateEvent,
DeviceHelper,
EventManager,
EvtBus,
AbsResourceManager as ResourceManager,
} from '@ohos/frameworkwrapper';
import { ResUtils } from '@ohos/windowscene';
import { PluginStyle } from '../style/PluginStyle';
import { PluginIconBgListener } from '../listener/PluginIconBgListener';
import { PluginListenerManager } from '../listener/PluginListenerManager';
import { PluginVM } from '../vm/PluginVM';
import { ComponentAnimState, PropertyType } from '@ohos/systemuicommon';
import { PluginRootController } from '../controller/PluginRootController';
import { LengthMetrics } from '@kit.ArkUI';
// instrument ignore file
const TAG = 'PluginRootComponent';
const log: LogHelper = LogHelper.getLogHelper(LogDomain.SYS_UI, TAG);
export const getPluginRootId = (pluginName: string): string => {
return `${TAG}_Stack_${pluginName}`
}
/**
* 自定义touch事件
*/
type CustomTouch = (event?: TouchEvent) => void;
/**
* 插件渲染前回调
*/
type BeforeShowIconEvent = (slot: string, priority: number) => void;
/**
* 注册插件实例
*/
type RegisterPluginEvent = (slot: string, context: PluginRootCompContext) => void;
/**
* plugin组件,处理本地、远程plugin
*
* @since 2022-10-18
*/
@Component
export struct PluginRootComponent {
@StorageProp('isAccessibilityMode') isAccessibilityMode: boolean = false;
private onAreaChangeEvent: (oldValue: Area, newValue: Area) => void = (oldValue: Area, newValue: Area): void => {}
@Prop pluginScaleX: number;
@Prop pluginScaleY: number;
@Prop pluginOpacity: number = 1;
@State delayShowOpacity: number = 1;
@State pluginVisibility: Visibility = Visibility.Visible;
@Prop pluginDisplayPriority: number;
@State pluginComponentWidthZero: boolean = false;
/**
* plugin数据信息
*/
@State pluginInfo: PluginInfo | null = null;
/**
* 动效状态管理
*/
@ObjectLink animState: ComponentAnimState;
/**
* plugin图标资源样式
*/
pluginStyle: PluginStyle = new PluginStyle();
/**
* 背景相关事件监听管理器
*/
bgListenerManager: PluginListenerManager<PluginIconBgListener> | null = null;
/**
* 本地图标组件
*/
@BuilderParam
localIconComponent?: (info: PluginLocalInfo, resourceMgr: ResourceManager | null) => void;
/**
* 图标可见性
*/
@State isIconVisible: boolean = true;
@State isIconSettingVisible: boolean = true;
@Consume @Watch('refreshControllerStatusBarType') statusBarType: number;
/**
* 不显示的图标集
*/
@Consume @Watch('onExcludeSlotListChange') excludeSlotList: string[];
/**
* 判断设备是否为折叠态(直板机状态为非折叠态)
*/
@Consume isFolded: boolean;
/**
* 跨孔前隐藏,避免闪动
*/
@State hiddenBeforeCrossCutout: boolean = false;
/**
* plugin图标唯一标示
*/
private pluginSlot: string = '';
/**
* 资源管理
*/
private resourceMgr: ResourceManager | null = null;
/**
* 触摸事件
*/
private touchEvent: CustomTouch = () => {
};
/**
* 插件渲染前事件回调
*/
private beforeShowIcon: BeforeShowIconEvent = ()=> {}
/**
* 注册插件实例
*/
private registerPluginEvent: RegisterPluginEvent = ()=> {}
/**
* 数据处理
*/
private pluginVM: PluginVM = new PluginVM();
/**
* 多事件管理器
*/
private eventMgr: EventManager = EvtBus.createEventManager();
private pluginRootController: PluginRootController | null = null;
aboutToAppear(): void {
this.pluginSlot = this.pluginInfo?.pluginParseInfo?.pluginSlot ?? '';
log.showDebug(`aboutToAppear slot: ${this.pluginSlot} parsInfo click ${this.pluginInfo?.pluginParseInfo?.clickInfo?.enableOperate}`);
this.pluginRootController = new PluginRootController({
slot: this.pluginSlot,
statusBarType: this.statusBarType,
callback: (isVisible) => {
this.isIconSettingVisible = isVisible;
if (this.isIconSettingVisible) {
this.delayShowOpacity = 0;
// 设置项改变,图标显示延迟、避免图标闪烁
setTimeout(() => {
this.delayShowOpacity = 1;
}, 20)
}
}
});
this.pluginRootController.onStart();
// 注册远程plugin push事件
if (this.pluginInfo) {
this.pluginVM.init(this.pluginInfo);
}
if (this.registerPluginEvent) {
// 只有系统图标右侧区域和左侧区域涉及动态设置为隐藏状态
if (this.pluginInfo?.pluginParseInfo.pluginPosition === PluginPosition.POSITION_RIGHT ||
this.pluginInfo?.pluginParseInfo.pluginPosition === PluginPosition.POSITION_SYSTEM_LEFT) {
this.registerPluginEvent(this.pluginSlot, this);
}
}
// 注册plugin数据更新事件
this.eventMgr.on(PluginUpdateEvent, this.onPluginUpdateEvent)
// 注册plugin请求事件
.on(PluginRequestEvent, this.onPluginRequestEvent, this.pluginSlot)
// 注册远程plugin push事件
.addOff(this.pluginVM.registerOnRemotePushListener(this.onRemotePushListener));
this.onExcludeSlotListChange();
}
refreshControllerStatusBarType() {
this.pluginRootController?.refreshWhenStatusBarChange(this.statusBarType);
}
aboutToDisappear(): void {
log.showDebug(`aboutToDisappear slot: ${this.pluginSlot} parsInfo click ${this.pluginInfo?.pluginParseInfo?.clickInfo?.enableOperate}`);
// 注销背景监听
if (this.bgListenerManager) {
this.bgListenerManager.unregisterListener(this.pluginSlot);
}
this.pluginRootController?.onStop();
// 注销事件
this.eventMgr.offAll();
}
build() {
// 图标不可见
if (this.isShowIcon() && this.pluginInfo && this.localIconComponent) {
Row() {
this.localIconComponent(this.pluginInfo, this.resourceMgr)
}
.alignItems(VerticalAlign.Center)
.justifyContent(this.isWidthAutoType() ? FlexAlign.Start : FlexAlign.Center)
.key(this.pluginSlot)
.id(getPluginRootId(this.pluginSlot))
.width(this.getIconWidth())
.height(this.pluginStyle?.iconHeight)
.borderRadius(this.pluginStyle?.iconContainerRadius)
// 动效期间不切割
.clip(this.isClip())
// 屏蔽事件,防止消费touch事件,状态胶囊自定义点击事件放开;enable为false时会影响屏幕朗读,所以在开启屏幕朗读时放开
.enabled(this.pluginStyle?.iconEnable || this.checkEnabled() || this.isAccessibilityMode)
.visibility(this.getIconVisibility())
.margin({
start: LengthMetrics.vp(0),
end: LengthMetrics.px(Math.floor(vp2px(this.pluginStyle?.iconContainerRightMargin as number)))
})
.onTouch(this.touchEvent)
// 胶囊出现、隐藏动效时,图标跟随位移
.translate({
x: this.getRootTranX()
})
.displayPriority(this.pluginDisplayPriority)
.onAreaChange((oldValue: Area, newValue: Area) => {
this.onAreaChangeEvent(oldValue, newValue);
this.pluginComponentWidthZero = newValue.width <= 0;
})
.scale({ x: this.pluginScaleX, y: this.pluginScaleY } )
.opacity(this.pluginOpacity * this.delayShowOpacity)
.zIndex(this.getIconZIndex())
}
}
private isShowIcon(): boolean {
return !CommonUtils.isInvalid(this.pluginInfo) && this.getIconVisible() && this.getIconBuildVisible();
}
/**
* 获取图标可见性标记,定位图标这里均设置为可见,通过getIconVisibility中Visible.None进行隐藏,提升创建性能。
*
* @returns 返回当前图标是否可见
*/
private getIconVisible(): boolean {
if (this.pluginSlot === PluginSlot.SLOT_STATUS_LOCATION) {
return true;
}
return this.isIconVisible;
}
/**
* 非通知图标使用if隐藏,通知图标使用Visible.hidden隐藏,避免重新渲染
* @returns
*/
private getIconBuildVisible() {
if (this.pluginSlot === PluginSlot.SLOT_STATUS_NOTIFICATION) {
return true;
}
return this.isIconSettingVisible;
}
private getIconZIndex(): number {
if (this.pluginSlot === PluginSlot.LIVE_VIEW_CAPSULE) {
return 1;
}
return 0;
}
/**
* 图标是否显示
*/
private onExcludeSlotListChange(): void {
this.pluginVisibility = this.excludeSlotList.includes(this.pluginSlot) ? Visibility.None : Visibility.Visible;
}
private getIconVisibility() {
let visibleState = Visibility.Visible;
let visibleReason: string = 'default';
if (this.pluginVisibility != Visibility.Visible) {
visibleState = this.pluginVisibility;
visibleReason = 'pluginVisibility';
} else if (this.pluginSlot === PluginSlot.SLOT_STATUS_LOCATION && !this.isIconVisible) {
visibleState = Visibility.None;
visibleReason = 'pluginLocationVisibilityNone';
} else if (this.pluginComponentWidthZero) {
visibleState = Visibility.Hidden;
visibleReason = 'pluginComponentWidthZero';
} else if (this.pluginSlot === PluginSlot.SLOT_STATUS_NOTIFICATION) {
visibleState = this.isIconSettingVisible ? Visibility.Visible : Visibility.Hidden;
visibleReason = 'notification';
} else if (this.hiddenBeforeCrossCutout) {
// 跨孔渲染前先设置为不展示,避免图标跨孔闪动
visibleState = Visibility.Hidden;
visibleReason = 'hiddenBeforeCrossCutout';
}
return visibleState;
}
/**
* 是否切割组件
*
* @returns true切割
*/
private isClip(): boolean {
// 默认切割
if (CommonUtils.isInvalid(this.animState)) {
return true;
}
return this.animState.isClipComponent();
}
/**
* 获取横向位移偏量
*
* @returns 位移偏量
*/
private getRootTranX(): number {
return this.animState?.getPropertyValue(PropertyType.TRAN_X) ?? 0;
}
/**
* 远程push事件回调
*
* @param info 数据
*/
private onRemotePushListener = (info: PluginComponentInfo): void => {
log.showInfo(`onRemotePushListener ${info}`);
// 图标可见性控制
if (this.isIconVisible != info?.requestVisible) {
this.isIconVisible = info?.requestVisible;
}
}
/**
* plugin图标请求事件回调
*
* @param event 事件
*/
private onPluginRequestEvent = (event: PluginRequestEvent): void => {
if (event.pluginSlot != this.pluginSlot) {
return;
}
// 图标可见性事件
if (event.requestType == PluginRequestEvent.REQUEST_TYPE_VISIBILITY) {
// 更新可见性
if (this.isIconVisible != event.isVisible()) {
log.showWarn('onPluginRequestEvent slot: ' + event.pluginSlot + ', visible: ' + event.isVisible());
if (event.isVisible()) {
// 图标渲染前计算系统图标区域是否可以放的下,如果放不下,提前把最后的图标移入左侧区域,避免闪动
if (this.pluginInfo?.pluginParseInfo.pluginPosition === PluginPosition.POSITION_RIGHT) {
this.beforeShowIcon(this.pluginSlot, this.pluginInfo.pluginParseInfo.pluginPriority)
}
}
this.isIconVisible = event.isVisible();
}
}
}
/**
* 更新plugin数据事件回调
*
* @param event 事件
*/
private onPluginUpdateEvent = (event: PluginUpdateEvent): void => {
log.showInfo('onPluginUpdateEvent slot: ' + event.newInfo?.pluginParseInfo?.pluginSlot + ', ori: ' + this.pluginSlot);
// 更新数据
if (this.pluginInfo && event.newInfo?.equals(this.pluginInfo)) {
this.pluginInfo = event.newInfo;
// 刷新VM数据
this.pluginVM?.init(event.newInfo);
log.showInfo(`onPluginUpdateEvent: ${this.statusBarType}`);
}
}
/**
* 获取图标宽度
*
* @return 图标宽度
*/
private getIconWidth(): Length | undefined {
// 非自适应宽度图标,直接取固定值
if (!this.isWidthAutoType()) {
return ResUtils.getNumberFromLength(this.pluginStyle?.iconWidth);
}
// 手机图标宽度自适应
return undefined;
}
/**
* 是否为宽度自适应
*
* @return 是否宽度自适应
*/
private isWidthAutoType(): boolean {
let iconType = this.pluginInfo?.pluginParseInfo?.pluginIconType;
return iconType == PluginIconType.TYPE_AUTO_ICON;
}
private checkEnabled(): boolean {
if (!this.pluginInfo) {
log.showError('checkEnabled pluginInfo is invalid');
return false;
}
const parseInfo = this.pluginInfo?.pluginParseInfo;
return PluginType.isPluginType(parseInfo?.pluginType) &&
(parseInfo?.clickInfo?.clickType === PluginClickType.TYPE_CUSTOM ||
parseInfo?.rightClickInfo?.clickType === PluginClickType.TYPE_CUSTOM) ||
parseInfo?.clickInfo?.enableOperate === 'true';
}
}
export interface PluginRootCompContext {
hiddenBeforeCrossCutout: boolean
}