/*
* 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 { NotificationBase, NotificationDataManager } from '@ohos/systemuicommon/newTsIndex';
import {
DeviceInfoVm,
DropdownVm,
INotificationClearButtonVm,
INotificationListVm,
INotificationMoreHeaderVm,
INotificationVmInjector,
NotificationBaseVm
} from '@ohos/systemuicommon/newIndex';
import { notificationManager } from '@kit.NotificationKit';
import { NotificationApiUtil } from '../utils/NotificationApiUtil';
import { NotificationClearAllAnimation } from '../animation/NotificationClearAllAnimation';
import { NotificationSysEventReporter } from '@ohos/systemuicommon';
import { DropDownPanelManager } from '@ohos/systemuicommon/Index';
import { LengthMetrics } from '@kit.ArkUI';
import { CommonUtils, LogDomain, LogHelper } from '@ohos/basicutils/src/main/ets/TsIndex';
import { HiDfxEventUtil } from '@ohos/frameworkwrapper/src/main/ets/TsIndex';
import { ItemUtils } from '@ohos/componenthelper';
import { NotificationStyle } from '../constants/NotificationStyle';
import {
NotificationMoreHeaderModel
} from '@ohos/systemuicommon/src/main/ets/notification/model/NotificationMoreHeaderModel';
import { StatusBarHeightVm } from '@ohos/systemuicommon/src/main/ets/statusbar/vm/StatusBarHeightVm';
import { CutoutLayout } from '@ohos/systemuicommon/src/main/ets/notification/interface/INotificationClearButtonVm';
const TAG = 'NotificationClearButtonVm';
const log = LogHelper.getLogHelper(LogDomain.NC, TAG);
const LIST_ITEM_SPACE = 8;
const NTF_MORE_HEADER_TAG = 'NotificationListView_MoreHeader';
const UNKNOWN_DELETED_NUMBER = 65535;
const HIDE_PANEL_DELAY = 417;
const MORE_NTF_COLLAPSE_DELAY = 333;
/**
* 一键清除按钮管理
*
* @since 2024-11-20
*/
@ObservedV2
export class NotificationClearButtonVm extends NotificationBaseVm implements INotificationClearButtonVm {
@Trace public cutoutLayout: CutoutLayout | undefined = undefined;
/**
* 一键清除按钮的缩放值
*/
@Trace public scale: number = 1;
/**
* 一键清除按钮的旋转度
*/
@Trace public angle: number = 0;
/**
* 实时背景模糊参数
*/
@Trace public backgroundEffectOptions?: BackgroundEffectOptions = this.vmInjector.getColorVm().ntfClearButtonBgEffect;
public marginBottom = LengthMetrics.resource($r('app.float.ntf_clear_btn_padding'));
/**
* 是否在一键清除
*/
@Trace public isClearingAll: boolean = false;
/**
* 一键删除向上移动item的index
*/
private moveUpwardItemIndexList: number[] = [];
/**
* 一键删除向下移动item的index
*/
private moveDownwardItemIndexList: number[] = [];
/**
* 一键删除向上移动item的目标bottomY值
*/
private targetBottomY: number = 0;
/**
* 一键删除向上移动item的动效延迟
*/
private upwardItemDelay: number = 0;
/**
* 待清除的通知列表
* @returns
*/
@Computed protected get pendingClearNtfList(): NotificationBase[] {
const listVm = this.vmInjector.getListVm?.();
const ntfList = listVm ? listVm.mainNtfList.concat(listVm.moreNtfList) : [];
return this.getClearNtfList(ntfList);
}
protected getClearNtfList(ntfList: NotificationBase[]) {
const clearNtfList: NotificationBase[] = [];
for (const ntf of ntfList) {
if (!ntf.isNormalGroup()) {
continue;
}
}
log.showInfo(`Pending clear ntf list length: ${clearNtfList.length}`);
return clearNtfList;
}
/**
* 是否显示清除按钮
* @returns
*/
@Computed get isShow(): boolean {
return this.pendingClearNtfList.length > 0;
}
private animation = new NotificationClearAllAnimation();
public setCutoutLayout(cutoutLayout?: CutoutLayout): void {
this.cutoutLayout = cutoutLayout;
}
@Computed
get margin(): Margin | LocalizedMargin {
if (this.vmInjector.getPanelVm().isLandscapeLayout) {
return {
end: LengthMetrics.vp(this.cutoutLayout ? this.cutoutLayout.clearBtnMarginEnd : 0),
// 横屏一键删除按钮,需要去除状态栏居中对齐
top: LengthMetrics.vp(StatusBarHeightVm.getInstance().getAvoidHeight()),
bottom: LengthMetrics.resource($r('app.float.ntf_none_vp'))
};
}
return { right: 0, bottom: this.marginBottom };
}
@Computed
get alignRules(): LocalizedAlignRuleOptions {
if (this.vmInjector.getPanelVm().isLandscapeLayout) {
return {
end: { anchor: '__container__', align: HorizontalAlign.End },
center: { anchor: '__container__', align: VerticalAlign.Center }
};
}
return {
bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
};
}
@Computed
get width(): string | Resource {
if (this.vmInjector.getPanelVm().isLandscapeLayout) {
return this.cutoutLayout ? CommonUtils.spliceVp(this.cutoutLayout.clearBtnWidth) :
`calc(37.5% - ${NotificationStyle.LANDSCAPE_HEAD_VIEW_WIDTH})`;
}
return $r('app.float.ntf_clear_button_icon_size');
}
public registerEventListener(): void {
}
public unRegisterEventListener(): void {
}
/**
* 修改一键清除按钮的属性
*
* @param scale 缩放
* @param angle 翻转
*/
updateClearButtonParams(scale?: number, angle?: number): void {
if (scale !== undefined) {
this.scale = scale;
}
if (angle !== undefined) {
this.angle = angle;
}
}
/**
* 开关背景模糊
*/
enableBackgroundEffect(enable: boolean): void {
if (enable) {
this.backgroundEffectOptions = this.vmInjector.getColorVm().ntfClearButtonBgEffect;
} else {
this.backgroundEffectOptions = undefined;
}
}
/**
* 清除通知
*/
clear(): void {
const listVm = this.vmInjector.getListVm?.();
const scrollerVm = this.vmInjector.getListScrollerVm?.();
const moreHeaderVm = this.vmInjector.getNotificationMoreHeaderVm();
if (!listVm) {
return;
}
const scroller: Scroller | undefined = scrollerVm?.scroller;
if (scroller) {
scroller.scrollTo({
xOffset: scroller.currentOffset().xOffset,
yOffset: scroller.currentOffset().yOffset,
animation: { duration: 0, canOverScroll: true }
});
}
NotificationDataManager.instance.freeze(NotificationDataManager.REASON.DROPDOWN_REMOVE, 1000);
this.isClearingAll = true;
this.enableBackgroundEffect(false);
const startIndex = scrollerVm?.startIndex ?? 0;
const endIndex = scrollerVm?.endIndex ?? 0;
this.initAnimationParams();
this.separateItemIndexList(startIndex, endIndex, listVm);
this.addDownwardAnimation(listVm);
this.addUpwardAnimation(listVm);
if (moreHeaderVm) {
moreHeaderVm.isInClearAllAnim = true;
}
for (let i = startIndex; i <= endIndex; i++) {
const ntf = listVm.getListItemByIndex(i);
if (!ntf) {
continue;
}
this.vmInjector.getCardVm(ntf).isInClearAllAnim = true;
}
this.animation.addClearButtonAnimation(this);
if (this.vmInjector.getMoreVm()?.isExtraNtfsCollapseStatusChanging &&
NotificationMoreHeaderModel.instance.isCollapsed) {
setTimeout(() => this.startClearAnimation(moreHeaderVm), MORE_NTF_COLLAPSE_DELAY);
setTimeout(() => this.deleteNtfAndHidePanel(), HIDE_PANEL_DELAY + MORE_NTF_COLLAPSE_DELAY);
} else {
this.startClearAnimation(moreHeaderVm);
setTimeout(() => this.deleteNtfAndHidePanel(), HIDE_PANEL_DELAY);
}
}
private deleteNtfAndHidePanel(): void {
try {
log.showInfo(`Start clear all notification, size: ${this.pendingClearNtfList.length}`);
DropdownVm.instance.hidePanel(true);
setTimeout(() => {
NotificationDataManager.instance.unfreeze(NotificationDataManager.REASON.DROPDOWN_REMOVE);
NotificationApiUtil.removeNtfByCancel(this.pendingClearNtfList);
HiDfxEventUtil.reportDeleteAllNotification();
this.reportClearAllData();
}, 400);
log.showInfo(`End clear all notification, clear size ${this.pendingClearNtfList.length}`);
} catch (e) {
log.error('Clear all notification error:', e);
}
}
/**
* 一键清除通知数量运营打点
*/
private reportClearAllData(): void {
let unknownNtfCount = 0;
let socialNtfCount = 0;
let serviceNtfCount = 0;
let contentNtfCount = 0;
let allNtfCount = 0;
for (const ntf of this.pendingClearNtfList) {
allNtfCount += 1;
if (ntf.slotType === UNKNOWN_DELETED_NUMBER) {
unknownNtfCount += 1;
} else if (ntf.slotType === notificationManager.SlotType.SOCIAL_COMMUNICATION) {
socialNtfCount += 1;
} else if (ntf.slotType === notificationManager.SlotType.SERVICE_INFORMATION) {
serviceNtfCount += 1;
} else if (ntf.slotType === notificationManager.SlotType.CONTENT_INFORMATION) {
contentNtfCount += 1;
}
}
NotificationSysEventReporter.notificationPanelClear({
TIME_STAMP: `${DropDownPanelManager.getLastDropdownTime()}`,
UNKNOWN_DELETED_NUMBER: unknownNtfCount,
SOCIAL_DELETED_NUMBER: socialNtfCount,
SERVICE_DELETED_NUMBER: serviceNtfCount,
CONTENT_DELETED_NUMBER: contentNtfCount,
DELETED_NUMBER: allNtfCount
});
}
/**
* 一键清除,更多通知标题是否向下移动
* @returns 可视区域内,只要有一条不能删除的更多通知,返回false
*/
private isMoreHeaderDisappear(startIndex: number, endIndex: number): boolean {
const listVm = this.vmInjector.getListVm();
for (let i = startIndex; i < endIndex + 1; i++) {
const ntf = listVm?.getListItemByIndex(i);
if (ntf?.isMoreNtf() && !ntf.isClearAllowed) {
return false;
}
}
return true;
}
/**
* 一键清除,初始化动效需要的成员变量
*/
private initAnimationParams(): void {
const listId = this.vmInjector.getId('NotificationListComponent');
const listRectTop = ItemUtils.getRectById(listId)?.top;
this.moveUpwardItemIndexList = [];
this.moveDownwardItemIndexList = [];
this.targetBottomY = listRectTop + (this.vmInjector.getListVm()?.contentStartOffset ?? 0);
this.upwardItemDelay = 0;
this.animation.isLandscapeLayout = DeviceInfoVm.instance.isLandscape;
log.showInfo(`initAnimationParams, isLandscape = ${this.animation.isLandscapeLayout} `)
}
/**
* 一键清除,区分通知列表可视区域中,item的动效移动方向
*
* @param startIndex 可视区域第一个item的index
* @param endIndex 可视区域最后一个item的index
* @param listVm
*/
private separateItemIndexList(startIndex: number, endIndex: number, listVm: INotificationListVm): void {
this.moveUpwardItemIndexList = [];
this.moveDownwardItemIndexList = [];
for (let i = startIndex; i <= endIndex; i++) {
const isMoreHeader: boolean = i === listVm.mainNtfList.length;
const ntf = listVm.getListItemByIndex(i);
if (!ntf) {
continue;
}
if (isMoreHeader) {
const isMoreHeaderDisappear: boolean = this.isMoreHeaderDisappear(startIndex, endIndex);
if (isMoreHeaderDisappear) {
this.moveDownwardItemIndexList.push(i);
} else {
this.moveUpwardItemIndexList.push(i);
}
} else {
if (ntf.isClearAllowed) {
this.moveDownwardItemIndexList.push(i);
} else {
this.moveUpwardItemIndexList.push(i);
}
}
}
}
/**
* 一键清除,添加向上位移item的动效
*
* @param listVm
*/
private addUpwardAnimation(listVm: INotificationListVm): void {
if (this.moveUpwardItemIndexList.length === 0) {
return;
}
this.moveUpwardItemIndexList.forEach((itemIndex, index) => {
const ntf = listVm.getListItemByIndex(itemIndex);
if (!ntf) {
return;
}
const isMoreHeader: boolean = itemIndex === listVm.mainNtfList.length;
const itemId: string = isMoreHeader ? this.vmInjector.getId(NTF_MORE_HEADER_TAG) : listVm.getItemId(ntf);
const itemRect = ItemUtils.getRectById(itemId);
const itemHeight: number = itemRect.height ?? 0;
this.targetBottomY += itemHeight + (index === 0 ? 0 : LIST_ITEM_SPACE);
log.showInfo(`Clear all animation, item upward, index = ${itemIndex}, ` +
` delay = ${this.upwardItemDelay}, transY = ${this.targetBottomY - itemRect.bottom}`)
if (itemIndex === listVm.mainNtfList.length) {
const moreHeaderVm = this.vmInjector.getNotificationMoreHeaderVm();
if (moreHeaderVm) {
this.animation.addMoreHeaderAnimationUpwards({
delay: this.upwardItemDelay,
moreHeaderVm: moreHeaderVm,
translateY: this.targetBottomY - itemRect.bottom,
});
}
} else {
// 动效开始之前将不可删除卡片裁剪置为true
this.vmInjector.getCardVm(ntf).containerClip = true;
this.vmInjector.getCardVm(ntf).contentClipParam = true;
this.animation.addListItemAnimationUpwards({
index: index,
itemId: listVm.getItemId(ntf),
delay: this.upwardItemDelay,
cardVm: this.vmInjector.getCardVm(ntf),
translateY: this.targetBottomY - itemRect.bottom,
});
}
});
}
/**
* 一键清除,添加向下位移item的动效
*
* @param listVm
*/
private addDownwardAnimation(listVm: INotificationListVm) {
if (this.moveDownwardItemIndexList.length === 0) {
return;
}
this.moveDownwardItemIndexList.forEach((itemIndex, index) => {
const ntf = listVm.getListItemByIndex(itemIndex);
if (!ntf) {
return;
}
const delayTime: number =
(this.moveDownwardItemIndexList.length - 1 - index) * NotificationClearAllAnimation.delayPerItem +
NotificationClearAllAnimation.clearButtonAniDuration;
log.showInfo(`Clear all animation, item downward, index = ${itemIndex}, delay = ${delayTime}`)
if (index === 0) {
this.upwardItemDelay = delayTime;
}
if (itemIndex === listVm.mainNtfList.length) {
const moreHeaderVm = this.vmInjector.getNotificationMoreHeaderVm();
if (moreHeaderVm) {
this.animation.addMoreHeaderDisappearAnimation({
delay: delayTime,
moreHeaderVm: moreHeaderVm,
});
}
} else {
this.animation.addListItemAnimationDownwards({
index: index,
itemId: listVm.getItemId(ntf),
delay: delayTime,
cardVm: this.vmInjector.getCardVm(ntf),
});
}
});
}
private startClearAnimation(moreHeaderVm: INotificationMoreHeaderVm | undefined): void {
this.animation.startAnimation().then(() => {
if (moreHeaderVm) {
moreHeaderVm.isInClearAllAnim = false;
}
log.showInfo('ClearNtAni/IntoNC All notification animation finish, mark renderGroup false.');
});
}
}