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