一. 入门知识
第一节 什么是M3G?
Mobile 3D Graphics API(简称为 M3G)是在 JSR 184(Java 规范请求,Java Specification Request)中定义的,JSR 184 是一项工业成就,用于为支持 Java 程序设计的移动设备提供标准 3D API。
M3G API 大致可分为两部分:快速模式和保留模式。在快速模式下,您渲染的是单独的 3D 对象;而在保留模式下,您需要定义并显示整个 3D 对象世界,包括其外观信息在内。可以将快速模式视为低级的 3D 功能实现方式,保留模式显示 3D 图像的方式更为抽象,令人感觉也更要舒适一些。
第二节 M3G的广泛应用
M3G 不是孤独的。HI Corporation 开发的 Mascot Capsule API 在日本国内非常流行,日本三大运营商均以不同形式选用了这项技术,在其他国家也广受欢迎。例如,Sony EriCsson 为手机增加了 M3G 和 HI Corporation 的特定 API。根据应用程序开发人员在 Sony Ericsson 网站上发布的报告,Mascot Capsule 是一种稳定且快速的 3D环境。
JSR 239 也就是 Java Bindings for OpenGL ES,它面向的设备与 M3G 相同。OpenGL ES 是人们熟知的 OpenGL 3D 库的子集,事实上已成为约束设备上本地 3D 实现的标准。JSR 239 定义了一个几乎与 OpenGL ES 的 C 接口相同的 Java API,使现有 OpenGL 内容的移植更为轻易。到 2005 年 9 月为止,JSR 239 还依然处于早期的蓝图设计状态。关于它是否会给手机带来深刻的影响,我只能靠推测。尽管 OpenGL ES 与其 API 不兼容,但却对 M3G 的定义产生了一定影响:JSR 184 专家组确保了 MSG 在 OpenGL ES 之上的有效实现。假如您了解 OpenGL,那么就会在 M3G 中看到许多似曾相识的属性。
尽管还有其他可选技术,但 M3G 获得了所有主要电话制造商和运营商的支持。之前我提到过,游戏是最大的吸引力所在,但 M3G 是一种通用 API,您可以将其用于创建各种 3D 内容。未来的几年中,手机将广泛采用 3D API。
JSR184标准(M3G:Mobile 3D Graphics)为Java移动应用程序定义了一个简洁的3D API接口,J2ME程序可以非常方便地使用M3G来实现3D应用比如游戏等等。
M3G被设计为非常轻量级的,整个API的完整实现不超过150kb。
第三节 J2ME里的M3G
M3G是J2ME的一个可选包,以OpenGL为基础的精简版,一共有30多个类,运行在CLDC1.1/CLDC2.0上(必须支持浮点运算),可以在MIDP1.0和MIDP2.0中使用。
M3G支持两种3D模式:立即模式(immediate mode)和保留模式(retained mode)。
在立即模式下,开发者必须手动渲染每一帧,从而获得较快的速度,但代码较繁琐;
在保留模式下,开发者只需设置好关键帧,剩下的动画由M3G完成,代码较简单,但速度较慢。
M3G也允许混合使用这两种模式。
M3G提供一个Loader类,允许直接从一个单一的.m3g文件中读入全部3D场景。
M3G文件可以通过3D Studio Max之类的软件创建。
在M3G中:
1、Graphics3D是3D渲染的屏幕接口;
2、World代表整个3D场景,包括Camera(用于设置观察者视角)、Light(灯光)、Background(背景)和树型结构的任意数量的3D物体。
3D对象在计算机中用点(Point, Pixel)、线(Line, Polyline, Spline)、面(Mesh)来描述,具体存储和运算(如旋转、投影)都是矩阵运算和变换。
SUN的WTK2.2已经内置了M3G的实现包,如果安装了WTK2.2,就可以在模拟器上运行3D MIDP程序。
在LWUIT出现之前,就能用使图片动态显示。Javax.microedition.lcdui.game 包有许多的特性来支持动画。例如:
- GameCanvas是一个专门的画布,用于为游戏程序绘制有效的动画图像,还能够查询为实现平滑动画效果而采取的脱屏图像缓冲技术有关的关键状态。
- 用sprite帧实现简单的2D动画
另外jsr 226 api支持SVG-based的动画, JSR 226参考实现,提供一系列API使得程序可以加载和渲染可伸缩的二维矢量图片,包括SVG Tiny 1.1格式的外部图片。在高层,这个RI允许应用程序:
加载静态SVG内容,播放SVG动画,添加/移除SVG元素上的事件监听器,改变视点来缩放内容。
二.Lwuit中的Animation:
lwuit用不同的方法来处理动画,使得这个性能应用广泛而且简便。在lwuit里面,animation从本质上,允许对象在固定的时间间隔内绘制连续的帧。Animation的实现也使得transition的实现更加简单。
第一节 Animation
为了能够产生动画,对象必须实现Animation接口,这个接口有两个方法:
animate:每一帧调用一次的回调方法
paint:如果animate返回true,则这个方法被调用,如果要产生动画的对象是Component,它会有一个paint方法并将被调用。
Component扩展自Animation。这使得每一个构件都可以实现动画。然而,为了真正实现动画,对象必须在其父亲form中调用registerAnimated方法来注册动画。对于一个已经注册的对象来说,每一个帧动画都将由Display调用一次animate方法。如果animate方法返回true,则接着调用paint方法,参见Form.java的代码:
private void loopAnimations(Vector v, Vector notIn) {
// we don't save size() in a varible since the animate method may deregister
// the animation thus invalidating the size
for (int iter = 0; iter < v.size(); iter++) {
Animation c = (Animation) v.elementAt(iter);
if(notIn != null && notIn.contains(c)) {
continue;
}
if (c.animate()) {
if (c instanceof Component) {
Rectangle rect = ((Component) c).getDirtyRegion();
if (rect != null) {
((Component) c).repaint(rect.getX(), rect.getY(), rect.getSize().getWidth(), rect.getSize().getHeight());
} else {
((Component) c).repaint();
}
} else {
Display.getInstance().repaint(c);
}
}
}
}
为了停止动画,必须调用deregisterAnimated方法
最简单的动画实例:
看LWUITDemo里面的AnimationDemo,一下截屏是动画过程中的两帧:
Figure 1. Two frames of the animation demo
第一个Demo是使用一张动态图,第二个Demo是连续播放一系列图片。.在这两个情况下共同的基类都是Button,使用其他的组件也能达到这样的效果。
第一个Button的实例代码:
Button animation = new Button(UIDemoMIDlet.
getResource("duke").getAnimation("duke3_1.gif"));
animation.setBorderPainted(false);
animation.getStyle().setBgTransparency(0);
f.setLayout(new BoxLayout(BoxLayout.Y_AXIS));
f.addComponent(animation);
必须注意的是,这里animation按钮不用调用registerAnimated方法。因为图片本身有动画效果,不用重绘。 这里的图片是gif。目前LWUIT仅仅支持gif动态图片。
第二个button里面,首先setup一个图片序列:
Resources images = UIDemoMIDlet.getResource("images");
Image a = images.getImage("progress0.png");
Image b = images.getImage("progress1.png");
Image c = images.getImage("progress2.png");
final Image[] secondAnimation = new Image[] {
a,
b,
c,
a.rotate(90),
b.rotate(90),
c.rotate(90),
a.rotate(180),
b.rotate(180),
c.rotate(180),
a.rotate(270),
b.rotate(270),
c.rotate(270),
};
第二个button的实例代码:
Button animation2 = new Button() {
private int currentImage = 0;
private long lastInvoke;
public boolean animate() {
long current = System.currentTimeMillis();
if (current - lastInvoke > 50) {
lastInvoke = current;
currentImage++;
if (currentImage == secondAnimation.length) {
currentImage = 0;
}
return true;
}
return false;
}
public void paint(Graphics g) {
g.drawImage(secondAnimation[currentImage],
getX(), getY());
}
};
这个button实现了animate和paint方法,. animate 方法每帧调用一次。如果50微秒过去了,animate增加指针下标并返回true, 然后paint得到调用,否则返回只是false
剩下的代码是控制视觉效果,注意这里最后调用了registerAnimated.
animation2.setPreferredSize(new Dimension
(secondAnimation[0].getWidth(),
secondAnimation[0].getHeight()));
animation2.setBorderPainted(false);
animation2.getStyle().setBgTransparency(0);
f.addComponent(animation2);
f.registerAnimated(animation2);
第二节 Transition
在LWUIT里面,使用transition能够使得在form之间的变换更加生动。这个特性不同于在ppt的不同页之间设置转换动画。有几种模式的动画:slide,fade,3D transition。 其中,slide transition 运行一个form从或左,或右,或上,或下方向移动来取代现有的form ;fade transition使得一个form在另外一个form出现的时候缓慢消失;而在支持 M3G(Mobile 3D Graphics) Api的设备上,3d transition 也是允许的(飞入,立方体旋转,翻转)。LWUIT transition类运行动画被添加到form(进或者出)。
实现transition的基类,继承自Animation,所以transitions跟animations工作原理类似,而且使用相同的回调机制。Transition和Animation的区别在于,一个Component能动态化自己的渲染过程,但是Transition控制form和dialog等的渲染。
还有不同的地方是,使用transition,不需要在form注册。当一个form的transition发生,Display对象直接将form放到一个接受回调的队列。
Lwuit的源码片段:
/**
* Displays the given Form on the screen.
*
* @param newForm the Form to Display
*/
void setCurrent(final Form newForm, boolean reverse) {
if (edt == null) {
throw new IllegalStateException(
"Initialize must be invoked before setCurrent!");
}
if (isVirtualKeyboardShowingSupported()) {
setShowVirtualKeyboard(false);
}
if (editingText) {
switch (showDuringEdit) {
case SHOW_DURING_EDIT_ALLOW_DISCARD:
break;
case SHOW_DURING_EDIT_ALLOW_SAVE:
impl.saveTextEditingState();
break;
case SHOW_DURING_EDIT_EXCEPTION:
throw new IllegalStateException("Show during edit");
case SHOW_DURING_EDIT_IGNORE:
return;
case SHOW_DURING_EDIT_SET_AS_NEXT:
impl.setCurrentForm(newForm);
return;
}
}
if (!isEdt()) {
callSerially(new RunnableWrapper(newForm, null, reverse));
return;
}
Form current = impl.getCurrentForm();
if (current != null) {
if (current.isInitialized()) {
current.deinitializeImpl();
}
}
if (!newForm.isInitialized()) {
newForm.initComponentImpl();
}
if (newForm.getWidth() != getDisplayWidth() ||
newForm.getHeight() != getDisplayHeight()) {
newForm.setShouldCalcPreferredSize(true);
newForm.layoutContainer();
}
synchronized (lock) {
boolean transitionExists = false;
if (animationQueue != null && animationQueue.size() > 0) {
Object o = animationQueue.lastElement();
if (o instanceof Transition) {
current = (Form) ((Transition) o).getDestination();
impl.setCurrentForm(current);
}
}
// make sure the fold menu occurs as expected then set the current
// to the correct parent!
if (current != null && current instanceof Dialog &&
((Dialog) current).isMenu()) {
Transition t = current.getTransitionOutAnimator();
if (t != null) {
// go back to the parent form first
if (((Dialog) current).getPreviousForm() != null) {
initTransition(t.copy(false), current,
((Dialog) current).getPreviousForm());
}
}
current = ((Dialog) current).getPreviousForm();
impl.setCurrentForm(current);
}
// prevent the transition from occurring from a form into itself
if (newForm != current) {
if ((current != null && current.getTransitionOutAnimator() != null) ||
newForm.getTransitionInAnimator() != null) {
if (animationQueue == null) {
animationQueue = new Vector();
}
// prevent form transitions from breaking our dialog based
// transitions which are a bit sensitive
if (current != null && (!(newForm instanceof Dialog))) {
Transition t = current.getTransitionOutAnimator();
if (current != null && t != null) {
initTransition(t.copy(reverse), current, newForm);
transitionExists = true;
}
}
if (current != null && !(current instanceof Dialog)) {
Transition t = newForm.getTransitionInAnimator();
if (t != null) {
initTransition(t.copy(reverse), current, newForm);
transitionExists = true;
}
}
}
}
lock.notify();
if (!transitionExists) {
if (animationQueue == null || animationQueue.size() == 0) {
setCurrentForm(newForm);
} else {
// we need to add an empty transition to "serialize" this
// screen change...
Transition t = CommonTransitions.createEmpty();
initTransition(t, current, newForm);
}
}
}
}
/**
* Initialize the transition and add it to the queue
*/
private void initTransition(Transition transition, Form source, Form dest) {
dest.setVisible(true);
transition.init(source, dest);
animationQueue.addElement(transition);
if(animationQueue.size() == 1) {
transition.initTransition();
}
}
Transition是一个虚类,不能实例化,他的两个可实例化的子类是:CommonTransition , Transition3D
CommonTransition有两种transition动画:
Slide:新的form滑进,推掉现在那个
Fade:新的form渐进,现在的渐出
CommonTransitions的辅助类,Motion,提供了模拟物理动作的模型,有三种类型的动作:Friction 摩擦,Spline锯齿,Linear线型
Transition3D按照M3G API (JSR 184)提供了3D效果的transition效果,JSR184的支持足够这些transition正常工作,所以不需要Motion支持类。
(注:JSR184:这个参考标准,规定了一个轻量级,交互的3D图形api,独立于J2ME和MIDP成为一个可选的包。这个api可扩展到一系列的应用程序,如游戏,动画信息,屏保,自定义用户接口,产品可视化等。它具备易扩展和容易使用的特点,目标的设备是CLDC设备,通常低处理能力和内存,么有3D硬件支持或浮点运算。Api必须能够合理的针对这些设备来实现。然而api同时要能简单的扩展到高端设备,具有彩色显示器,dsp,浮点运算单元,甚至特别的3d图形设备。
有些设备只有黑白显示,96 * 64的分辨率,有些则有320*240分辨率同时16位彩色,在将来甚至更高的分辨率和色深都会出现,同样的处理器速度也大有不同,api必须适应这些不同,同时充分发挥设备的功能,如3d加速。
这套api的主要需求是:
- 支持3d图形在一系列应用的使用
- 不假设存在特定的高端硬件
- 不武断的限制3d内容的大小和复杂度
- 在低端平台上同时能支持复杂的特性
- 支持低rom和ram的器件
为了同时达到足够的通用并且不给开发者带来如画像素这样的困扰,api必须至少在抽象层能接收命令绘制静态3d对象。如果api同时能够处理,如创建和维护场景的层次体系,进行遮挡删选,动态化场景对象,选择视点,这样的功能都会很有用处,因为高层抽象减低了应用程序开发的难度,减少了应用程序大小,增加了本地代码运算的比重。相对低层的抽象,被证实是成功的,例如SGI的OPENGL,MS的 Direct3D,同时高层抽象被使用在OPENINVENTOR,Java3D,其他游戏引擎如ID的Quake。但是其他成功的方法,如将一个文件类型和一个播放器结合,如VRML和Flash。
所有的3D渲染都将在本地代码执行,基于速度和空间的考虑。实现应该使用拥有的硬件加速。)
一个限制是这个类只能运用于Form,但很快会改变。3d transition包包含了以下内容:
Cube:模拟一个旋转的立方体,当前的form移出屏幕,而新的form移入
Fly:新的form飞入
Rotation:Cube的二维版本
Static Rotation :从一个form转变成一个Dialog,只有Dialog转动,form保持静止
Swing In :进来的form从高到低或从低到高飞入
使用工厂方法来创建Transition :
//creates an out transition
//with a right-to-left outgoing slide
out = CommonTransitions.createSlide(
CommonTransitions.SLIDE_HORIZONTAL, false, runSpeed);
//creates an out transition
//with a left-to-right incoming slide
in = CommonTransitions.createSlide(
CommonTransitions.SLIDE_HORIZONTAL, true, runSpeed);
.
.
.
//creates outgoing and incoming fade transitions
out = CommonTransitions.createFade(runSpeed);
in = CommonTransitions.createFade(runSpeed);
.
.
.
//creates outgoing and incoming fly in transitions
out = Transition3D.createFlyIn(runSpeed);
in = Transition3D.createFlyIn(runSpeed);
接着必须安装到form,
//f is the form for which transitions are being set
f.setTransitionOutAnimator(out);
f.setTransitionInAnimator(in);
让我们来实现一种比较简单的自定义Transition:
为了让问题简单,我们给出以下限制:
- 不支持dialog
- 不能配置,只能有一种动作:水平从左到右
- 不检查在真机中会遇到的一切问题
StepTransition继承Transition,StepMotion继承自Motion,来作为model
Transition的虚函数:
- public abstract Transition copy()
- public abstract boolean animate()
- public abstract void paint(Graphics g)
同时有一个空函数initTransition,可以按照需求进行实现。initTranstion是一个在每个transition开始时候调用的回调函数。是一个初始化全部参数,来启动动画的地方,我们同时初始化StepMotion对象,来计算出每一帧的渲染位置,
public void initTransition()
{
//initialize variables
//required to set up motion
Component source = getSource();
int width = source.getWidth();
position = 0;
int startoffset = 0;
//create a StepMotion object for this transition
motion = new StepMotion(startoffset,
width, duration, step);
//start the movement for the transition
motion.start();
}
Animate方法是每帧调用一次的,这个方法是来查看motion对象是否丢失或者transtion是否结束。Transition完成的检查包括:调用StepMotion的isFinisthed,如果结束则返回true。如果没有motion对象,或者动画完成了,animate返回false,transition动作结束,否则,transition会继续,而且得到下一帧的绘制的位置。最后animate返回True来绘制下一帧。
public boolean animate()
{
//see if there is a motion object
//and check if the transition is completed
if(motion == null || motion.isFinished())
{
//in either case terminate transition
return false;
}
//if transition is to continue
//get the new position
position = motion.getStep();
//continue with transition
return true;
}
Paint方法是用来刷新目标屏幕的每一帧。如上代码,目标屏幕的绘制位置有StepMotion的getStep获得。如果自从上一帧下来位置不变,则getStep返回-1,paint方法检查position的值,调用paintSlideAtPostion来进行实际的绘画操作。
public void paint(Graphics g)
{
//paint only if new position is available
if(position > -1)
{
paintSlideAtPosition(g, position, 0);
return;
}
else
{
return;
}
}
private void paintSlideAtPosition(
Graphics g, int slideX, int slideY)
{
Component source = getSource();
//if this is the first form being displayed we
//can't do a step transition
//since we have no source form
if (source == null)
{
return;
}
Component dest = getDestination();
int w = -source.getWidth();
int h = 0;
//set the clips
//uncomment the two following lines
//for the "push" effect
//g.setClip(source.getAbsoluteX(),
source.getAbsoluteY(), source.getWidth(),
source.getHeight());
//paint(g, source, slideX , slideY );
g.clipRect(dest.getAbsoluteX(),
dest.getAbsoluteY(), source.getWidth(),
source.getHeight());
paint(g, dest, slideX + w, slideY + h);
}
private void paint(Graphics g, Component cmp, int x, int y)
{
//get original clip details
int cx = g.getClipX();
int cy = g.getClipY();
int cw = g.getClipWidth();
int ch = g.getClipHeight();
//translate to proper position
//for drawing the form
g.translate(x, y);
//get it painted
cmp.paintComponent(g, false);
//restore original values
g.translate(-x, -y);
g.setClip(cx, cy, cw, ch);
}
//return a functionally equivalent transition object
public Transition copy()
{
return new StepTransition(duration, step);
}
public StepMotion(int sourcevalue, int destinationvalue,
int duration, int steps)
{
this.destinationvalue = destinationvalue;
this.duration = duration;
this.steps = steps;
//the size of a step
stepsize = (destinationvalue - sourcevalue)/steps;
//the time interval between two successive steps
interval = duration/steps;
}
//save the time as the beginning of an interval
public void start()
{
starttime = System.currentTimeMillis();
}
//return true if all steps have been taken care of
public boolean isFinished()
{
return stepnumber > steps;
}
//return the position
public int getStep()
{
//check if interval for next step is over
//if so increment stepnumber
//and return (stepsize*stepnumber)
//also reset starttime by calling start()
//if (stepsize*stepnumber>destinationvalue)
//then return destinationValue
//if interval not over return -1
if(System.currentTimeMillis() >=
starttime + interval)
{
stepnumber++;
int offset = stepnumber*stepsize;
if(offset > destinationvalue)
{
offset = destinationvalue;
}
start();
return offset;
}
else
{
return -1;
}
}
MIDlet里面的代码:
绘制第一个form:
//init the LWUIT Display
Display.init(this);
//duration of transition in milliseconds
int duration = 3000;
//number of steps in transition
int numofsteps = 10;
//get the theme and set it
try
{
Resources r = Resources.open("/SDTheme1.res");
UIManager.getInstance().setThemeProps(
r.getTheme("SDTheme1"));
}
catch (java.io.IOException e)
{
}
//create two labels
Label srclabel = new Label("This is the source screen");
Label dstlabel = new Label(
"This is the destination screen");
//create the first form
first = new Form("Source");
//we make the titlebar different
//from the theme setting
first.getTitleStyle().setBgImage(null);
//add srclabel to the first form
first.addComponent(srclabel);
//add 'Next' command to first form
//the command id is 1
first.addCommand(new Command("Next", 1));
//add 'Exit' command to first form
//the command id is 0
first.addCommand(new Command("Exit", 0));
//this MIDlet is the listener
//for the first form's commands
first.setCommandListener(this);
//show the first form
first.show();
第二个form:
//create the second form
second = new Form("Destination");
//set the Out and In transitions for the second form
second.setTransitionOutAnimator(new StepTransition
(duration, numofsteps));
second.setTransitionInAnimator(new StepTransition
(duration, numofsteps));
//set Menu and background images for second form
try
{
second.getMenuStyle().setBgImage(Image.createImage
("/pattern3.png"));
}
catch(java.io.IOException ioe)
{
}
try
{
second.getStyle().setBgImage(Image.createImage
("/sdsym4.png"));
}
catch(java.io.IOException ioe)
{
}
//add dstlabel to second form
second.addComponent(dstlabel);
//add 'Previous' command to first form
//the command id is 2
second.addCommand(new Command("Previous", 2));
//this MIDlet is the listener
//for the second form's commands
second.setCommandListener(this);
上面的两端代码都在startApp
剩下来:
public void actionPerformed(ActionEvent ae)
{
Command cmd = ae.getCommand();
switch (cmd.getId())
{
//'Exit' command
case 0:
notifyDestroyed();
break;
//'Next' command
//show second form
case 1:
second.show();
break;
//'Previous' command
//show first form
case 2:
first.show();
}
}
第三节 GIF动画
gif :现在的gif只能用lwuit自己的一套Resouce Editor工具来制作gif,所以说,不能直接从文件读取,由于resouce editor没有用过,不知道在最后的.res文件里面这张gif大概保存了什么,感觉可能还是划分成帧了,至于你建议的方法,我这里找了个参考实现。
第四节时间间隔的控制
时间间隔 : edt的主循环里面,如果animateQueue空的话,就等待一定的时间间隔,再处理其他的事件(按键,重绘,序列call),如果animateQueue不空的话,直接进行动画绘制,这时候,如果是Transition , 成员变量有开始值和结束值,开始时间,持续时间,速度,而且如果是commonTransition的话,有一个Motion的model,,在这里修改每帧动画的位置, 当animate调用motion获得新位置并返回true之后,调用paint,但是这时候可能动画还没有结束,所以,该transition还在animateQueue中,下一次事件轮询,跟上面一样,一直到motion的duration+startTime<系统现在的时间。
Display.java中的代码:
private void paintTransitionAnimation() {
Animation ani = (Animation) animationQueue.elementAt(0);
if (!ani.animate()) {
animationQueue.removeElementAt(0);
if (ani instanceof Transition) {
Form source = (Form) ((Transition) ani).getSource();
restoreMenu(source);
if (animationQueue.size() > 0) {
ani = (Animation) animationQueue.elementAt(0);
if (ani instanceof Transition) {
((Transition) ani).initTransition();
}
} else {
Form f = (Form) ((Transition) ani).getDestination();
restoreMenu(f);
if (source == null || source == impl.getCurrentForm() ||
source == getCurrent()) {
setCurrentForm(f);
}
((Transition) ani).cleanup();
}
return;
}
}
ani.paint(lwuitGraphics);
impl.flushGraphics();
//个人认为可以在这里控制帧率
if (transitionDelay > 0) {
// yield for a fraction, some devices don't "properly" implement
// flush and so require the painting thread to get CPU too.
try {
synchronized (lock) {
lock.wait(transitionDelay);
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
/**
* Implementation of the event dispatch loop content
*/
void edtLoopImpl() {
try {
// transitions shouldn't be bound by framerate
if (animationQueue == null || animationQueue.size() == 0) {
// prevents us from waking up the EDT too much and
// thus exhausting the systems resources. The + 1
// prevents us from ever waiting 0 milliseconds which
// is the same as waiting with no time limit
synchronized (lock) {
lock.wait(Math.max(30, framerateLock - (time)));
}
} else {
// paint transition or intro animations and don't do anything else if such
// animations are in progress...
paintTransitionAnimation();
return; //动画结束后 马上返回 保证不被其他//事件打断
}
} catch (Exception ignor) {
ignor.printStackTrace();
}
…
…
}