锁定老帖子 主题:Swing第五刀:走马观花看世博
该帖已经被评为精华帖
|
|
---|---|
作者 | 正文 |
发表时间:2010-07-16
最后修改:2010-07-22
没错,这依旧是一篇技术文章,而不是世博会游记。其实至今尚未参观世博会,虽然就生活在这个城市,却没有外地朋友的那番激情和热度。在上下班地铁站上与蜂拥而至的旅游团队挤地铁、看着地铁车厢屏幕上跳动的接近50万的世博会当日入园人数统计、画面上如广州火车站春运搬的人流,想想这接近40度的桑拿天和时不时骤降的倾盆大雨,这最后的一点去看一看的激情也被无情的抹杀了。 不是很轻松不得不说,用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); } double percent = (double) (y * 100) / h / 100d; for (int i = 0; i < fadeSpeed; i++) { percent = percent * percent; } alpha = (int) (alpha * percent); alpha = Math.min(alpha, 150); 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; 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); //移动物体 }
如何计算图片停留点位置
在每个位置点,图片都有对应的、固定的中心点和尺寸,我们提前计算好并记录下来就行了。在Main.java中,createNodes函数创建了所有图片节点,同时又返回了每个停留点的边界: private Rectangle[] positions = createNodes();
如何连续移动以上解决了移动一个位置的动画问题。如果一个图片需要从1点直接跳到3点,该如何处理?为了简化程序,同时保持“一步一步走”的动画效果,我们还是把两段动画连续播放,而不是直接忽悠一下移动过去。也就是说,先让图片从1点移动到2点,停顿一下再移动到3点。这样就要连接两个线程。
由于线程是异步的,一旦run起来我们就没法控制其暂停和停止以及后续动作。所以,我们要改造一下Mover:在创建的时候,可以给他一个Runnable,当线程执行结束后,可以执行这个Runnable进行“收尾”或“桥接”。例如,这个Runnable可以在线程结束后重新设置一下图片的前后遮挡关系,也可以用于链接、执行下一个动画。可见,这个改进非常有必要。
观察Mover.java的构造函数: public Mover(ArrayList<MoverInfo> infos, Runnable action) if (action != null) { SwingUtilities.invokeLater(action); }
有了这个机制,我们就可以实现两段动画的连接:把第二段动画放在第一段动画的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); } } });
为什么点击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环境。有任何疑问请给我留言。
另外,程序中用到的素材图片均来自网络,版权归作者所有;世博场馆造型设计版权归各自场馆和国家所有,本人决没有侵权的意思。
再次感谢大家捧场! 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2010-07-16
非常不错。其实SWING能做的东西不是很多
|
|
返回顶楼 | |
发表时间:2010-07-17
最后修改:2010-07-17
kyo19 写道 非常不错。其实SWING能做的东西不是很多
完全不同意,这种效果用swing就能做,而且完全不需要第三方jar包,不过是swing的API比较低级,什么都要自己来就是了 xiaozhonghua同学的开源精神令人佩服 |
|
返回顶楼 | |
发表时间:2010-07-17
从第一刀开始,一直关注你的相关文章,太棒了
|
|
返回顶楼 | |
发表时间:2010-07-17
真不容易,起来这么早都没抢到个沙发。从第一刀就开始拜读了,每天不停的刷je只为等待你的下一刀,swing大牛们都别潜水了吧,非常喜欢swing,楼主加油吧,期待你的第N刀
|
|
返回顶楼 | |
发表时间:2010-07-17
swing牛人
|
|
返回顶楼 | |
发表时间:2010-07-17
太棒了,你的每一刀都挥舞的那么的迷人!
|
|
返回顶楼 | |
发表时间:2010-07-17
JavaFX好像加一个效果就可以直接出来倒影了吧?
|
|
返回顶楼 | |
发表时间:2010-07-17
好刀啊,必须顶,继续向楼主深入学习swing。
|
|
返回顶楼 | |
发表时间:2010-07-17
对于一个swing FANS 来说, 更钦佩楼主研究的“刀法”
|
|
返回顶楼 | |