How to make the Canvas clip its contents in Flex? - apache-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>

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.

how to create up/down movement of sprite with as3 with a mouse

I need to move a sprite only vertically on mouse move. How do I implement it with as3?
Thanks
Flash version
var s:Sprite = new Sprite();
s.x = 20;
s.graphics.beginFill(0xFF0000);
s.graphics.drawRect(0,0,20,20);
addChild(s);
stage.addEventListener(MouseEvent.MOUSE_MOVE,moveSprite);
function moveSprite(e:MouseEvent):void
{
s.y = e.localY;
}
Flex version
<mx:Canvas width="100" height="100">
<mx:mouseMove>
<![CDATA[
s.y = event.localY;
]]>
</mx:mouseMove>
<mx:Canvas id="s" backgroundColor="#ff0000" width="20" height="20"/>
</mx:Canvas>
Each of these you can paste in and will do what you said. it will create a 20x20 red box that is vertically the same as the mouse but fixed horizontally. The flex version your mouse has to be within the containing Canvas.
addEventListener(MouseEvent.CLICK, clickHandler);
function clickHandler(e:MouseEvent):void{
mySprite.y += amount;
}
Ok, dragging is a little more complicated. You need to define a rectangle for the bounds of the dragging. If you want to just drag along one axis then you make the rectangle have a width of 0. In this example I've restricted the amount of scrolling and and down to different numbers that you can change below.
import flash.events.MouseEvent;
import flash.geom.Rectangle;
mySprite.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
function mouseDownHandler(event:MouseEvent):void{
stage.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
var scrollUpAmount:int = 10;
var scrollDownAmount:int = 200;
var boundsRect:Rectangle = new Rectangle(mySprite.x,mySprite.y-scrollUpAmount,0,mySprite.y+scrollDownAmount);
mySprite.startDrag(false, boundsRect);
}
function mouseUpHandler(event:MouseEvent):void{
stage.removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
mySprite.stopDrag();
}

Absolute positioning in Flex?

I need to programmatically add a set of controls with some amount of pixels between them. I can't seem to find how to do this in the Flex docs. How can I do it?
Most Containers have some logic to place the items for you, e.g. vertically or horizontally. I.e. if you want to place them horizontally with 5 pixels of space you would use a HBox (VBox for vertical layout):
<mx:HBox horizontalGap="5">
<Component1/>
<Component2/>
<etc.../>
</mx:HBox>
Or script:
...
var box: HBox = new HBox();
box.horizontalGap = 5;
box.addChild(new Component1());
box.addChild(new Component2());
addChild(box);
But if you want to place them yourself using x,y coordinates (i.e. absolute positioning) you can use Canvas:
<mx:Canvas>
<Component1 x="100" y="100"/>
<Component2 x="100" y="200"/>
<etc.../>
</mx:Canvas>
script version:
var canvas: Canvas = new Canvas();
var component1: Component1 = new Component1();
component1.x = 100;
component1.y = 100;
canvas.addChild(component1);
var component2: Component2 = new Component2();
component2.x = 100;
component2.y = 100;
canvas.addChild(component2);
Inside a container with absolute positioning, e.g. Canvas, you can position elements with x and y (or right, left, top, bottom)
elem.x = 100;
elem.y = 200;
canvas.addChild(elem);
You can also use Spacer to add some space in between components.
<mx:HBox>
<Component1 />
<mx:Spacer width="10" />
<Component2 />
</mx:HBox>
If your window can be re-sized, absolute layout is not recommended - It may be a good idea to use width="100%" and height=100% and then use the minHeight/minWidth/maxWidth etc.
In your case, you could set the minimum width/height of the spacer (between 2 components) so that the page scales proportionately.

How to create HBox with only the top corners rounded in Flex?

I'm trying to create an HBox in flex that has only the top corners rounded. HBox (and all the other flex containers) have a cornerRadius style, but it applies to all corners. I'm not seeing a way to specify the style of individual corners.
Is there any built-in way to do this, or will I need to write my own custom drawing code to draw the background with rounded corners?
It depends what border skin you're using. If you're using the built in skins (HaloBorder), there's a style that you can specify "roundedBottomCorners" (which is a boolean), which allows you to control whether the bottom corners are rounded or not. If you're using your own border skin, you can add in whatever styles you want to control the corners.
Alas, the only solution I know is to draw the background yourself, using Programmatic Skinning -- specifically, orderriding RectangularBorder::
package
{
import mx.skins.RectangularBorder;
public class HBoxSkin extends RectangularBorder
{
public function HBoxSkin()
{
super();
}
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
super.updateDisplayList(unscaledWidth, unscaledHeight);
var cr:Number = getStyle("cornerRadius");
var bc:Number = getStyle("backgroundColor");
graphics.clear();
graphics.beginFill(bc, 1);
// Draw the rectangle manually, rounding only the top two corners
graphics.drawRoundRectComplex(0, 0, unscaledWidth, unscaledHeight, cr, cr, 0, 0);
graphics.endFill();
}
}
}
... and then applying the change using the borderSkin property of your HBox:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
<!-- Apply the skin using the borderSkin property of your HBox -->
<mx:HBox borderSkin="HBoxSkin" backgroundColor="#FFFFFF" cornerRadius="10" height="100" width="100" top="10" left="10" />
</mx:Application>
The Flex Documentation gives an example of how to implement programmatic skinning. It's beyond beginner level, unfortunately (this stuff gets a lot easier in Flex 4), but if you follow the code in my example and in the documentation's, you should be able to get something together. Good luck!

Flex collision testing with hitTestObject

I'm trying to test clipping on two canvases. Both canvases are 100px wide. They're 20px apart. I've placed a label inside one and made it 200px wide. Scroll bars will show up on the canvas. When I don't have the label inside and use hitTestObject it returns false. When I place the label inside it returns true. Is there any way to alter the canvas with the label inside so that it doesn't expand to the width of the label?
<?xml version="1.0" encoding="utf-8"?>
private function init() : void {
var hitBox:Sprite = new Sprite;
hitBox.graphics.drawRect(box1.x, box1.y, 100, 100);
box1.hitArea = hitBox;
box1.mouseEnabled = false;
trace('box hit area: ' + box1.getBounds(box1));
trace('hitbox: ' + hitBox);
trace('box hit test: ' + box1.hitTestObject(box2));
}
]]>
</mx:Script>
<mx:Canvas id="box1" x="10" y="10" width="100" height="100" backgroundColor="#FFFFFF">
<mx:Label text="This is a test" width="200" />
</mx:Canvas>
<mx:Canvas id="box2" x="120" y="10" width="100" height="100" backgroundColor="#FFFFFF" />
Unfortunately, it doesn't look like you can accomplish what you want with hitTestObject. I played around with setting the clipContent and horizontalScrollPolicy properties on your canvases, to no use. What I think is happening is that hitTestObject considers your canvas to be as wide as the longest child component, regardless of any clip masks or scroll bars.
Are you forced to use hitTestObject? If not, I'd suggest writing your own collision detection function along the lines of:
public static function componentsCollide( obj1:DisplayObject, obj2:DisplayObject ):Boolean {
var bounds1:Rectangle = new Rectangle( obj1.x, obj1.y, obj1.width, obj1.height );
var bounds2:Rectangle = new Rectangle( obj2.x, obj2.y, obj2.width, obj2.height );
return bounds1.intersects( bounds2 );
}

Resources