CustomDialogController弹窗状态变更但UI不刷新问题分析报告
1 关键字 CustomDialogController;UI刷新 2 问题描述 问题来源: https://gitee.com/openharmony/arkui_ace_engine/issues/I5BFFM?from=project-issue 能复现问题的代码如下(3.1release及截至当前[2022/10/13] master版本可复现): // custom-dialog-
1 关键字
CustomDialogController;UI刷新
2 问题描述
问题来源: https://gitee.com/openharmony/arkui_ace_engine/issues/I5BFFM?from=project-issue 能复现问题的代码如下(3.1release及截至当前[2022/10/13] master版本可复现):
// custom-dialog-demo.ets
@CustomDialog
struct DialogExample {
controller: CustomDialogController;
action: () => void;
build() {
Row() {
Button ("Close CustomDialog")
.onClick(() => {
this.action();
})
}.padding(20)
}
}
@Entry
@Component
struct CustomDialogUser {
@State flag:boolean = true;
_ : CustomDialogController = new CustomDialogController({
builder: DialogExample({action: this.onAccept}),
cancel: this.existApp,
autoCancel: this.flag,
alignment : !this.flag ?DialogAlignment.Top : DialogAlignment.Bottom
});
onAccept() {
console.log("onAccept");
}
existApp() {
}
build() {
Column() {
Button("OpenDialog", { type:ButtonType[this.flag ?'Normal':'Capsule']})
.onClick(() => {
this._.open()
}).height(200)
Button("change falg" + this.flag).height(200)
.onClick(() => {
this.flag = !this.flag;
})
}
}
}
测试步骤:
-
点击按钮'OpenDialog',弹出自定义弹窗
-
点击非弹窗区域,关闭弹窗
-
点击按钮'change falg'
-
重复步骤1
以上第3步骤点击按钮'change falg',flag变量变更,引起UI刷新:按钮"OpenDialog"变成圆角模式,同样的,又CustomDialogController实现的自定义弹窗的对齐模式根据代码逻辑更改为DialogAlignment.Top,此时点击'OpenDialog',弹出自定义弹窗,但是弹窗的对齐模式仍然为 DialogAlignment.Bottom,而不是预期中的DialogAlignment.Top。
3 问题原因
3.1 正常机制
当@State装饰的变量更改时,组件会重新渲染更新UI。
3.2 异常机制
@State装饰的变量更改时,组件会新渲染更新UI,但自定义弹窗UI未刷新。
4 解决方案
在事件函数内使用new CustomDialogController重新生成新的自定义弹窗,或使用自定义组件实现类似弹窗效果
5 定位过程
对于Button类的'普通'组件,@State装饰的变量更改致UI刷新的表现与预期一致,大致流程如下: 1: 执行 this.flag = ...变更变量状态 2: 通知关联的组件View,执行propertyHasChanged:
//3.1release(下同)对应源码路径 \frameworks\bridge\declarative_frontend\engine\contentStorage.js
notifyHasChanged(newValue, isCrossWindow) {
var registry = SubscriberManager.Get();
//遍历状态变量关联的view
this.subscribers_.forEach((subscribedId) => {
var subscriber = registry.get(subscribedId);
if (subscriber) {
if ('propertyHasChanged' in subscriber) {
//变量关联的view触发propertyHasChanged
subscriber.propertyHasChanged(this.info_, isCrossWindow);
}
}
});
}
3:触发组件View的markNeedUpdate方法,其实现逻辑在 \frameworks\bridge\declarative_frontend\jsview\js_view.cpp
propertyHasChanged(info) {
if (info) {
...
if (this.propsUsedForRender.has(info)) {
aceConsole.debug(`${this.constructor.name}: propertyHasChanged ['${info || "unknowm"}']. View needs update`);
// 执行markNeedUpdate 最终c++侧执行相关逻辑
this.markNeedUpdate();
}
this.restoreInstanceId();
} // if info avail.
}
c++侧实现逻辑,关键在于给组件打上'脏'标记:
/**
* 标记组件需要更新(重新渲染)
*/
// \frameworks\bridge\declarative_frontend\jsview\js_view.cpp
void JSView::MarkNeedUpdate()
{
...
auto element = GetElement().Upgrade();
if (element) {
//标记组件'脏'标记
element->MarkDirty();
}
needsUpdate_ = true;
}
//添加组件到set: dirtyElements_
// \frameworks\core\pipeline\pipeline_context.cpp
void PipelineContext::AddDirtyElement(const RefPtr<Element>& dirtyElement)
{
CHECK_RUN_ON(UI);
if (!dirtyElement) {
LOGW("dirtyElement is null");
return;
}
dirtyElements_.emplace(dirtyElement);
hasIdleTasks_ = true;
window_->RequestFrame();
}
4:因用户点击行为致渲染管线进行一系列Flush操作,此处可能会导致相关组件界面刷新(FlushBuild、 FlushLayout 、 FlushRender):
void PipelineContext::FlushBuild()
{
...
isRebuildFinished_ = false;
//是否有'脏'标记组件
if (dirtyElements_.empty()) {
isRebuildFinished_ = true;
if (FrameReport::GetInstance().GetEnable()) {
FrameReport::GetInstance().EndFlushBuild();
}
return;
}
decltype(dirtyElements_) dirtyElements(std::move(dirtyElements_));
//处理'脏'标记组件
for (const auto& elementWeak : dirtyElements) {
auto element = elementWeak.Upgrade();
// maybe unavailable when update parent
if (element && element->IsActive()) {
auto stageElement = AceType::DynamicCast<StageElement>(element);
...
//组件Rebuild
element->Rebuild();
}
}
5:带有'脏'标记组件执行Element::Rebuild(ace_engine\frameworks\core\pipeline\base\element.cpp)、执行ComposedElement::PerformBuild() (ace_engine\frameworks\core\pipeline\base\composed_element.cpp)、执行组件 RenderFunction:
void ComposedElement::PerformBuild()
{
auto context = context_.Upgrade();
//执行组件RenderFunction
auto component = HasRenderFunction() ? CallRenderFunction(component_) : BuildChild();
auto child = children_.empty() ? nullptr : children_.front();
//执行组件子级组件的UpdateChild
UpdateChild(child, component);
}
RenderFunction最终源自ets文件编译后的render函数(此处因篇幅主题原因不做详细表述),render函数类似如下:
class CustomDialogUser extends View {
constructor(compilerAssignedUniqueChildId, parent, params) {
...
}
...
render() {
Column.create();
Button.createWithLabel("OpenDialog", { type: ButtonType[this.flag ? 'Normal' : 'Capsule'] });
Button.onClick(() => {
this._.open();
});
...
}
}
重新执行以上的render()会更新Button组件,Button的type属性根据this.flag变更,如此实现了Button的界面刷新,表现为按钮在非圆角模式与圆角模式之间切换。
以上分析针对的为Button类的'普通'组件,但是对基于CustomDialogController实现的自定义弹窗,并没有同样或类似的处理机制,即状态变更无法作用于自定义弹窗,组件生成的相关逻辑处于constructor内,而不在render内:
class CustomDialogUser extends View {
// View constructor
constructor(compilerAssignedUniqueChildId, parent, params) {
...
//组件生成的相关逻辑处于constructor内
this._ = new CustomDialogController({
builder: () => {
let jsDialog = new DialogExample("2", this, { action: this.onAccept });
jsDialog.setController(this._);
View.create(jsDialog);
},
cancel: this.existApp,
autoCancel: this.flag,
alignment: !this.flag ? DialogAlignment.Top : DialogAlignment.Bottom
}, this);
6 知识分享
CustomDialogController类用于控制显示自定义弹窗,包括定义弹窗的布局,及设置弹窗内容构造器。
更多推荐

所有评论(0)