闲谈解释器模式——基于UI动画框架

15 篇文章 0 订阅
9 篇文章 0 订阅

  本文及后续的几篇博文将围绕UI动画框架的“框架”来讨论。前文中的示例说明了配置动画的使用方式。就让我们从配置开始说起吧。

1. 配置

  一段动画的配置是这样的:

<!-- 定义一个动画策略(组),该策略可以绑定到已有的窗口,使得窗口具有该策略所描述的动画效果 -->
<strategys id = "123" desc = "窗口慢慢向上飘起,慢慢缩小,并慢慢消失" >
    <strategy id = "frame" param = "30" /> <!-- 30ms一帧 -->
    <strategy id = "move" param = "vertical;10;-20" /> <!-- 从起始位置向上移动10次,每次20像素 -->
    <strategy id = "scale" param = "1;0.8;0.8;6" /> <!-- 尺寸缩小10次,每次长宽均缩小0.8 -->
    <strategy id = "fade" param = "1;0.8" /> <!-- alpha从1.0起每次淡化0.8 -->
</strategys>

或者这样的:

<!-- 定义一个完整的动画,该动画不需要用户指定窗口,而是由框架自身创建窗口来表现 -->
<animation id = "500" desc = "法器升级的光效" > <!-- 描述一个完整的动画序列 -->
    <!-- 该动画序列只包含一个动画,资源来自set:effects animation:faqi序列帧 -->
    <single image = "set:effect animation:faqi" > 
        <strategys type = "parallel" > <!-- 该动画有3个策略,这些策略并行执行 -->
            <strategy id = "frame" param = "30" /> <!-- 30ms一帧 -->
            <strategy id = "jmp_frame" param = "2" /> <!-- 每一次在序列帧中跳2帧 -->
            <strategy id = "times" param = "16" /> <!-- 播放总共重复16次 -->
        </strategys>
    </single>
</animation> 

<!-- 定义一个完整的动画,该动画不需要用户指定窗口,而是由框架自身创建窗口来展现 -->
<animation id = "600" desc = "获得物品动画" > <!-- 描述一个完整的动画序列 -->
    <!-- 该动画序列包含2个动画,这两个动画串行执行 -->
    <group type = "serial" >
        <single>
            <strategys type = "parallel" >
                <strategy id = "frame" param = "40" />
                <strategy id = "move" param = "6;30" />
            </strategys>
        </single>
        <single image = "set:effects animation:tips">
            <strategys type = "serial" > <!-- 该动画有2个策略,这些策略串行执行 -->
                <strategy id = "jmp_frame" />
                <strategy id = "times" param = "16" />
            </strategys>
        </single>
    </group>
</animation>

这些配置就是唯一需要我们告诉动画框架的东西了。上面的几段“话”,动画框架是怎么听懂的呢?它如何理解strategys、strategy、frame、move这些词汇都是什么意思?接下来我们来解释背后的机制。

2. 解释器模式

  现在我们遇到了UI动画框架中的第一个设计模式,也是23个设计模式中最大的模式:“解释器模式”

给定一个语言,定义它的文法的一种表示形式,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

这是“解释器模式”的定义,坦白说,这不像是在说人话。

  很显然,我们已经定义了一个动画配置语言,这个语言的“文法”如下:

 - strategys → strategys | strategy  ; “策略组”可以扩展为“策略组”或“单个策略”
 - animation → group | single        ; “动画”可以扩展为“动画组”或“单个动画”
 - group → group | single            ; “动画组”可以扩展为“动画组”或“单个动画”
 - single → strategys                ; “单个动画”可以扩展为“策略组”

可以看出“strategy”是“终结符”,也就是不可再分的单元(我没有解释配置中像type、param、id及desc等属性字段,这些字段和我们讨论的“解释器模式”无关)。我们用这样的“规则”来编写想要的动画“代码”,然后把这样的“代码”提交给动画框架,由框架来实现我们的想法。

  框架里面必然有一个叫作“解释器”的东西,用来解释我们编写的动画代码,以实现代码的意图。我们先来看看所谓“解释器模式”的类图:

解释器模式类图

  AbstractExpression:定义解释器的接口。

  TerminalExpression:终结符解释器,用来实现语法规则中和终结符相关的操作,不再包含其它的解释器,通常一个解释器模式中只有一个终结符表达式,但有多个实例,对应多个终结符。

  NonterminalExpression:非终结符解释器,用来实现语法规则中非终结符相关的操作。文法中的每条规则对应一个非终结符表达式,通常每个文法规则对应一个非终结符表达式。

  Context:上下文,通常包含各个解释器需要的数据或公共的功能,上下文也可以理解成解释器运行的环境。

  Client:使用解释器的客户端,通常在这里将按照该语言的语法写成的“代码”转化成用解释器对象描述的抽象语法树,然后调用解释操作。

  这是“解释器模式”的典型解释,同样的非人类语言,这里就不展开讨论了,有兴趣的同学可以百度一下“解释器模式”的定义。

  动画配置语言中的animation、group、single以及strategys都是解释器模式中的非终结符解释器,只有strategy是终结符解释器。但在具体实现时,会有一些调整,从配置语言的文法规则可以看出animation除了比group多一个id外,没什么不同,它们都是扩展成group | single,因此在实现时,animation就被直接展开成group或single,也就不存在一个animation类(或对象),而是用<id, group | single>这样的映射来表示一个animation。另外在具体实现时,group和single属于一个类体系,而strategys和strategy属于另一个类体系,它们并不是严格按照解释器模式类图中所示,都从一个抽象的解释器接口类派生,而是从不同的抽象解释器接口派生:

在这里插入图片描述

从图中可以看出strategy有很多实例(在设计时,实际上是很多个类),它们都是“终结符”,分别负责解释一个具体的动画策略,比如前面提到的:frame(帧率)、move(位移)、fade(淡入淡出)、scale(缩放)、times(重复)等等。

  用这个动画配置语言编写出来的一段动画代码,经过框架编译,最终生成一棵“抽象语法树”。树的层次和配置基本一致。在运行时,框架解释这棵抽象语法树,解释的过程就是执行相应对象的功能的过程,也就实现了客户编写动画的意图。接下来,简单解释下抽象语法树上的各个对象。

3. 实现

3.1. Animation

  Animation是动画类的基类,其主要的接口如下:

class Animation
{
public:
    virtual ~Animation() = 0;
    // 同一个动画配置可能会有多个实例运行,因此需要一个Clone接口
    virtual Animation* Clone() const = 0;
    // GroupAnimation包含有多个Animation
    virtual void AddAnimation(Animation* ani) = 0;
    // 客户代码通过Create创建一个动画(窗口),并传入一个窗口表示动画在哪个窗口中运行
    virtual bool Create(int id, Window* parent) = 0;
    
    virtual bool Run() = 0; // 运行动画
    virtual bool IsFinished() const = 0;
};

3.2. SingleAnimation

  SingleAnimation描述一个单一动画:

class SingleAnimation : public Animation
{
private:
    virtual SingleAnimation* Clone() const; // 协变机制返回
    // 单一动画的AddAnimation什么都不做
    virtual void AddAnimation(Animation* ani) {}
    virtual bool Create(int id, Window* parent);
    virtual bool Run();
    virtual bool IsFinished() const;

private:
    bool                clipped;   // 是否被父窗口裁剪
    Window*             aniWnd;    // 动画窗口对象
    IStrategy*          strategy;  // 动画的控制策略
};

动画窗口是框架创建的,客户只需指定动画在哪个窗口里,哪个位置开始播放。

3.3. GroupAnimation

  GroupAnimation描述一组动画:

class GroupAnimation : public Animation
{
private:
    virtual void AddAnimation(Animation* ani);
    virtual bool Create(int id, Window* parent);

    virtual bool IsFinished(void) const
    {
        for (auto animation : animations)
            if (!animation->Finished())
                return false;

        return true;
    }

protected:
    std::vector<Animation*> animations;
};

// 串行动画
class GroupAnimationSerial : public GroupAnimation
{
public:
    virtual GroupAnimationSerial* Clone() const; // 协变机制返回
    virtual bool Run()
    {
        for (auto animation : animations)
            if (animation->Run()) // 串行动画,只要有一个子动画还能启动成功,就返回
                return true; // 返回值表面动画启动成功

        return false; // 返回值表面动画未启动
    }
};

// 并行动画
class GroupAnimationParallel : public GroupAnimation
{
public:
    virtual GroupAnimationParallel* Clone() const; // 协变机制返回
    virtual bool Run()
    {
        for (auto animation : animations)
            animation->Run(); // 所有子动画并行执行

        return true;
    }
};

3.4. IStrategy

  IStrategy是控制窗口行为的动画策略基类:

class IStrategy
{
public:
    virtual ~IStrategy() = 0;
    // 同一个策略可能会有多个实例,因此需要一个Clone
    virtual IStrategy* Clone() const = 0;
    virtual void Reset() = 0;
    // 设置某个策略的参数,以便支持精细化控制动画行为
    virtual bool SetStrategyParm(std::vector<int>& ids, const String& parm);
    virtual bool IsFinished(Window& aniWnd) const = 0;
    virtual bool Run(Window& aniWnd) = 0;
};

3.5. Strategy

  Strategy是单一的动画策略,这样的策略有很多种,比如控制动画播放流畅性的帧率策略:

class FrameStrategy : public IStrategy
{
private:
    virtual FrameStrategy* Clone() const; // 协变机制返回
    bool IsFinished(Window&) const
    {
        return true;
    }
    
    // 返回值表示本策略的控制是否结束了,如果结束了,框架可能会再后续继续调度后面的策略
    virtual bool Run(Window&)
    {
        long ot = rkt::getTickCount();
        if (ot - lastTick > interval)
        {
            lastTick = ot;
            return true; // 逝去的时间满足要求,返回true表示本策略控制结束了
        }
        return false; // 返回false表示本策略还在施加控制
    }

private:
    mutable int  interval;  // 两次播放之间的间隔毫秒数
    long         lastTick;  // 逝去的时间
};

  比如淡入淡出的策略:

class FadeStrategy : public IStrategy
{
private:
    virtual FadeStrategy* Clone() const; // 协变机制返回
    
    // alpha已超出控制范围,即表示淡入淡出策略控制已结束
    virtual bool IsFinished(Window& aniWnd) const
    {
        float alpha = aniWnd.getAlpha();
        return ((delta < 1.0 && alpha < minAlpha) || (delta > 1.0 && alpha >= maxAlpha));
    }
    
    // 除了FrameStrategy,其它的策略Run都返回true,表示一次控制结束
    virtual bool Run(Window& aniWnd)
    {
        float alpha = aniWnd.getAlpha();
        if ((delta < 1.0 && alpha >= minAlpha) || (delta > 1.0 && alpha < maxAlpha))
        {
            float xalpha = alpha * delta;
            aniWnd.setAlpha(xalpha > 1.0f ? 1.0f : xalpha);
        }
        return true;
    }

private:
    const float  iniAlpha;      // alpha初始值
    float        delta;         // 帧与帧之间的alpha变化的百分比
    const float  minAlpha;      // alpha的下限
    const float  maxAlpha;      // alpha的上限
};

3.6. Strategys

  Strategys是动画策略组,组里面的策略可以串行施加控制,或者并行施加控制:

class Strategys : public IStrategy
{
private:
    virtual bool IsFinished(Window&) const
    {
        return times <= 0;
    }
    void AddStrategy(IStrategy* strategy)
    {
        strategys.push_back(strategy);
    }

protected:
    std::vector<IStrategy*> strategys;
    int             times; // 策略组施加控制的次数,默认为1次
    const int       ctimes; // 用于恢复
};

// 串行策略组
class StrategysSerial : public Strategys
{
private:
    virtual bool Run(Window& aniWnd)
    {
        if (times <= 0) return true;
        if (cur >= 0 && cur < strategys.size())
        {
            if (strategys[cur]->Run(aniWnd) && strategys[cur]->IsFinished(aniWnd))
            {
                strategys[cur]->Reset();
                ++cur; // 一个策略控制结束了,才步进到下一个策略
            }
        }

        if (cur >= strategys.size()) // 策略组完成一次控制,次数减一,cur复位,为下一次准备
        {
            --times;	
            cur = 0;
        }

        return true;
    }

private:
    int  cur; // 当前正在施加控制的策略的索引
};

// 并行策略组
class StrategysParallel : public Strategys
{
private:
    virtual bool Run(Window& aniWnd)
    {
        if (times <= 0) return true;
		
        // 一般地,除了控制流畅性的FrameStrategy,其它的策略每一次Run都返回true,从而实现并行
        for (; cur != strategys.end(); ++cur)
            if (!(*cur)->Run(aniWnd)) { break; }

        if (cur == strategys.end()) // 一轮结束,复位,准备下一次
            cur = strategys.begin();

        bool allFinished = true;
        for (auto strategy : strategys)
            allFinished = (allFinished && strategy->IsFinished(aniWnd));

        if (allFinished)
        {
            --times;
            for (auto strategy : strategys)
                strategy->Reset();
        }
        return true;
    }

private:
    std::vector<IStrategy*>::const_iterator cur; // 当前执行到哪个策略,下一帧从这个策略开始继续执行
};

4. 编译过程

  再说回解释器模式,从解释器模式的定义和类图上可以看出,模式中的“终结符表达式”和“非终结符表达式”已经是“抽象语法树”上的组件,也就是说模式里面的东西是某一个过程的输出,而这个过程,我们称之为“编译过程”。

  开发者在文件中用定义好的语言编写动画代码(配置),这些代码不过是具有某种意义的字符串而已,我们需要一种工具来把这些字符串转换成最终的抽象语法树。比如:

<strategys id = "123" desc = "窗口慢慢向上飘起,慢慢缩小,并慢慢消失" >
    <strategy id = "frame" param = "30" />
    <strategy id = "move" param = "vertical;10;-20" />
    <strategy id = "scale" param = "1;0.8;0.8;6" />
    <strategy id = "fade" param = "1;0.8" />
</strategys>

这段配置里面的"<"、“strategys”、“id”、"="、“123”、“desc”、“strategy”、“frame”、“param”、"move"以及"vertical;10;-20"等等这些字母组成的词汇都是什么意思?

  将这些字母(符号)集,识别为一个个词汇的过程就是词法分析,词法分析之后还需要根据语言的语法规则进行语法分析,最终将客户用该语言写成的“源代码”翻译(编译)成一个抽象语法树。前面我提到“解释器模式”是23个设计模式中最大的模式,原因其实是“词法分析”和“语法分析”并不是解释器模式的一部分(解释器模式直接享受这两种分析所得的成果),但却是必不可少的两个步骤,而这两个步骤通常是手工完成;如果“源代码”用一些流行的文本格式编写,比如xml,则可以借助开源的xml解析器来进行词法分析,否则如果使用自定义格式“撰写源代码”,则需要手工编写词法分析器,工作量巨大!真的是“非一日之功”。本人职业生涯中亲自实现过两个这样的框架,指导他人实现过一个框架,每一次都会在这两个步骤上花费很多时间。因为,这其实是在“发明”并实现一门语言,和C++、Python、Lua之类的通用编程语言没有本质上的差别。

  虽然解释器模式成本较高,但一旦实现,收益巨大!

  • 解释器模式易于实现语法和扩展新的语法,而且很容易做到“零缺陷”!就像你实现了一门语言(编译器),编译器本身一旦稳定,那么再出问题就可能是“代码”的问题。解释器模式用一个框架,把功能描述和功能实现分开的同时,也把代码质量的责任转嫁给了使用你这门语言的开发者。
  • 如果你定义的语言支持了选择、迭代结构以及变量等特性,它就是一个图灵完备的语言了,理论上可以计算任何东西!这就意味着,可以不用再动框架(也就是你实现的编译器&解释器)的任何代码,即可支持开发者实现新的功能(不就是写代码嘛!),当然你可能需要扩展你的语言,或者为你的语言增加一些库函数,以便开发者调用。
  • 如果真的被不断的要求支持新的特性,你也会欣然接受,因为你是在扩展一门语言,而不是实现业务相关的具体功能,因此你在框架中撸代码时,身心是愉悦的,这种感觉只有你撸过才能体会到!

5. 结语

  UI框架远不止这些内容,“完整”的解释器模式仍有值得讨论的东西,比如本文并没有详细解释“编译过程”,限于篇幅,本文就到此为止,下一篇博文继续讨论“框架”和“解释器”的细节。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值