1 /***************************************************************************
2  *                (C) Copyright 2003-2015 - Faiumoni e.V.                  *
3  ***************************************************************************
4  ***************************************************************************
5  *                                                                         *
6  *   This program is free software; you can redistribute it and/or modify  *
7  *   it under the terms of the GNU General Public License as published by  *
8  *   the Free Software Foundation; either version 2 of the License, or     *
9  *   (at your option) any later version.                                   *
10  *                                                                         *
11  ***************************************************************************/
12 /*
13  * Inspired by MadProgrammer's example at
14  * https://stackoverflow.com/a/14541651/2471439
15  */
16 package games.stendhal.client.gui.layout;
17 
18 import java.awt.Component;
19 import java.awt.Container;
20 import java.awt.Dimension;
21 import java.awt.LayoutManager2;
22 import java.awt.Point;
23 import java.awt.Rectangle;
24 import java.awt.event.ActionEvent;
25 import java.awt.event.ActionListener;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.HashMap;
29 import java.util.Map;
30 import java.util.WeakHashMap;
31 
32 import javax.swing.Timer;
33 
34 /**
35  * A forwarding layout manager that uses smooth animations for layout changes.
36  */
37 public class AnimatedLayout implements LayoutManager2 {
38 	/** The layout manager used to determine the desired final layout. */
39 	private LayoutManager2 proxy;
40 	/** Mapping of animations managed by this layout. */
41 	private Map<Container, Animator> animations;
42 	/** Flag for showing or suppressing animation. */
43 	private boolean animated = true;
44 
45 	/**
46 	 * Create a new AnimatedLayout.
47 	 *
48 	 * @param proxy layout manager to be used for determining the desired layout
49 	 */
AnimatedLayout(LayoutManager2 proxy)50 	public AnimatedLayout(LayoutManager2 proxy) {
51 		this.proxy = proxy;
52 		animations = new WeakHashMap<Container, Animator>(5);
53 	}
54 
55 	/**
56 	 * Turn animations on or off.
57 	 *
58 	 * @param animate <code>true</code> if subsequent layout changes are
59 	 * animated, otherwise <code>false</code>
60 	 */
setAnimated(boolean animate)61 	public void setAnimated(boolean animate) {
62 		if (animated != animate) {
63 			animated = animate;
64 		}
65 	}
66 
67 	@Override
addLayoutComponent(String name, Component comp)68 	public void addLayoutComponent(String name, Component comp) {
69 		proxy.addLayoutComponent(name, comp);
70 	}
71 
72 	@Override
removeLayoutComponent(Component comp)73 	public void removeLayoutComponent(Component comp) {
74 		proxy.removeLayoutComponent(comp);
75 	}
76 
77 	@Override
preferredLayoutSize(Container parent)78 	public Dimension preferredLayoutSize(Container parent) {
79 		return proxy.preferredLayoutSize(parent);
80 	}
81 
82 	@Override
minimumLayoutSize(Container parent)83 	public Dimension minimumLayoutSize(Container parent) {
84 		return proxy.minimumLayoutSize(parent);
85 	}
86 
87 	@Override
layoutContainer(Container parent)88 	public void layoutContainer(Container parent) {
89 		if (!animated) {
90 			proxy.layoutContainer(parent);
91 			return;
92 		}
93 
94 		Map<Component, Rectangle> startPositions = new HashMap<Component, Rectangle>(parent.getComponentCount());
95 		for (Component comp : parent.getComponents()) {
96 			startPositions.put(comp, new Rectangle(comp.getBounds()));
97 		}
98 
99 		proxy.layoutContainer(parent);
100 
101 		Collection<BoundData> changes = new ArrayList<BoundData>();
102 		for (Component comp : parent.getComponents()) {
103 			Rectangle bounds = comp.getBounds();
104 			Rectangle startBounds = startPositions.get(comp);
105 			if (!startBounds.equals(bounds)) {
106 				comp.setBounds(startBounds);
107 				changes.add(new BoundData(comp, startBounds, bounds));
108 			}
109 		}
110 
111 		if (!changes.isEmpty()) {
112 			Animator animator = animations.get(parent);
113 			if (animator == null) {
114 				animator = new Animator(parent, changes);
115 				animations.put(parent, animator);
116 			} else {
117 				animator.setBounds(changes);
118 			}
119 			animator.restart();
120 		} else {
121 			Animator animator = animations.get(parent);
122 			if (animator != null) {
123 				animator.stop();
124 				animations.remove(parent);
125 			}
126 		}
127 	}
128 
129 
130 	@Override
addLayoutComponent(Component comp, Object constraints)131 	public void addLayoutComponent(Component comp, Object constraints) {
132 		proxy.addLayoutComponent(comp, constraints);
133 	}
134 
135 	@Override
maximumLayoutSize(Container target)136 	public Dimension maximumLayoutSize(Container target) {
137 		return proxy.maximumLayoutSize(target);
138 	}
139 
140 	@Override
getLayoutAlignmentX(Container target)141 	public float getLayoutAlignmentX(Container target) {
142 		return proxy.getLayoutAlignmentX(target);
143 	}
144 
145 	@Override
getLayoutAlignmentY(Container target)146 	public float getLayoutAlignmentY(Container target) {
147 		return proxy.getLayoutAlignmentY(target);
148 	}
149 
150 	@Override
invalidateLayout(Container target)151 	public void invalidateLayout(Container target) {
152 		proxy.invalidateLayout(target);
153 	}
154 
155 	/**
156 	 * Class for holding Components' initial and final bounds.
157 	 */
158 	private static class BoundData {
159 		/** The component whose bounds are stored. */
160 		private final Component component;
161 		/** Initial bounds. */
162 		private final Rectangle startBounds;
163 		/** Final bounds. */
164 		private final Rectangle finalBounds;
165 
166 		/**
167 		 * Create new BoundData.
168 		 *
169 		 * @param component component whose bounds are stored
170 		 * @param startBounds bounds of the component at the start of the animation
171 		 * @param finalBounds bounds of the component at the end of the animation,
172 		 */
BoundData(Component component, Rectangle startBounds, Rectangle finalBounds)173 		BoundData(Component component, Rectangle startBounds, Rectangle finalBounds) {
174 			this.component = component;
175 			this.startBounds = startBounds;
176 			this.finalBounds = finalBounds;
177 		}
178 
179 		/**
180 		 * Get the component to which the bounds belong to.
181 		 *
182 		 * @return component
183 		 */
getComponent()184 		Component getComponent() {
185 			return component;
186 		}
187 
188 		/**
189 		 * Get the maximum coordinate change.
190 		 *
191 		 * @return maximum change
192 		 */
getMaxDistance()193 		int getMaxDistance() {
194 			int maxDist = Math.abs(startBounds.x - finalBounds.x);
195 			maxDist = Math.max(maxDist, Math.abs(startBounds.x + startBounds.width - finalBounds.x - finalBounds.width));
196 			maxDist = Math.max(maxDist, Math.abs(startBounds.y - finalBounds.y));
197 			return Math.max(maxDist, Math.abs(startBounds.y + startBounds.height - finalBounds.y - finalBounds.height));
198 		}
199 
200 		/**
201 		 * Get the bounds at specified progress state.
202 		 * @param progress state of progress
203 		 *
204 		 * @return bounds at specified progress state
205 		 */
getBounds(double progress)206 		Rectangle getBounds(double progress) {
207 			return interpolate(startBounds, finalBounds, progress);
208 		}
209 
210 		/**
211 		 * Calculate interpolated bounds based on the initial and final bounds,
212 		 * and the state of progress.
213 		 *
214 		 * @param startBounds initial bounds
215 		 * @param finalBounds final bounds
216 		 * @param progress state of progress
217 		 * @return interpolated bounds
218 		 */
interpolate(Rectangle startBounds, Rectangle finalBounds, double progress)219 		private Rectangle interpolate(Rectangle startBounds, Rectangle finalBounds, double progress) {
220 			Rectangle bounds = new Rectangle();
221 			bounds.setLocation(interpolate(startBounds.getLocation(), finalBounds.getLocation(), progress));
222 			bounds.setSize(interpolate(startBounds.getSize(), finalBounds.getSize(), progress));
223 
224 			return bounds;
225 		}
226 
227 		/**
228 		 * Calculate interpolated dimensions based on the initial and final
229 		 * dimensions, and the state of progress.
230 		 *
231 		 * @param startSize initial size
232 		 * @param finalSize final size
233 		 * @param progress state of progress
234 		 * @return interpolated size
235 		 */
interpolate(Dimension startSize, Dimension finalSize, double progress)236 		private Dimension interpolate(Dimension startSize, Dimension finalSize, double progress) {
237 			Dimension size = new Dimension();
238 			size.width = interpolate(startSize.width, finalSize.width, progress);
239 			size.height = interpolate(startSize.height, finalSize.height, progress);
240 
241 			return size;
242 		}
243 
244 		/**
245 		 * Calculate interpolated location based on the initial and final
246 		 * location, and the state of progress.
247 		 *
248 		 * @param startPoint initial location
249 		 * @param finalPoint final location
250 		 * @param progress state of progress
251 		 * @return interpolated location
252 		 */
interpolate(Point startPoint, Point finalPoint, double progress)253 		private Point interpolate(Point startPoint, Point finalPoint, double progress) {
254 			Point point = new Point();
255 			point.x = interpolate(startPoint.x, finalPoint.x, progress);
256 			point.y = interpolate(startPoint.y, finalPoint.y, progress);
257 
258 			return point;
259 		}
260 
261 		/**
262 		 * Calculate interpolated value based on the initial and final values,
263 		 * and the state of progress.
264 		 *
265 		 * @param startValue initial value
266 		 * @param endValue final value
267 		 * @param progress state of progress
268 		 * @return interpolated value
269 		 */
interpolate(int startValue, int endValue, double progress)270 		private int interpolate(int startValue, int endValue, double progress) {
271 			int distance = endValue - startValue;
272 			double distanceDone;
273 			// quadratic ease in and out
274 			if (progress <= 0.5) {
275 				distanceDone = 2 * progress * progress;
276 			} else {
277 				distanceDone = -2 * progress * progress + 4 * progress - 1;
278 			}
279 
280 			return (int) (distance * distanceDone) + startValue;
281 		}
282 	}
283 
284 	/**
285 	 * Object for managing the animations.
286 	 */
287 	private static class Animator implements ActionListener {
288 		/** Minimum animation speed. Larger is faster. */
289 		private static final double MINIMUM_SPEED = 0.032;
290 
291 		/** Timer used for the animation steps. */
292 		private final Timer timer;
293 		/** The container of the animated components. */
294 		private Container parent;
295 		/** Bound data of the animated components. */
296 		private Collection<BoundData> boundList;
297 		/** Current animation state. */
298 		private double progress;
299 		/** Current animation speed. */
300 		private double progressRate;
301 
302 		/**
303 		 * Create a new Animator.
304 		 *
305 		 * @param parent The container of the animated components
306 		 * @param bounds Bound data of the animated components
307 		 */
Animator(Container parent, Collection<BoundData> bounds)308 		Animator(Container parent, Collection<BoundData> bounds) {
309 			setBounds(bounds);
310 			timer = new Timer(16, this);
311 			this.parent = parent;
312 		}
313 
314 		/**
315 		 * Set bound data for the animations.
316 		 * @param bounds bound data
317 		 */
setBounds(Collection<BoundData> bounds)318 		final void setBounds(Collection<BoundData> bounds) {
319 			// Base the animation speed on the longest animated distance
320 			int maxDist = 0;
321 			for (BoundData ab : bounds) {
322 				maxDist = Math.max(maxDist, ab.getMaxDistance());
323 			}
324 			progressRate = Math.max(MINIMUM_SPEED, 1.0 / maxDist);
325 			this.boundList = bounds;
326 		}
327 
328 		/**
329 		 * Start, or restart the animation.
330 		 */
restart()331 		void restart() {
332 			progress = 0;
333 			timer.restart();
334 		}
335 
336 		/**
337 		 * Stop the animation.
338 		 */
stop()339 		void stop() {
340 			timer.stop();
341 		}
342 
343 		@Override
actionPerformed(ActionEvent e)344 		public void actionPerformed(ActionEvent e) {
345 			progress += progressRate;
346 			if (progress >= 1) {
347 				progress = 1;
348 				timer.stop();
349 			}
350 
351 			for (BoundData bounds : boundList) {
352 				Component comp = bounds.getComponent();
353 				comp.setBounds(bounds.getBounds(progress));
354 			}
355 
356 			parent.repaint();
357 		}
358 	}
359 }
360