| /* |
| * Copyright 2009 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package com.google.gwt.layout.client; |
| |
| import static com.google.gwt.dom.client.Style.Unit.PX; |
| |
| import com.google.gwt.animation.client.Animation; |
| import com.google.gwt.core.client.GWT; |
| import com.google.gwt.dom.client.Element; |
| import com.google.gwt.dom.client.Style.Unit; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Helper class for laying out a container element and its children. |
| * |
| * <p> |
| * This class is typically used by higher-level widgets to implement layout on |
| * their behalf. It is intended to wrap an element (usually a <div>), and |
| * lay its children out in a predictable fashion, automatically accounting for |
| * changes to the parent's size, and for all elements' margins, borders, and |
| * padding. |
| * </p> |
| * |
| * <p> |
| * To use this class, create a container element (again, usually a <div>) |
| * and pass it to {@link #Layout(Element)}. Rather than attaching child elements |
| * directly to the element managed by this {@link Layout}, use the |
| * {@link Layout#attachChild(Element)} method. This will attach the child |
| * element and return a {@link Layout.Layer} object which is used to manage the |
| * child. |
| * </p> |
| * |
| * <p> |
| * A separate {@link Layout.Layer} instance is associated with each child |
| * element. There is a set of methods available on this class to manipulate the |
| * child element's position and size. In order for changes to a layer to take |
| * effect, you must finally call one of {@link #layout()} or |
| * {@link #layout(int)}. This allows many changes to different layers to be |
| * applied efficiently, and to be animated. |
| * </p> |
| * |
| * <p> |
| * On most browsers, this is implemented using absolute positioning. It also |
| * contains extra logic to make IE6 work properly. |
| * </p> |
| * |
| * <p> |
| * <h3>Example</h3> |
| * {@example com.google.gwt.examples.LayoutExample} |
| * </p> |
| * |
| * <p> |
| * NOTE: This class will <em>only</em> work in standards mode, which requires |
| * that the HTML page in which it is run have an explicit <!DOCTYPE> |
| * declaration. |
| * </p> |
| * |
| * <p> |
| * NOTE: This class is still very new, and its interface may change without |
| * warning. Use at your own risk. |
| * </p> |
| */ |
| public class Layout { |
| |
| /** |
| * Callback interface used by {@link Layout#layout(int, AnimationCallback)} |
| * to provide updates on animation progress. |
| */ |
| public interface AnimationCallback { |
| |
| /** |
| * Called immediately after the animation is complete, and the entire layout |
| * is in its final state. |
| */ |
| void onAnimationComplete(); |
| |
| /** |
| * Called at each step of the animation, for each layer being laid out. |
| * |
| * @param layer the layer being laid out |
| */ |
| void onLayout(Layer layer, double progress); |
| } |
| |
| /** |
| * This class is used to set the position and size of child elements. |
| * |
| * <p> |
| * Each child element has three values associated with each axis: {left, |
| * right, width} on the horizontal axis, and {top, bottom, height} on the |
| * vertical axis. Precisely two of three values may be set at a time, or the |
| * system will be over- or under-contrained. For this reason, the following |
| * methods are provided for setting these values: |
| * <ul> |
| * <li>{@link #setLeftRight(double, Unit, double, Unit)}</li> |
| * <li>{@link #setLeftWidth(double, Unit, double, Unit)}</li> |
| * <li>{@link #setRightWidth(double, Unit, double, Unit)}</li> |
| * <li>{@link #setTopBottom(double, Unit, double, Unit)}</li> |
| * <li>{@link #setTopHeight(double, Unit, double, Unit)}</li> |
| * <li>{@link #setBottomHeight(double, Unit, double, Unit)}</li> |
| * </ul> |
| * </p> |
| * |
| * <p> |
| * By default, each layer is set to fill the entire parent (i.e., {left, top, |
| * right, bottom} = {0, 0, 0, 0}). |
| * </p> |
| */ |
| public class Layer { |
| final Element container, child; |
| |
| Object userObject; |
| |
| boolean setLeft, setRight, setTop, setBottom, setWidth, setHeight; |
| boolean setTargetLeft = true, setTargetRight = true, setTargetTop = true, |
| setTargetBottom = true, setTargetWidth, setTargetHeight; |
| Unit leftUnit, topUnit, rightUnit, bottomUnit, widthUnit, heightUnit; |
| Unit targetLeftUnit = PX, targetTopUnit = PX, targetRightUnit = PX, |
| targetBottomUnit = PX, targetWidthUnit, targetHeightUnit; |
| double left, top, right, bottom, width, height; |
| double sourceLeft, sourceTop, sourceRight, sourceBottom, sourceWidth, |
| sourceHeight; |
| double targetLeft, targetTop, targetRight, targetBottom, targetWidth, |
| targetHeight; |
| |
| Layer(Element container, Element child, Object userObject) { |
| this.container = container; |
| this.child = child; |
| this.userObject = userObject; |
| } |
| |
| /** |
| * Gets the container element associated with this layer. |
| * |
| * <p> |
| * This is the element that sits between the parent and child elements. It |
| * is normally necessary to operate on this element only when you need to |
| * modify certain CSS properties, such as visibility. |
| * </p> |
| * |
| * @return the container element |
| */ |
| public Element getContainerElement() { |
| return container; |
| } |
| |
| /** |
| * Gets the user-data associated with this layer. |
| * |
| * @return the layer's user-data object |
| */ |
| public Object getUserObject() { |
| return this.userObject; |
| } |
| |
| /** |
| * Sets the layer's bottom and height values. |
| * |
| * @param bottom |
| * @param bottomUnit |
| * @param height |
| * @param heightUnit |
| */ |
| public void setBottomHeight(double bottom, Unit bottomUnit, double height, |
| Unit heightUnit) { |
| this.setTargetBottom = this.setTargetHeight = true; |
| this.setTargetTop = false; |
| this.targetBottom = bottom; |
| this.targetHeight = height; |
| this.targetBottomUnit = bottomUnit; |
| this.targetHeightUnit = heightUnit; |
| } |
| |
| /** |
| * Sets the layer's left and right values. |
| * |
| * @param left |
| * @param leftUnit |
| * @param right |
| * @param rightUnit |
| */ |
| public void setLeftRight(double left, Unit leftUnit, double right, |
| Unit rightUnit) { |
| this.setTargetLeft = this.setTargetRight = true; |
| this.setTargetWidth = false; |
| this.targetLeft = left; |
| this.targetRight = right; |
| this.targetLeftUnit = leftUnit; |
| this.targetRightUnit = rightUnit; |
| } |
| |
| /** |
| * Sets the layer's left and width values. |
| * |
| * @param left |
| * @param leftUnit |
| * @param width |
| * @param widthUnit |
| */ |
| public void setLeftWidth(double left, Unit leftUnit, double width, |
| Unit widthUnit) { |
| this.setTargetLeft = this.setTargetWidth = true; |
| this.setTargetRight = false; |
| this.targetLeft = left; |
| this.targetWidth = width; |
| this.targetLeftUnit = leftUnit; |
| this.targetWidthUnit = widthUnit; |
| } |
| |
| /** |
| * Sets the layer's right and width values. |
| * |
| * @param right |
| * @param rightUnit |
| * @param width |
| * @param widthUnit |
| */ |
| public void setRightWidth(double right, Unit rightUnit, double width, |
| Unit widthUnit) { |
| this.setTargetRight = this.setTargetWidth = true; |
| this.setTargetLeft = false; |
| this.targetRight = right; |
| this.targetWidth = width; |
| this.targetRightUnit = rightUnit; |
| this.targetWidthUnit = widthUnit; |
| } |
| |
| /** |
| * Sets the layer's top and bottom values. |
| * |
| * @param top |
| * @param topUnit |
| * @param bottom |
| * @param bottomUnit |
| */ |
| public void setTopBottom(double top, Unit topUnit, double bottom, |
| Unit bottomUnit) { |
| this.setTargetTop = this.setTargetBottom = true; |
| this.setTargetHeight = false; |
| this.targetTop = top; |
| this.targetBottom = bottom; |
| this.targetTopUnit = topUnit; |
| this.targetBottomUnit = bottomUnit; |
| } |
| |
| /** |
| * Sets the layer's top and height values. |
| * |
| * @param top |
| * @param topUnit |
| * @param height |
| * @param heightUnit |
| */ |
| public void setTopHeight(double top, Unit topUnit, double height, |
| Unit heightUnit) { |
| this.setTargetTop = this.setTargetHeight = true; |
| this.setTargetBottom = false; |
| this.targetTop = top; |
| this.targetHeight = height; |
| this.targetTopUnit = topUnit; |
| this.targetHeightUnit = heightUnit; |
| } |
| } |
| |
| private static LayoutImpl impl = GWT.create(LayoutImpl.class); |
| |
| private List<Layer> layers = new ArrayList<Layer>(); |
| private final Element parentElem; |
| private Animation animation; |
| |
| /** |
| * Constructs a new layout associated with the given parent element. |
| * |
| * @param parent the element to serve as the layout parent |
| */ |
| public Layout(Element parent) { |
| this.parentElem = parent; |
| impl.initParent(parent); |
| } |
| |
| /** |
| * Asserts that the given child element is managed by this layout. |
| * |
| * @param elem the element to be tested |
| */ |
| public void assertIsChild(Element elem) { |
| assert elem.getParentElement().getParentElement() == this.parentElem : "Element is not a child of this layout"; |
| } |
| |
| /** |
| * Attaches a child element to this layout. |
| * |
| * <p> |
| * This method will attach the child to the layout, removing it from its |
| * current parent element. Use the {@link Layer} it returns to manipulate the |
| * child. |
| * </p> |
| * |
| * @param child the child to be attached |
| * @return the {@link Layer} associated with the element |
| */ |
| public Layer attachChild(Element child) { |
| return attachChild(child, null); |
| } |
| |
| /** |
| * Attaches a child element to this layout. |
| * |
| * <p> |
| * This method will attach the child to the layout, removing it from its |
| * current parent element. Use the {@link Layer} it returns to manipulate the |
| * child. |
| * </p> |
| * |
| * @param child the child to be attached |
| * @param before the child element before which to insert |
| * @return the {@link Layer} associated with the element |
| */ |
| public Layer attachChild(Element child, Element before) { |
| return attachChild(child, before, null); |
| } |
| |
| /** |
| * Attaches a child element to this layout. |
| * |
| * <p> |
| * This method will attach the child to the layout, removing it from its |
| * current parent element. Use the {@link Layer} it returns to manipulate the |
| * child. |
| * </p> |
| * |
| * @param child the child to be attached |
| * @param userObject an arbitrary object to be associated with this layer |
| * @return the {@link Layer} associated with the element |
| */ |
| public Layer attachChild(Element child, Object userObject) { |
| return attachChild(child, null, userObject); |
| } |
| |
| /** |
| * Attaches a child element to this layout. |
| * |
| * <p> |
| * This method will attach the child to the layout, removing it from its |
| * current parent element. Use the {@link Layer} it returns to manipulate the |
| * child. |
| * </p> |
| * |
| * @param child the child to be attached |
| * @param before the child element before which to insert |
| * @param userObject an arbitrary object to be associated with this layer |
| * @return the {@link Layer} associated with the element |
| */ |
| public Layer attachChild(Element child, Element before, Object userObject) { |
| Element container = impl.attachChild(parentElem, child, before); |
| Layer layer = new Layer(container, child, userObject); |
| layers.add(layer); |
| return layer; |
| } |
| |
| /** |
| * Causes the parent element to fill its own parent. |
| * |
| * <p> |
| * This is most useful for top-level layouts that need to follow the size of |
| * another element, such as the <body>. |
| * </p> |
| */ |
| public void fillParent() { |
| impl.fillParent(parentElem); |
| } |
| |
| /** |
| * Returns the size of one unit, in pixels, in the context of this layout. |
| * |
| * <p> |
| * This will work for any unit type, but be aware that certain unit types, |
| * such as {@link Unit#EM}, and {@link Unit#EX}, will return different values |
| * based upon the parent's associated font size. {@link Unit#PCT} is dependent |
| * upon the parent's actual size, and the axis to be measured. |
| * </p> |
| * |
| * @param unit the unit type to be measured |
| * @param vertical whether the unit to be measured is on the vertical or |
| * horizontal axis (this matters only for {@link Unit#PCT}) |
| * @return the unit size, in pixels |
| */ |
| public double getUnitSize(Unit unit, boolean vertical) { |
| return impl.getUnitSizeInPixels(parentElem, unit, vertical); |
| } |
| |
| /** |
| * Updates this layout's children immediately. This method <em>must</em> be |
| * called after updating any of its children's {@link Layer layers}. |
| */ |
| public void layout() { |
| for (Layer l : layers) { |
| l.left = l.sourceLeft = l.targetLeft; |
| l.top = l.sourceTop = l.targetTop; |
| l.right = l.sourceRight = l.targetRight; |
| l.bottom = l.sourceBottom = l.targetBottom; |
| l.width = l.sourceWidth = l.targetWidth; |
| l.height = l.sourceHeight = l.targetHeight; |
| |
| l.setLeft = l.setTargetLeft; |
| l.setTop = l.setTargetTop; |
| l.setRight = l.setTargetRight; |
| l.setBottom = l.setTargetBottom; |
| l.setWidth = l.setTargetWidth; |
| l.setHeight = l.setTargetHeight; |
| |
| l.leftUnit = l.targetLeftUnit; |
| l.topUnit = l.targetTopUnit; |
| l.rightUnit = l.targetRightUnit; |
| l.bottomUnit = l.targetBottomUnit; |
| l.widthUnit = l.targetWidthUnit; |
| l.heightUnit = l.targetHeightUnit; |
| |
| impl.layout(l); |
| } |
| |
| impl.finalizeLayout(parentElem); |
| } |
| |
| /** |
| * Updates the layout by animating it over time. |
| * |
| * @param duration the duration of the animation |
| * @see #layout(int, AnimationCallback) |
| */ |
| public void layout(int duration) { |
| layout(duration, null); |
| } |
| |
| /** |
| * Updates the layout by animating it over time, with a callback on each frame |
| * of the animation, and upon completion. |
| * |
| * @param duration the duration of the animation |
| * @param callback the animation callback |
| */ |
| public void layout(int duration, final AnimationCallback callback) { |
| // Deal with constraint changes (e.g. left-width => right-width, etc) |
| int parentWidth = parentElem.getClientWidth(); |
| int parentHeight = parentElem.getClientHeight(); |
| for (Layer l : layers) { |
| adjustHorizontalConstraints(parentWidth, l); |
| adjustVerticalConstraints(parentHeight, l); |
| } |
| |
| // Cancel the old animation, if there is one. |
| if (animation != null) { |
| animation.cancel(); |
| } |
| |
| animation = new Animation() { |
| @Override |
| protected void onCancel() { |
| onComplete(); |
| } |
| |
| @Override |
| protected void onComplete() { |
| layout(); |
| if (callback != null) { |
| callback.onAnimationComplete(); |
| } |
| animation = null; |
| } |
| |
| @Override |
| protected void onUpdate(double progress) { |
| for (Layer l : layers) { |
| if (l.setTargetLeft) { |
| l.left = l.sourceLeft + (l.targetLeft - l.sourceLeft) * progress; |
| } |
| if (l.setTargetRight) { |
| l.right = l.sourceRight + (l.targetRight - l.sourceRight) |
| * progress; |
| } |
| if (l.setTargetTop) { |
| l.top = l.sourceTop + (l.targetTop - l.sourceTop) * progress; |
| } |
| if (l.setTargetBottom) { |
| l.bottom = l.sourceBottom + (l.targetBottom - l.sourceBottom) |
| * progress; |
| } |
| if (l.setTargetWidth) { |
| l.width = l.sourceWidth + (l.targetWidth - l.sourceWidth) |
| * progress; |
| } |
| if (l.setTargetHeight) { |
| l.height = l.sourceHeight + (l.targetHeight - l.sourceHeight) |
| * progress; |
| } |
| |
| impl.layout(l); |
| if (callback != null) { |
| callback.onLayout(l, progress); |
| } |
| } |
| impl.finalizeLayout(parentElem); |
| } |
| }; |
| |
| animation.run(duration); |
| } |
| |
| /** |
| * This method must be called when the parent element becomes attached to the |
| * document. |
| * |
| * @see #onDetach() |
| */ |
| public void onAttach() { |
| impl.onAttach(parentElem); |
| } |
| |
| /** |
| * This method must be called when the parent element becomes detached from |
| * the document. |
| * |
| * @see #onAttach() |
| */ |
| public void onDetach() { |
| impl.onDetach(parentElem); |
| } |
| |
| /** |
| * Removes a child element from this layout. |
| * |
| * @param layer the layer associated with the child to be removed |
| */ |
| public void removeChild(Layer layer) { |
| impl.removeChild(layer.container, layer.child); |
| layers.remove(layer); |
| } |
| |
| private void adjustHorizontalConstraints(int parentWidth, Layer l) { |
| double leftPx = l.left * getUnitSize(l.leftUnit, false); |
| double rightPx = l.right * getUnitSize(l.rightUnit, false); |
| double widthPx = l.width * getUnitSize(l.widthUnit, false); |
| |
| if (l.setLeft && !l.setTargetLeft) { |
| // -left |
| l.setLeft = false; |
| |
| if (!l.setWidth) { |
| // +width |
| l.setTargetWidth = true; |
| l.sourceWidth = (parentWidth - (leftPx + rightPx)) |
| / getUnitSize(l.targetWidthUnit, false); |
| } else { |
| // +right |
| l.setTargetRight = true; |
| l.sourceRight = (parentWidth - (leftPx + widthPx)) |
| / getUnitSize(l.targetRightUnit, false); |
| } |
| } else if (l.setWidth && !l.setTargetWidth) { |
| // -width |
| l.setWidth = false; |
| |
| if (!l.setLeft) { |
| // +left |
| l.setTargetLeft = true; |
| l.sourceLeft = (parentWidth - (rightPx + widthPx)) |
| / getUnitSize(l.targetLeftUnit, false); |
| } else { |
| // +right |
| l.setTargetRight = true; |
| l.sourceRight = (parentWidth - (leftPx + widthPx)) |
| / getUnitSize(l.targetRightUnit, false); |
| } |
| } else if (l.setRight && !l.setTargetRight) { |
| // -right |
| l.setRight = false; |
| |
| if (!l.setWidth) { |
| // +width |
| l.setTargetWidth = true; |
| l.sourceWidth = (parentWidth - (leftPx + rightPx)) |
| / getUnitSize(l.targetWidthUnit, false); |
| } else { |
| // +left |
| l.setTargetLeft = true; |
| l.sourceLeft = (parentWidth - (rightPx + widthPx)) |
| / getUnitSize(l.targetLeftUnit, false); |
| } |
| } |
| |
| l.setLeft = l.setTargetLeft; |
| l.setRight = l.setTargetRight; |
| l.setWidth = l.setTargetWidth; |
| |
| l.leftUnit = l.targetLeftUnit; |
| l.rightUnit = l.targetRightUnit; |
| l.widthUnit = l.targetWidthUnit; |
| } |
| |
| private void adjustVerticalConstraints(int parentHeight, Layer l) { |
| double topPx = l.top * getUnitSize(l.topUnit, true); |
| double bottomPx = l.bottom * getUnitSize(l.bottomUnit, true); |
| double heightPx = l.height * getUnitSize(l.heightUnit, true); |
| |
| if (l.setTop && !l.setTargetTop) { |
| // -top |
| l.setTop = false; |
| |
| if (!l.setHeight) { |
| // +height |
| l.setTargetHeight = true; |
| l.sourceHeight = (parentHeight - (topPx + bottomPx)) |
| / getUnitSize(l.targetHeightUnit, true); |
| } else { |
| // +bottom |
| l.setTargetBottom = true; |
| l.sourceBottom = (parentHeight - (topPx + heightPx)) |
| / getUnitSize(l.targetBottomUnit, true); |
| } |
| } else if (l.setHeight && !l.setTargetHeight) { |
| // -height |
| l.setHeight = false; |
| |
| if (!l.setTop) { |
| // +top |
| l.setTargetTop = true; |
| l.sourceTop = (parentHeight - (bottomPx + heightPx)) |
| / getUnitSize(l.targetTopUnit, true); |
| } else { |
| // +bottom |
| l.setTargetBottom = true; |
| l.sourceBottom = (parentHeight - (topPx + heightPx)) |
| / getUnitSize(l.targetBottomUnit, true); |
| } |
| } else if (l.setBottom && !l.setTargetBottom) { |
| // -bottom |
| l.setBottom = false; |
| |
| if (!l.setHeight) { |
| // +height |
| l.setTargetHeight = true; |
| l.sourceHeight = (parentHeight - (topPx + bottomPx)) |
| / getUnitSize(l.targetHeightUnit, true); |
| } else { |
| // +top |
| l.setTargetTop = true; |
| l.sourceTop = (parentHeight - (bottomPx + heightPx)) |
| / getUnitSize(l.targetTopUnit, true); |
| } |
| } |
| |
| l.setTop = l.setTargetTop; |
| l.setBottom = l.setTargetBottom; |
| l.setHeight = l.setTargetHeight; |
| |
| l.topUnit = l.targetTopUnit; |
| l.bottomUnit = l.targetBottomUnit; |
| l.heightUnit = l.targetHeightUnit; |
| } |
| } |