0
回答
让 Draw2D 的组件也 Tween 起来
注册华为云得mate10,2.9折抢先购!>>>   

文章来自 IBM developerWorks

如果你听说过 Flex 的 TweenLite 就不会陌生,TweenLite 定义了缓动效果,使得在组件 Effect 的作用开始、结束阶段有一段缓冲的效果。常见的 Effect 有 Move、Fade 等,缓动效果包括 Bounce、Back 等等。Draw2D 作为优秀的 Java 2D 技术,用于绘制基于 SWT 的图形程序,目前主要用于配合 GEF 或 GMF 使用。在 GEF 中,Draw2D 的组件或许稍显单调,本文将缓动技术引入到了 Draw2D 当中,读者不仅可以了解缓动的原理,同时也可以将缓动用到实际的项目中去,为你的 Draw2D 增添色彩

引言

Draw2D 作为 Java 2D 技术,用于绘制 Java 图形程序,目前主要是结合 GEF 在图形建模领域得到广泛应用,如 IBM 的 WBM(Websphere Business Modeler)。然而当我们熟悉 Draw2D 时,有时就会突然发觉 Draw2D 组件的单调,至少在笔者接触的很多 Draw2D 应用中,都没有使用到动画效果,与 Flex 开发的流程建模工具的对比,也促使读者研究了 Flex 的 Tween 技术,本文将 Flex 中的 Tween 概念以及相关算法引入到 Draw2D 当中,使得 Draw2D 具有一定的缓动效果(具体可以查看示例程序),读者可以将这些效果的算法应用到实际的 Draw2D 项目当中,相信可以使你的程序丰富多彩,吸引眼球。

示例程序简介

为了让读者对该文章的目的有直观的印象,示例程序如图 1 所示,它基于 Move Effect,即点击“执行”按钮后,红色方块会移动到 Target 的黑色圆点处,经历的时间则是由 duration 的文本输入框决定,单位毫秒,移动的路径是直线。缓动效果包括 Back(到达作用点时会继续移动一定距离,再直线返回到作用点,构成“返回”的缓动效果)、Bounce(”碰撞”效果,到达作用点的感觉就好像是撞击 到了作用点,然后逐渐放慢,最后停在了作用点位置)等等,easeIn、easeOut、easeInOut 则是选择作用点,easeIn 代表缓动效果作用于起始点(图中方块所在位置),easeOut 则代表作用于目标点,而 easeInOut 则是同时作用于起始点和目标点。其中起始点的坐标为(20, 20),目标点的坐标为(450,200)。


图 1. 示例程序界面
图 1. 示例程序界面

由于无法在 Web 上播放效果,建议读者将代码下载下来,导入 Eclipse 直接运行 com.test.demo. HelloWorld 类查看效果。

点击执行后,方块在移动的过程中会在控制台上实时的打印出当前红色方块的坐标位置,如图 2 所示。


图 2. 控制台显示坐标
图 2. 控制台显示坐标

上图是 Bounce 效果、easeIn、duration = 1000 的坐标变化。坐标值就是使用缓动算法计算出来。从上图也可以看出,方块的移动实际上是在很短的时间间隔连续的改变坐标位置而产生的效果。

类结构

为了尽可能使得程序易扩展,定义了如图 3 所示的类结构。其中 Effect、EaseFunction 为接口,AbstractEffect 为抽象类,其他为普通类。


图 3. 示例程序类结构
图 3. 示例程序类结构

由于实现 Tween 的缓动效果最重要的是它的算法,这里使用了设计模式中的策略模式,将算法封装到 EaseFunction 内,不同的效果具有不同的算法实现。EaseType 是枚举类型,代表用户选择的 ease 类型,IN 对应于 easeIn, OUT 对应于 easeOut, INOUT 对应于 easeInOut。Effect 的 target 是 Draw2D 的 Figure 对象,是缓动效果作用的目标对象。Tween 类的 public Effect move(Figure target, long duration, int fromX, int fromY, int toX, int toY, Class<? extends EaseFunction> ef, EaseType type) 方法,指定了执行 Move Effect 需要的所有参数,它是一个静态方法。它的实现如清单 1 所示。


清单 1. Tween 的 move 方法实现
        
 public Effect move(Figure target, long duration, int fromX, int fromY, int toX, int toY, 
       Class<? extends EaseFunction> ef, EaseType type) { 
     MoveEffect e = new MoveEffect(); 
     e.setFromX(fromX); 
     e.setFromY(fromY); 
     e.setToX(toX); 
     e.setToY(toY); 
     e.setTarget(target); 
     e.setDuration(duration); 
     try { 
       e.setEaseFunction((EaseFunction) ef.newInstance()); 
     } catch (Exception e1) { 
       e1.printStackTrace(); 
     } 
     e.run(type); 
     return e; 
   } 

实现 Figure 移动的原理,是在较短时间内,改变 Figure 的 location。在示例程序中,每 10 毫秒重新设置 Figure 的 location,新的 location 的位置由 easeFunction 计算得来。由于时间间隔非常短,因此人眼感觉移动是连续的。如果将间隔时间增加,就会显示跳跃式的移动。

缓动算法实现

观察 EaseFunction 接口的方法,ease* 方法具有四个参数,它们的意义如下列表。

  • double t:当前时间
  • double b:初始值
  • double c:变化量
  • double d:持续时间

函数返回当前时刻的位置信息。举个例子解释上述参数的意义,从(20,20)移动到(450,200),duration 为 1000 毫秒,对于 X 坐标来讲,他的初始值(b)为 20,变化值(c)为 450-20=430,持续时间为(d)1000/10=100, 其中 10 代表步长,即每 10 毫秒算一个时刻,调用 ease* 方法,以 ease* 的返回值刷新目标位置,当前时间(t)的初始值为 0,每过一个步长的时间,t 加 1,直到 t >=d,缓动结束。以 Bounce 效果为例,它的算法如清单 2 所示。


清单 2. Bounce 算法
        
 public class Bounce implements EaseFunction { 

   @Override 
   public double easeIn(double t, double b, double c, double d) { 
     return c - easeOut(d - t, 0, c, d) + b; 
   } 

   @Override 
   public double easeInOut(double t, double b, double c, double d) { 

     if (t < d / 2) 
       return easeIn(t * 2, 0, c, d) * .5 + b; 
     else 
       return easeOut(t * 2 - d, 0, c, d) * .5 + c * .5 + b; 
   } 

   @Override 
   public double easeOut(double t, double b, double c, double d) { 
     if ((t /= d) < (1 / 2.75)) { 
       return c * (7.5625 * t * t) + b; 
     } else if (t < (2 / 2.75)) { 
       return c * (7.5625 * (t -= (1.5 / 2.75)) * t + .75) + b; 
     } else if (t < (2.5 / 2.75)) { 
       return c * (7.5625 * (t -= (2.25 / 2.75)) * t + .9375) + b; 
     } else { 
       return c * (7.5625 * (t -= (2.625 / 2.75)) * t + .984375) + b; 
     } 
   } 
 } 

算法来自于 Flex Tween 源码,有兴趣的读者可以下载查看,其他效果的算法源码在附件中都有单独的类,在 draw2dtween.tween.ease.impl 包下,如果你还有兴趣,可以实现自己的算法,比如可以让方块按螺旋状移动等等。

MoveEffect 的 run 方法

了解了类结构和缓动算法实现后,就可以组织起来实现方块的移动和缓动了。计算出 EaseFunction 算法需要的 b、c、d 数值,而 t 的初始值为 0。利用 Java 的 Timer 和 TimerTask 技术,每 10 毫秒(一个步长)刷新方块的位置,它的源代码如清单 3 所示。


清单 3. MoveEffect 的 run 方法实现
        
   private int fromX, fromY, toX, toY; 

   // 以下 3 个变量是在父类 AbstractEffect 中定义的

   protected EaseFunction ease;// 用户选择的 easeFunction 

   protected Figure target;//MoveEffect 作用的 Figure 对象

   protected long duration;// 用户输入的持续时间

   @Override 
   public void run(final EaseType type) { 
     final Timer timer = new Timer(); 
     long b = fromX, c = toX - fromX, d = duration / 10, t = 0; 
     long b2 = fromY, c2 = toY - fromY; 
     // 每隔 10 毫秒执行 MyTimer 的 run 方法
     timer.schedule(new MyTimer(c, type, d, t, b, c2, b2), 0, 10); 
   } 
   private final class MyTimer extends TimerTask { 
     private long c; 
     private EaseType type; 
     private long d; 
     private long t; 
     private long b; 
     private long c2; 
     private long b2; 
     double curX = -1; 
     double curY = -1; 

     private MyTimer(long c, EaseType type, long d, long t, long b, long c2, 
         long b2) { 
       this.c = c; 
       this.type = type; 
       this.d = d; 
       this.t = t; 
       this.b = b; 
       this.c2 = c2; 
       this.b2 = b2; 
     } 

     @Override 
     public void run() { 
         // 判断用户选择的 Ease 类型
       if (type == EaseType.IN) { 

         curX = Math.ceil(ease.easeIn(t, b, c, d));// 计算当前 X 坐标
         curY = Math.ceil(ease.easeIn(t, b2, c2, d)); // 计算当前 Y 坐标
       } else if (type == EaseType.OUT) { 

         curX = Math.ceil(ease.easeOut(t, b, c, d)); 
         curY = Math.ceil(ease.easeOut(t, b2, c2, d)); 
       } else if (type == EaseType.INOUT) { 

         curX = Math.ceil(ease.easeInOut(t, b, c, d)); 
         curY = Math.ceil(ease.easeInOut(t, b2, c2, d)); 
       } 
       if (t < d) 
         t += 1;// 刷新一次 t 要加 1 
       else 
         this.cancel();// 代表效果执行时间到,停止
       if (target != null) { 
         Display.getDefault().asyncExec(new Runnable() { 

           @Override 
           public void run() { 
             // 刷新 target 的位置
             target.setLocation(new Point(curX, curY)); 
           } 
         }); 
       } 
       // 打印到控制台
       System.out.println(curX + "," + curY); 
     } 
   } 

上述清单可以看出,每隔 10 毫秒执行 MyTimer 的 run 方法,在该 run 方法里,根据用户选择的缓动类型(IN、OUT、INOUT)和缓动函数(Bounce、Back 等),调用缓动函数的 ease* 方法,计算出当前数值,同时 t 加 1,然后在 Display.getDefault().asyncExec 下刷新方块的 location( 非 UI 线程刷新 UI 需要在该函数内执行 ),打印出当前位置。因为算法是一维的,坐标是二维的,所以在计算过程中,对 X 轴和 Y 轴的坐标要分别进行计算,但是该算法可以确保算出来的坐标仍然在一条直线上,物体沿直线移动。对于 duration 为 1000,步长为 10 的移动,刷新物体位置 100 次,每次都调用缓动函数计算新的位置 , 当然读者可以修改步长,使其可设置。

具体应用实例

下载示例程序,运行 com.test.demo. SampleApplication,效果如图 4 所示。


图 4. 示例程序
图 4. 示例程序

这是一个扑克游戏的简单原型,当点击底排的三张牌时,会将牌收到牌堆当中。牌移动的过程则使用了 Move Effect 的效果,使用 Bounce、easeIn 的缓动。将两张牌收起来后的效果如图 5 所示。


图 5. 两张牌收起后
图 5. 两张牌收起后

该效果实现的关键代码如清单 4 所示。为底部的每张牌添加点击的事件监听,当点击牌时,执行移动缓动效果。


清单 4. 示例程序关键代码
        
 final Figure cardItem = new RoundedRectangle(); 
 cardItem.setLocation(new Point(320, 350)); 
 cardItem.setSize(100, 110); 
 cardItem.setBackgroundColor(ColorConstants.lightBlue); 
 cardItem.setLayoutManager(new ToolbarLayout()); 
 final Figure label = new org.eclipse.draw2d.Label("Click It"); 
 cardItem.add(label); 
 cardItem.addMouseListener(new MouseListener() { 
     public void mousePressed(MouseEvent arg0) { 
       //lastPut 代表最近被移动到牌堆的那张牌
       // 最关键:调用 Tween 的 move 方法,执行 move 效果
       Tween.instance().move(cardItem, 1000, 
               cardItem.getLocation().x, cardItem.getLocation().y, 
               lastPut.getLocation().x, 
               lastPut.getLocation().y + 20, Bounce.class, 
               EaseType.IN); 
       lastPut = cardItem; 
     } 
     public void mouseReleased(MouseEvent arg0) { 
     } 
     public void mouseDoubleClicked(MouseEvent arg0) { 
     } 
 }); 

从示例程序可以看出,该缓动效果是可以被具体应用到项目当中去的,使用起来也非常方便。读者可以下载附件后,亲自操作查看效果,也可以发挥自己的想象,使用该方法制作别的缓动应用。

小结

本文基于 Flex 中的 Tween 算法,在 Draw2D 中进行了实现,通过将缓动添加到 Draw2D 中,可以使得 Draw2D 的组件变得丰富多彩,让你的程序更加吸引眼球。由于本人知识水平有限,文中出现的错误欢迎读者与我联系批评指正。

举报
IBMdW
发帖于7年前 0回/303阅
顶部