PDA

View Full Version : Position Anchor Layout (anchor your component's position and size to their container)



tiago.silva
9 Dec 2008, 2:31 PM
Synopsis: Layout that allows one to anchor the component's position and size to the parent container, plus graphical aid to use this layout with.

Hi,

I have never been very pleased with the layout managers that have gone mainstream in many technologies. It is very common for people to rely on table and other grid based layouts to position their gui components, and they all work very well for a certain subset of design challenges. They do however, in my humble opinion, fail miserably when someone wants to easily map out a designer's vision into code.

When one wants to do that, an absolute layout is normally in order, since it allows the developer to freely position his components. All is well with this layout, but it does not handle the challenge of keeping the layout coherent with the window's size. ExtJS's AbsoluteLayout is an extension of AnchorLayout, and does therefore provide size anchors, meaning that we can position a component anywhere in the interface by providing its coordinates, and then telling it to anchor its size to the right and/or bottom edges of its container. If the container resizes, the component will resize to acommodate itself to the new container size. But what if I just wanted to anchor the position of the component and not of its size (ex: the component must always be 30px away from the container's right edge and 20px away from the container's bottom edge)? Apparently there is no easy way to do this with the current SDK. Given this restriction I wanted to discuss with the community about the viability of implementing a constraint layout.

A constraint layout is one where you can anchor any of the component's dimension properties to those of another component. For example, with this kind of layout I could in theory tell that component A must always be 30px away from component B's right edge, 20px above component C's top edge, and scale its width to 100% of the available space without overlapping any conflicting component (some priority system would be necessary here). This would be for me, the ultimate layout, and if it was fast enough, I wouldn't have a need for any other. Has anyone ever tackled this challenge or put some thought into it? Currently I don't have enough time to design and implement a solution (though I have put some thought and code into it and it's certainly not the easiest thing in the world :-?).

Meanwhile, even though I have not implemented my ideal layout, for the time being I made a layout class and design methodology that solves most of my problems. So I put it here, in hope it is of help to someone else and to hopefully spark some healthy discussion on how to make it as easy as possible to design and maintain nice ExtJs interfaces with minimal effort.

Here is the code:



Ext.override(Ext.layout.AbsoluteLayout, {
onLayout : function(ct, target) {
if (this.isForm) {
ct.body.position();
} else {
var items = ct.items;
items.each(function(item, index) {

if (item.rendered) {
if (item.anchors != null) {
var containerSize = ct.getSize();
var containerWidth = containerSize["width"];
var containerHeight = containerSize["height"];

var itemSize = item.getSize();
var currentWidth = item.width
? item.width
: itemSize["width"];
var currentHeight = item.height
? item.height
: itemSize["height"];

var itemPosition = item.getPosition();
var currentX = item.x ? item.x : itemPosition[0];
var currentY = item.y ? item.y : itemPosition[1];

var x = item.anchors["x"];
var y = item.anchors["y"];
var width = item.anchors["width"];
var height = item.anchors["height"];

// if an x anchor was defined then apply it
var targetX = currentX;
if (x != undefined && typeof(x) == "number") {
// if the anchor is a float then the X coordinate
// must be calculated by dividing the container's width
if (("" + x).indexOf(".") > -1) {

// if the anchor is negative, then the component must be
// positioned by its right edge
if (x < 0) {
targetX = containerWidth * x - currentWidth;
} else { // otherwise position it by its left edge
targetX = containerWidth * x;
}
} else { // otherwise the anchor is a padding relative to the container's
// left or right edges

// if the anchor is negative, then the component must be
// positioned by its right edge
if (x < 0) {
targetX = containerWidth + x - currentWidth;
} else { // otherwise position it by its left edge
targetX = x;
}
}
}

// if an y anchor was defined then apply it
var targetY = currentY;
if (y != undefined && typeof(y) == "number") {

// if the anchor is a float then the Y coordinate
// must be calculated by dividing the container's height
if (("" + y).indexOf(".") > -1) {

// if the anchor is negative, then the component must be
// positioned by its bottom edge
if (y < 0) {
targetX = containerHeight * y
- currentHeight;
} else { // otherwise position it by its top edge
targetY = containerHeight * y;
}
} else { // otherwise the anchor is a padding relative to the container's
// top or bottom edges

// if the anchor is negative, then the component must be
// positioned by its bottom edge
if (y < 0) {
targetY = containerHeight + y
- currentHeight;
} else { // otherwise position it by its top edge
targetY = y;
}
}
}

// if a width anchor was defined then apply it
var targetWidth = currentWidth;
if (width != undefined && typeof(width) == "number"
&& width > 0 && width <= 1) {
var remainingWidth = containerWidth - targetX;
targetWidth = remainingWidth * width;
}

// if an height anchor was defined then apply it
var targetHeight = currentHeight;
if (height != undefined && typeof(height) == "number"
&& height > 0 && height <= 1) {
var remainingHeight = containerHeight - targetY;
targetHeight = remainingHeight * height;
}

// move the component to the new position
item.setPosition(targetX != currentX
? targetX
: undefined, targetY != currentY
? targetY
: undefined);

// resize the component to its new size
item.setSize(targetWidth != currentWidth
? targetWidth
: undefined,
targetHeight != currentHeight
? targetHeight
: undefined);
}
}
}, this);

target.position();
}

Ext.layout.AbsoluteLayout.superclass.onLayout.call(this, ct, target);
}
});I am overriding the AbsoluteLayout class because it's useful in my particular case, but
feel free to simply extend it. The usage is quite simple, you just need to provide your component with an "anchors" property to tell it how to anchor itself to its container.

Examples:




/* this will make a constant padding of 50px between the component's and the container's
right edges, keep the component always 10px below the container's top edge, and give it 50% of the remaining width and 100% of the remaining height after it has been positioned */

var panel = new Ext.Panel({anchors:{x: -50, y : 10, width : 0.5, height : 1}});

/* this will make a constant padding of 50% of the container's width between the component's and the container's left edges, and 50% of the container's height between the component's top edge, and the container's bottom edge */

var panel = new Ext.Panel({anchors:{x: 0.5, y : 0.5}});

/* this will make a constant padding of 50% of the container's width between the component's and the container's right edges */

var panel = new Ext.Panel({anchors:{x: -0.5}});

These examples should be enough to understand how the layout works. With this layout I now have great freedom in terms of positioning my components, and don't lose points when I resize the window. The only problem is that its still a lot of guesswork to figure out which coordinates and anchors to provide. It would be really helpful if I could just put all the components in the container, visually drag them to figure out their coordinates and then put them in the code. To do this, I leveraged some of the work done in a great extjs gui builder that's available online at http://www.tof2k.com/ext/formbuilder/ (thanks Tof (http://extjs.com/forum/member.php?u=1009) :D), and overrided the Ext.Component class to be able to drag components when they are inside an AbsoluteLayout container. To get this feature you can use this code:



var enableExtJsGuiBuilder = function() {
extJsGuiBuilderResizeLayer = new Ext.Layer({
style : "background: red;"
});

extJsGuiBuilderResizeLayer.setOpacity(0.5);
extJsGuiBuilderResizeLayer.resizer = new Ext.Resizable(
extJsGuiBuilderResizeLayer, {
handles : "all",
draggable : true,
dynamic : true,
animate : true
});

extJsGuiBuilderResizeLayer.resizer.dd.lock();
extJsGuiBuilderResizeLayer.resizer.on("resize", function(r, width, height) {
var component = extJsGuiBuilderSelectedComponent;

var componentWidth = component.getSize()["width"];
var widthDifference = width - componentWidth;
if (widthDifference > 0) {
widthDifference = "+" + widthDifference;
}

var componentHeight = component.getSize()["height"];
var heightDifference = height - componentHeight;
if (heightDifference > 0) {
heightDifference = "+" + heightDifference;
}

component.setSize(width, height);
console.info("Component resized to w = " + width + " ("
+ widthDifference + "), h = " + height + " ("
+ heightDifference + ")");
this.extJsGuiBuilderResizeLayer.hide();
}, this);

extJsGuiBuilderResizeLayer.resizer.dd.endDrag = function(e) {
var resizeLayerPosition = extJsGuiBuilderResizeLayer.getXY();
var resizeLayerX = resizeLayerPosition[0];
var resizeLayerY = resizeLayerPosition[1];

var containerPosition = extJsGuiBuilderSelectedComponent.ownerCt.el.getXY();
var containerPositionX = containerPosition[0];
var containerPositionY = containerPosition[1];
var x = resizeLayerX - containerPositionX;
var y = resizeLayerY - containerPositionY;

var componentX = extJsGuiBuilderSelectedComponent.getPosition()[0]
- containerPositionX;
var xDifference = x - componentX;
if (xDifference > 0) {
xDifference = "+" + xDifference;
}

var componentY = extJsGuiBuilderSelectedComponent.getPosition()[1]
- containerPositionY;
var yDifference = y - componentY;
if (yDifference > 0) {
yDifference = "+" + yDifference;
}

extJsGuiBuilderSelectedComponent.setPosition(x, y);
console.info("Component moved to x = " + x + " (" + xDifference
+ "), y = " + y + " (" + yDifference + ")");
extJsGuiBuilderResizeLayer.hide();
}.createDelegate(this);

Ext.override(Ext.Component, {
render : function(container, position) {
if (!this.rendered
&& this.fireEvent("beforerender", this) !== false) {
if (!container && this.el) {
this.el = Ext.get(this.el);
container = this.el.dom.parentNode;
this.allowDomMove = false;
}
this.container = Ext.get(container);
if (this.ctCls) {
this.container.addClass(this.ctCls);
}
this.rendered = true;
if (position !== undefined) {
if (typeof position == 'number') {
position = this.container.dom.childNodes[position];
} else {
position = Ext.getDom(position);
}
}
this.onRender(this.container, position || null);
if (this.autoShow) {
this.el.removeClass(['x-hidden', 'x-hide-' + this.hideMode]);
}
if (this.cls) {
this.el.addClass(this.cls);
delete this.cls;
}
if (this.style) {
this.el.applyStyles(this.style);
delete this.style;
}
this.fireEvent("render", this);
this.afterRender(this.container);
if (this.hidden) {
this.hide();
}
if (this.disabled) {
this.disable();
}

this.initStateEvents();
}

var componentElement = this.getEl();
var clickRepeater = new Ext.util.ClickRepeater(componentElement, {
listeners : {
"mousedown" : {
fn : function(componentElement) {
var componentElementId = componentElement.el.id;
var component = Ext.getCmp(componentElementId);
if (component != null
&& component.ownerCt != null
&& component.ownerCt.layout.extraCls == "x-abs-layout-item") {
extJsGuiBuilderSelectedComponent = component;
extJsGuiBuilderResizeLayer.resizer.dd.unlock();
extJsGuiBuilderResizeLayer.resizer.dd.constrainTo(extJsGuiBuilderSelectedComponent.ownerCt.body);
extJsGuiBuilderResizeLayer.setBox(componentElement.el.getBox());
extJsGuiBuilderResizeLayer.show();
}
},
scope : this
}
}
});

return this;
}
});
}
Just call the function once, and from then on you will be able to drag components in your interface, and check the firebug console to know the new coordinates, size, and offsets from the new to the last dimensions.

Thanks,
Tiago Silva

P.S: I am definitely not an expert with ExtJs layouts so I'm quite sure this code is not stable, efficient, or even if there isn't already a better solution to this problem in the ExtJs SDK that I am not aware of, so please feel free to critic, offer suggestions, fix and improve the code, as long as you keep it public and available :).

ansmith
12 Jan 2009, 7:29 PM
This is very, very useful for me. Thank you. It's almost exactly what I needed. Couple of things though...

1: I think you have a bug where setting a negative y value. You're setting targetX instead of targetY. I'm guessing you've noticed that already.

2: I'm surprised you base the width and height on remaining width and height. This doesn't make sense for me and really I'm just trying to figure out when it would and what your rationale was. If I want to position three panels next to one another each taking up a third of the screen, the values I have to set for width and height are pretty counterintuitive.