blob: cffc008fd500e83a4125c0a7a67139523e2dd15a [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// 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 mx.collections.IList;
import mx.core.IVisualElement;
import mx.core.mx_internal;
import mx.events.CollectionEvent;
import mx.events.CollectionEventKind;
import mx.events.EffectEvent;
import mx.events.FlexEvent;
import mx.events.ResizeEvent;
import mx.events.SandboxMouseEvent;
import mx.events.TouchInteractionEvent;
import mx.states.OverrideBase;
import spark.components.supportClasses.ListBase;
import spark.effects.Animate;
import spark.events.RendererExistenceEvent;
import spark.layouts.VerticalSpinnerLayout;
import spark.layouts.supportClasses.LayoutBase;
use namespace mx_internal;
[Exclude(name="accentColor", kind="style")]
[Exclude(name="chromeColor", kind="style")]
[Exclude(name="layout", kind="property")]
[Exclude(name="requireSelection", kind="property")]
[Exclude(name="changing", kind="event")]
[Exclude(name="itemRollOut", kind="event")]
[Exclude(name="itemRollOver", kind="event")]
//--------------------------------------
// Other metadata
//--------------------------------------
[DefaultTriggerEvent("change")]
[IconFile("SpinnerList.png")]
/**
* The SpinnerList component displays a list of items. The item in the center of the
* list is always the selectedItem. By default, the list wraps around.
*
* <p>The following images shows an example of a SpinnerList control:</p>
*
* <p>
* <img src="../../images/spinnerlist_example.png" alt="DateSpinner types"/>
* </p>
*
* <p>You can have multiple SpinnerList controls arranged horizontally within a single border by
* wrapping then in a SpinnerListContainer.</p>
*
* @mxml <p>The <code>&lt;s:SpinnerList&gt;</code> tag inherits all of the tag
* attributes of its superclass and adds the following tag attributes:</p>
*
* <pre>
* &lt;s:SpinnerList
* <strong>Properties</strong>
* wrapElements="true|false"
* /&gt;
* </pre>
*
* @see spark.components.SpinnerListContainer
*
* @includeExample examples/SpinnerListExample.mxml -noswf
* @includeExample examples/SpinnerListContainerExample.mxml -noswf
*
* @langversion 3.0
* @playerversion AIR 3
* @productversion Flex 4.6
*/
public class SpinnerList extends ListBase
{
/**
* Constructor.
*
* @langversion 3.0
* @playerversion AIR 3
* @productversion Flex 4.6
*/
public function SpinnerList()
{
super();
addEventListener(TouchInteractionEvent.TOUCH_INTERACTION_END, touchInteractionEnd);
addEventListener(ResizeEvent.RESIZE, resizeHandler);
super.requireSelection = true;
useVirtualLayout = true;
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
mx_internal static const FORCE_NO_WRAP_ELEMENTS_CHANGE:String = "forceNoWrapElementsChange";
mx_internal static const ENABLED_PROPERTY_NAME:String = "_enabled_";
private var scrollToSelection:Boolean = false;
private var numElementsChanged:Boolean = false;
private var dispatchChangeEventOnAnimate:Boolean = false;
/**
* @private
*/
private function get spinnerLayout():VerticalSpinnerLayout
{
return layout as VerticalSpinnerLayout;
}
//--------------------------------------------------------------------------
//
// Skin parts
//
//--------------------------------------------------------------------------
//----------------------------------
// scroller
//----------------------------------
[SkinPart(required="false")]
/**
* The optional Scroller that is used to scroll the List.
*
* @langversion 3.0
* @playerversion AIR 3
* @productversion Flex 4.6
*/
public var scroller:Scroller;
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// wrapElements
//----------------------------------
private var forceNoWrapElements:Boolean = false;
private var _wrapElements:Boolean = true;
private var wrapElementsChanged:Boolean = false;
/**
* When true, scrolling past the last element scrolls to the first element.
*
* @langversion 3.0
* @playerversion AIR 3
* @productversion Flex 4.6
*
* @default true
*/
public function get wrapElements():Boolean
{
if (forceNoWrapElements)
return false;
else
return _wrapElements;
}
/**
* @private
*/
public function set wrapElements(value:Boolean):void
{
if (_wrapElements == value)
return;
_wrapElements = value;
wrapElementsChanged = true;
invalidateProperties();
}
//--------------------------------------------------------------------------
//
// Overridden Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// caretIndex
//----------------------------------
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion AIR 3
* @productversion Flex 4.6
*/
override public function get caretIndex():Number
{
// CaretIndex is always equivalent to the selectedIndex
return selectedIndex;
}
//----------------------------------
// layout
//----------------------------------
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion AIR 3
* @productversion Flex 4.6
*/
override public function set layout(value:LayoutBase):void
{
// Layout is not allowed to be set
return;
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion AIR 3
* @productversion Flex 4.6
*/
override public function set requireSelection(value:Boolean):void
{
// Selection is always required
return;
}
/**
* @copy spark.components.DataGroup#dataProvider
*
* @langversion 3.0
* @playerversion AIR 3
* @productversion Flex 4.6
*/
override public function set dataProvider(value:IList):void
{
super.dataProvider = value;
numElementsChanged = true;
invalidateProperties();
}
//--------------------------------------------------------------------------
//
// Overridden methods
//
//--------------------------------------------------------------------------
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion AIR 3
* @productversion Flex 4.6
*/
override protected function commitProperties():void
{
super.commitProperties();
if (numElementsChanged)
{
numElementsChanged = false;
// When the DP changes, the DataGroup sets the scroll position to 0
// So we need to wait for updateComplete before resetting the scroll position to
// display the selected item in the center
if (!scrollToSelection)
{
scrollToSelection = true;
addEventListener(FlexEvent.UPDATE_COMPLETE, updateCompleteHandler);
}
}
if (wrapElementsChanged)
{
if (spinnerLayout)
{
spinnerLayout.requestedWrapElements = _wrapElements;
}
if (scroller)
{
scroller.pullEnabled = !wrapElements;
scroller.bounceEnabled = !wrapElements;
}
wrapElementsChanged = false;
}
}
/**
* @private
*
* @langversion 3.0
* @playerversion AIR 3
* @productversion Flex 4.6
*/
override protected function commitSelection(dispatchChangedEvents:Boolean=true):Boolean
{
var result:Boolean = super.commitSelection(dispatchChangedEvents);
// SnapElement requires a layout pass in order to properly center the selection
// The listener for updateComplete calls commitSelection
if (initialized && spinnerLayout)
scroller.snapElement(spinnerLayout.getClosestUnwrappedElementIndex(selectedIndex), false);
return result;
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion AIR 3
* @productversion Flex 4.6
*/
override protected function partAdded(partName:String, instance:Object):void
{
super.partAdded(partName, instance);
if (instance == scroller)
{
scroller.pullEnabled = !wrapElements;
scroller.bounceEnabled = !wrapElements;
scroller.scrollSnappingMode = ScrollSnappingMode.CENTER;
}
else if (instance == dataGroup)
{
if (dataGroup.layout)
dataGroup.layout.addEventListener(FORCE_NO_WRAP_ELEMENTS_CHANGE, forceNoWrapElementsChangeHandler);
}
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion AIR 3
* @productversion Flex 4.6
*/
override protected function partRemoved(partName:String, instance:Object):void
{
super.partRemoved(partName, instance);
if (instance == dataGroup)
{
if (dataGroup.layout)
dataGroup.layout.removeEventListener(FORCE_NO_WRAP_ELEMENTS_CHANGE, forceNoWrapElementsChangeHandler);
}
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* @private
* Animate a spin from the current position to the new index.
*/
mx_internal function animateToSelectedIndex(index:int, dispatchChangeEvent:Boolean = false):void
{
if (scroller && spinnerLayout)
{
var animate:Animate = scroller.snapElement(
spinnerLayout.getClosestUnwrappedElementIndex(index), true);
if (animate)
{
animate.addEventListener(EffectEvent.EFFECT_END, animateToIndex_effectEndHandler);
dispatchChangeEventOnAnimate = dispatchChangeEvent;
}
}
}
override protected function itemSelected(index:int, selected:Boolean):void
{
super.itemSelected(index, selected);
var renderer:Object = dataGroup ? dataGroup.getElementAt(index) : null;
if (renderer is IItemRenderer)
{
IItemRenderer(renderer).selected = selected;
}
}
/**
* @private
*/
mx_internal function animateToIndex_effectEndHandler(event:EffectEvent):void
{
if (spinnerLayout)
{
// Commit the center item as the selectedItem
var centerElementIndex:int = spinnerLayout.getIndexAtVerticalCenter();
if (dispatchChangeEventOnAnimate)
setSelectedIndex(centerElementIndex, true);
else
selectedIndex = centerElementIndex;
}
Animate(event.currentTarget).removeEventListener(EffectEvent.EFFECT_END, animateToIndex_effectEndHandler);
}
//--------------------------------------------------------------------------
//
// Event handlers
//
//--------------------------------------------------------------------------
/**
* @private
*/
private function resizeHandler(event:ResizeEvent):void
{
if ((event.oldWidth != width || event.oldHeight != height) && !scrollToSelection)
{
scrollToSelection = true;
addEventListener(FlexEvent.UPDATE_COMPLETE, updateCompleteHandler);
}
}
/**
* @private
*/
private function touchInteractionEnd(event:TouchInteractionEvent):void
{
// Commit the center item as the selectedItem when the scroll has completed
if (spinnerLayout)
{
var centerElementIndex:int = spinnerLayout.getIndexAtVerticalCenter();
setSelectedIndex(centerElementIndex, true);
}
}
/**
* @private
*/
override protected function dataProvider_collectionChangeHandler(event:Event):void
{
super.dataProvider_collectionChangeHandler(event);
if (event is CollectionEvent)
{
var ce:CollectionEvent = CollectionEvent(event);
if (ce.kind == CollectionEventKind.ADD ||
ce.kind == CollectionEventKind.REMOVE ||
ce.kind == CollectionEventKind.RESET ||
ce.kind == CollectionEventKind.REFRESH)
{
numElementsChanged = true;
invalidateProperties();
}
}
}
/**
* @private
* Called when an item has been added to this component.
*/
override protected function dataGroup_rendererAddHandler(event:RendererExistenceEvent):void
{
super.dataGroup_rendererAddHandler(event);
var renderer:IVisualElement = event.renderer;
if (!renderer)
return;
renderer.addEventListener(MouseEvent.CLICK, item_mouseClickHandler);
}
/**
* @private
* Called when an item has been removed from this component.
*/
override protected function dataGroup_rendererRemoveHandler(event:RendererExistenceEvent):void
{
super.dataGroup_rendererRemoveHandler(event);
var renderer:Object = event.renderer;
if (!renderer)
return;
renderer.removeEventListener(MouseEvent.CLICK, item_mouseClickHandler);
}
/**
* @private
* When an item is clicked, animate it to the center
*/
private function item_mouseClickHandler(event:MouseEvent):void
{
var newIndex:int;
if (event.currentTarget is IItemRenderer)
newIndex = IItemRenderer(event.currentTarget).itemIndex;
else
newIndex = dataGroup.getElementIndex(event.currentTarget as IVisualElement);
// If an item is disabled, then don't animate to that item
if (event.currentTarget["enabled"] == undefined ||
event.currentTarget["enabled"] == true)
animateToSelectedIndex(newIndex, true);
}
/**
* @private
* Animate the selectedItem to the center once we have performed a layout pass
*/
private function updateCompleteHandler(event:FlexEvent):void
{
if (scrollToSelection && spinnerLayout)
{
scrollToSelection = false;
scroller.snapElement(spinnerLayout.getClosestUnwrappedElementIndex(selectedIndex), false);
removeEventListener(FlexEvent.UPDATE_COMPLETE, updateCompleteHandler);
}
}
/**
* @private
* Called if the layout has automatically switched wrap modes.
*/
private function forceNoWrapElementsChangeHandler(event:Event):void
{
invalidateProperties();
forceNoWrapElements = spinnerLayout.forceNoWrapElements;
wrapElementsChanged = true;
}
}
}