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;
        })
    }
  }
}

测试步骤:

  1. 点击按钮'OpenDialog',弹出自定义弹窗

  2. 点击非弹窗区域,关闭弹窗

  3. 点击按钮'change falg'

  4. 重复步骤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类用于控制显示自定义弹窗,包括定义弹窗的布局,及设置弹窗内容构造器。

 

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐