鸿蒙原生系列之动画效果(属性动画)

鸿蒙原生系列之动画效果

〇、前言

开始之前,先说下题外话,就是我自己经过在华为官方鸿蒙开发者平台工单系统上的不懈追问,终于掌握了 NDK UI 中图片节点的 NODE_IMAGE_SRC 属性,如何使用包内图片作为图片资源进行显示:ArkUI_AttributeItem item = {.string = "resource:///logo.png"};;解决了包内图片的使用后,下一步就该解决如何使用网络图片了,所以,我又开始了新一轮的追问。

言归正传,动画效果在应用UI实现过程中会经常遇到,正确使用动画效果可以让 UI 具备灵动的特性,从而吸引用户,那么,在鸿蒙 NDK UI 中,如何实现动画效果呢?下面就让我由简入繁、带领大家学习如何用 C++ 代码实现动画效果。

一、动画分类

首先,了解一下 UI 中的动画效果分为几种?目前,UI动画效果主要有:

1)属性动画

2)组件出现/消失转场动画

3)关键帧动画

4)帧动画

1、属性动画

属性动画,在四种动画效果中,属于相对简单的那种;在 NDK UI 开发方式中,实现属性动画,需要借助 ArkUI_AnimateOption 去设置具体的动画参数。ArkUI_AnimateOption 位于 native_animate.h 中,因此,实现属性动画的时候,必须导入该头文件。

动画本身是一种事件,因此,属性动画的实现代码中,少不了回调函数的存在,而在鸿蒙 NDK UI 中,主要需要一个 ArkUI_ContextCallback 类型的 update 回到,和一个 ArkUI_AnimateCompleteCallback 类型的 complete 回调,分别对应动画的过程和结束即完成。

1.1、ArkUI_AnimateOption

ArkUI_AnimateOption 对象实例的获取,不是 new 出来的,而是通过专门的创建方法 OH_ArkUI_AnimateOption_Create 进行创建。获取了 ArkUI_AnimateOption 对象实例后,就可以使用相关方法对动画的持续时长、播放速度、动画曲线、动画延迟播放时间、动画播放次数、动画播放模式和动画的期望帧率等关键属性进行设置。

1.1.1、动画时长

动画时长,该属性的设置,使用 OH_ArkUI_AnimateOption_SetDuration(ArkUI_AnimateOption* option, int32_t value) 来完成,时间值的单位是毫秒

1.1.2、播放速度

播放速度,用 OH_ArkUI_AnimateOption_SetTempo(ArkUI_AnimateOption* option, float value) 完成,速度值跟视频播放速度的概念一样,属于倍率体系。

1.1.3、动画曲线

动画曲线的设置方法有两个,分别是 OH_ArkUI_AnimateOption_SetCurve(ArkUI_AnimateOption* option, ArkUI_AnimationCurve value)OH_ArkUI_AnimateOption_SetICurve(ArkUI_AnimateOption* option, ArkUI_CurveHandle value)后者由于前者生效

第一个设置方法,具有默认值,默认使用 ARKUI_CURVE_LINEAR ,而合法的动画曲线候选值共有如下:

1.1.4、动画延迟播放时间

使用 OH_ArkUI_AnimateOption_SetDelay(ArkUI_AnimateOption* option, int32_t value) 进行设置,而时间值的单位同样是毫秒

1.1.5、动画播放次数

OH_ArkUI_AnimateOption_SetIterations(ArkUI_AnimateOption* option, int32_t value)可以完成动画播放次数的设置。

1.1.6、动画播放模式

OH_ArkUI_AnimateOption_SetPlayMode(ArkUI_AnimateOption* option, ArkUI_AnimationPlayMode value),用于设置动画播放模式;现有的动画播放模式共有:

动画播放模式无默认值。

1.1.7、动画播放期望帧率

OH_ArkUI_AnimateOption_SetExpectedFrameRateRange(ArkUI_AnimateOption* option, ArkUI_ExpectedFrameRateRange* value) 进行设置,而 ArkUI_ExpectedFrameRateRange 对象的结构如下:

1.2、ArkUI_AnimateCompleteCallback

ArkUI_AnimateCompleteCallback 对象的结构如下:

成员变量中的 ArkUI_FinishCallbackType ,具体枚举值有:

ArkUI_AnimateCompleteCallback 对象实例,使用 new 方式创建

1.3、ArkUI_ContextCallback

ArkUI_ContextCallback 对象实例,同样使用new 方式 创建,它的对象结构与 ArkUI_AnimateCompleteCallback 类似:

二、动画实现

1、属性动画

1.1、新增 TextInput 节点

本着逐一掌握如何使用C++代码实现UI节点的原则,本文选择使用 TextInput 作为动画『演武场』;TextInput 节点的实现代码如下:

cpp 复制代码
#ifndef NATIVEPC_ARKUITEXTINPUTNODE_H
#define NATIVEPC_ARKUITEXTINPUTNODE_H
#include "ArkUINode.h"
namespace NativeModule { 
    class ArkUITextInputNode : public ArkUINode {
        public:
            ArkUITextInputNode()
                : ArkUINode(NativeModuleInstance::GetInstance()->GetNativeNodeAPI()->createNode(ARKUI_NODE_TEXT_INPUT)){}
            
    };
}

#endif //NATIVEPC_ARKUITEXTINPUTNODE_H

由于不需要设置 TextInput 的私有属性,所以类定义代码就比较简洁。

1.2、页面结构

动画的播放由按钮进行控制,点击按钮就能触发下方的 textinput 开始遵循预定义的动画效果进行变化。

由于需要将按钮和文本输入框分隔开来,避免组件边界紧挨着,因此,需要一个新的组件共有属性设置方法 SetMargin:

cpp 复制代码
void SetMargin(float top, float right, float bottom, float left){
     assert(handle_);
     ArkUI_NumberValue value[] = {{.f32 = top}, {.f32 = right}, {.f32 = bottom}, {.f32 = left}};
     ArkUI_AttributeItem item = {value, 4};
     nativeModule_->setAttribute(handle_, NODE_MARGIN, &item);
 }

1.3、ArkUI_NativeAnimateAPI_1

播放动画,需要用到一个全新的 API------ArkUI_NativeAnimateAPI_1;说实在的,它长得跟之前认识的 ArkUI_NativeNodeAPI_1 很像,所以,ArkUI_NativeAnimateAPI_1 的获取,不妨也按照 ArkUI_NativeNodeAPI_1 的获取那样,在 NativeModule.h 文件中进行定义:

cpp 复制代码
class NativeModuleInstance
{
    public:
        static NativeModuleInstance *GetInstance()
        {
            static NativeModuleInstance instance;
            return &instance;
        }

        NativeModuleInstance()
        {
            // 获取NDK接口的函数指针结构体对象,用于后续操作。
            OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, arkUINativeNodeApi_);
            assert(arkUINativeNodeApi_);
            OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_ANIMATE, ArkUI_NativeAnimateAPI_1, animateApi_);
            assert(animateApi_);
        }
        // 暴露给其他模块使用。
        ArkUI_NativeNodeAPI_1 *GetNativeNodeAPI() { return arkUINativeNodeApi_; }
        ArkUI_NativeAnimateAPI_1 *GetNativeAnimateAPI() { return animateApi_; }

    private:
        ArkUI_NativeNodeAPI_1 *arkUINativeNodeApi_ = nullptr;
        ArkUI_NativeAnimateAPI_1 *animateApi_ = nullptr;
};

ArkUI_NativeAnimateAPI_1,ArkUI提供的Native侧动画接口集合,拥有如下成员函数:

animateTo() 便是实现属性动画所必须的:

1.4、testPropertiesAnimate()

不妨新建一个 testAnimate.h 文件,用于专门存放动画效果相关的实现案例。

作为一名合格的程序员,尤其是从事开发岗位或自动化测试岗位的,必须具备一定的英语水平,因为这可以有效避免在为脚本、变量、方法或类进行取名的时候,找不到可以正确描述功能意义的单词;当然了,大学毕业后,谁也不可能还记得那么多单词,因此,是否有专业翻译的意识,便成了代码可读性的分水岭。

首先,给出整个案例的完整代码:

cpp 复制代码
#ifndef NATIVEPC_TESTANIMATE_H
#define NATIVEPC_TESTANIMATE_H
#include "ArkUIBaseNode.h"
#include "ArkUIButtonNode.h"
#include "ArkUIColumnNode.h"
#include "ArkUITextInputNode.h"
#include "ArkUITextNode.h"
#include "NativeEntry.h"
namespace NativeModule
{
    std::shared_ptr<ArkUIButtonNode> g_animator_button = nullptr;
    std::shared_ptr<ArkUITextInputNode> g_animator_textInput = nullptr;

    std::shared_ptr<ArkUIBaseNode> testPropertiesAnimate()
    {
        auto column = std::make_shared<ArkUIColumnNode>();
        column->SetPadding(10, false);
        column->SetPercentHeight(1.0);
        column->SetPercentWidth(1.0);
        // 创建一个按钮节点
        auto button = std::make_shared<ArkUIButtonNode>();
        button->SetEnable();
        button->SetPercentWidth(0.4);
        button->SetHeight(60);
        button->SetBackgroundColor(0xFF000000);
        button->SetLabel("开始动画");
        g_animator_button = button;
        column->AddChild(button);
        auto textInput = std::make_shared<ArkUITextInputNode>();
        textInput->SetBackgroundColor(0xff27253b);
        textInput->SetHeight(120);
        textInput->SetPercentWidth(0.4);
        textInput->SetMargin(50.0, 0.0, 0.0, 0.0);
        g_animator_textInput = textInput;
        column->AddChild(textInput);
        button->RegisterNodeEvent(button->GetHandle(), NODE_ON_CLICK, 4, nullptr);
        auto onTouchPlay = [](ArkUI_NodeEvent *event)
        {
            // 准备动画参数
            ArkUI_AnimateOption *option = OH_ArkUI_AnimateOption_Create();
            OH_ArkUI_AnimateOption_SetDuration(option, 2000);
            OH_ArkUI_AnimateOption_SetTempo(option, 1.1);
            OH_ArkUI_AnimateOption_SetCurve(option, ARKUI_CURVE_EASE);
            OH_ArkUI_AnimateOption_SetDelay(option, 20);
            OH_ArkUI_AnimateOption_SetIterations(option, 1);
            OH_ArkUI_AnimateOption_SetPlayMode(option, ARKUI_ANIMATION_PLAY_MODE_REVERSE);
            ArkUI_ExpectedFrameRateRange *range = new ArkUI_ExpectedFrameRateRange;
            range->min = 10;
            range->max = 120;
            range->expected = 60;
            OH_ArkUI_AnimateOption_SetExpectedFrameRateRange(option, range);
            // 用户自定义参数
            struct UserData
            {
                    int32_t data;
            };
            UserData *onFinishUser = new UserData;
            onFinishUser->data = 101;
            // 设置完成的回调
            ArkUI_AnimateCompleteCallback *completeCallback = new ArkUI_AnimateCompleteCallback;
            completeCallback->userData = onFinishUser;
            completeCallback->type = ARKUI_FINISH_CALLBACK_REMOVED;
            completeCallback->callback = [](void *userData)
            {
                OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager",
                             "CreateNativeNode  onFinishCallback %{public}d",
                             reinterpret_cast<UserData *>(userData)->data);
            };
            // 用户自定义参数
            UserData *eventUser = new UserData;
            eventUser->data = 201;
            static bool isBack = true;
            ArkUI_ContextCallback *update = new ArkUI_ContextCallback;
            update->userData = eventUser;
            update->callback = [](void *user)
            {
                OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode  animateTo %{public}d",
                             reinterpret_cast<UserData *>(user)->data);
                    // 对应的属性变化 width height
                    if (isBack) {
                        g_animator_textInput->SetWidth(200);
                        g_animator_textInput->SetHeight(80);
                    }
                    else {
                        g_animator_textInput->SetWidth(100);
                        g_animator_textInput->SetHeight(40);
                    }
                    isBack = !isBack;
            };
            // 获取ArkUI_NativeAnimateAPI接口
            auto animateApi = NativeModuleInstance::GetInstance()->GetNativeAnimateAPI();
            // 获取context对象
            static ArkUI_ContextHandle context = nullptr;
            context = OH_ArkUI_GetContextByNode(g_animator_button->GetHandle());
            // 执行对应的动画
            animateApi->animateTo(context, option, update, completeCallback);
        };
        button->RegisterNodeEventReceiver(onTouchPlay);
        return column;
    }
    
} // namespace NativeModule

#endif // NATIVEPC_TESTANIMATE_H

下面逐一讲解整个案例的实现思路:

1.4.1、必须的全局指针字段

虽然,全局变量存在如果没有控制好使用方式会导致程序内部异常 的风险,但在此次的案例中,是必须定义一个 g_animator_buttong_animator_textInput,否则,在相关动画回调函数中,就没办法对定义在回调函数外的UI节点进行操作,而 ArkUI_AnimateCompleteCallback 对象的 callback 方法不支持捕获外部数据:

1.4.2、完成基本骨架

在 UI 开发过程中,一定要有先后意识 ,这个 『先』就是先完成直接可见的东西,也就是页面的整体结构,把需要的页面元素先定义出来,而『后』对应的就是不可见的东西,通常就是用户交互的处理代码,因此,一开始可以先在 testPropertiesAnimate 方法中编写如下代码,完成页面的整体:

如果页面元素的尺寸不合适、或者元素间距需要调整,这些简单的代码便能助力快速定位到对应的行。

1.4.3、由外到内实现回调函数

为了避免定义太多全局字段,将逻辑相关的代码直接放到回调函数内,是高效编程所必须的操作,在这个案例中,由于动画效果的触发是通过按钮的单击事件来完成的,所以嘛,理所当然的就需要注册按钮的点击事件以及相应的事件处理回调,所以,先在 UI 元素定义代码之后,接着编写如下代码片段:

cpp 复制代码
button->RegisterNodeEvent(button->GetHandle(), NODE_ON_CLICK, 4, nullptr);
auto onTouchPlay = [](ArkUI_NodeEvent *event)
{
    
};
button->RegisterNodeEventReceiver(onTouchPlay);

先让按钮的回调函数里面啥也没有,或者就是一行简单的log打印,然后真机跑一下,看看是否顺利运行。

在按钮点击事件处理回调函数成功执行后,再具体地根据前面学习的属性动画的实现原理,逐步骤地编码:先准备ArkUI_AnimateOption和设置动画属性,再准备ArkUI_AnimateCompleteCallback 和 ArkUI_ContextCallback,最后用 animateTo 播放动画,而 ArkUI_AnimateCompleteCallback 和 ArkUI_ContextCallback 身上的回调函数,同样的先置空:

cpp 复制代码
auto onTouchPlay = [](ArkUI_NodeEvent *event)
{
    // 准备动画参数
    ArkUI_AnimateOption *option = OH_ArkUI_AnimateOption_Create();
    OH_ArkUI_AnimateOption_SetDuration(option, 2000);
    OH_ArkUI_AnimateOption_SetTempo(option, 1.1);
    OH_ArkUI_AnimateOption_SetCurve(option, ARKUI_CURVE_EASE);
    OH_ArkUI_AnimateOption_SetDelay(option, 20);
    OH_ArkUI_AnimateOption_SetIterations(option, 1);
    OH_ArkUI_AnimateOption_SetPlayMode(option, ARKUI_ANIMATION_PLAY_MODE_REVERSE);
    ArkUI_ExpectedFrameRateRange *range = new ArkUI_ExpectedFrameRateRange;
    range->min = 10;
    range->max = 120;
    range->expected = 60;
    OH_ArkUI_AnimateOption_SetExpectedFrameRateRange(option, range);
    // ....
    // 设置完成的回调
    ArkUI_AnimateCompleteCallback *completeCallback = new ArkUI_AnimateCompleteCallback;
    completeCallback->userData = onFinishUser;
    completeCallback->type = ARKUI_FINISH_CALLBACK_REMOVED;
    completeCallback->callback = [](void *userData)
    {
        
    };
    // ...
    ArkUI_ContextCallback *update = new ArkUI_ContextCallback;
    update->userData = eventUser;
    update->callback = [](void *user)
    {
        
    };
    // ...
    // 执行对应的动画
    animateApi->animateTo(context, option, update, completeCallback);
};

同样真机部署试着运行,无错误之后,再将剩下的两个内层回调函数的实现代码补全。

1.5、实现效果

最终实现效果如下所示:

属性动画演示效果

相关推荐
江澎涌3 小时前
JWorker——一套简单易用的基于鸿蒙 Worker 的双向 RPC 通讯机制
typescript·harmonyos·arkts
晚霞的不甘3 小时前
Flutter + OpenHarmony 国际化与无障碍(i18n & a11y)深度实践:打造真正包容的鸿蒙应用
flutter·华为·harmonyos
song5014 小时前
鸿蒙 Flutter 离线缓存架构:多层缓存与数据一致性
人工智能·分布式·flutter·华为·开源鸿蒙
威哥爱编程12 小时前
【鸿蒙开发案例篇】定点出击!鸿蒙6.0视频碰一碰流转+实时进度同步案例
harmonyos·arkts·arkui
嗝o゚13 小时前
鱼与熊掌可兼得?用Flutter+鸿蒙的混合架构破解性能与UI的世纪难题
flutter·架构·harmonyos
遇到困难睡大觉哈哈16 小时前
HarmonyOS 应用数据持久化概述:Preferences、KV-Store、RelationalStore 到底怎么选?
笔记·华为·harmonyos
宇擎智脑科技16 小时前
Flutter 对接高德地图 SDK 适配鸿蒙踩坑记录与通信架构解析
flutter·架构·harmonyos
嗝o゚17 小时前
鸿蒙智慧屏与Flutter适配:无硬件功能的兼容处理
flutter·华为·开源·harmonyos
luxy200417 小时前
HarmonyOS简易时钟应用
华为·harmonyos