Swing第五刀:走马观花看世博

没错,这依旧是一篇技术文章,而不是世博会游记。其实至今尚未参观世博会,虽然就生活在这个城市,却没有外地朋友的那番激情和热度。在上下班地铁站上与蜂拥而至的旅游团队挤地铁、看着地铁车厢屏幕上跳动的接近50万的世博会当日入园人数统计、画面上如广州火车站春运搬的人流,想想这接近40度的桑拿天和时不时骤降的倾盆大雨,这最后的一点去看一看的激情也被无情的抹杀了。
 
作为遵纪守法、积极用户党和政府的三好公民,就算未亲临世博,也要积极宣传世博,为城市做贡献,让城市更美好不是么?思前想后,还不如直接用我们程序员最熟悉的工具——代码——来展示一下世博之美。
 
网上随便转一下,用Flash制作的“旋转的图片画廊”效果的例子,可谓比比皆是,大家都见怪不怪了。所以,Flash/Flex自然也被贴上了“酷炫到底”的贵族标签。没错,Flash/Flex有内置的动画、渲染、滤镜等机制,制作这类小把戏具有先天的优势,可谓是小菜一碟,各方大牛的例子也是让人心潮澎湃、此起彼伏啊。既然咱们是玩Swing的,那自然会想:Swing这把大砍刀,也玩玩这瓷器活?!能行么?
 
可以肯定的说,答案肯定是肯定的。看看我一个上午的小成果:


不是很轻松

不得不说,用Swing做这类趣味程序效果,并非很轻松。可以说是大菜刀修指甲、大扫帚修眉毛,做是能做,就要看你的功力如何了。本例子纯属把Swing的能力和一些编程技巧和大家展示,真正用Swing做动画这类程序,还是推荐用Flash/Flex、JavaFx之类,除非你的“菜刀功法”比较深厚,也就无所谓了。

 

下面我们就把一些关键的“刀法”一一解读。

 

用到的第三方包

没错,这里用到了TWaver这个包。但是要强调的是,这里所介绍的Swing技巧和TWaver无关,用它只是为了更加方便,减少一些代码量。对第三方的东西有强烈“排斥恐惧症”的朋友,可以静下心来仔细研究一下代码,我相信一定会有所收获。

 

如何生成“镜面倒影”

程序中的每个图片,在下方都有一个灰度的、缩小的、渐暗的、翻转的镜面倒影。这是怎么做到的?是直接PS处理、存在原图片中吗?不,那太就没技术含量了。如果都靠美工,如何体现我们程序员的价值?这个倒影是通过一定的算法,动态生成的内存图片。大家仔细观察ImageNode.java中的createShadowImage和convertPixel函数。如果理解了这里的代码,也就搞清楚了这一算法和原理。

 

我们需要的倒影是:翻转的、灰度的(去掉颜色)、渐变的、缩短的图。为了在内存中这个图,我们对原图的每个像素进行抓取,然后根据一定算法来对像素的红、绿、蓝以及alpha透明通道进行处理。首先对原图的像素进行抓取:

int w = image.getWidth(null);
int h = image.getHeight(null);
int[] pixels = new int[w * h];
PixelGrabber pg = new PixelGrabber(image, 0, 0, w, h, pixels, 0, w);
try {
  pg.grabPixels();
} catch (Exception e) {
  return null;
}

 

以上代码将原图的所有像素以int形式存放在pixels数组中。然后,对每个像素进行处理,包括渐变、压缩等等:

for (int i = 0; i < w; i++) {
    pixels[j * w + i] = convertPixel(i, j, w, h, pixels[j * w + i], fadeSpeed);
}

 
其中convertPixel函数负责了具体处理。这个函数里面有几个地方需要注意:percent是这个倒影的“消失”的速度,这个通过透明度来控制。越下面的像素行,透明度越大。当然,fadeSpeed值越大,percent越大,倒影消失的越快:

double percent = (double) (y * 100) / h / 100d;
for (int i = 0; i < fadeSpeed; i++) {
    percent = percent * percent;
}
alpha = (int) (alpha * percent);

 
而下面这句话保证了alpha不大于150,这样,即使最接近原图底部的倒影部分,也保持一定的“暗度”,而不是和原图一样的“亮”,保证了倒影的效果和感觉。这里本人经过反复实验,还是感觉150的透明度不错。

alpha = Math.min(alpha, 150);

 
然后是把像素变灰。像素变灰有很多种算法,大家可以Google一下相关原理或看一下图像处理的相关书籍,这里不再赘述。我们就使用最简单的平均法:r、g、b平均作为新的灰度:

int gray = (red + green + blue) / 3;

 
这样,像素就处理好了。当每个像素处理结束后,再对所有像素进行镜面翻转。镜面翻转的原理是:像素所在的列不变,行取反。代码如下:

int[] newPixels = new int[w * h];
for (int j = 0; j < h; j++) {
  for (int i = 0; i < w; i++) {
    newPixels[j * w + i] = pixels[(h - j - 1) * w + i];
  }
}

 
最后,再将像素装箱打包,生成图片:

MemoryImageSource source = new MemoryImageSource(w, h, newPixels, 0, w);
return new ImageIcon(Toolkit.getDefaultToolkit().createImage(source));

 
好了,大家再仔细欣赏一下我们的成果,看效果是不是很不错?

如何处理“倒影跟随”

大家可以对图片进行任意拖动操作,可以发现几个特征:

  • 倒影不能被鼠标选中(就像倒影不是图片本身的一部分);
  • 倒影总是跟随图片移动;
  • 倒影的大小随原图的大小等比例变化;

这是如何做到的呢?这里利用了TWaver的Follower机制。

 

首先,程序中的每个图片,都是一个TWaver的ResizableNode节点;然后,倒影是一个Follower节点,并设置其host为图片节点。这样,就实现了倒影跟随;然后,整个画布是一个TWaver的Network,定制其“鼠标点选”,忽略倒影的点选动作:

network.addSelectableFilter(new SelectableFilter() {

  public boolean isSelectable(Element element) {
      return !(element instanceof Follower);
  }
});

 
然后,在图片节点中添加属性监听,当图片和尺寸发生变化时候,调整其倒影节点的大小:

addPropertyChangeListener(new PropertyChangeListener() {

  public void propertyChange(PropertyChangeEvent evt) {
    if (TWaverUtil.getPropertyName(evt).equalsIgnoreCase(TWaverConst.PROPERTYNAME_IMAGE)) {
      updateImage();
    }
    if (TWaverUtil.getPropertyName(evt).equalsIgnoreCase(TWaverConst.PROPERTYNAME_SIZE)) {
      updateSize();
    }
  }
});

 
在事件发生后,通过以下代码动态调整倒影的尺寸和位置:

private void updateSize() {
  shadowNode.setSize(getSize().width, (int) (getSize().height * 1));
  shadowNode.setLocation(this.getLocation().x, this.getLocation().y + this.getHeight() + shadowGap);
}

 

如何动画

动画的部分比较复杂。

 

首先大家要了解TWaver的Node的处理图片的方法:当我们给ResizableNode或Follower直接设置其宽高之后,其对应的图片会被直接压缩或拉伸。这个和我们用Graphics2D.drawImage(image,x,y,width,height,null)是一样的道理,没什么可说的。

 

所以,接下来的工作主要是:如何将图片节点从位置A动画的移动到位置B,尺寸从X动画的变为Y呢?在Mover.java里,我们继承了Thread线程,用于动画处理。这个线程可以完成这样一个任务:给我图片的老宽高和新宽高,我可以在规定的时间动画的把图片宽高修改过去;同时还可以动画的处理图片的位置。在这个线程里面,我们把线程的生存时间拆分为90个时间片,每个片是5毫秒(定义在delay变量中)。Run函数中,循环这90个时间片,每个时间片处理一个“步进”并sleep 5毫秒。

 

为什么是90个时间片呢?为了增加动画的质感,如果直接使用线性函数来做步进,就会显得很生硬。看一下Flash做的动画效果,都有二次甚至三次函数对动画进行处理。例如那种慢——快——慢,甚至“急刹车”或“刹车过头”的动画方式,我们一定印象深刻。这里我们选择了最简单的三角函数,用两段拼接的正弦函数来模拟慢——快——慢的效果:

这样大家就会明白为何使用90:可以方便的把时间轴作为角度值,来处理一个动画周期。在具体处理时,先用45个时间片来处理前半周期,此时用cos函数:

int movementX = (int) (info.getCenterChangeX() / 2 * (1 - Math.cos(Math.toRadians(i * 2))));
int x = info.getOldCenterX() + movementX;
int movementY = (int) (info.getCenterChangeY() / 2 * (1 - Math.cos(Math.toRadians(i * 2))));
int y = info.getOldCenterY() + movementY;

 
注意i*2这样就把45个时间片放在了完整的90度范围中,利用了正弦曲线的完整形状。下半个动画周期使用sin函数:

int movementX = (int) (info.getCenterChangeX() / 2 * Math.sin(Math.toRadians(i * 2)));
int x = info.getOldCenterX() + info.getCenterChangeX() / 2 + movementX;
int movementY = (int) (info.getCenterChangeY() / 2 * Math.sin(Math.toRadians(i * 2)));
int y = info.getOldCenterY() + info.getCenterChangeY() / 2 + movementY;

这样出来的效果明显比线性的要有动感。具体算法请看Mover.java类。

如何做到“齐头并动”

以上解决了一个图片的动画问题。在程序中,是每个图片都在同时、并行的动画。这个如何处理?难道多个Mover线程吗?那样肯定消耗资源而且效果不佳。最好的方法肯定是在每个时间片中,同时移动所有需要移动的图片和物体。

 

在Mover中我们做以下改进:接受多个图片节点的动画信息并保存;在动画过程中,提前算好每个物体需要移动的步进和路径,然后在90个时间片中,同时移动所有需要移动的物体。在Mover的run函数中:

for (int index = 0; index < infos.size(); index++) {
MoverInfo info = infos.get(index);
//移动物体
}

 

如何计算图片停留点位置


在本例子中,图片一共有5个停留位置:

在每个位置点,图片都有对应的、固定的中心点和尺寸,我们提前计算好并记录下来就行了。在Main.java中,createNodes函数创建了所有图片节点,同时又返回了每个停留点的边界:

private Rectangle[] positions = createNodes();

 
这样,当一个图片需要从1点移动到2点时,我们就直接把对应index的Rectangle拿出来,交给Mover去move就行了。

 

如何连续移动

以上解决了移动一个位置的动画问题。如果一个图片需要从1点直接跳到3点,该如何处理?为了简化程序,同时保持“一步一步走”的动画效果,我们还是把两段动画连续播放,而不是直接忽悠一下移动过去。也就是说,先让图片从1点移动到2点,停顿一下再移动到3点。这样就要连接两个线程。

 

由于线程是异步的,一旦run起来我们就没法控制其暂停和停止以及后续动作。所以,我们要改造一下Mover:在创建的时候,可以给他一个Runnable,当线程执行结束后,可以执行这个Runnable进行“收尾”或“桥接”。例如,这个Runnable可以在线程结束后重新设置一下图片的前后遮挡关系,也可以用于链接、执行下一个动画。可见,这个改进非常有必要。

 

观察Mover.java的构造函数:

public Mover(ArrayList<MoverInfo> infos, Runnable action)

 
最后一个参数就是这个Runnable,我们可以叫它action,一个动作。在run函数结束的时候:

if (action != null) {
  SwingUtilities.invokeLater(action);
}

 
也就是说,如果action不为空,就在Swing线程中执行它。为什么在Swing线程中执行它呢?因为当前线程并非Swing线程,而action大多情况是处理一些Swing事务,所以这样就避免了外部再次写SwingUtilities.invokeLater带来的啰嗦。如果还不熟悉Swing事件派发线程(EDT)和SwingUtilities.invokeLater机制的同学,那就要多读书、多加油喽!

 

有了这个机制,我们就可以实现两段动画的连接:把第二段动画放在第一段动画的action里面就行了。

private void moveTwoSteps(final boolean unclockwise) {
  Mover mover = createNodeMover(unclockwise, new Runnable() {

    public void run() {
      Mover mover = createNodeMover(unclockwise, null);
      mover.start();
    }
  });
  mover.start();
}

 

 

增加点击移动事件

有了以上的动画能力,还要把它通过一定的事件触发出来。当然是“点击图片移动它到面前”的方式最好了,也符合这类程序的一般设计感觉。我们在TWaver的Network画布上添加一个节点点击事件:

network.addElementClickedActionListener(new ActionListener() {

  public void actionPerformed(ActionEvent e) {
    Object source = e.getSource();
    if (source instanceof ImageNode) {
      ImageNode node = (ImageNode) source;
      move(node);
    }
  }
});

 
当一个图片节点点击后,调用move函数进行移动。在move函数中,首先判断当前点击图片的位置编号。如果是1、5号位,则分别逆时针、顺时针移动2个位置;如果是2、4号位,则分别逆时针、顺时针移动1个位置;如果3号位,则不动。

 

为什么点击3号位,不移动呢?因为按理说5号位是中锋的位置,3号位是小前锋,应当跑动最多的球员。在NBA中,目前公认的最佳5号位中锋是我们的姚明。虽然一年多没打球了,但是实力依然不可小视。不过最近火箭正在吸收当帕特里克•帕特森,一个刚刚结束在肯塔基大学第二年的学生来打3号位,这孩子身体素质如牛,弹跳爆发力极强,让人十分期待。怎么扯到篮球了?继续看代码:

int index = (Integer) node.getUserObject();
if (index == 1) {
  moveTwoSteps(true);
}
if (index == 2) {
  moveOneStep(true);
}
if (index == 4) {
  moveOneStep(false);
}
if (index == 5) {
  moveTwoSteps(false);
}

 

其中moveOneStep和moveTwoSteps函数都是封装好了的移动一步、两步的方法。另外,boolean参数是控制逆时针或顺时针方向。

 

如何避免动画错乱

什么?这么精妙的动画算法怎么会错乱?那可不一定,要看谁玩儿了。你没发现我们程序员都有一个习惯:逮着一个程序,尤其花里胡哨的那种程序,鼠标冲上去就是一通speed>10次/秒的速度狂戳,而且四处乱戳,毫无规律可言。如果这样暴力,这个动画一定会乱:第一段动画还每完,第二段、第三段又起来了。

 

怎么办?对付这种变态的用户,只能加一把“线程锁”:通过一个唯一存在的“信号灯”来做标记。当动画尚未结束的时候,让其它动画线程一律:a、等待;b、去死。我选择了b。原因很简单,因为它简单。

 

这个例子中的具体做法是:在Mover类里面定义了一个static的信号变量:

private static boolean moving = false;

 

并用两个synchronized函数来负责存取:

public static synchronized boolean isMoving() {
        return moving;
    }

    public static synchronized void setMoving(boolean moving) {
        Mover.moving = moving;
    }

 

在动画开始的时候进行如下判断:

public void run() {
        if (!isMoving()) {
            setMoving(true);

 
如果信号灯开启,线程直接结束;否则,打开信号灯,然后执行动画。

 

没错,我承认这里处理的还很粗糙:一个static的信号灯太粗鲁了一点。不过在这个简单的、只有一个动画场景的环境里面,就不搞的太复杂了。有兴趣的同学可以弄的再科学一点。

 

当然,这样处理后,界面我随你鼠标狂风暴雨,我自岿然不动。不是不动,是悠然自得的慢慢的动。

 

处理图片的遮挡关系

别小看这个话题。当图片动起来后,其不断变化的前后遮挡关系要及时处理,很是烦人。错误的遮挡关系会严重破坏程序的感觉。图片的遮挡关系应当符合这个原则:最中间的图片离眼睛最近,所以应当在最上面;1号位(弗老大的位置)和5号位(姚明的位置)应当离眼睛最远,应当在最下面。

 

 

在代码中,我们把每个位置的图片用一个大小不同的数字来代表其前后关系,然后把数字作为key,把每个节点放入一个可自动排序的TreeMap哈希表:

if (index == 3) {
  sortedNodes.put(5, node);
}
if (index == 2) {
  sortedNodes.put(4, node);
}
if (index == 4) {
  sortedNodes.put(3, node);
}
if (index == 1) {
  sortedNodes.put(2, node);
}
if (index == 5) {
  sortedNodes.put(0, node);
}

 

然后在每段动画结束的时候,用一个runnable对排序进行重新整理,更新其遮挡关系:

Runnable action = new Runnable() {

public void run() {
  Iterator<Integer> iterator = sortedNodes.keySet().iterator();
  while (iterator.hasNext()) {
    int index = iterator.next();
    ImageNode node = sortedNodes.get(index);
    sendToTop(node);
  }
  if (followingAction != null) {
    followingAction.run();
  }
}
};

 

 

其他小伎俩

 

设置Tooltip这个就简单了:直接在node上用setToolTipText函数就行了。注意用点HTML的小伎俩。

node.setToolTipText("<html><b>中国馆</b>"
                    + "<br>中国馆你没见过吗?不会吧童鞋?"
                    + "<br>点我,闲着也是闲着");

 

另外还可以修改背景颜色。黑色的也挺酷,不是么?

不足与改进空间

这个例子最大的缺点是:写死了只能处理5张图片,而不是任意多。动画的控制函数也比较单一。不过通过以上原理介绍后,大家完全可以自行改进。欢迎感兴趣的童鞋积极动手,欢迎各位大侠给出宝贵意见。

 

例子与源代码下载

老规矩,有福同享、有难我当。可执行程序、源代码的下载请见本页面下方。注意需要twaver.jar包以及JDK6环境。有任何疑问请给我留言。

 

另外,程序中用到的素材图片均来自网络,版权归作者所有;世博场馆造型设计版权归各自场馆和国家所有,本人决没有侵权的意思。

 

再次感谢大家捧场!

转载于:https://my.oschina.net/darkness/blog/370203

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值