//////////////////////////////////////////////////////////////////////////////// // // 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.DisplayObjectContainer; import flash.display.StageAspectRatio; import flash.events.Event; import mx.core.EventPriority; import mx.core.FlexGlobals; import mx.core.IDeferredInstance; import mx.core.mx_internal; import mx.events.FlexEvent; import mx.events.ResizeEvent; import spark.components.supportClasses.ViewNavigatorBase; import spark.events.ElementExistenceEvent; import spark.events.PopUpEvent; import spark.transitions.ViewTransitionBase; use namespace mx_internal; //-------------------------------------- // SkinStates //-------------------------------------- /** * The skin state when the aspectRatio of the main * application is portrait. * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ [SkinState("portrait")] /** * The skin state when the aspectRatio of the main * application is landscape. * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ [SkinState("landscape")] //-------------------------------------- // Other metadata //-------------------------------------- [IconFile("SplitViewNavigator.png")] /** * The SplitViewNavigator container displays multiple * ViewNavigator or TabbedViewNavigator components at the same time in a single container. * Each child of the SplitViewNavigator defines one pane of the screen. * Because its children are view navigators, the view navigator for each * pane contains its own view stack and action bar. * *

The following image shows the SplitViewNavigator container in a * horizontal layout with a master and detail pane:

* *

* SplitViewNavigator *

* *

SplitViewNavigator takes ViewnNavigatorBase objects as children, and lays them out as * defined by its layout property. * This component is useful for creating a master/detail interface on a mobile device. * There is no limit to the amount of child navigators this component can manage.

* *

Note: Because of the screen space required to display multiple panes * simultaneously, Adobe recommends that you only use the SplitViewNavigator on a tablet * device.

* *

If the autoHideFirstViewNavigator property is set to * true, the SplitViewNavigator automatically hides the * navigator that is at index 0 when the top level application has a * portrait aspect ratio. * The navigator reappears when the application is reoriented to a landsacpe aspect ratio. * You can manually hide and show a navigator by toggling the visble * flag on the child. When set, the includeInLayout property * of that child will be set to match.

* *

SplitViewNavigator lets you display the first navigator in a * popup. By default, it uses a Callout component. * When you call the showFirstViewNavigatorInPopUp() method, the * first navigator is displayed in a popup until the * hideViewNavigatorPopUp() method is called, or the user * touches outside the bounds of the popup. * It is important to note that when a navigator is displayed in a popup, * it is reparented as a child of that container. * This means that using IVisualElementContainer methods such as * getElementAt() on SplitViewNavigator may not return the * expected results. * If you want to access the first or second navigator, Adobe recommends * that you use the getViewNavigatorAt() method. * This method always returns the correct navigator regardless of whether * a navigator is in a popup or not.

* *

By default, when the popup is opened, it is sized to the preferred width * and height of the first navigator. The size of the popup can be changed by * explicitly setting its width and height or by reskinning the component.

* *

Note: When a SplitViewNavigator is used as a child of a * TabbedViewNavigator, changes to the tabBarVisible on the * active views will not be honored by the parent TabbedViewNavigator.

* * @mxml

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

* *
 *  <s:SplitViewNavigator
 *    Properties
 *    autoHideFirstViewNavigator="false"
 *  />
 *  
* * @see spark.skins.mobile.SplitViewNavigatorSkin * @see spark.components.supportClasses.ViewNavigatorBase * @see spark.components.ViewNavigator * @see spark.components.TabbedViewNavigator * @see spark.components.Application#aspectRatio * @see spark.components.Callout * @see spark.components.SkinnablePopUpContainer * * @includeExample examples/SplitViewNavigatorExample.mxml -noswf * @includeExample examples/views/DetailView.mxml -noswf * @includeExample examples/views/MasterView.mxml -noswf * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ public class SplitViewNavigator extends ViewNavigatorBase { //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * Constructor. * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ public function SplitViewNavigator() { super(); } //-------------------------------------------------------------------------- // // Skin Parts // //-------------------------------------------------------------------------- [SkinPart(required="false")] /** * The popUp used to display a navigator when * showFirstViewNavigatorInPopUp() is called. * When creating a custom MXML skin, this component should not be on the display list, * but instead declared inside a fx:Declarations tag. * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ public var viewNavigatorPopUp:SkinnablePopUpContainer; //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- /** * @private * Tracks what the last aspectRatio of the application was to determine * whether the component should hide or show the first navigator when * autoHideFirstNavigator is set to true. */ private var lastAspectRatio:String; /** * @private * Stores the original element index of the navigator that is reparented * into the popup. */ private var _popUpNavigatorIndex:int = -1; /** * @private * A reference to the navigator that is being displayed inside the popUp */ private var _popUpNavigator:ViewNavigatorBase = null; /** * @private * Object is used to store old layout constraints of a view navigator * before opening it in a popup. */ private var _popUpNavigatorSizeCache:Object = null; //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- //---------------------------------- // autoHideFirstViewNavigator //---------------------------------- private var _autoHideFirstViewNavigator:Boolean = false; [Inspectable(category="General", defaultValue="false")] /** * Specifies whether the visibility of the first view navigator should * automatically be toggled when the device receives an orientation change event. * When true, the first navigator is hidden as the device enters the portrait * orientation, and shown when entering a landscape orientation. * *

If this flag is set to true while the application is in a portrait * orientation, the navigator is not hidden until the next orientation * resize event.

* * @default false * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ public function get autoHideFirstViewNavigator():Boolean { return _autoHideFirstViewNavigator; } /** * @private */ public function set autoHideFirstViewNavigator(value:Boolean):void { if (_autoHideFirstViewNavigator == value) return; _autoHideFirstViewNavigator = value; } //-------------------------------------------------------------------------- // // Overridden Properties // //-------------------------------------------------------------------------- //---------------------------------- // mxmlContentFactory //---------------------------------- [InstanceType("Array")] [ArrayElementType("spark.components.supportClasses.ViewNavigatorBase")] /** * @private * Override this setter so that we can restrict the mxmlContent type to * ViewNavigatorBase. */ override public function set mxmlContentFactory(value:IDeferredInstance):void { super.mxmlContentFactory = value; } //---------------------------------- // initialized //---------------------------------- /** * @private */ override public function set initialized(value:Boolean):void { // Add application resize event listener. We want this navigator's // resize handler to run before any others due to conflicts with // states. See SDK-31575. FlexGlobals.topLevelApplication.addEventListener(ResizeEvent.RESIZE, application_resizeHandler, false, EventPriority.BINDING, true); // Toggle visibility of the first ViewNavigator if autoHideFirstViewNavigator is true if (numViewNavigators > 0 && autoHideFirstViewNavigator) application_resizeHandler(null); super.initialized = value; } //---------------------------------- // numViewNavigators //---------------------------------- /** * The number of view navigators managed by this container. * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ public function get numViewNavigators():int { return _popUpNavigatorIndex == -1 ? numElements : numElements + 1; } //-------------------------------------------------------------------------- // // Public Methods // //-------------------------------------------------------------------------- /** * Returns a specific child navigator independent of the container's * child display hierarchy. * Since a child navigator is not parented by this * container when visible inside a popup, this method should be used instead of * getElementAt(). * *

When a popup is open, the navigator at index 0 refers to the * navigator in the popup.

* * @param index Index of the navigator to retrieve. * * @return The navigator at the specified index, null if one does not exist. * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ public function getViewNavigatorAt(index:int):ViewNavigatorBase { // If the index is the navigator currently in the popup, return it if (_popUpNavigatorIndex == index) return _popUpNavigator; // Since a popup is visible, one of the navigators has been removed // from this display tree. If the index of the navigator in the popup // is before the passed index, decrement it by 1 so that it reflects // the current indicies of the child navigatos. if (_popUpNavigatorIndex != -1 && _popUpNavigatorIndex < index) index--; if (index >= numElements) return null; return getElementAt(index) as ViewNavigatorBase; } /** * Displays the child navigator at index 0 inside a popup. * The navigator is reparented as a child of the popop. * *

Since the navigator is reparented, using getElementAt(0) * does not return the first navigator, but returns the second. * Adobe recommends that you use the getViewNavigatorAt() * method to access the navigators.

* *

The popup height is not set by this component. The height sizes to * fit the View currently active in the ViewNavigator. A height can be set * by reskinning this component or by manually setting the height * property on the viewNavigatorPopUp skinPart.

* *

If the popup is already open or the popup skin part does not exist, * this method does nothing.

* *

It is recommended that this method only be used on ViewNavigators * that are currently hidden.

* * @param owner The owner of the popup. The popup is positioned relative to * its owner. * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ public function showFirstViewNavigatorInPopUp(owner:DisplayObjectContainer):void { showNavigatorAtIndexInPopUp(0, owner); } /** * Hides the navigator popup if its open. The navigator that was * displayed in the popup is reparented as a child of this * SplitViewNavigator. * *

This method is automatically called if the user touches outside * of the popup when it is visible. The navigator inside the popup * is hidden after the popup closes.

* *

After closing the popup, the navigator that was shown remains * invisible unless autoHideFirstViewNavigator is true * and the device orientation is landscape. * In all other cases, the visibility of the first navigator needs to be set * to true to make it visible again.

* * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ public function hideViewNavigatorPopUp():void { if (!viewNavigatorPopUp|| !viewNavigatorPopUp.isOpen) return; // Since view transitions can temporarily alter the display tree, we // need to end all view transitions so that the display tree is restored // to a correct state. ViewTransitionBase.endTransitions(); // Cleanup handled by viewNavigatorPopUp_closeHandler viewNavigatorPopUp.close(true); } //-------------------------------------------------------------------------- // // Private Methods // //-------------------------------------------------------------------------- /** * @private * Shows the navigator at the specified index in the popup component. */ mx_internal function showNavigatorAtIndexInPopUp(index:int, owner:DisplayObjectContainer):void { if (index >= numElements || !viewNavigatorPopUp|| viewNavigatorPopUp.isOpen) return; // Since view transitions can temporarily alter the display tree, we // need to end all view transitions so that the display tree is restored // to a correct state. ViewTransitionBase.endTransitions(); _popUpNavigatorIndex = index; _popUpNavigator = getElementAt(index) as ViewNavigatorBase; // Save navigators current layout constraints _popUpNavigatorSizeCache = { percentWidth: _popUpNavigator.percentWidth, percentHeight: _popUpNavigator.percentHeight, explicitWidth: _popUpNavigator.explicitWidth, explicitHeight: _popUpNavigator.explicitHeight }; // If an explict width or height is not set on the navigator, change // the percent bounds to 100% so that the navigator fills the entire // bounds of the popup. if (isNaN(_popUpNavigator.explicitWidth)) _popUpNavigator.percentWidth = 100; if (isNaN(_popUpNavigator.explicitHeight)) _popUpNavigator.percentHeight = 100; viewNavigatorPopUp.addEventListener(PopUpEvent.OPEN, viewNavigatorPopUp_openHandler); viewNavigatorPopUp.addEventListener(PopUpEvent.CLOSE, viewNavigatorPopUp_closeHandler); viewNavigatorPopUp.addEventListener('mouseDownOutside', navigatorPopUp_mouseDownOutsideHandler, false, 0, true); viewNavigatorPopUp.addElement(_popUpNavigator); // Make sure the first navigator is visible _popUpNavigator.visible = true; // Open the popup viewNavigatorPopUp.open(owner, true); } /** * @private */ private function viewNavigatorPopUp_openHandler(event:Event):void { event.target.removeEventListener(PopUpEvent.OPEN, viewNavigatorPopUp_openHandler); // Since an open transition may have the disableLayout property set to true, // the popup size may not be valid yet. Force a validation to ensure that the // component is properly laid out. viewNavigatorPopUp.validateNow(); _popUpNavigatorSizeCache.viewNavigatorPopUpWidth = viewNavigatorPopUp.explicitWidth; _popUpNavigatorSizeCache.viewNavigatorPopUpHeight = viewNavigatorPopUp.explicitHeight; // If the width or height of the popup isn't explicitly set, we size the // popup so that the size doesn't change as the active view of the navigator // changes. if (isNaN(viewNavigatorPopUp.explicitWidth)) viewNavigatorPopUp.explicitWidth = viewNavigatorPopUp.width; if (isNaN(viewNavigatorPopUp.explicitHeight)) viewNavigatorPopUp.explicitHeight = viewNavigatorPopUp.height; } /** * @private */ private function viewNavigatorPopUp_closeHandler(event:PopUpEvent):void { viewNavigatorPopUp.removeEventListener(PopUpEvent.CLOSE, viewNavigatorPopUp_closeHandler); viewNavigatorPopUp.removeEventListener('mouseDownOutside', navigatorPopUp_mouseDownOutsideHandler); if (_popUpNavigator) restoreNavigatorInPopUp(); // When an orientation change occurs, the popup's visibility may be set to false. // Set it back to true so that it is visible the next time it is opened. viewNavigatorPopUp.visible = true; } /** * @private */ private function restoreNavigatorInPopUp():void { // Restore old layout constraints viewNavigatorPopUp.explicitWidth = _popUpNavigatorSizeCache.viewNavigatorPopUpWidth; viewNavigatorPopUp.explicitHeight = _popUpNavigatorSizeCache.viewNavigatorPopUpHeight; _popUpNavigator.percentWidth = _popUpNavigatorSizeCache.percentWidth; _popUpNavigator.percentHeight = _popUpNavigatorSizeCache.percentHeight; _popUpNavigator.explicitWidth = _popUpNavigatorSizeCache.explicitWidth; _popUpNavigator.explicitHeight = _popUpNavigatorSizeCache.explicitHeight; _popUpNavigatorSizeCache = null; // Restore navigator parent addElementAt(_popUpNavigator, _popUpNavigatorIndex); if (autoHideFirstViewNavigator && _popUpNavigatorIndex == 0) { toggleFirstNavigatorVisibility(); } else { _popUpNavigator.visible = false; } _popUpNavigator = null; _popUpNavigatorIndex = -1; } /** * @private */ private function elementAddHandler(event:ElementExistenceEvent):void { var navigator:ViewNavigatorBase = event.element as ViewNavigatorBase; if (navigator) setupNavigator(navigator); } /** * @private */ private function elementRemoveHandler(event:ElementExistenceEvent):void { if (event.element != _popUpNavigator) { var navigator:ViewNavigatorBase = event.element as ViewNavigatorBase; if (navigator) cleanUpNavigator(event.element as ViewNavigatorBase); } } /** * @private */ mx_internal function application_resizeHandler(event:ResizeEvent):void { if (numViewNavigators == 0) return; var aspectRatio:String = FlexGlobals.topLevelApplication.aspectRatio; // Only do the logic below if the aspectRatio has changed if (lastAspectRatio == aspectRatio) return; lastAspectRatio = aspectRatio; // Since view transitions can temporarily alter the display tree, we // need to end all view transitions so that the display tree is restored // to a correct state. ViewTransitionBase.endTransitions(); if (autoHideFirstViewNavigator) { // The navigator in the popup needs to be immediately reparented // when the orientation changes since we don't provide orientation // effects. Because of this, we don't want to see any close // transitions to play on the popup. Since the transitions are // defined on the skin, there is no way to intercept them, so instead // the popup is hidden. restoreNavigatorInPopUp() will call // toggleFirstNavigatorVisibility() if needed. if (_popUpNavigator) { restoreNavigatorInPopUp(); viewNavigatorPopUp.visible = false; } else { toggleFirstNavigatorVisibility(); } } // The navigator popup is always hidden if the orientation changes hideViewNavigatorPopUp(); } /** * @private */ private function toggleFirstNavigatorVisibility():void { if (numViewNavigators == 0) return; var aspectRatio:String = FlexGlobals.topLevelApplication.aspectRatio; var firstViewNavigator:ViewNavigatorBase = getViewNavigatorAt(0); if (aspectRatio == StageAspectRatio.PORTRAIT) { firstViewNavigator.visible = false; } else { firstViewNavigator.visible = true; } } /** * @private */ private function navigatorPopUp_mouseDownOutsideHandler(event:Event):void { hideViewNavigatorPopUp(); } /** * @private */ private function setupNavigator(navigator:ViewNavigatorBase):void { navigator.setParentNavigator(this); // Add weak listeners for hide and show events on the navigator. When // a navigator is hidden, its container is removed from layout. navigator.addEventListener(FlexEvent.HIDE, navigator_visibilityChangedHandler, false, EventPriority.DEFAULT, true); navigator.addEventListener(FlexEvent.SHOW, navigator_visibilityChangedHandler, false, EventPriority.DEFAULT, true); // Remove the navigator from layout if it isn't visible if (navigator.visible == false) navigator.includeInLayout = false; } /** * @private */ private function cleanUpNavigator(navigator:ViewNavigatorBase):void { navigator.setParentNavigator(null); navigator.removeEventListener(FlexEvent.HIDE, navigator_visibilityChangedHandler); navigator.removeEventListener(FlexEvent.SHOW, navigator_visibilityChangedHandler); } /** * @private */ private function navigator_visibilityChangedHandler(event:FlexEvent):void { var navigator:ViewNavigatorBase = event.target as ViewNavigatorBase; if (event.type == FlexEvent.HIDE && navigator == _popUpNavigator) hideViewNavigatorPopUp(); navigator.includeInLayout = navigator.visible; } //-------------------------------------------------------------------------- // // Overriden Methods: ViewNavigatorBase // //-------------------------------------------------------------------------- /** * @private */ override public function updateControlsForView(view:View):void { // This method is a no-op so that child views can't impact the // display properties of navigators above it. This essentially prevents // View.tabBarVisible from interacting with TabbedViewNavigators that // are parents of this SplitViewNavigator. } /** * @private * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ override public function loadViewData(value:Object):void { super.loadViewData(value); var dataArray:Array = value.dataArray; // If the data array is null, we won't restore any data if (!dataArray) return; // Restore each navigators' persistence data for (var i:int = 0; i < numViewNavigators; i++) { if (i >= dataArray.length) break; getViewNavigatorAt(i).loadViewData(dataArray[i]); } } /** * @private * * @langversion 3.0 * @playerversion AIR 3 * @productversion Flex 4.6 */ override public function saveViewData():Object { var object:Object = super.saveViewData(); var dataArray:Array = new Array(); // Push each navigator's persistence object to the data array for (var i:int = 0; i < numViewNavigators; i++) dataArray.push(getViewNavigatorAt(i).saveViewData()); object.dataArray = dataArray; return object; } //-------------------------------------------------------------------------- // // Overridden Methods: UIComponent // //-------------------------------------------------------------------------- /** * @private */ override protected function partAdded(partName:String, instance:Object):void { // Add event listeners before the child are added to the contentGroup if (instance == contentGroup) { contentGroup.addEventListener(ElementExistenceEvent.ELEMENT_ADD, elementAddHandler); contentGroup.addEventListener(ElementExistenceEvent.ELEMENT_REMOVE, elementRemoveHandler); } super.partAdded(partName, instance); } /** * @private */ override protected function partRemoved(partName:String, instance:Object):void { super.partRemoved(partName, instance); if (instance == contentGroup) { contentGroup.removeEventListener(ElementExistenceEvent.ELEMENT_ADD, elementAddHandler); contentGroup.removeEventListener(ElementExistenceEvent.ELEMENT_REMOVE, elementRemoveHandler); } } /** * @private */ override public function validateNow():void { // If a navigator is currently inside a popup, force a validation on it as well. // This was added because ViewNavigator will call validateNow on its parentNavigator // when preparing to do a view transition. Since the popup navigator is no longer // a child of this SplitViewNavigator, the navigator in the popup isn't validated // as expected. if (_popUpNavigatorIndex != -1) _popUpNavigator.validateNow(); super.validateNow(); } } }