| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // Licensed to the Apache Software Foundation (ASF) under one or more |
| // contributor license agreements. See the NOTICE file distributed with |
| // this work for additional information regarding copyright ownership. |
| // The ASF licenses this file to You 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 spark.components |
| { |
| |
| import flash.display.CapsStyle; |
| import flash.display.Graphics; |
| import flash.display.LineScaleMode; |
| import flash.display.Shape; |
| import flash.display.Sprite; |
| import flash.events.Event; |
| import flash.events.TimerEvent; |
| import flash.geom.Matrix; |
| import flash.geom.Matrix3D; |
| import flash.geom.Point; |
| import flash.geom.Transform; |
| import flash.geom.Vector3D; |
| import flash.utils.Timer; |
| |
| import mx.core.DesignLayer; |
| import mx.core.DPIClassification; |
| import mx.core.FlexGlobals; |
| import mx.core.IUIComponent; |
| import mx.core.IVisualElement; |
| import mx.core.UIComponent; |
| import mx.core.mx_internal; |
| import mx.events.FlexEvent; |
| import mx.events.PropertyChangeEvent; |
| import mx.geom.TransformOffsets; |
| |
| use namespace mx_internal; |
| |
| //-------------------------------------- |
| // Styles |
| //-------------------------------------- |
| |
| /** |
| * The interval to delay, in milliseconds, between rotations of this |
| * component. Controls the speed at which this component spins. |
| * |
| * @default 50 |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 2.5 |
| * @productversion Flex 4.5 |
| * |
| */ |
| [Style(name="rotationInterval", type="Number", format="Time", inherit="no")] |
| |
| /** |
| * Color of the spokes of the spinner. |
| * |
| * @default 0x000000 |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 2.5 |
| * @productversion Flex 4.5 |
| */ |
| [Style(name="symbolColor", type="uint", format="Color", inherit="yes", theme="spark,mobile")] |
| |
| //-------------------------------------- |
| // Other metadata |
| //-------------------------------------- |
| |
| [IconFile("BusyIndicator.png")] |
| |
| /** |
| * The BusyIndicator defines a component to display when a long-running |
| * operation is in progress. This component creates a spinner with twelve spokes. |
| * The color of the spokes is controlled by the value of the <code>symbolColor</code> style. |
| * The transparency of this component can be modified using the <code>alpha</code> property, |
| * but the alpha value of each spoke cannot be modified. |
| * |
| * <p>The following image shows the BusyIndicator at the bottom of the screen next |
| * to the Submit button:</p> |
| * |
| * <p> |
| * <img src="../../images/bi_busy_indicator_bi.png" alt="Busy indicator" /> |
| * </p> |
| * |
| * <p>The speed at which this component spins is controlled by the <code>rotationInterval</code> |
| * style. The <code>rotationInterval</code> style sets the delay, in milliseconds, between |
| * rotations. Decrease the <code>rotationInterval</code> value to increase the speed of the spin.</p> |
| * |
| * <p>The BusyIndicator has the following default characteristics:</p> |
| * <table class="innertable"> |
| * <tr><th>Characteristic</th><th>Description</th></tr> |
| * <tr><td>Default size</td><td>160 DPI: 26x26 pixels<br> |
| * 240 DPI: 40x40 pixels<br> |
| * 320 DPI: 52x52 pixels<br> |
| * 380 DPI: 80x80 pixels<br></td></tr> |
| * <tr><td>Minimum size</td><td>20x20 pixels</td></tr> |
| * <tr><td>Maximum size</td><td>No limit</td></tr> |
| * </table> |
| * |
| * <p>The diameter of the BusyIndicator's spinner is the minimum of the width and |
| * height of the component. The diameter must be an even number, and is |
| * reduced by one if it is set to an odd number.</p> |
| * |
| * @mxml |
| * |
| * <p>The <code><s:BusyIndicator></code> tag inherits all of the tag |
| * attributes of its superclass and adds the following tag attributes:</p> |
| * |
| * <pre> |
| * <s:BusyIndicator |
| * <strong>Common Styles</strong> |
| * rotationInterval=50 |
| * |
| * <strong>Spark Styles</strong> |
| * symbolColor="0x000000" |
| * |
| * <strong>Mobile Styles</strong> |
| * symbolColor="0x000000" |
| * > |
| * </pre> |
| * |
| * @includeExample examples/BusyIndicatorExample.mxml -noswf |
| * @includeExample examples/views/BusyIndicatorExampleHomeView.mxml -noswf |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 2.5 |
| * @productversion Flex 4.5 |
| */ |
| public class BusyIndicator extends UIComponent |
| { |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Class constants |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * @private |
| */ |
| static private const DEFAULT_ROTATION_INTERVAL:Number = 50; |
| |
| /** |
| * @private |
| */ |
| static private const DEFAULT_MINIMUM_SIZE:Number = 20; |
| |
| /** |
| * @private |
| */ |
| static private const RADIANS_PER_DEGREE:Number = Math.PI / 180; |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Constructor |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * Constructor. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 2.5 |
| * @productversion Flex 4.5 |
| */ |
| public function BusyIndicator() |
| { |
| super(); |
| |
| alpha = 0.60; // default alpha |
| |
| // Listen to added to stage and removed from stage. |
| // Start rotating when we are on the stage and stop |
| // when we are removed from the stage. |
| addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler); |
| addEventListener(Event.REMOVED_FROM_STAGE, removedFromStageHandler); |
| } |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Variables |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * @private |
| */ |
| private var effectiveVisibility:Boolean = false; |
| |
| /** |
| * @private |
| */ |
| private var effectiveVisibilityChanged:Boolean = true; |
| |
| /** |
| * @private |
| */ |
| private var oldUnscaledHeight:Number; |
| |
| /** |
| * @private |
| */ |
| private var oldUnscaledWidth:Number; |
| |
| /** |
| * @private |
| */ |
| private var rotationTimer:Timer; |
| |
| /** |
| * @private |
| * |
| * Current rotation of this component in degrees. |
| */ |
| private var currentRotation:Number = 0; |
| |
| /** |
| * @private |
| * |
| * Diameter of the spinner for this component. |
| */ |
| private var spinnerDiameter:int; |
| |
| /** |
| * @private |
| * |
| * Cached value of the spoke color. |
| */ |
| private var spokeColor:uint; |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Private Properties |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * @private |
| * Cache the last value of applicationDPI. |
| */ |
| private var _applicationDPI:Number; |
| |
| /** |
| * @private |
| * |
| * Get the applicationDPI in use. |
| */ |
| private function get applicationDPI():Number |
| { |
| if (_applicationDPI) |
| return _applicationDPI; |
| |
| var application:Object = FlexGlobals.topLevelApplication; |
| |
| if ("applicationDPI" in application) |
| _applicationDPI = application["applicationDPI"]; |
| |
| return _applicationDPI; |
| } |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Overridden properties: UIComponent |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * @private |
| */ |
| override public function set designLayer(value:DesignLayer):void |
| { |
| super.designLayer = value; |
| |
| effectiveVisibilityChanged = true; |
| invalidateProperties(); |
| } |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Overridden methods: UIComponent |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * @private |
| */ |
| override protected function commitProperties():void |
| { |
| super.commitProperties(); |
| |
| if (effectiveVisibilityChanged) |
| { |
| // if visibility changed, re-compute them here |
| computeEffectiveVisibility(); |
| |
| if (canRotate()) |
| startRotation(); |
| else |
| stopRotation(); |
| |
| effectiveVisibilityChanged = false; |
| } |
| } |
| |
| /** |
| * @private |
| */ |
| override protected function measure():void |
| { |
| super.measure(); |
| |
| // Set the default measured size depending on the |
| // applicationDPI |
| if (applicationDPI == DPIClassification.DPI_640) |
| { |
| measuredWidth = 104; |
| measuredHeight = 104; |
| } |
| else if (applicationDPI == DPIClassification.DPI_480) |
| { |
| measuredWidth = 80; |
| measuredHeight = 80; |
| } |
| else if (applicationDPI == DPIClassification.DPI_320) |
| { |
| measuredWidth = 52; |
| measuredHeight = 52; |
| } |
| else if (applicationDPI == DPIClassification.DPI_240) |
| { |
| measuredWidth = 40; |
| measuredHeight = 40; |
| } |
| else if (applicationDPI == DPIClassification.DPI_160) |
| { |
| measuredWidth = 26; |
| measuredHeight = 26; |
| } |
| else if (applicationDPI == DPIClassification.DPI_120) |
| { |
| measuredWidth = 20; |
| measuredHeight = 20; |
| } |
| else |
| { |
| measuredWidth = DEFAULT_MINIMUM_SIZE; |
| measuredHeight = DEFAULT_MINIMUM_SIZE; |
| } |
| |
| measuredMinWidth = DEFAULT_MINIMUM_SIZE; |
| measuredMinHeight = DEFAULT_MINIMUM_SIZE; |
| } |
| |
| /** |
| * @private |
| * Override so we know when visibility is set. The initialized |
| * property calls setVisible() with noEvent == true |
| * so we wouldn't get a visibility event if we just listened |
| * for events. |
| */ |
| override public function setVisible(value:Boolean, |
| noEvent:Boolean = false):void |
| { |
| super.setVisible(value, noEvent); |
| |
| effectiveVisibilityChanged = true; |
| invalidateProperties(); |
| } |
| |
| /** |
| * @private |
| */ |
| override public function styleChanged(styleProp:String):void |
| { |
| super.styleChanged(styleProp); |
| |
| var allStyles:Boolean = !styleName || styleName == "styleName"; |
| |
| // Check for skin/icon changes here. |
| // We could only throw out any skins that change, |
| // but since dynamic re-skinning is uncommon, we'll take |
| // the simpler approach of throwing out all skins. |
| if (allStyles || styleName == "rotationInterval") |
| { |
| // Update the timer if the rotation interval has changed. |
| if (isRotating()) |
| { |
| stopRotation(); |
| startRotation(); |
| } |
| } |
| |
| if (allStyles || styleName == "symbolColor") |
| { |
| updateSpinner(spinnerDiameter); |
| } |
| } |
| |
| /** |
| * @private |
| */ |
| override protected function updateDisplayList(unscaledWidth:Number, |
| unscaledHeight:Number):void |
| { |
| super.updateDisplayList(unscaledWidth, unscaledHeight); |
| |
| // If the size changed, then create a new spinner. |
| if (oldUnscaledWidth != unscaledWidth || |
| oldUnscaledHeight != unscaledHeight) |
| { |
| var newDiameter:Number; |
| |
| newDiameter = calculateSpinnerDiameter(unscaledWidth, unscaledHeight); |
| updateSpinner(newDiameter); |
| |
| oldUnscaledWidth = unscaledWidth; |
| oldUnscaledHeight = unscaledHeight; |
| } |
| } |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Methods |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * @private |
| * |
| * Apply the rules to calculate the spinner diameter from the width |
| * and height. |
| * |
| * @param width new width of this component |
| * @param height new height of this component |
| * |
| * @return true if the spinner's diameter changes, false otherwise. |
| */ |
| private function calculateSpinnerDiameter(width:Number, height:Number):Number |
| { |
| var diameter:Number = Math.min(width, height); |
| diameter = Math.max(DEFAULT_MINIMUM_SIZE, diameter); |
| if (diameter % 2 != 0) |
| diameter--; |
| |
| return diameter; |
| } |
| |
| /** |
| * @private |
| * |
| * Update the spinner properties and redraw. |
| */ |
| private function updateSpinner(diameter:Number):void |
| { |
| var isRotating:Boolean = isRotating(); |
| |
| if (isRotating) |
| stopRotation(); |
| |
| spinnerDiameter = diameter; |
| spokeColor = getStyle("symbolColor"); |
| |
| drawSpinner(); |
| |
| if (isRotating) |
| startRotation(); |
| } |
| |
| /** |
| * @private |
| * |
| * Draw the spinner using the graphics property of this component. |
| */ |
| mx_internal function drawSpinner():void |
| { |
| var g:Graphics = graphics; |
| var spinnerRadius:int = spinnerDiameter / 2; |
| var spinnerWidth:int = spinnerDiameter; |
| var spokeHeight:Number = spinnerDiameter / 3.7; |
| var insideDiameter:Number = spinnerDiameter - (spokeHeight * 2); |
| var spokeWidth:Number = insideDiameter / 5; |
| var eHeight:Number = spokeWidth / 2; |
| var spinnerPadding:Number = 0; |
| |
| // Undocumented styles to modified the spokeWidth |
| // and spokeHeight. |
| // if (getStyle("spokeWidth") !== undefined) |
| // { |
| // spokeWidth = getStyle("spokeWidth"); |
| // eHeight = spokeWidth / 2; |
| // } |
| // |
| // if (getStyle("spokeHeight") !== undefined) |
| // spokeHeight = getStyle("spokeHeight"); |
| // |
| // // spinnerPadding is the padding between the outside |
| // // edge of the circle and the edge of a spoke. |
| // if (getStyle("spinnerPadding") !== undefined) |
| // spinnerPadding = getStyle("spinnerPadding"); |
| // |
| // trace("spoke height = " + spokeHeight); |
| // trace("spoke width = " + spokeWidth); |
| // trace("center = " + center); |
| |
| g.clear(); |
| |
| // 1 |
| drawSpoke(0.20, currentRotation + 300, spokeWidth, spokeHeight, spokeColor, spinnerRadius, eHeight, spinnerPadding); |
| |
| // 2 |
| drawSpoke(0.25, currentRotation + 330, spokeWidth, spokeHeight, spokeColor, spinnerRadius, eHeight, spinnerPadding); |
| |
| // 3 |
| drawSpoke(0.30, currentRotation, spokeWidth, spokeHeight, spokeColor, spinnerRadius, eHeight, spinnerPadding); |
| |
| // 4 |
| drawSpoke(0.35, currentRotation + 30, spokeWidth, spokeHeight, spokeColor, spinnerRadius, eHeight, spinnerPadding); |
| |
| // 5 |
| drawSpoke(0.40, currentRotation + 60, spokeWidth, spokeHeight, spokeColor, spinnerRadius, eHeight, spinnerPadding); |
| |
| // 6 |
| drawSpoke(0.45, currentRotation + 90, spokeWidth, spokeHeight, spokeColor, spinnerRadius, eHeight, spinnerPadding); |
| |
| // 7 |
| drawSpoke(0.50, currentRotation + 120, spokeWidth, spokeHeight, spokeColor, spinnerRadius, eHeight, spinnerPadding); |
| |
| // 8 |
| drawSpoke(0.60, currentRotation + 150, spokeWidth, spokeHeight, spokeColor, spinnerRadius, eHeight, spinnerPadding); |
| |
| // 9 |
| drawSpoke(0.70, currentRotation + 180, spokeWidth, spokeHeight, spokeColor, spinnerRadius, eHeight, spinnerPadding); |
| |
| // 10 |
| drawSpoke(0.80, currentRotation + 210, spokeWidth, spokeHeight, spokeColor, spinnerRadius, eHeight, spinnerPadding); |
| |
| // 11 |
| drawSpoke(0.90, currentRotation + 240, spokeWidth, spokeHeight, spokeColor, spinnerRadius, eHeight, spinnerPadding); |
| |
| // 12 |
| drawSpoke(1.0, currentRotation + 270, spokeWidth, spokeHeight, spokeColor, spinnerRadius, eHeight, spinnerPadding); |
| } |
| |
| |
| /** |
| * @private |
| * |
| * @param spokeAlpha: alpha value of the spoke. |
| * @param spokeWidth: width of the spoke in points. |
| * @param spokeHeight: the lenght of the spoke in pixels. |
| * @param spokeColor: the color of the spoke. |
| * @param spinnerRadius: radius of the spinner. |
| * @param eHeight: estimated height of the rounded end of the spinner. |
| * @param spinnerPadding: number of pixels between the outside |
| * radius of the spinner and the spokes. This is used to make |
| * spinners with skinny spokes look better by moving them |
| * closer to the center of the spinner. |
| */ |
| private function drawSpoke(spokeAlpha:Number, degrees:int, |
| spokeWidth:Number, |
| spokeHeight:Number, |
| spokeColor:uint, |
| spinnerRadius:Number, |
| eHeight:Number, |
| spinnerPadding:Number):void |
| { |
| var g:Graphics = graphics; |
| |
| g.lineStyle(spokeWidth, spokeColor, spokeAlpha, false, LineScaleMode.NORMAL, CapsStyle.ROUND); |
| var outsidePoint:Point = calculatePointOnCircle(spinnerRadius, spinnerRadius - eHeight - spinnerPadding, degrees); |
| var insidePoint:Point = calculatePointOnCircle(spinnerRadius, spinnerRadius - spokeHeight + eHeight - spinnerPadding, degrees); |
| g.moveTo(outsidePoint.x, outsidePoint.y); |
| g.lineTo(insidePoint.x, insidePoint.y); |
| |
| } |
| |
| /** |
| * @private |
| */ |
| private function calculatePointOnCircle(center:Number, radius:Number, degrees:Number):Point |
| { |
| var point:Point = new Point(); |
| var radians:Number = degrees * RADIANS_PER_DEGREE; |
| point.x = center + radius * Math.cos(radians); |
| point.y = center + radius * Math.sin(radians); |
| |
| return point; |
| } |
| |
| /** |
| * @private |
| */ |
| private function startRotation():void |
| { |
| if (!rotationTimer) |
| { |
| var rotationInterval:Number = getStyle("rotationInterval"); |
| if (isNaN(rotationInterval)) |
| rotationInterval = DEFAULT_ROTATION_INTERVAL; |
| |
| if (rotationInterval < 16.6) |
| rotationInterval = 16.6; |
| |
| rotationTimer = new Timer(rotationInterval); |
| } |
| |
| if (!rotationTimer.hasEventListener(TimerEvent.TIMER)) |
| { |
| rotationTimer.addEventListener(TimerEvent.TIMER, timerHandler); |
| rotationTimer.start(); |
| } |
| |
| } |
| |
| /** |
| * @private |
| */ |
| private function stopRotation():void |
| { |
| if (rotationTimer) |
| { |
| rotationTimer.removeEventListener(TimerEvent.TIMER, timerHandler); |
| rotationTimer.stop(); |
| rotationTimer = null; |
| } |
| } |
| |
| /** |
| * @private |
| */ |
| private function isRotating():Boolean |
| { |
| return rotationTimer != null; |
| } |
| |
| /** |
| * The BusyIndicator can be rotated if it is both on the display list and |
| * visible. |
| * |
| * @returns true if the BusyIndicator can be rotated, false otherwise. |
| */ |
| private function canRotate():Boolean |
| { |
| if (effectiveVisibility && stage != null) |
| return true; |
| |
| return false; |
| } |
| |
| /** |
| * @private |
| */ |
| private function computeEffectiveVisibility():void |
| { |
| |
| // Check our design layer first. |
| if (designLayer && !designLayer.effectiveVisibility) |
| { |
| effectiveVisibility = false; |
| return; |
| } |
| |
| // Start out with true visibility and enablement |
| // then loop up parent-chain to see if any of them are false. |
| effectiveVisibility = true; |
| var current:IVisualElement = this; |
| |
| while (current) |
| { |
| if (!current.visible) |
| { |
| if (!(current is IUIComponent) || !IUIComponent(current).isPopUp) |
| { |
| // Treat all pop ups as if they were visible. This is to |
| // fix a bug where the BusyIndicator does not spin when it |
| // is inside modal popup. The problem is in we do not get |
| // an event when the modal window is made visible in |
| // PopUpManagerImpl.fadeInEffectEndHandler(). When the modal |
| // window is made visible, setVisible() is passed "true" so |
| // as to not send an event. When do get events when the |
| // non-modal windows are popped up. Only modal windows are |
| // a problem. |
| // The downside of this fix is BusyIndicator components that are |
| // inside of hidden, non-modal, popup windows will paint themselves |
| // on a timer. |
| effectiveVisibility = false; |
| break; |
| } |
| } |
| |
| current = current.parent as IVisualElement; |
| } |
| } |
| |
| /** |
| * @private |
| * Add event listeners for SHOW and HIDE on all the ancestors up the parent chain. |
| * Adding weak event listeners just to be safe. |
| */ |
| private function addVisibilityListeners():void |
| { |
| var current:IVisualElement = this.parent as IVisualElement; |
| while (current) |
| { |
| // add visibility listeners to the parent |
| current.addEventListener(FlexEvent.HIDE, visibilityChangedHandler, false, 0, true); |
| current.addEventListener(FlexEvent.SHOW, visibilityChangedHandler, false, 0, true); |
| |
| current = current.parent as IVisualElement; |
| } |
| } |
| |
| /** |
| * @private |
| * Remove event listeners for SHOW and HIDE on all the ancestors up the parent chain. |
| */ |
| private function removeVisibilityListeners():void |
| { |
| var current:IVisualElement = this; |
| while (current) |
| { |
| current.removeEventListener(FlexEvent.HIDE, visibilityChangedHandler, false); |
| current.removeEventListener(FlexEvent.SHOW, visibilityChangedHandler, false); |
| |
| current = current.parent as IVisualElement; |
| } |
| } |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Overridden event handlers: UIComponent |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * @private |
| */ |
| override protected function layer_PropertyChange(event:PropertyChangeEvent):void |
| { |
| super.layer_PropertyChange(event); |
| |
| if (event.property == "effectiveVisibility") |
| { |
| effectiveVisibilityChanged = true; |
| invalidateProperties(); |
| } |
| } |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Event Handlers |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * @private |
| */ |
| private function addedToStageHandler(event:Event):void |
| { |
| // Check our visibility here since we haven't added |
| // visibility listeners yet. |
| computeEffectiveVisibility(); |
| |
| if (canRotate()) |
| startRotation(); |
| |
| addVisibilityListeners(); |
| } |
| |
| /** |
| * @private |
| */ |
| private function removedFromStageHandler(event:Event):void |
| { |
| stopRotation(); |
| |
| removeVisibilityListeners(); |
| } |
| |
| /** |
| * @private |
| * Event call back whenever the visibility of us or one of our ancestors |
| * changes |
| */ |
| private function visibilityChangedHandler(event:FlexEvent):void |
| { |
| effectiveVisibilityChanged = true; |
| invalidateProperties(); |
| } |
| |
| /** |
| * @private |
| * |
| * Rotate the spinner once for each timer event. |
| */ |
| private function timerHandler(event:TimerEvent):void |
| { |
| currentRotation += 30; |
| if (currentRotation >= 360) |
| currentRotation = 0; |
| |
| drawSpinner(); |
| event.updateAfterEvent(); |
| } |
| |
| } |
| } |