scroll beyond maximum vertical position in flex datagrid - apache-flex

I have a situation where I need to show several 1-column DataGrids one beside the other and control them with 1 vertical scroll bar. (the result looks like 1 DataGrid but since I use a variable row height, each "column" can have a different row height on each row.
In order to achieve the required result, I created several grids and stacked them in an HBox (I know I should use spark but I want to retain my styles and my entire application is in Halo). Outside of the HBox, I have a VerticalScrollBar and I'm able to control the grid's scroll position since I extended the DataGrid and added a vertical scroll bar getter.
For simplicity, I've stripped the code to the bare essentials:
<mx:Canvas width="100%" height="100%" horizontalScrollPolicy="off" verticalScrollPolicy="off">
<mx:HBox id="viewPort" top="1" right="15" bottom="0" left="1" minWidth="0"
horizontalScrollPolicy="auto" verticalScrollPolicy="off">
</mx:HBox>
<mx:VScrollBar id="vScrollBar" top="24" right="0" bottom="0" scroll="onScroll(event)" />
</mx:Canvas>
private function populateGrids() : void
{
var item:Object;
var maxScrollPosition:Number = 0;
var dataGrid:CustomDataGrid;
var dataProvider:Object = {'1':[11,12,13,14,15,16], '2':[21,22,23,24,25,26,27,28], '3':[31]};
viewPort.removeAllChildren();
for (item in dataProvider) {
var col:DataGridColumn = new DataGridColumn();
col.headerText = "Grid" + item;
col.sortable = false;
dataGrid = new CustomDataGrid();
viewPort.addChild(dataGrid);
dataGrid.minWidth = 200;
dataGrid.percentHeight = 100;
dataGrid.percentWidth = 100;
dataGrid.headerHeight = 23;
dataGrid.selectable = false;
dataGrid.variableRowHeight = true;
dataGrid.columns = [col];
dataGrid.dataProvider = dataProvider[item];
}
viewPort.validateNow();
for (item in dataProvider) {
dataGrid = (viewPort.getChildAt(int(item)) as CustomDataGrid);
maxScrollPosition = Math.max(maxScrollPosition, dataGrid.vScrollBar.maxScrollPosition);
}
vScrollBar.maxScrollPosition = maxScrollPosition;
vScrollBar.scrollPosition = 0;
// need to shut-off the vertical scroller only after the maxScrollPosition is calculated
// otherwise, it doesn't calculate it ...
for (item in dataProvider) {
dataGrid = (viewPort.getChildAt(int(item)) as CustomDataGrid);
dataGrid.verticalScrollPolicy = "off";
}
}
private function onScroll(e:ScrollEvent) : void
{
var grids:Array = viewPort.getChildren();
for each (var grid:CustomDataGrid in grids) {
grid.verticalScrollPosition = e.currentTarget.scrollPosition;
}
}
While this does show the grids and scrolls them, the item renderers are not reused correctly (probably due to the verticalScrollPositon which might be larger than the actual maximum scroll position for each grid).
Am I going at it from a wrong position? Is there a better way to achieve what I want?
If not, is there a way to assign a higher-than-max verticalScrollPosition to a DataGrid and have it show a blank cell where no data exists?
I Hope this was coherent enough ...
BTW: I'm using Flex 4.5.1

Related

How to splice 2 mx canvas objects with repeat-x backgrounds?

I have 2 canvas objects that both contain a tiling image. My client recently requested that both the images be able to appear at the same time. I simply allowed them to be both enabled at the same time thinking they would tile appropriately but they do not.
After some careful thought I realized this is because they are of varying width so although my tiling image is 7 pixels in width, it may not always end at 7 pixels thus causing it to not appear to tile cleanly.
I can't just lop off the remainders because the imagery is being used to assess quantities of items (without going into too much detail) and having item 1 with 97 quantity next to item 2 with 200 quantity on one row compared to row 2 with item 1 having 100 quantity and item 2 having 300 quantity will show up strange to the end-user.
Does anyone know of how I could get started with maybe splicing the two canvas objects together or rather using 1 canvas object and then using BOTH background images and setting a percentWidth or something that the other one comes into affect?
You can use the copyPixels() method of the BitmapData class to achieve this sort of result. To do this, you need to load each image with a Loader. When the images are loaded, you have two Bitmap objects. From there, you access the corresponding BitmapData for each Bitmap and can splice them together.
After the splice, you create a new Bitmap object from the combined BitmapData. Then you feed the combined Bitmap to some other component. Unfortunately, a Canvas won't accept a Bitmap for its backgroundImage style. But you can add the combined Bitmap to a UIComponent or set it as the source of an Image component.
Finally, add either the UIComponent or Image to a Canvas. Since you can layer things on the Canvas, you effectively have a background image as long as place (and leave) the combined image at index 0 of the Canvas.
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
layout="vertical"
minWidth="955" minHeight="600"
creationComplete="onCreationComplete()">
<mx:Script>
<![CDATA[
import mx.containers.Canvas;
import mx.controls.Image;
import mx.core.UIComponent;
import mx.events.FlexEvent;
private var loader1:Loader;
private var loader2:Loader;
private var bitmap1:BitmapData;
private var bitmap2:BitmapData;
protected function onCreationComplete():void
{
loader1 = new Loader();
loader1.load(new URLRequest("Android_Robot_100.png"));
loader1.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoader1Complete, false,0,true);
loader2 = new Loader();
loader2.load(new URLRequest("rails.png"));
loader2.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoader2Complete, false,0,true);
}
protected function onLoader1Complete(event:Event):void
{
bitmap1 = (loader1.content as Bitmap).bitmapData;
if (bitmap1 && bitmap2)
copyPixels();
}
private function onLoader2Complete(event:Event):void
{
bitmap2 = (loader2.content as Bitmap).bitmapData;
if (bitmap1 && bitmap2)
copyPixels();
}
protected function copyPixels():void
{
// false = non-transparent background
var combined:BitmapData = new BitmapData(200,200, false);
var sourceRect1:Rectangle = new Rectangle(0,0,bitmap1.width, bitmap1.height);
var sourceRect2:Rectangle = new Rectangle(0,0,bitmap2.width, bitmap2.height);
combined.copyPixels(bitmap1, sourceRect1, new Point(0,0));
combined.copyPixels(bitmap2, sourceRect2, new Point(bitmap1.width, 0));
var result:Bitmap = new Bitmap(combined);
var image:Image = new Image();
image.source = result;
var canvas:Canvas = new Canvas();
canvas.addChildAt(image, 0);
addChild(canvas);
// as an alternative, just add the bitmap to a UIComponent
// note you can't do this and the above at the same time,
// becuase the 'result` Bitmap can only be the child of one
// object, adding it to one thing removes it from the other...
// var uic:UIComponent = new UIComponent();
// uic.addChild(result);
// addChild(uic);
}
]]>
</mx:Script>
</mx:Application>
Ultimately I was overcomplicating it. I simply did this with masking like...
<mx:Canvas width="100%" height="100%">
<mx:Image id="myFirstUnrelatedImage" width="50%" height="65%" maintainAspectRatio="false"
verticalCenter="0" left="3"
source="#Embed('../assets/image1.png')" visible="true"/>
<!-- not sure why borderStyle here is required but it won't show up without? -->
<mx:Canvas id="image1mask" width="30%" height="94%"
borderStyle="none" visible="false"/>
<mx:Canvas id="image1" width="100%" height="100%"
minWidth="0" styleName="myStyle"
mask="{image1mask}"/>
<!-- not sure why borderStyle here is required but it won't show up without? -->
<mx:Canvas id="image2mask" width="20%" height="94%"
right="0" borderStyle="none" visible="false"
includeInLayout="false"/>
<mx:Canvas id="image2" width="100%" height="100%"
minWidth="0" styleName="myStyle2"
mask="{image2mask}"/>
</mx:Canvas>
Where my styles were controlling the backgroundImage and backgroundRepeat properties in css.

Scroll inside an item renderer result in an empty item

I'm developing a mobile app. I have a view with an horizontal list, each item has a long description so I would need a vertical scroll for that independent information. I'm trying to add an scroll as a Child of an itemRenderer. And I dont get it. Anyone knows what I'm doing bad?
I have a class with inheritance of ItemRenderer (I tried BaseRenderer from AsFusion too, that it seems to have more performance for mobile apps, with the same result).
And here is the part of the scroll from my code:
override protected function createChildren():void
{
super.createChildren();
scroller = new VScrollBar();
scroller.percentHeight = 100;
scroller.setStyle('right', 0);
scrollGroup = new Group();
scrollGroup.percentHeight = 100;
scrollGroup.percentWidth = 100;
super.addElement(scroller);
super.addElement(scrollGroup);
scroller.viewport = scrollGroup;
}
I also tried
override protected function createChildren():void
{
super.createChildren();
scroller = new Scroller();
scroller.percentHeight = 100;
scroller.percentWidth = 100;
scrollGroup = new Group();
scrollGroup.percentHeight = 100;
scrollGroup.percentWidth = 100;
super.addElement(scroller);
super.addElement(scrollGroup);
scroller.viewport = scrollGroup;
}
And the result is the same. An empty item in the list. I can change page (pagesnapping of the list horizontal scroll) and the next item is also empty. If I delete addElement(scroller) I can see the items perfectly, but without the vertical scroll that I really need. So the problem is in the scroller. Any idea what I'm doing so bad?? Please? I need the solution in actionscript, I have more itemrenderers done and I will make inheritance, and the performance for the mobile is better in actionscript. Thank you in advance.
I've never used scroll bars in an item renderer... But you might check out the Scroller component? Something like this:
<s:ItemRenderer>
<s:Scroller width="100%" height="100%">
<s:Group>
<Your_components_here />
</s:Group>
</s:Scroller>
</s:ItemRenderer>
Not sure if it would behave any different though.
In order to work, scrolls need an actual width and height. It seems that the groups you're passing are actually empty, although they do have a set percentWidth. Add content into them.
If you're scrolling text, it might be more viable to use the built in scroll of the TextArea.
I solved, with the guidance of Grigorash Vasilij I noticed that the content of the scroller was not showing because in the group the content size was 0 and private visible variable was false. So the percent sizes of the scroller was not working, I updated it in the method updateDisplayList.
override protected function updateDisplayList( unscaledWidth:Number, unscaledHeight:Number ):void
{
super.updateDisplayList( unscaledWidth, unscaledHeight );
scroller.width = unscaledWidth;
scroller.height = unscaledHeight;
...
}
After that, the scroller was horizontal, then the horizontal scroll wasn't work, I wanted a verticalScroll if needed in the item Renderer, so after the constructor of Scroller I set the horizontalPolicy of the scroll equal to off. The result is the next:
override protected function createChildren():void
{
super.createChildren();
scroller = new Scroller();
scroller.percentHeight = 100;
scroller.percentWidth = 100;
scroller.setStyle("horizontalScrollPolicy", "off");
scrollGroup = new Group();
scrollGroup.percentHeight = 100;
scrollGroup.percentWidth = 100;
addChild(scrollGroup);
scroller.viewport = scrollGroup;
addChild(scroller);
}
My class is inheriting of BaseRenderer from Asfusion If you inherit of itemrenderer use addElement instead of addChild.

Spark TextArea or RichText autosize

I have done lots of searching on this subject, but it seems what I am finding is either out of date or just does not seem to work.
With TextFields in the past, you could set the TextField to a certain width, set wordWrap to true and you would end up with a textfield that changed height according to the text you added.
Now I am trying to do this with either the Spark TextArea or RichText.
I tried this HeightInLines = NAN, but that seems to be out of date.
I also tried this routine:
var totalHeight:uint = 10;
this.validateNow();
var noOfLines:int = this.mx_internal::getTextField().numLines;
for (var i:int = 0; i < noOfLines; i++)
{
var textLineHeight:int =
this.mx_internal::getTextField().getLineMetrics(i).height;
totalHeight += textLineHeight;
}
this.height = totalHeight;
But the mx_internal is not in the Spark components.
I am trying to do this with AS3, not MXML. If anyone has any suggestions or links that could help me figure this out using AS3, I'd really appreciate it.
Been struggling with this all afternoon. But it looks like the RichEditableText component will autosize if you set its width and leave its height undefined.
This works fine:
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600">
<s:TextArea updateComplete="event.currentTarget.heightInLines = NaN" />
</s:Application>
Found in comments here. You can do the same in ActionScript using the same updateComplete event.
This is how I set the height of a TextArea to fit its content when used inside an ItemRenderer (e.g. for a List component):
private function onUpdateComplete( e: Event ): void
{
// autoresize the text area
if ( theText ) {
var actualNumOfLines: int = theText.textFlow.flowComposer.numLines;
theText.heightInLines = actualNumOfLines;
invalidateSize();
}
}
ItemRenderer must have this property set:
<s:ItemRenderer ... updateComplete="onUpdateComplete(event)>
Maybe the updateComplete event is not the optimal trigger for auto-resize actions but works fine for me.
You can remove scrollers from TextArea's skin and it becomes autoresizable. You can download completed skin here: http://www.yumasoft.com/node/126
Here's a solution for spark text areas (it works as mx text areas do):
var ta_height:int;
for(var i:int=0; i < StyleableTextField(myTextArea.textDisplay).numLines; i++) {
ta_height += StyleableTextField(myTextArea.textDisplay).getLineMetrics(i).height;
}
myTextArea.height = ta_height;
This seems to work for me:
<s:TextArea id="testTextArea"
change="testTextArea_changeHandler(event)"
updateComplete="testTextArea_updateCompleteHandler(event)"/>
<fx:Script>
<![CDATA[
protected function testTextArea_changeHandler(event:TextOperationEvent):void
{
testTextArea.height = RichEditableText(testTextArea.textDisplay).contentHeight + 2;
}
protected function testTextArea_updateCompleteHandler(event:FlexEvent):void
{
testTextArea.height = RichEditableText(testTextArea.textDisplay).contentHeight + 2;
}
]]>
</fx:Script>
Been doing the same head banging over that s:TextArea, and then found out that this gets the job done :
<s:RichEditableText id="txtArea" left="0" right="0" backgroundColor="#F7F2F2"
change="textChanged()" />

How to make the Canvas clip its contents in Flex?

I draw a line on a Canvas object with the moveTo and lineTo graphics methods. If one end of the line lies outside the Canvas, the line spills out and is drawn over or under other elements in the application.
How do I make the Canvas keep the line contained within itself?
Thanks!
<mx:Canvas id="canvas" top="0" right="51" left="0" bottom="32">
<mx:Canvas x="-1" y="0" width="0" height="0"> <!-- This is a HACK to make the canvas clip -->
</mx:Canvas>
</mx:Canvas>
I had a similar problem some time ago. You need to embed another container inside the canvas, and draw the primitive graphics in that instead. I believe this is because the Canvas component only clips child components, and not primitive graphics.
Example here: http://www.adobe.com/cfusion/webforums/forum/messageview.cfm?forumid=60&catid=585&threadid=1421196. It includes some sample code about half way down the page.
The link in the recommended answer is broken. I solved the problem by placing another canvas inside my canvas that is larger than the outer canvas.
Example:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" applicationComplete="onComplete()">
<mx:Script><![CDATA[
private function onComplete():void
{
canvas.graphics.lineStyle(1);
canvas.graphics.moveTo( -100, -100);
canvas.graphics.lineTo(400, 400);
}
]]></mx:Script>
<mx:Canvas id="window"
height="300"
width="300"
clipContent="true"
horizontalScrollPolicy="off"
verticalScrollPolicy="off"
backgroundColor="white">
<mx:Canvas id="canvas" width="301" height="301">
</mx:Canvas>
</mx:Canvas>
</mx:Application>
If the window Canvas is going to be resized at runtime, add a resize event listener to resize the canvas Canvas also.
I have just developed a Flex Box component, which acts as a regular component container, but draws a rounded rectangle background, with another rounded rectangle indicated a fill-level. For that I needed to clip the upper section that should not get filled. Drawing the fill rectangle to the fill height was no option since the rounded corners would not match.
What I learned:
I created a Canvas component just for drawing the fill-level with bounds 0/0 and width/height of the Box
I added that canvas to the Box at index 0 via addChildAt()
I set the includeInLayout property to false for that canvas since it should not take part in the layouting of the Box itself but rather act as some floating drawing pane on top
I then added another Canvas as the mask to that fill-canvas (addChild(), and set mask property)
Here is some code:
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
// super
super.updateDisplayList(unscaledWidth, unscaledHeight);
// prep
var g:Graphics = this.graphics;
var fgColor:int = this.getStyle("fillColor");
var bgColor:int = this.getStyle("backgroundFillColor");
var radius:int = this.getStyle("cornerRadius");
// clear
g.clear();
// draw background
g.beginFill(bgColor, 1);
g.drawRoundRect(0, 0, unscaledWidth, unscaledHeight, radius, radius);
g.endFill();
// draw fill level
if (this._fillLevel > 0) {
var fillHeight:int = int(unscaledHeight * this._fillLevel);
// extra component for drawing fill-level, so we can apply mask just to this
if (this._fillLevelCanvas == null) {
this._fillLevelCanvas = new Canvas();
this._fillLevelCanvas.x = 0;
this._fillLevelCanvas.y = 0;
this._fillLevelCanvas.includeInLayout = false;
this.addChildAt(this._fillLevelCanvas, 0);
}
this._fillLevelCanvas.width = unscaledWidth;
this._fillLevelCanvas.height = unscaledHeight;
// mask
if (this._fillLevelMask == null) {
this._fillLevelMask = new Canvas();
this._fillLevelMask.x = 0;
this._fillLevelMask.y = 0;
this._fillLevelCanvas.addChild(this._fillLevelMask);
this._fillLevelCanvas.mask = this._fillLevelMask;
}
this._fillLevelMask.width = this.width;
this._fillLevelMask.height = this.height;
this._fillLevelMask.graphics.beginFill(0xFFFFFF);
this._fillLevelMask.graphics.drawRect(0, this.height-fillHeight, this._fillLevelMask.width, this._fillLevelMask.height);
this._fillLevelMask.graphics.endFill();
this._fillLevelCanvas.graphics.beginFill(fgColor, 1);
this._fillLevelCanvas.graphics.drawRoundRect(0, 0, unscaledWidth, unscaledHeight, radius, radius);
this._fillLevelCanvas.graphics.endFill();
}
}
Looks like this might be useful:
http://forums.adobe.com/message/199071#199071
Set ClipToBounds property of the Canvas to true:
<Canvas ClipToBounds="True">... Content ...</Canvas>

Fit Flex datagrid to data

I have rows of text data that can vary between 0 and 100, and all need to be visible on the screen at one time. The default behavior is fine for the grid until the rows * rowHeight > gridHeight.
Basically I need a hook into the item height, or row height to calculate it based on the height of the grid. I've set paddingTop and paddingBottom to zero, but there is still a considerable amount of white space in between rows.
My datagrid component...
<mx:DataGrid xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="OnCreationComplete()"
paddingTop="0"
paddingBottom="0"
>
<mx:Script>
<![CDATA[
private function OnCreationComplete():void
{
//setRowHeight(10);
}
override protected function setRowHeight(v:Number):void
{
super.setRowHeight(v);
}
]]>
</mx:Script>
</mx:DataGrid>
setRowHeight() helps, but the itemRender for the cell bigger than the cell, if I set the row height to something like 10.
You might want to look at the DataGrid.variableRowHeight property as, when this is set to false (the default) all rows will be the same height as the largest itemRenderer. You could also look into writing your own itemRenderer for each DataColumn.
If all you really want to do is set the row height based on the number of items in the dataProvider though, you could just set the DataGrid.rowHeight property like this (assuming your grid has a fixed height, say 100%) :
myDataGrid.dataProvider = myArray;
myGrid.rowHeight = Math.floor((myGrid.height - myGrid.headerHeight)/myArray.length);
(I'm taking the floor here because if you end up with a fractional value that rounds up, you'll need a scroll bar)
The only problem with this approach, as I think you've noticed, is that the itemRenderer might not display properly in a row that's too small. I guess you could solve this by changing the font within the renderer based on its height.
Thank you inferis, that helped me a lot. This is my final grid component. It's not really self contained because of a few call-outs, but if it helps someone else get theirs to work, great!
<?xml version="1.0" encoding="utf-8"?>
<mx:DataGrid xmlns:mx="http://www.adobe.com/2006/mxml"
paddingTop="-3"
paddingBottom="-3"
resize="OnResize(event)"
>
<mx:Script>
<![CDATA[
import mx.containers.Panel;
import mx.core.Application;
import mx.events.ResizeEvent;
protected function OnResize(event:ResizeEvent):void
{
this.invalidateDisplayList();
}
/**
* #private
* Sizes and positions the column headers, columns, and items based on the
* size of the DataGrid.
*/
override protected function updateDisplayList(unscaledWidth:Number,
unscaledHeight:Number):void
{
if( this.collection.length > 0 ) // so don't divide by zero
{
var appHeight:Number = Application.application.height;
var appMinusMenuHeight:Number = appHeight - Application.application.hboxPrintBar.height;
var boxHeight:Number = Application.application.vboxViewAll.height;
if( boxHeight > 0 )
{
var gridHeight:Number = (appMinusMenuHeight - this.headerHeight) * 0.93;
var calcHeight:Number = gridHeight / this.collection.length;
var calcHeightFloor:Number = Math.floor( calcHeight );
setRowHeight( calcHeightFloor );
//var p:Panel = this.parent as Panel;
//p.title = calcHeightFloor.toString();
if( calcHeightFloor <= 10 )
this.setStyle("fontSize",calcHeightFloor);
}
}
super.updateDisplayList(unscaledWidth,unscaledHeight);
}
]]>
</mx:Script>
</mx:DataGrid>

Resources