HarmonyOS 应用开发-ArkUI事件机制

871 篇文章 15 订阅
694 篇文章 17 订阅

ArkUI提供了事件机制,这些事件提供了不同的信息用于处理程序交互逻辑,ArKUI事件按照功能来讲,可以分为以下几种:

  • 点击事件
  • 触摸事件
  • 挂载卸载事件
  • 拖拽事件
  • 按键事件
  • 焦点事件
  • 鼠标事件
  • 组件区域变化事件
  • 组件可见区域变化事件
  • 组件快捷键事件

按照事件触发后是否向父节点传递,事件可以分为:

  • 冒泡事件:是当一个组件上的事件被触发后,该事件向父节点传递。
  • 非冒泡事件:是当一个组件上的事件被触发后,该事件不会向父节点传递。

示例讲解

下面以组件区域变化事件为例,结合代码进行分析。

组件区域变化事件介绍

当UI组件大小、位置发生变化时触发该事件,应用可以通过该事件箭头UI组件的大小位置变化,该事件属于非冒泡事件。

示例代码

// xxx.ets
@Entry
@Component
struct AreaExample {
  @State value: string = 'Text'
  @State sizeValue: string = ''

  build() {
    Column() {
      Text(this.value)
        .backgroundColor(Color.Green).margin(30).fontSize(20)
        .onClick(() => {
          this.value = this.value + 'Text'
        })
        .onAreaChange((oldValue: Area, newValue: Area) => {
          console.info(`Ace: on area change, oldValue is ${JSON.stringify(oldValue)} value is ${JSON.stringify(newValue)}`)
          this.sizeValue = JSON.stringify(newValue)
        })
      Text('new area is: \n' + this.sizeValue).margin({ right: 30, left: 30 })
    }
    .width('100%').height('100%').margin({ top: 30 })
  }
}

示例效果:

每次点击Text框,Text组件区域大小都发生变化,每次将位置信息打印到控制台。

在上述示例代码中,Text组件通过绑定onAreaChange实现了对组件区域变化事件的监听,onAreaChange事件的入参位一个回调函数,该回调函数在Text组件区域发生变化时进行调用。

代码实现原理分析

1. 编译后生成的JS代码

"use strict";
class AreaExample extends ViewPU {
    constructor(parent, params, __localStorage, elmtId = -1) {
        super(parent, __localStorage, elmtId);
        this.__value = new ObservedPropertySimplePU('Text', this, "value");
        this.__sizeValue = new ObservedPropertySimplePU('', this, "sizeValue");
        this.setInitiallyProvidedValue(params);
    }
    setInitiallyProvidedValue(params) {
        if (params.value !== undefined) {
            this.value = params.value;
        }
        if (params.sizeValue !== undefined) {
            this.sizeValue = params.sizeValue;
        }
    }
    updateStateVars(params) {
    }
    purgeVariableDependenciesOnElmtId(rmElmtId) {
        this.__value.purgeDependencyOnElmtId(rmElmtId);
        this.__sizeValue.purgeDependencyOnElmtId(rmElmtId);
    }
    aboutToBeDeleted() {
        this.__value.aboutToBeDeleted();
        this.__sizeValue.aboutToBeDeleted();
        SubscriberManager.Get().delete(this.id__());
        this.aboutToBeDeletedInternal();
    }
    get value() {
        return this.__value.get();
    }
    set value(newValue) {
        this.__value.set(newValue);
    }
    get sizeValue() {
        return this.__sizeValue.get();
    }
    set sizeValue(newValue) {
        this.__sizeValue.set(newValue);
    }
    initialRender() {
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Column.create();
            Column.width('100%');
            Column.height('100%');
            Column.margin({ top: 30 });
            if (!isInitialRender) {
                Column.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Text.create(this.value);
            Text.backgroundColor(Color.Green);
            Text.margin(30);
            Text.fontSize(20);
            Text.onClick(() => {
                this.value = this.value + 'Text';
            });
            Text.onAreaChange((oldValue, newValue) => {
                console.info(`Ace: on area change, oldValue is ${JSON.stringify(oldValue)} value is ${JSON.stringify(newValue)}`);
                this.sizeValue = JSON.stringify(newValue);
            });
            if (!isInitialRender) {
                Text.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        Text.pop();
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Text.create('new area is: \n' + this.sizeValue);
            Text.margin({ right: 30, left: 30 });
            if (!isInitialRender) {
                Text.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        Text.pop();
        Column.pop();
    }
    rerender() {
        this.updateDirtyElements();
    }
}
ViewStackProcessor.StartGetAccessRecordingFor(ViewStackProcessor.AllocateNewElmetIdForNextComponent());
loadDocument(new AreaExample(undefined, {}));
ViewStackProcessor.StopGetAccessRecording();
//# sourceMappingURL=Index.js.map

代码分为两部分,一部分为定义了AreaExample的类,第二部分是js的入口执行程序,详细内容参考ArkUI框架学习。我们这里重点关注Text.onAreaChange方法。

2. JS与C++代码的绑定关系

Text组件对应到ace_engine后端引擎中的JSText类:


class JSText : public JSContainerBase {
public:
    static void JSBind(BindingTarget globalObj);
    static void Create(const JSCallbackInfo& info);
    static void SetWidth(const JSCallbackInfo& info);
    static void SetHeight(const JSCallbackInfo& info);
    static void SetFont(const JSCallbackInfo& info);
    static void GetFontInfo(const JSCallbackInfo& info, Font& font);
    static void SetFontSize(const JSCallbackInfo& info);
    static void SetFontWeight(const std::string& value);
    static void SetTextColor(const JSCallbackInfo& info);
    static void SetTextShadow(const JSCallbackInfo& info);
    static void SetTextOverflow(const JSCallbackInfo& info);
    static void SetMaxLines(const JSCallbackInfo& info);
    static void SetTextIndent(const JSCallbackInfo& info);
    static void SetFontStyle(int32_t value);
    static void SetAlign(const JSCallbackInfo& info);
    static void SetTextAlign(int32_t value);
    static void SetLineHeight(const JSCallbackInfo& info);
    static void SetFontFamily(const JSCallbackInfo& info);
    static void SetMinFontSize(const JSCallbackInfo& info);
    static void SetMaxFontSize(const JSCallbackInfo& info);
    static void SetLetterSpacing(const JSCallbackInfo& info);
    static void SetTextCase(int32_t value);
    static void SetBaselineOffset(const JSCallbackInfo& info);
    static void SetDecoration(const JSCallbackInfo& info);
    static void SetCopyOption(const JSCallbackInfo& info);
    static void SetHeightAdaptivePolicy(int32_t value);
    static void JsOnClick(const JSCallbackInfo& info);
    static void JsRemoteMessage(const JSCallbackInfo& info);
    static void JsOnDragStart(const JSCallbackInfo& info);
    static void JsOnDragEnter(const JSCallbackInfo& info);
    static void JsOnDragMove(const JSCallbackInfo& info);
    static void JsOnDragLeave(const JSCallbackInfo& info);
    static void JsOnDrop(const JSCallbackInfo& info);
    static void JsFocusable(const JSCallbackInfo& info);
    static void JsDraggable(const JSCallbackInfo& info);
    static void JsMenuOptionsExtension(const JSCallbackInfo& info);

private:
    static RefPtr<TextComponentV2> GetComponent();
};

查看代码发现JSText中未直接定义onAreaChange方法,在JSText::bind中也未绑定相关的方法,结合onAreaChange方法不止在Text组件中使用,也可以在其他组件中使用,因此继续往JSText继承的基类中找。

class JSContainerBase : public JSViewAbstract, public JSInteractableView {
public:
    static void Pop();
    static void JSBind(BindingTarget globalObj);
};

继续往上层找,发现在JSViewAbstract中定义了JsOnAreaChange方法:

class JSViewAbstract {
    // 忽略掉其他不相关代码片段
     static void JsOnAreaChange(const JSCallbackInfo& info);
}

在 JSViewAbstract::JSBind中实现了JS代码与C++代码的绑定关系

void JSViewAbstract::JSBind(BindingTarget globalObj)
{
    JSClass<JSViewAbstract>::Declare("JSViewAbstract");
    
    JSClass<JSViewAbstract>::StaticMethod("onAreaChange", &JSViewAbstract::JsOnAreaChange);

}

3. ArkTS onAreaChange接口定义

接口在interface/sdk-js/api/@internal/component/ets/common.d.ts中进行定义,两个参数,一个位变化前的组件区域信息,另一个为变化后的组件区域信息。

/**
* This callback is triggered when the size or position of this component change finished.
*
* @param { function } event event callback.
* @returns { T }
* @syscap SystemCapability.ArkUI.ArkUI.Full
* @crossplatform
* @since 10
*/
onAreaChange(event: (oldValue: Area, newValue: Area) => void): T;

4.前端回调函数保存逻辑

JSViewAbstract::JsOnAreaChange代码实现:

void JSViewAbstract::JsOnAreaChange(const JSCallbackInfo& info)
{
    if (info[0]->IsUndefined() && IsDisableEventVersion()) {
        LOGD("JsOnAreaChange callback is undefined");
        ViewAbstractModel::GetInstance()->DisableOnAreaChange();
        return;
    }
    std::vector<JSCallbackInfoType> checkList { JSCallbackInfoType::FUNCTION };
    if (!CheckJSCallbackInfo("JsOnAreaChange", info, checkList)) {
        return;
    }
    auto jsOnAreaChangeFunction = AceType::MakeRefPtr<JsOnAreaChangeFunction>(JSRef<JSFunc>::Cast(info[0]));

    auto onAreaChanged = [execCtx = info.GetExecutionContext(), func = std::move(jsOnAreaChangeFunction)](
                             const Rect& oldRect, const Offset& oldOrigin, const Rect& rect, const Offset& origin) {
        JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx);
        ACE_SCORING_EVENT("onAreaChange");
        func->Execute(oldRect, oldOrigin, rect, origin);
    };
    ViewAbstractModel::GetInstance()->SetOnAreaChanged(std::move(onAreaChanged));
}

关键点:
JSCallbackInfo为从JS前端传过来的参数,对参数合法性进行校验后,从这里面获取到绑定到onAreaChange上的回调函数以及回调函数,然后继续往ViewAbstractModel中传。

ViewAbstractModel是一个纯虚接口类,其实现有ViewAbstractModelNG和ViewAbstractModelImpl两个类,其中ViewAbstractModelNG位新的架构下的实现类,我们重点关注这个类的实现。

void SetOnAreaChanged(
    std::function<void(const Rect& oldRect, const Offset& oldOrigin, const Rect& rect, const Offset& origin)>&&
        onAreaChanged) override
{
    auto areaChangeCallback = [areaChangeFunc = std::move(onAreaChanged)](const RectF& oldRect,
                                  const OffsetF& oldOrigin, const RectF& rect, const OffsetF& origin) {
        areaChangeFunc(Rect(oldRect.GetX(), oldRect.GetY(), oldRect.Width(), oldRect.Height()),
            Offset(oldOrigin.GetX(), oldOrigin.GetY()), Rect(rect.GetX(), rect.GetY(), rect.Width(), rect.Height()),
            Offset(origin.GetX(), origin.GetY()));
    };
    ViewAbstract::SetOnAreaChanged(std::move(areaChangeCallback));
}

这里也没有做更多的东西,继续调用ViewAbstract::SetOnAreaChanged方法。
看下ViewAbstract::SetOnAreaChanged实现:

void ViewAbstract::SetOnAreaChanged(
    std::function<void(const RectF& oldRect, const OffsetF& oldOrigin, const RectF& rect, const OffsetF& origin)>&&
        onAreaChanged)
{   
    auto pipeline = PipelineContext::GetCurrentContext();
    CHECK_NULL_VOID(pipeline);
    auto frameNode = ViewStackProcessor::GetInstance()->GetMainFrameNode();
    CHECK_NULL_VOID(frameNode);
    frameNode->SetOnAreaChangeCallback(std::move(onAreaChanged));
    pipeline->AddOnAreaChangeNode(frameNode->GetId());
}

这里是关键点:

  1. 通过ViewStackProcessor获取到当前正在处理的frameNode,将前端一路传递过来的回调函数保存到frameNode中。
  2. 将frameNode的ID加入到渲染管线PipeContext中保存。
// FrameNode中保存回调函数,将回调函数保存到事件总线eventHub_
void FrameNode::SetOnAreaChangeCallback(OnAreaChangedFunc&& callback)
{
    if (!lastFrameRect_) {
        lastFrameRect_ = std::make_unique<RectF>();
    }
    if (!lastParentOffsetToWindow_) {
        lastParentOffsetToWindow_ = std::make_unique<OffsetF>();
    }
    eventHub_->SetOnAreaChanged(std::move(callback));
}

// 事件总线中保存回调函数
void EventHub::SetOnAreaChanged(OnAreaChangedFunc&& onAreaChanged)
{
    onAreaChanged_ = std::move(onAreaChanged);
}

//管线中保存FrameNode
void PipelineContext::AddOnAreaChangeNode(int32_t nodeId)
{
    onAreaChangeNodeIds_.emplace(nodeId);
}

6.回调函数调用处理

从上面的分析看,最终回调函数是保存在EventHub中的onAreaChanged_,那么只需要根据onAreaChanged_在哪里调用,然后再一路查看调用点,就可以看到详细的调用栈,这里不再贴详细的代码,直接贴出来最终的调用链:

PipelineContext::FlushVsync->PipelineContext::HandleOnAreaChangeEvent->FrameNode::TriggerOnAreaChangeCallback->EventHub::FireOnAreaChanged

从调用栈可以看到,在接受到底层vsync事件时,会触发PipelineContext::FlushVsync,之后按照上述的调用链,最终调用到前台绑定到Text.onAreaChange上的回调函数。

7. 代码分析总结

整体的类图关系如下:

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》:

如何快速入门:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ……

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

基于ArkTS 开发:https://qr21.cn/FV7h05

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. ……

鸿蒙开发面试真题(含参考答案):https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值