//////////////////////////////////////////////////////////////////////////////// // // 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.transitions { import flash.display.DisplayObject; import flash.events.Event; import flash.geom.Matrix3D; import flash.geom.PerspectiveProjection; import flash.geom.Point; import flash.geom.Vector3D; import mx.core.FlexGlobals; import mx.core.ILayoutElement; import mx.core.IVisualElement; import mx.core.UIComponent; import mx.core.mx_internal; import mx.effects.IEffect; import mx.effects.Parallel; import mx.events.EffectEvent; import mx.events.FlexEvent; import mx.geom.TransformOffsets; import mx.managers.SystemManager; import spark.components.Application; import spark.components.Group; import spark.components.TabbedViewNavigator; import spark.components.ViewNavigator; import spark.components.supportClasses.ButtonBarBase; import spark.components.supportClasses.SkinnableComponent; import spark.effects.*; import spark.effects.animation.Keyframe; import spark.effects.animation.MotionPath; import spark.effects.animation.SimpleMotionPath; import spark.primitives.BitmapImage; use namespace mx_internal; /** * The FlipViewTransition class performs a simple flip transition for views. * The flip transition supports two modes (card and cube) * as well as an optional direction (up, down, left, or right). * *

The default duration of a FlipViewTransition is 400 ms.

* *

Note:Create and configure view transitions in ActionScript; * you cannot create them in MXML.

* * @see FlipViewTransitionMode * @see ViewTransitionDirection * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ public class FlipViewTransition extends ViewTransitionBase { //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * Constructor. * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ public function FlipViewTransition() { super(); // Defaut duration of 450 yields a smoother result. duration = 450; } //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- /** * @private * Property bag used to save any start view properties that * are then restored after the transition is complete. */ private var startViewProps:Object = {}; /** * @private * Property bag used to save any end view properties that * are then restored after the transition is complete. */ private var endViewProps:Object = {}; /** * @private * Property bag used to save any navigator centric properties that * are then restored after the transition is complete. */ private var navigatorProps:Object = {}; /** * @private * Parents our start and end view elements while flipping. */ private var transitionGroup:Group; /** * @private * Flag to denote if we're flipping vertically or horizontally. */ private var vertical:Boolean; /** * @private * Used to save off our pending view's transform matrix while we animate. */ private var savedStartMatrix:Matrix3D; /** * @private * Used to save off our pending view's transform matrix while we animate. */ private var savedEndMatrix:Matrix3D; /** * @private */ private var viewWidth:Number; /** * @private */ private var viewHeight:Number; /** * @private */ private var directionModifier:int; /** * @private */ private var animatedProperty:String; /** * @private */ private var flipEffect:IEffect; //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- //--------------------------------- // direction //--------------------------------- private var _direction:String = ViewTransitionDirection.LEFT; [Inspectable(category="General", enumeration="left,right,up,down", defaultValue="left")] /** * Specifies the direction of flip transition. * * @default ViewTransitionDirection.LEFT * * @see ViewTransitionDirection * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ public function get direction():String { return _direction; } /** * @private */ public function set direction(value:String):void { _direction = value; } //--------------------------------- // mode //--------------------------------- private var _mode:String = "card"; // avoid deprecation warning for FlipViewTransitionMode.CARD; [Inspectable(category="General", enumeration="card,cube", defaultValue="card")] /** * Specifies the type of flip transition to perform. * * @default FlipViewTransitionMode.CARD * * @see FlipViewTransitionMode * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ public function get mode():String { return _mode; } /** * @private */ public function set mode(value:String):void { _mode = value; } //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * @private * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ override public function captureStartValues():void { super.captureStartValues(); // Initialize the property bag used to save some of our // properties that are then restored after the transition is over. navigatorProps = new Object(); // Snapshot the entire navigator. var oldVisibility:Boolean = endView.visible; endView.visible = false; cachedNavigator = getSnapshot(targetNavigator, 0, cachedNavigatorGlobalPosition); endView.visible = oldVisibility; } /** * @private * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ override protected function createViewEffect():IEffect { // Don't bother transitioning if we're missing views. if (!startView || !endView) return null; return (mode == "card") ? // avoid deprecation warning for FlipViewTransitionMode.CARD prepareCardViewEffect() : prepareCubeViewEffect(); } /** * @private * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ override protected function createConsolidatedEffect():IEffect { // If we have no cachedNavigator then there is not much we can do. if (!cachedNavigator) return null; return mode == "card" ? // avoid deprecation warning for FlipViewTransitionMode.CARD prepareConsolidatedCardViewEffect() : prepareConsolidatedCubeViewEffect(); } //-------------------------------------------------------------------------- // // Private Methods // //-------------------------------------------------------------------------- /** * @private * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ override public function prepareForPlay():void { actionBarTransitionDirection = direction; super.prepareForPlay(); // Enable clipping on the contentGroups of the views if (startView && startView.contentGroup) { startViewProps.clipAndEnableScrolling = startView.contentGroup.clipAndEnableScrolling; startView.contentGroup.clipAndEnableScrolling = true; } if (endView && endView.contentGroup) { endViewProps.clipAndEnableScrolling = endView.contentGroup.clipAndEnableScrolling; endView.contentGroup.clipAndEnableScrolling = true; } // Work-around for SDK-29118 if (dpiScale != 1 && transitionGroup) applyDPIScaleToElements(transitionGroup, dpiScale); } //-------------------------------------------------------------------------- // Card Flip Effect //-------------------------------------------------------------------------- /** * @private */ private function prepareCardViewEffect():IEffect { // Initialize are transition parameters, create our temporary // transitionGroup, and parent our view elements. setupTransition(); // Align underside 'face' of our card. alignCardFaces(endView); return createCardFlipAnimation(endView.width); } /** * @private */ private function prepareConsolidatedCardViewEffect():IEffect { // Initialize are transition parameters, create our temporary // transitionGroup, and parent our view elements. setupConsolidatedTransition(); // Capture original targetNavigator x and y coordinates before // alignCardFaces changes them navigatorProps.x = targetNavigator.x; navigatorProps.y = targetNavigator.y; // Align underside 'face' of our card. alignCardFaces(targetNavigator); // When doing a consolidated card effect, the targetNavigator and actionBar // need to be hidden in some use cases to prevent them from renderering // through transparent backgrounds. The visibility of the elements // are retoggled in effectUpdateHandler(). if (actionBar) { navigatorProps.actionBarVisible = actionBar.visible; actionBar.visible = false; } targetNavigator.setVisible(false, true); return createCardFlipAnimation(endView.width); } /** * @private */ private function get dpiScale():Number { return (Application(FlexGlobals.topLevelApplication).runtimeDPI / Application(FlexGlobals.topLevelApplication).applicationDPI); } /** * @private * Shared helper routine which serves as our effect factory for both standard * and consolidated transitions. */ protected function createCardFlipAnimation(width:Number):IEffect { // Now offset our transform center as appropriate for the transition direction transitionGroup.transformX = viewWidth / 2; transitionGroup.transformY = viewHeight / 2; // If we are doing a consolidate flip effect, the transform center needs // to be translated by the parents x and y since the transition group is not // the view but the navigators parent if (consolidatedTransition) { transitionGroup.transformX += navigatorProps.x; transitionGroup.transformY += navigatorProps.y; } // Validate our transition group prior to the start of our animation. transitionGroup.validateNow(); var animation:Animate = new Animate(); // Create motion path for our rotation property. var vector:Vector. = new Vector.(); vector.push(new SimpleMotionPath(animatedProperty, 0, directionModifier * 180)); // Configure the remainder of our animation parameters and install // an update listener so that we can hide the old view once it's out // of view. animation.motionPaths = vector; animation.target = transitionGroup; animation.duration = duration; animation.addEventListener("effectUpdate", effectUpdateHandler); animation.easer = easer; flipEffect = animation; return animation; } /** * @private * Update listener on our flip effect that hides our start view * once it is no longer visible to the user. */ private function effectUpdateHandler(event:EffectEvent):void { var animatedProp:String = vertical ? "rotationX" : "rotationY"; if(Math.abs(transitionGroup[animatedProp]) > 90) { if (!consolidatedTransition) { startView.visible = false; } else { targetNavigator.setVisible(true, true); if (actionBar) actionBar.visible = navigatorProps.actionBarVisible; cachedNavigator.displayObject.visible = false; } } if (mode == "card") // avoid deprecation warning for FlipViewTransitionMode.CARD { var topRight:Vector3D = new Vector3D(viewWidth, 0, 0); var bottomLeft:Vector3D = new Vector3D(0, viewHeight, 0); var matrix:Matrix3D = new Matrix3D(); matrix.identity(); if (vertical) { // Flipping around X axis matrix.appendTranslation(0, -viewHeight/2, 0); matrix.appendRotation(transitionGroup.rotationX, new Vector3D(1, 0, 0)); matrix.appendTranslation(0, viewHeight/2, 0); } else { // Flipping around Y axis matrix.appendTranslation(-viewWidth/2, 0, 0); matrix.appendRotation(transitionGroup.rotationY, new Vector3D(0, 1, 0)); matrix.appendTranslation(viewWidth/2, 0, 0); } var newTopRight:Vector3D = matrix.transformVector(topRight); var newBottomLeft:Vector3D = matrix.transformVector(bottomLeft); if (!transitionGroup.postLayoutTransformOffsets) transitionGroup.postLayoutTransformOffsets = new TransformOffsets(); transitionGroup.postLayoutTransformOffsets.z = -Math.min(newTopRight.z, newBottomLeft.z); } // Ensure transform matrix is updated even when layout is disabled. transitionGroup.validateDisplayList(); } //-------------------------------------------------------------------------- // Cube Flip Effect //-------------------------------------------------------------------------- /** * @private */ private function prepareCubeViewEffect():IEffect { // Initialize are transition parameters, create our temporary // transitionGroup, and parent our view elements. setupTransition(); // Position the 'faces' of our cube. alignCubeFaces(startView, endView); // Construct our animation sequence now that actors are configured. return createCubeFlipAnimation(vertical ? viewHeight : viewWidth); } /** * @private */ private function prepareConsolidatedCubeViewEffect():IEffect { // Initialize are transition parameters, create our temporary // transitionGroup, and parent our view elements. setupConsolidatedTransition(); // Position the 'faces' of our cube. alignCubeFaces(cachedNavigator, targetNavigator); // Construct our animation sequence now that actors are configured. return createCubeFlipAnimation(vertical ? viewHeight : viewWidth); } /** * @private * Shared helper routine which serves as our effect factory for both standard * and consolidated transitions. */ protected function createCubeFlipAnimation(depth:Number):IEffect { // Validate our transition group prior to the start of our animation. transitionGroup.validateNow(); var p:Parallel = new Parallel(); p.target = transitionGroup; // Zoom out var m1:Move3D = new Move3D(); m1.zTo = depth / 1.45; m1.duration = duration/2; m1.addEventListener("effectUpdate", cubeEffectUpdateHandler); flipEffect = m1; // Zoom back in var m2:Move3D = new Move3D(); m2.zTo = depth / 2; m2.duration = duration/2; m2.startDelay = duration/2; // Rotate our 'cube'. var r1:Rotate3D = new Rotate3D(); if (animatedProperty == "rotationY") r1.angleYTo= directionModifier * 90; else r1.angleXTo = directionModifier * 90; r1.duration = duration; p.addChild(m1); p.addChild(m2); p.addChild(r1); return p; } /** * @private * Update listener on our cube flip effect that hides our start view * once it's no longer facing the user. */ private function cubeEffectUpdateHandler(event:EffectEvent):void { var face:DisplayObject = (!consolidatedTransition) ? startView : cachedNavigator.displayObject; var frontFacing:Boolean = isFrontFacing(face); if (!frontFacing) face.visible = false; // Ensure transform matrix is updated even when layout is disabled. transitionGroup.validateDisplayList(); } /** * @private * Determine if a given display object with 2.5d transform * is front facing, by testing the winding direction of three * points on the object translated to user viewport (via cross * product of vectors). */ private function isFrontFacing(target:DisplayObject):Boolean { var pA:Point = target.localToGlobal( POINT_A ); var pB:Point = target.localToGlobal( POINT_B ); var pC:Point = target.localToGlobal( POINT_C ); return (pB.x-pA.x) * (pC.y-pA.y) - (pB.y-pA.y) * (pC.x-pA.x) > 0; } private static const POINT_A:Point = new Point(0, 0); private static const POINT_B:Point = new Point(100, 0); private static const POINT_C:Point = new Point(0, 100); //-------------------------------------------------------------------------- // Shared helpers //-------------------------------------------------------------------------- /** * @private */ private function alignCardFaces(face:DisplayObject):void { // In order for device text to render properly when negatively scaled, we // must ensure our view has a transform matrix 3D active. face.z = .01; // Mirror our end view so that it can serve as the reverse face of our // card transition. if (vertical) { face.y += viewHeight; face.scaleY = -1; } else { face.x += viewWidth; face.scaleX = -1; } } /** * @private */ private function alignCubeFaces(startFace:Object, endFace:Object):void { // Position the xform center of outer transitionGroup. transitionGroup.x += viewWidth / 2; transitionGroup.y += viewHeight / 2; transitionGroup.z = vertical ? (viewHeight / 2) : (viewWidth / 2); // Position the 'faces' of our cube. if (vertical) { endFace.x = -viewWidth / 2; endFace.y = -directionModifier * viewHeight / 2; endFace.z = directionModifier * viewHeight / 2; endFace["rotationX"] = -directionModifier * 90; } else { endFace.x = directionModifier * viewWidth / 2; endFace.y = -viewHeight / 2; endFace.z = -directionModifier * viewWidth / 2; endFace["rotationY"] = -directionModifier * 90; } startFace.x = -viewWidth/2; startFace.y = -viewHeight/2; startFace.z = vertical ? (-viewHeight/2) : (-viewWidth/2); } /** * @private */ private function createCenteredProjection():PerspectiveProjection { var projection:PerspectiveProjection = new PerspectiveProjection(); projection.fieldOfView = 45; var centerPoint:Point = new Point(viewWidth / 2, viewHeight / 2); if (consolidatedTransition) { centerPoint.x += transitionGroup.x; centerPoint.y += transitionGroup.y; } projection.projectionCenter = centerPoint; return projection; } /** * @private * Initializes our vertical, viewWidth, viewHeight, animatedProperty, * and directionModifier properties. */ private function initTransitionParameters(width:Number, height:Number):void { vertical = (direction == ViewTransitionDirection.DOWN || direction == ViewTransitionDirection.UP); viewWidth = width; viewHeight = height; animatedProperty = vertical ? "rotationX" : "rotationY"; directionModifier = (direction == ViewTransitionDirection.LEFT || direction == ViewTransitionDirection.DOWN) ? 1 : -1; } /** * @private * Initialize are transition parameters, create our temporary * transitionGroup, and parent our view elements. */ private function setupTransition():void { // Initialize temporaries based on our currently flip direction. initTransitionParameters(endView.width, endView.height); // Disable start view layout. startViewProps = { includeInLayout:startView.includeInLayout }; startView.includeInLayout = false; // Disable end view layout. endViewProps = { includeInLayout:endView.includeInLayout, usesAdvancedLayout:(endView._layoutFeatures != null) }; endView.includeInLayout = false; // Save our end view's transform matrix. savedEndMatrix = endView.transform.matrix3D; // Create a temporary transition group to serve as the parent of our // views while flipping. Offset our transition group as necessary to // ensure we flip relative to our center. transitionGroup = new Group(); // Add transition group to the parent of the endView addComponentToContainer(transitionGroup, UIComponent(endView.parent)); // This transition does a lot of reparenting of the views which will cause // multiple ADD and REMOVE events to be dispatched. Since this event was already // dispatched before the transition began, we don't want that setup code // to be run multiple times. So we listen for the events and prevent // them from propagating. if (endView.hasEventListener(FlexEvent.ADD)) endView.addEventListener(FlexEvent.ADD, stopNavigatorEventFromPropagating, false, 1); if (endView.hasEventListener(FlexEvent.REMOVE)) endView.addEventListener(FlexEvent.REMOVE, stopNavigatorEventFromPropagating, false, 1); if (startView.hasEventListener(FlexEvent.ADD)) startView.addEventListener(FlexEvent.ADD, stopNavigatorEventFromPropagating, false, 1); if (startView.hasEventListener(FlexEvent.REMOVE)) startView.addEventListener(FlexEvent.REMOVE, stopNavigatorEventFromPropagating, false, 1); // Reparent the start and end views into the transition group transitionGroup.addElement(endView); transitionGroup.addElement(startView); // Setup our transition group's perspective projection properties. transitionGroup.transform.perspectiveProjection = createCenteredProjection(); } /** * @private */ private function stopNavigatorEventFromPropagating(event:FlexEvent):void { event.stopImmediatePropagation(); } /** * @private * Initialize are transition parameters, create our temporary * transitionGroup, and parent our view elements (for a consolidated * transition). */ private function setupConsolidatedTransition():void { // Initialize temporaries based on our currently flip direction. initTransitionParameters(targetNavigator.width, targetNavigator.height); // Since we are using it to host our transition group, ensure the // parent of our targetNavigator is fully validated, when its not // our transition goes haywire because our bounds are wrong. UIComponent(targetNavigator.parent).validateNow(); // Save the child index of the target navigator navigatorProps.childIndex = getComponentChildIndex(targetNavigator, targetNavigator.parent as UIComponent); // Add a temporary group to contain our snapshot view of the navigator // while we animate. transitionGroup = new Group(); // Size the transitionGroup to match the width and height of the navigator // so that the parent of the targetNavigator's layout remains unchanged transitionGroup.width = targetNavigator.width; transitionGroup.height = targetNavigator.height; transitionGroup.x = targetNavigator.x; transitionGroup.y = targetNavigator.y; // The cached navigator and targetNavigator are currently position at their original // coordinates. Since they will be parented into the transition group that already // takes that into consideration, we want to reposition the components to be at the // origin of the container. targetNavigator.x -= transitionGroup.x; targetNavigator.y -= transitionGroup.y; // Add the transition group at the same index of the target navigator addComponentToContainerAt(transitionGroup, DisplayObject(targetNavigator).parent as UIComponent, navigatorProps.childIndex); transitionGroup.addElement(targetNavigator); cachedNavigator.includeInLayout = false; addCachedElementToGroup(transitionGroup, cachedNavigator, cachedNavigatorGlobalPosition); // Save our end view's transform matrix. savedEndMatrix = targetNavigator.transform.matrix3D; // Setup our transition group's perspective projection properties. transitionGroup.transform.perspectiveProjection = createCenteredProjection(); // Disable layout for our targetNavigator temporarily. navigatorProps.targetNavigatorIncludeInLayout = targetNavigator.includeInLayout; navigatorProps.usesAdvancedLayout = (targetNavigator._layoutFeatures != null); targetNavigator.includeInLayout = false; } /** * @private * For all children of the group, if they are SkinnableComponents: * then applies "scale" to the skin and inverse scale to the component. * If "scale" is "1", then it clears the scale from both the component * and its skin. * * This is used as a work-around for SDK-29118, to force the player to * use texture with size that takes the dpi scale into account. */ private function applyDPIScaleToElements(group:Group, scale:Number):void { var count:int = group.numElements; for (var i:int = 0; i < count; i++) { var element:IVisualElement = group.getElementAt(i); if (element is SkinnableComponent) { var comp:SkinnableComponent = SkinnableComponent(element); comp.skin.scaleX = scale; comp.skin.scaleY = scale; if (scale == 1) { comp.scaleX = scale; comp.scaleY = scale; } else { comp.scaleX /= scale; comp.scaleY /= scale; } comp.validateDisplayList(); } } } //-------------------------------------------------------------------------- // Cleanup related methods. //-------------------------------------------------------------------------- /** * @private * Cleanup method which restores any temporary properties set up * on our view elements. */ override protected function cleanUp():void { // Work-around for SDK-29118 // Clean-up any scale that we have applied to the elements if (dpiScale != 1 && transitionGroup) applyDPIScaleToElements(transitionGroup, 1); // Clean up clipping on the contentGroups of the views if (startView && startView.contentGroup) startView.contentGroup.clipAndEnableScrolling = startViewProps.clipAndEnableScrolling; if (endView && endView.contentGroup) endView.contentGroup.clipAndEnableScrolling = endViewProps.clipAndEnableScrolling; if (!consolidatedTransition && transitionGroup) { if (endView) endView.transform.matrix3D = savedEndMatrix; // Restore our views to their natural location. if (startView && endView) { navigator.contentGroup.addElement(startView); navigator.contentGroup.addElement(endView); } // Remove add handlers added to the views if (endView.hasEventListener(FlexEvent.ADD)) endView.removeEventListener(FlexEvent.ADD, stopNavigatorEventFromPropagating); if (endView.hasEventListener(FlexEvent.REMOVE)) endView.removeEventListener(FlexEvent.REMOVE, stopNavigatorEventFromPropagating); if (startView.hasEventListener(FlexEvent.ADD)) startView.removeEventListener(FlexEvent.ADD, stopNavigatorEventFromPropagating); if (startView.hasEventListener(FlexEvent.REMOVE)) startView.removeEventListener(FlexEvent.REMOVE, stopNavigatorEventFromPropagating); // Extract our temporary transition group. Group(transitionGroup.parent).removeElement(transitionGroup); // Restore startView properties. if (startView) { startView.includeInLayout = startViewProps.includeInLayout; startView.visible = true; startViewProps = null; } // Restore endView properties. if (endView) { endView.includeInLayout = endViewProps.includeInLayout; // Restore the end view to normal layout mode if if // that's the mode it was in before the transition. if (!endViewProps.usesAdvancedLayout) endView.clearAdvancedLayoutFeatures(); endViewProps = null; } } else if (transitionGroup) { if (targetNavigator) targetNavigator.transform.matrix3D = savedEndMatrix; // Restore our views to their natural location. removeComponentFromContainer(targetNavigator as UIComponent, transitionGroup as UIComponent); addComponentToContainerAt(targetNavigator as UIComponent, transitionGroup.parent as UIComponent, navigatorProps.childIndex); removeComponentFromContainer(transitionGroup as UIComponent, transitionGroup.parent as UIComponent); // Restore targetNavigator properties. targetNavigator.includeInLayout = navigatorProps.targetNavigatorIncludeInLayout; // Restore the target navigator to normal layout mode if // that's the mode it was in before the transition. if (!navigatorProps.usesAdvancedLayout) targetNavigator.clearAdvancedLayoutFeatures(); } transitionGroup = null; cachedNavigator = null; flipEffect.removeEventListener("effectUpdate", cubeEffectUpdateHandler); flipEffect.removeEventListener("effectUpdate", effectUpdateHandler); flipEffect = null; super.cleanUp(); } } }