本文及后续的几篇博文将围绕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框架远不止这些内容,“完整”的解释器模式仍有值得讨论的东西,比如本文并没有详细解释“编译过程”,限于篇幅,本文就到此为止,下一篇博文继续讨论“框架”和“解释器”的细节。