没错,这依旧是一篇技术文章,而不是世博会游记。其实至今尚未参观世博会,虽然就生活在这个城市,却没有外地朋友的那番激情和热度。在上下班地铁站上与蜂拥而至的旅游团队挤地铁、看着地铁车厢屏幕上跳动的接近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环境。有任何疑问请给我留言。
另外,程序中用到的素材图片均来自网络,版权归作者所有;世博场馆造型设计版权归各自场馆和国家所有,本人决没有侵权的意思。
再次感谢大家捧场!

- 大小: 260.6 KB

- 大小: 108.2 KB

- 大小: 20.3 KB

- 大小: 6 KB

- 大小: 238.5 KB
分享到:
相关推荐
主题:Swing是一把刀 ...主题:Swing第二刀:枝间新绿一重重 ...主题:Swing第二小刀刀:星星之火可以燎原 ...Swing第五刀:走马观花看世博 http://joshuaxiao.iteye.com/blog/726318 Swing第六刀:老婆不能换,窗户框可以
Swing大刀系列: 1.1 Swing是一把刀 1.2 Swing第二刀:枝间新绿一重重 1.3 Swing第三刀:做套ERP,要配得上我的登录界面!...1.5 Swing第五刀:走马观花看世博 1.6 Swing第六刀:老婆不能换,窗户框可以
55links友情链接网址跟踪器,放在桌面,每次直接打开就可以访问55links友情链接交易平台,方便快捷。
AB PLC例程代码项目案例 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动化、电子信息等)在校学生、专业老师或者企业员工下载使用。 3、用途:项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 4、如果基础还行,或热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载!欢迎交流学习!不清楚的可以私信问我!
moore_01_0909
FIBR English learning
AB PLC例程代码项目案例 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动化、电子信息等)在校学生、专业老师或者企业员工下载使用。 3、用途:项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 4、如果基础还行,或热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载!欢迎交流学习!不清楚的可以私信问我!
OIF_IEEE802.3_liaison_19OCt09
做网络安全FTP内容的实验必备
nagarajan_01_1107
AB PLC例程代码项目案例 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动化、电子信息等)在校学生、专业老师或者企业员工下载使用。 3、用途:项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 4、如果基础还行,或热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载!欢迎交流学习!不清楚的可以私信问我!
mellitz_3cd_01_0318
PyQt6实战派 配套代码
陕西省省级非物质文化遗产经纬度数据统计表 统计内容包含以下字段: 1. 项目名称 2. 遗产类别 3. 入选批次 4. 所属地区 5. 申报地区/单位 6. 地理经度 7. 地理纬度 该统计表系统记录了陕西省省级非物质文化遗产的地理空间信息,为文化遗产的数字化保护与研究工作提供了重要的数据支撑。
ran_3ck_02a_0918
毕业设计_基于springboot+vue开发的汽车租赁管理系统【源码+sql+可运行】【50308】.zip 全部代码均可运行,亲测可用,尽我所能,为你服务; 1.代码压缩包内容 代码:springboo后端代码+vue前端页面代码; 脚本:数据库SQL脚本 效果图:运行结果请看资源详情效果图 2.环境准备: - JDK1.8+ - maven3.6+ - nodejs14+ - mysql5.6+ - redis 3.技术栈 - 后台:springboot+mybatisPlus+Shiro - 前台:vue+iview+Vuex+Axios - 开发工具: idea、navicate 4.功能列表 - 系统设置:用户管理、角色管理、资源管理、系统日志 - 业务管理:汽车管理、客户管理、租赁订单 3.运行步骤: 步骤一:修改数据库连接信息(ip、port修改) 步骤二:找到启动类xxxApplication启动 4.若不会,可私信博主!!!
# Runcorder - 跑步训练管理系统 Runcorder 是一款专为跑步爱好者、马拉松运动员及高校体育生设计的本地化跑步训练管理工具,基于 Python 开发,结合 Tkinter 图形界面与强大的数据处理能力,为用户提供从训练记录到数据分析的全方位支持。无论是初学者还是专业跑者,Runcorder 都能帮助你科学规划训练、精准追踪进度,并通过可视化图表直观呈现训练成果,让你的跑步训练更智能、更高效! - **多用户管理**:支持创建、加载和删除用户档案,每个用户的数据独立存储,确保隐私与安全。 - **科学训练记录**:全维度记录跑步数据,包括日期、里程、配速、自评和晨跑标记,支持智能输入校验,避免数据错误。 - **多维数据分析**:通过动态可视化图表展示跑步里程趋势、平均配速曲线,支持自定义 Y 轴范围,帮助用户深入理解训练效果。 - **高阶功能**:提供 4 种科学训练模式(有氧/无氧/混合),支持历史记录修改与删除,数据以 JSON 格式持久化存储,跨平台兼容。
paatzsch_01_0708
AnythingLLM是一个全栈应用程序,您可以使用流行的开源大语言模型,再结合向量数据库解决方案构建个人本地AI大模型知识库
mellitz_3ck_02_0519