Spark Layouts with Flex 4 Beta
Just a quick post to link to my layouts article on the Adobe dev connection. It uses the same FlowLayout as an example, so no surprises in that department. Check it out, comments and feedback greatly appreciated.
Just a quick post to link to my layouts article on the Adobe dev connection. It uses the same FlowLayout as an example, so no surprises in that department. Check it out, comments and feedback greatly appreciated.
Hello Evtim, I generally don’t do this but I’m getting crazy with a Flash Builder 4 problem about layouts, I really don’t know why I am getting a ” BasicLayout doesn’t support virtualization.” error. I would really appreciate your help in this, the only thing I know is that the error is being fired from a dataGroup which is inside a skin from a List component.
Thanx for your work in the SDK…
Hi Andres,
The BasicLayout does not support virtualization. You should set a DataGroup’s layout to one that supports virtualization (like VerticalLayout, HorizontalLayout or TileLayout) or turn off virtualization by setting useVirtualLayout=”false”.
Of course, this could be a SDK bug. If you’re still seeing problems, please go ahead an log a bug here https://bugs.adobe.com/flex/, or report it on the forums here http://forums.adobe.com/community/labs/gumbo/
-Evtim
Hello Evtim, thanx for the tip about the virtual layout, I was testing with the scrollbar I put inside of the component, but I think I will take you tip in this…
Thanx
Hi Evtim, I’ve jumped into custom layouts, which has led me to the following situation and question. The custom layout has the ability to see and set some properties of the items in the dataprovider for the container I apply the custom layout to. What I’m struggling with is that I need to access data in the dataprovider (ArrayCollection), to help me in the layout. For example, I store x-position, and width, as a part of the data in the ArrayCollection and I also need to sort the ArrayCollection source at various times, while I am calculating the y-position and the height. How do I get access the the data in the dataprovider for the container? I’m using a DataGroup container.
Hi Jason,
I’d be curious to learn a little bit more about your use case. Usually the layouts I’ve seen cache layout specific data in local (to the layout) structure instead of in the dataProvider, but probably your situation is different. It’s easy to get access to the dataProvider and also to the data item that corresponds to a particular renderer from within the layout:
The layout target property is typed as GroupBase, so you’d need to cast it to DataGroup like this: “var dataProvider:IList = DataGroup(target).dataProvider”.
Alternatively to get the data item for a particular item renderer you can either go through the dataProvider or do something like this: “var item:Object = IItemRenderer(target.getVirtualElementAt(i)).data”. Note that when getVirutalElementAt() is called during layout it will force creation for an item renderer if the renderer didn’t exist already, so you should be careful with that. Here’s link to one very nice article about virtualization http://flexponential.com/2010/03/06/learn-how-to-create-a-simple-virtual-layout-in-flex-4/
@Evtim
Evtim, thank you for the response! I can’t get into details publically, but if you’d e-mail me directly, I’d be would really like to get into more detail. I have other questions as well, such as, which of the two options you listed above would be appropriate/better for drag & drop support in the DataGroup container? Virtual layouts are something I came across a couple days ago and, if I can figure out how to implement it correctly, would really be beneficial. Depending on the view state, there could be hundreds of elements that wouldn’t need to be rendered.
Hey Jason,
The whole point of the virtualization is to create only the renderers for the items that are displayed on the screen at a given point in time. The link that I posted in my previous comment points to a nice virtual layout article.
In a virtual layout case, every time you call getVirtualElementAt(i), the DataGroup will ensure there’s an item renderer at index “i”. So if you need to access data items that you aren’t necessarily going to display (items that fall outside of the scroll rect), you better get the data through the dataProvider.
If you’re using a List component with your layout, then there’s built-in drag & drop support in there that communicates with the layout through a standard set of APIs. If that’s your case, then you can simply override these protected methods in your layout class calculateDropIndex() – the layout returns index of insertion, calculateDropIndicatorBounds() – the layout calculates the size and position of the drop indicator, and optionally – calculateDragScrollDelta() – this is called by the List to determine if it should be drag-scrolling (the default implementation in the base class is functional though).
@Evtim
In my custom layout package, I sort and then loop through the dataprovider a couple times. What I’ve encountered is that the sort gets the element index out of sync with the dataprovider index, so while the size and positioning of the elements are correct, the actual display pieces in my item render, such as label text, are assocated with the incorrect element.
@Jason
I can do a nested loop to compare a uniqueID from the DataProvider to the itemRenderer.data.uniqueID, and everything is positioned and displays correctly. I’m not particularly fond of nested looping, but sometimes it’s necessary. Any better ideas?
Hi Jason,
I’m not sure why are you sorting the dataProvider, but doing that from withing layout’s updateDisplayList() doesn’t seem quite right. As soon as you change the dataProvider, the DataGroup will mark itemRenderers as invalid or not in use and if they are requested later, they will be re-assigned to different items. In addition the DataGroup will be invalidated and this will trigger a second layout pass. If in that second pass your layout keeps re-sorting the dataProvider, you may end up with an infinite loop…
Now I wonder why do you sort the dataProvider, is it because you want to render different elements on top? If so, then you should instead take advantage of the element’s “depth” property to control rendering order. If you’re sorting the dataProvider for some other reason, then you should do it before the layout’s updateDisplayList() is called. Think of the layout as the view for your data model – it should only reflect the current state of the data model without modifying it. Hope this helps!
@Evtim
I have to sort the dataProvider multiple times to assist with grouping items with certain traits. It’s hard to explain without showing it to you, but I’m not able to post it publically.
It’s not about bringing elements to the top.
Except for one oddity where FlashPlayer appears to stop updating the width of elements (it’s most likely my code), it seems to be working pretty much as I had hoped, however, your response above gives me concern as to my current approach.
@Evtim
Hopefully, this sample code will give you a better idea of what I’m trying to do. If I take the initial functions outside of updateDisplayList(), I can’t seem to access the target or the dataProvider, I get null errors. Example code is as follows:
package MyLayout
{
import mx.collections.ArrayCollection;
import mx.core.ILayoutElement;
import spark.components.DataGroup;
import spark.components.supportClasses.GroupBase;
import spark.components.supportClasses.ItemRenderer;
import spark.layouts.supportClasses.LayoutBase;
public class calLayout extends LayoutBase {
override public function updateDisplayList(containerWidth:Number, containerHeight:Number):void {
var x:Number = 0;
var y:Number = 0;
var layoutTarget:GroupBase = target;
var count:int = layoutTarget.numElements;
var dataProvider:ArrayCollection = ArrayCollection(DataGroup(target).dataProvider);
layoutOne();
function layoutOne():void {
dataProvider.source.sortOn([“y”, “Height”], [Array.NUMERIC, Array.NUMERIC | Array.DESCENDING]);
for (var i:int = 1; i < dataProvider.length; i++) {
//Sets group and maxGroup
}
layoutTwo();
}
function layoutTwo():void {
dataProvider.source.sortOn([“Group”, “y”, “Height”], [Array.NUMERIC, Array.NUMERIC, Array.NUMERIC | Array.DESCENDING]);
for (var ii:int = 1; ii < maxGroup+1; ii++) {
//Does something else, but is dependant on values calculated in previous
//function, and must be calculated for all items in Array before proceeding.
}
layoutThree();
}
function setColumnPosition1(): void {
dataProvider.source.sortOn([“Group”, “y”, “Height”], [Array.NUMERIC, Array.NUMERIC, Array.NUMERIC | Array.DESCENDING]);
for (var vi:int=0; vi < dataProvider.length; vi++) {
//Does something else, but is dependant on values calculated in previous
//function, and must be calculated for all items in Array before proceeding.
}
}
for (var viv:int = 0; viv < count; viv++) {
// get the current element, we’re going to work with the
// ILayoutElement interface
var element:ILayoutElement = layoutTarget.getElementAt(viv);
var item:ItemRenderer = target.getElementAt(viv) as ItemRenderer;
// Resize the element to its preferred size by passing
// NaN for the width and height constraints
element.setLayoutBoundsSize(NaN, NaN);
// Find out the element’s dimensions sizes.
// We do this after the element has been already resized
// to its preferred size.
var elementWidth:Number = element.getLayoutBoundsWidth();
var elementHeight:Number = element.getLayoutBoundsHeight();
var dataID:Number;
for (var vv:int = 0; vv < dataProvider.length; vv++) {
if (dataProvider[vv].apID == item.data.apID) {
y = dataProvider[vv].y;
x = SOME FORMULA
// Find maximum element extents. This is needed for
// the scrolling support.
maxWidth = dataProvider[vv].Width;
maxHeight = target.height;
break;
}
}
// Position the element
element.setLayoutBoundsPosition(x, y);
element.setLayoutBoundsSize(dataProvider[vv].Width, dataProvider[vv].Height);
}
// Scrolling support – update the content size
layoutTarget.setContentSize(maxWidth, maxHeight);
}
}
}
@Evtim
Evtim,
I’ve made significant progress with some of your pointers!! However, I’m still stuck on one area, and that is the sorting the dataProvider is throwing things off, if I change the underlying data in the dataProvider after the initial updateDisplayList.
I have an example FXP project that demonstrates the issue, but I don’t have a place to post it since I can’t upload to the Adobe Forums, so I’ll try and copy the relevant pieces below. If I comment out this line:
dataProvider = ArrayCollection(DataGroup(target).dataProvider);
The display elements update when I reSize an item, however, I need to use the sorting. The sorting is not relevant to this example application, but I need to use it.
customLayout.as
package myLayout
{
import mx.collections.ArrayCollection;
import mx.core.ILayoutElement;
import spark.components.DataGroup;
import spark.components.supportClasses.GroupBase;
import spark.components.supportClasses.ItemRenderer;
import spark.layouts.supportClasses.LayoutBase;
public class customLayout extends LayoutBase {
public var layoutTarget:GroupBase;
public var count:int;
public var dataProvider:ArrayCollection;
public var maxHeight:Number = 0;
public var maxWidth:Number;
public var x:Number;
public var y:Number;
public function setLayout():void {
layoutTarget = this.target;
dataProvider = ArrayCollection(DataGroup(target).dataProvider);
count = layoutTarget.numElements;
dataProvider.source.sortOn([“Height”], [Array.NUMERIC | Array.DESCENDING]);
if (dataProvider[0].Height > dataProvider[1].Height) {
dataProvider[0].xPos = 0;
dataProvider[1].xPos = 60;
}
else if (dataProvider[1].Height > dataProvider[0].Height) {
dataProvider[1].xPos = 0;
dataProvider[0].xPos = 60;
}
}
override public function updateDisplayList(containerWidth:Number, containerHeight:Number):void {
for (var ii:int = 0; ii < count; ii++) {
// get the current element, we’re going to work with the
// ILayoutElement interface
var element:ILayoutElement = layoutTarget.getElementAt(ii);
var item:ItemRenderer = target.getElementAt(ii) as ItemRenderer;
// Resize the element to its preferred size by passing
// NaN for the width and height constraints
element.setLayoutBoundsSize(NaN, NaN);
// Find out the element’s dimensions sizes.
// We do this after the element has been already resized
// to its preferred size.
var elementWidth:Number = element.getLayoutBoundsWidth();
var elementHeight:Number = element.getLayoutBoundsHeight();
item.uid = String(ii);
y = 0;
x = dataProvider[ii].xPos;
// Find maximum element extents. This is needed for
// the scrolling support.
maxWidth = 50;
maxHeight = dataProvider[ii].Height;
// Position the element
element.setLayoutBoundsPosition(x, y);
element.setLayoutBoundsSize(maxWidth, maxHeight);
}
// Scrolling support – update the content size
layoutTarget.setContentSize(maxWidth, maxHeight);
}
}
}
DataGroup.mxml
49) {
myAC[itemID].Height = myAC[itemID].Height + event.stageY – initY;
resizingElement.height = resizingElement.height + event.stageY – initY;
initY = event.stageY;
}
}
protected function mouseUpHandler(event:MouseEvent):void {
cLayout.setLayout();
this.invalidateDisplayList();
systemManager.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler, true);
systemManager.removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler, true);
}
]]>
Jason
@Jason
Slight correcting…I comment out the following line and the display updates correctly, however, I need the sort to assist in calculating width and x.
dataProvider.source.sortOn([“Height”], [Array.NUMERIC | Array.DESCENDING]);
Hi Jason,
How about creating a separate Array with references to the items from the dataProvider and sorting that internal array and operating on it? This way you still have the items sorted, but you don’t modify the dataProvider. If you have to modify the dataProvider, then I think your best bet is to find a way to do it outside of the layout’s updateDisplayList().
-Evtim
@Evtim
I’m not sure I want to carry the data twice in my app, as it can be a lot of data, so I tried something along those lines. What I did was add an item to the array (e.g. RowNum) and looped through the data provider, when the DataGroup is initialized, to set the original order of the array. Then, I sort the array back to the original order before the updateDisplayList or as the first step in updateDisplayList.
Something to consider for future releases is a tighter integration between rendered element and the items in the data provider, so if the data provider gets sorted, it doesn’t throw off the display. Sorting is pretty common, so I imagine others will struggle with this as well.
Evtim, I was wondering if you could help me understand a couple of behaviors that are tripping me up a bit.
1. updateDisplayList is called for a DataGroup even when the dataProvider is empty when the application first launches.
2. Normally, this isn’t such a big deal, however, depending on how you add data to the dataProvider, it can throw an error.
For example, the following works, however, it triggers an updateDisplayList for each item:
acOne.addAll(acOneTemp);
So, to be a bit more efficient, I wanted to pass the entire array to the DataGroups dataProvider, so it could loop through the dataProvider 1 time to perform the layout. So I tried this:
acOne.source = acOneTemp.source;
For some reason, this causes a run-time error and it doesn’t like this line:
element.setLayoutBoundsSize(NaN, NaN);
TypeError: Error #1009: Cannot access a property or method of a null object reference.
Hi Jason,
Yup, when the dataProvider is null or empty the layout still runs, but it should be easy to do an early return in that case.
Not quite sure what’s going on there… are you adding the items within your updateDisplayList or outside of the layout?
Regarding my previous suggestion – about having a separate array to sort so that you don’t modify the dataProvider within your layout – this is not going to double your data since every Array element is going to be simply pointing to an item in the dataProvider. Essentially you’ll temporarily create an Array of references to the original items, sort that Array and work on it, then throw it away.