blob: 2bd46bfb4fa481692b9d58401dcdfe6a49a2e8b3 [file] [log] [blame]
/*
* 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 &lt;div&gt;), 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 &lt;div&gt;)
* 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 &lt;!DOCTYPE&gt;
* 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 {
/**
* Used to specify the alignment of child elements within a layer.
*/
public enum Alignment {
/**
* Positions an element at the beginning of a given axis.
*/
BEGIN,
/**
* Positions an element at the beginning of a given axis.
*/
END,
/**
* Stretches an element to fill the layer on a given axis.
*/
STRETCH;
}
/**
* 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;
Alignment hPos = Alignment.STRETCH, vPos = Alignment.STRETCH;
boolean visible = true;
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 CSS properties that are not directly modeled by the Layer class.
* </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 child element's horizontal position within the layer.
*
* @param position
*/
public void setChildHorizontalPosition(Alignment position) {
this.hPos = position;
}
/**
* Sets the child element's vertical position within the layer.
*
* @param position
*/
public void setChildVerticalPosition(Alignment position) {
this.vPos = position;
}
/**
* 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;
}
/**
* Sets the layer's visibility.
*
* @param visible
*/
public void setVisible(boolean visible) {
this.visible = visible;
}
}
private 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 &lt;body&gt;.
* </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() {
layout(0);
}
/**
* 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) {
// If there's no actual animation going on, don't do any of the expensive
// constraint calculations or anything like that.
if (duration == 0) {
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);
if (callback != null) {
callback.onAnimationComplete();
}
return;
}
// 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;
}
}