//////////////////////////////////////////////////////////////////////////////// // // 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.events.Event; import flash.events.MouseEvent; import flash.geom.Point; import flash.utils.Timer; import mx.core.IVisualElement; import mx.core.UIComponent; import mx.core.mx_internal; import mx.events.FlexEvent; import mx.events.ResizeEvent; import spark.components.supportClasses.ToggleButtonBase; import spark.core.IDisplayText; import spark.effects.Animate; import spark.effects.animation.Animation; import spark.effects.animation.IAnimationTarget; import spark.effects.animation.MotionPath; import spark.effects.animation.SimpleMotionPath; import spark.effects.easing.Linear; import spark.effects.easing.Sine; import spark.utils.MouseEventUtil; use namespace mx_internal; //-------------------------------------- // Styles //-------------------------------------- /** * Color applied to highlight the selected side of the ToggleSwitch control. * * @default 0x3F7FBA * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ [Style(name="accentColor", type="uint", format="Color", inherit="yes")] /** * The duration, in milleseconds, for an animation of the thumb * as it slides between the selected and unselected sides of the track. * For animations between any two arbitrary positions on the track, * the animation takes a proportionally shorter amount of time. * For example, after dragging the thumb half way along the track, * the animation for the slide the remainder of the way along the track * takes half the duration. * * @default 125 * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ [Style(name="slideDuration", type="Number", format="Time", inherit="no")] /** * The alpha of text shadows. * * @default 0.65 * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ [Style(name="textShadowAlpha", type="Number", inherit="yes", minValue="0.0", maxValue="1.0", theme="mobile")] /** * The color of text shadows. * * @default 0x000000 * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ [Style(name="textShadowColor", type="uint", format="Color", inherit="yes", theme="mobile")] //-------------------------------------- // Excluded APIs //-------------------------------------- [Exclude(name="textAlign", kind="style")] //-------------------------------------- // Other metadata //-------------------------------------- [IconFile("ToggleSwitch.png")] /** * The Spark ToggleSwitch control defines a binary switch that * can be in the selected or unselected position. * The ToggleSwitch consists of a thumb skin part that moves between * the two ends of the track skin part, similar to the Spark Slider control. * *

The ToggleSwitch control has two positions: selected and unselected. * By default, the label OFF corresponds to the unselected position and ON * corresponds to the selected position.

* *

The following image shows a ToggleSwitch control. * The labels below the control show the current switch label and position:

* *

* Toggle switch *

* *

Clicking anywhere in the control toggles the position. * You can also slide the thumb along the track to change position. * When you release the thumb, it moves to the position, selected or unselected, * that is closest to the thumb location.

* *

The ToggleSwitch control uses the following default values for * the unselected and selected labels: OFF (unselected) and ON (selected). * Define a custom skin to change the labels, or to change other visual * characteristics of the control.

* *

The following skin class, defined as a subclass of * spark.skins.mobile.ToggleSwitchSkin, changes the labels to Yes and No:

* *
 *  package skins
 *  // components\mobile\skins\MyToggleSwitchSkin.as
 *  {
 *      import spark.skins.mobile.ToggleSwitchSkin;
 *      
 *      public class MyToggleSwitchSkin extends ToggleSwitchSkin
 *      {
 *          public function MyToggleSwitchSkin()
 *          {
 *              super();
 *              // Set properties to define the labels 
 *              // for the selected and unselected positions.
 *              selectedLabel="Yes";
 *              unselectedLabel="No"; 
 *          }
 *      }
 *  }
 *  
* * @mxml * *

The <s:ToggleSwitch> tag inherits all of the tag * attributes of its superclass and adds the following tag attributes:

* *
 *  <s:ToggleSwitch
 *   Properties
 *    selected="null"
 * 
 *   Common Styles
 *    accentColor="0x3F7FBA"
 *    slideDuration="125"
 * 
 *   Mobile Styles
 *    textShadowAlpha="0.65"
 *    textShadowColor="0x000000"
 *  >
 *  
* * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 * * @includeExample examples/ToggleSwitchExample.mxml -noswf * * @see spark.components.ToggleButton * @see spark.components.HSlider * @see spark.skins.mobile.ToggleSwitchSkin */ public class ToggleSwitch extends ToggleButtonBase { //---------------------------------------------------------------------------------------------- // // Class constants // //---------------------------------------------------------------------------------------------- /** * The amount the mouse can move before we consider the event a drag and not a click */ private static const MOUSE_MOVE_TOLERANCE:Number = 0.15; //---------------------------------------------------------------------------------------------- // // Constructor // //---------------------------------------------------------------------------------------------- /** * Constructor. * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ public function ToggleSwitch() { super(); stickyHighlighting = true; animator = new Animation(); addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler); } //---------------------------------------------------------------------------------------------- // // Variables // //---------------------------------------------------------------------------------------------- /** * The last position of the mouse during a drag gesture */ private var lastMouseX:Number = 0; /** * Controls the mouse events driving a drag gesture on a * ToggleSwitch */ private var mouseDragUtil:MouseDragUtil; /** * Whether the mouse has moved during the current drag * gesture */ private var mouseMoved:Boolean = false; /** * Where the thumb should be after the current animation ends */ private var slideToPosition:Number = 0; /** * The point where the current mouse gesture began */ private var stageOffset:Point; /** * The thumbPosition when the current mouse gesture began */ private var positionOffset:Number = 0; //---------------------------------------------------------------------------------------------- // // Skin parts // //---------------------------------------------------------------------------------------------- [SkinPart(required="false")] /** * A skin part that can be dragged along the track. * The thumbPosition property contains the thumb's * current position along the track. * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ public var thumb:IVisualElement; [SkinPart(required="false")] /** * A skin part that defines the bounds along which the thumb can * be dragged. * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ public var track:IVisualElement; //---------------------------------------------------------------------------------------------- // // Overridden properties // //---------------------------------------------------------------------------------------------- //---------------------------------- // selected //---------------------------------- /** * The current position of the toggle switch. * A value of false corresponds to the unselected position, * and a value of 1 corresponds to the selected position. * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ override public function set selected(value:Boolean):void { super.selected = value; var newValue:Number = selectedToPosition(value); slideToPosition = newValue; setThumbPosition(newValue); } //---------------------------------------------------------------------------------------------- // // Properties // //---------------------------------------------------------------------------------------------- //---------------------------------- // thumbPosition //---------------------------------- /** * The animation between thumbPosition and slideToPosition, which * drives the thumb's position. exposed for testing. */ private var _animator:Animation = null; /** * @private */ mx_internal function get animator():Animation { return _animator; } /** * @private * additional setup is performed on mouse events, before * we start the animation */ mx_internal function set animator(value:Animation): void { _animator = value; } //---------------------------------- // thumbPosition //---------------------------------- /** * Storage for thumbPosition */ private var _thumbPosition:Number = 0.0; [Bindable(event="thumbPositionChanged")] /** * The thumb's current position along the track. * The range of value is between 0.0, unselected, * and 1.0, selected. * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ public function get thumbPosition():Number { return _thumbPosition; } //---------------------------------------------------------------------------------------------- // // Overridden Methods // //---------------------------------------------------------------------------------------------- /** * @private */ override protected function updateDisplayList(w:Number, h:Number):void { super.updateDisplayList(w, h); updateSkinDisplayList(); } /** * @private */ override protected function attachSkin():void { super.attachSkin(); skin.addEventListener(FlexEvent.UPDATE_COMPLETE, updateCompleteHandler); skin.addEventListener(ResizeEvent.RESIZE, resizeHandler); } /** * @private */ override protected function addHandlers():void { super.addHandlers(); mouseDragUtil = new MouseDragUtil(this, mouseDownHandler, mouseDragHandler, thinnedMouseDragHandler, mouseUpHandler); mouseDragUtil.setupHandlers(); } //---------------------------------------------------------------------------------------------- // // Methods // //---------------------------------------------------------------------------------------------- /** * Convert the given thumb position to a selected value */ private function positionToSelected(value:Number):Boolean { return value > 0.5; } /** * Convert the selected value to a thumb position */ private function selectedToPosition(value:Boolean):Number { return value ? 1.0 : 0.0; } /** * Specify the new position the toggle switch should animate to. * At the end of the animation, selection will update. */ private function moveToPositionAndSelect(newPosition:Number):void { var slideDuration:Number = getStyle("slideDuration"); if (newPosition != thumbPosition && slideDuration > 0) { // Finish any current animation before we start the next one. if (animator.isPlaying) stopAnimation(); // holds the final value to be set when animation ends slideToPosition = newPosition; var duration:Number = slideDuration * (Math.abs(thumbPosition - slideToPosition)); animateToPosition(animator, thumbPosition, newPosition, duration); } else { // we are either at the destination position, // or we should update immediately setSelected(positionToSelected(newPosition)); } } /** * Specify the new position the toggle switch should animate to * during mouse drag. * Selection does not update after the animation. */ private function moveToPosition(newPosition:Number):void { if (newPosition != thumbPosition) { // Finish any current animation before we start the next one. if (animator.isPlaying) stopAnimation(); animateToPosition(animator, thumbPosition, newPosition, MouseDragUtil.MAX_UPDATE_RATE); } } /** * When the selection is changed by user interactions, fire a change event * when appropriate. */ private function setSelected(value:Boolean):void { var valueChanged:Boolean = value != selected; selected = value; if (valueChanged) dispatchEvent(new Event(Event.CHANGE)); } /** * Prepare the animator for use as a single animation to the edge of * the track, or for use in a series of animations following mouse * drag events. */ private function setupAnimator(animator:Animation, selectAtCompletion:Boolean):void { stopAnimation(); var animTarget:AnimationTargetHelper = animator.animationTarget as AnimationTargetHelper; if (!animTarget) animator.animationTarget = animTarget = new AnimationTargetHelper(); animTarget.updateFunction = animationUpdateHandler; animator.motionPaths = new [null]; if (selectAtCompletion) { animTarget.endFunction = animationEndHandler; animator.easer = new Sine(0); } else { animTarget.endFunction = null; animator.easer = new Linear(); } } /** * Animate the thumb moving from startPosition to endPosition */ private function animateToPosition(animator:Animation, startPosition:Number, endPosition:Number, duration:Number):void { if (animator.isPlaying) stopAnimation(); animator.duration = duration; animator.motionPaths[0] = new SimpleMotionPath("position", startPosition, endPosition); animator.play(); } /** * Set the thumb position and update the component */ private function setThumbPosition(value:Number):void { if (value == _thumbPosition) return; value = Math.min(value, 1.0); value = Math.max(value, 0.0); if (value == _thumbPosition) return; _thumbPosition = value; invalidateDisplayList(); if (hasEventListener("thumbPositionChanged")) dispatchEvent(new Event("thumbPositionChanged")); } /** * Stops a running animation prematurely */ private function stopAnimation():void { animator.stop(); } /** * Position the thumb along the track based on thumbPosition. * The usable width of the track is equal to the track's layout width * less the thumb's width, as the thumb should always be contained by * the track. */ private function updateSkinDisplayList():void { if (!thumb || !track || !thumb.parent || !track.parent) return; // Perform calculations in global space, since track & thumb may have different coordinate spaces var globalThumbXOffset:Number = getGlobalTrackRange() * thumbPosition; var globalThumbPos:Point = track.parent.localToGlobal(new Point(track.getLayoutBoundsX())); globalThumbPos.x += globalThumbXOffset; var localThumbPos:Point = thumb.parent.globalToLocal(globalThumbPos); thumb.setLayoutBoundsPosition(Math.round(localThumbPos.x), thumb.getLayoutBoundsY()); } /** * Determine the global range of motion for the thumb on the track */ private function getGlobalTrackRange():Number { var globalTrackDims:Point = track.parent.localToGlobal(new Point(track.getLayoutBoundsWidth())); var globalThumbDims:Point = thumb.parent.localToGlobal(new Point(thumb.getLayoutBoundsWidth())); return globalTrackDims.x - globalThumbDims.x; } /** * Is an animation running that will result in setting selected */ private function isSelectionAnimationRunning():Boolean { return animator && animator.isPlaying && (AnimationTargetHelper(animator.animationTarget).endFunction != null); } /** * Adjust the skin when it updates */ private function updateCompleteHandler(event:FlexEvent):void { updateSkinDisplayList(); skin.removeEventListener(FlexEvent.UPDATE_COMPLETE, updateCompleteHandler); } /** * Adjust the skin after it has finished resizing */ private function resizeHandler(event:ResizeEvent):void { skin.addEventListener(FlexEvent.UPDATE_COMPLETE, updateCompleteHandler); } /** * Adjust the thumb when the component is added */ private function addedToStageHandler(event:Event):void { updateSkinDisplayList(); } //---------------------------------------------------------------------------------------------- // // Overridden Event handlers // //---------------------------------------------------------------------------------------------- /** * @private * ignore mouse events while animating */ override protected function mouseEventHandler(event:Event):void { if (isSelectionAnimationRunning()) return; super.mouseEventHandler(event); } /** * @private * If the thumb has moved, snap the it to the closest * end of the track. If not, move it to the opposite end * of where it is or was animating to. */ override protected function buttonReleased():void { if (isSelectionAnimationRunning()) return; setupAnimator(animator, true); var newPosition:Number; if (mouseMoved) // the result of a drag is the nearest current position newPosition = selectedToPosition(thumbPosition >= .5); else // the result of a click is the opposite side of the current // destination (not always the current value, as when animating) newPosition = selectedToPosition(slideToPosition < .5); moveToPositionAndSelect(newPosition); mouseMoved = false; } //---------------------------------------------------------------------------------------------- // // Event handlers // //---------------------------------------------------------------------------------------------- /** * These mouse handlers are responsible for ToggleSwitch's dragging * behavior. The toggling behavior is handled by buttonReleased and * ToggleButtonBase. */ private function mouseDownHandler(event:MouseEvent):void { if (isSelectionAnimationRunning()) return; setupAnimator(animator, false); mouseMoved = false; stageOffset = new Point(event.stageX, event.stageY); positionOffset = thumbPosition; } /** * Cache the most recent mouse position for use when the thinning code * is called */ private function mouseDragHandler(event:MouseEvent):void { lastMouseX = event.stageX; } /** * Move the thumb to line up with the current mouse position * The mouse needs to move at least MOUSE_MOVE_TOLERANCE before * the thumb begins to move. * On touch devices, the mouse position fired near mouse up can * be unreliable. */ private function thinnedMouseDragHandler(event:Event):void { if (mouseCaptured && track && thumb) { var deltaX:Number = (lastMouseX - stageOffset.x) / getGlobalTrackRange(); // don't set mouseMoved unless the mouse position has changed enough // don't rely on the last drag handler fired as part of mouse up if (!mouseMoved && Math.abs(deltaX) > MOUSE_MOVE_TOLERANCE && event.type != MouseEvent.MOUSE_UP) mouseMoved = true; // only move if we have passed the threshold // dampen the movement by that threshold if (mouseMoved) { moveToPosition(positionOffset + deltaX + (selected ? MOUSE_MOVE_TOLERANCE : -MOUSE_MOVE_TOLERANCE)); } } } /** * We need to force a buttonRelease when mouse up occurs outside of * the component, and the button has already released mouseCaptured */ private function mouseUpHandler(event:Event):void { // mouseCaptured unset from ButtonBase.systemManager_mouseUpHandler if (event.target != this && !mouseCaptured) buttonReleased(); } /** * Handles events from the Animation that runs the animated slide. * We just update thumbPosition with the current animated value */ private function animationUpdateHandler(animation:Animation):void { setThumbPosition(animation.currentValue["position"]); } /** * Handles end event from the Animation that runs the animated slide. * We update selected once the animation ends */ private function animationEndHandler(animation:Animation):void { setSelected(positionToSelected(slideToPosition)); } } } //---------------------------------------------------------------------------------------------- // // Out-of-package Helper Classes // //---------------------------------------------------------------------------------------------- import spark.effects.animation.Animation; import spark.effects.animation.IAnimationTarget; /** * @private * simple helper implementation of IAnimationTarget */ class AnimationTargetHelper implements IAnimationTarget { public var updateFunction:Function; public var endFunction:Function; public function AnimationTargetHelper(updateFunction:Function = null, endFunction:Function = null) { this.updateFunction = updateFunction; this.endFunction = endFunction; } public function animationStart(animation:Animation):void { } public function animationEnd(animation:Animation):void { if (endFunction != null) endFunction(animation); } public function animationStop(animation:Animation):void { } public function animationRepeat(animation:Animation):void { } public function animationUpdate(animation:Animation):void { if (updateFunction != null) updateFunction(animation); } } import spark.utils.MouseEventUtil; import flash.events.MouseEvent; import flash.utils.Timer; import mx.core.UIComponent; import flash.events.TimerEvent; import flash.events.Event; /** * @private * A helper class responsible for handling mouse drag gestures, * and ensuring we do not update on every mouse move event */ class MouseDragUtil { private var mouseDownHandler:Function; private var mouseMoveEveryHandler:Function; private var mouseMoveThinnedHandler:Function; private var mouseUpHandler:Function; private var target:UIComponent; private var dragPending:Boolean; private var dragTimer:Timer; public static const MAX_UPDATE_RATE:Number = 30; public function MouseDragUtil(target:UIComponent, handleDown:Function, handleMove:Function, handleThinnedMove:Function, handleUp:Function) { this.target = target; this.mouseDownHandler = handleDown; this.mouseMoveEveryHandler = handleMove; this.mouseMoveThinnedHandler = handleThinnedMove; this.mouseUpHandler = handleUp; } public function setupHandlers():void { MouseEventUtil.addDownDragUpListeners(target, mouseDownHandlerWrapper, mouseDragHandlerWrapper, mouseUpHandlerWrapper); } public function removeHandlers():void { MouseEventUtil.removeDownDragUpListeners(target, mouseDownHandlerWrapper, mouseDragHandlerWrapper, mouseUpHandlerWrapper); if (dragTimer) { dragTimer.stop(); dragTimer.removeEventListener(TimerEvent.TIMER, dragTimerHandler); dragTimer = null; } } private function mouseDownHandlerWrapper(event:MouseEvent):void { mouseDownHandler(event); } private function mouseDragHandlerWrapper(event:MouseEvent):void { mouseMoveEveryHandler(event); if (!dragTimer) { dragTimer = new Timer(1000 / MAX_UPDATE_RATE, 0); dragTimer.addEventListener(TimerEvent.TIMER, dragTimerHandler); } if (!dragTimer.running) { mouseMoveThinnedHandler(event); dragPending = false; dragTimer.start(); } else { dragPending = true; } } private function dragTimerHandler(event:TimerEvent):void { if (dragPending) { mouseMoveThinnedHandler(event); dragPending = false; } else { dragTimer.stop(); } } private function mouseUpHandlerWrapper(event:Event):void { if (dragTimer) { if (dragPending) { mouseMoveThinnedHandler(event); dragPending = false; } dragTimer.stop(); dragTimer.removeEventListener(TimerEvent.TIMER, dragTimerHandler); dragTimer = null; } mouseUpHandler(event); } }