blob: 3ca9155901ff9d261598e4c2ab09b3bca9688af4 [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.display.DisplayObject;
import flash.events.Event;
import flash.events.FocusEvent;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.events.SoftKeyboardEvent;
import flash.events.TimerEvent;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.system.Capabilities;
import flash.text.TextField;
import flash.ui.Keyboard;
import flash.utils.Timer;
import mx.core.EventPriority;
import mx.core.FlexGlobals;
import mx.core.IFactory;
import mx.core.IInvalidating;
import mx.core.IVisualElement;
import mx.core.IVisualElementContainer;
import mx.core.InteractionMode;
import mx.core.LayoutDirection;
import mx.core.UIComponent;
import mx.core.mx_internal;
import mx.events.EffectEvent;
import mx.events.FlexEvent;
import mx.events.FlexMouseEvent;
import mx.events.PropertyChangeEvent;
import mx.events.TouchInteractionEvent;
import mx.managers.IFocusManager;
import mx.managers.IFocusManagerComponent;
import mx.styles.IStyleClient;
import spark.components.supportClasses.GroupBase;
import spark.components.supportClasses.ScrollerLayout;
import spark.components.supportClasses.SkinnableComponent;
import spark.components.supportClasses.TouchScrollHelper;
import spark.core.IGraphicElement;
import spark.core.IViewport;
import spark.core.NavigationUnit;
import spark.effects.Animate;
import spark.effects.ThrowEffect;
import spark.effects.animation.MotionPath;
import spark.effects.animation.SimpleMotionPath;
import spark.events.CaretBoundsChangeEvent;
import spark.layouts.supportClasses.LayoutBase;
import spark.utils.MouseEventUtil;
use namespace mx_internal;
include "../styles/metadata/BasicInheritingTextStyles.as"
include "../styles/metadata/AdvancedInheritingTextStyles.as"
include "../styles/metadata/SelectionFormatTextStyles.as"
//--------------------------------------
// Events
//--------------------------------------
/**
* Dispatched when the scroll position is going to change due to a
* <code>mouseWheel</code> event.
*
* <p>If there is a visible verticalScrollBar, then by default
* the viewport is scrolled vertically by <code>event.delta</code> "steps".
* The height of the step is determined by the viewport's
* <code>getVerticalScrollPositionDelta</code> method using
* either <code>UP</code> or <code>DOWN</code>, depending on the scroll
* direction.</p>
*
* <p>Otherwise, if there is a visible horizontalScrollBar, then by default
* the viewport is scrolled horizontally by <code>event.delta</code> "steps".
* The width of the step is determined by the viewport's
* <code>getHorizontalScrollPositionDelta</code> method using
* either <code>LEFT</code> or <code>RIGHT</code>, depending on the scroll
* direction.</p>
*
* <p>Calling the <code>preventDefault()</code> method
* on the event prevents the scroll position from changing.
* Otherwise if you modify the <code>delta</code> property of the event,
* that value will be used as the number of "steps".</p>
*
* @eventType mx.events.FlexMouseEvent.MOUSE_WHEEL_CHANGING
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
[Event(name="mouseWheelChanging", type="mx.events.FlexMouseEvent")]
//--------------------------------------
// Styles
//--------------------------------------
/**
* @copy spark.components.supportClasses.GroupBase#style:alternatingItemColors
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="alternatingItemColors", type="Array", arrayType="uint", format="Color", inherit="yes", theme="spark, mobile")]
/**
* The alpha of the content background for this component.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="contentBackgroundAlpha", type="Number", inherit="yes", theme="spark, mobile")]
/**
* @copy spark.components.supportClasses.GroupBase#style:contentBackgroundColor
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="contentBackgroundColor", type="uint", format="Color", inherit="yes", theme="spark, mobile")]
/**
* @copy spark.components.supportClasses.GroupBase#style:downColor
*
* @langversion 3.0
* @playerversion Flash 10.1
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
[Style(name="downColor", type="uint", format="Color", inherit="yes", theme="mobile")]
/**
* @copy spark.components.supportClasses.GroupBase#style:focusColor
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="focusColor", type="uint", format="Color", inherit="yes", theme="spark, mobile")]
/**
* Indicates under what conditions the horizontal scroll bar is displayed.
*
* <ul>
* <li>
* <code>ScrollPolicy.ON</code> ("on") - the scroll bar is always displayed.
* </li>
* <li>
* <code>ScrollPolicy.OFF</code> ("off") - the scroll bar is never displayed.
* The viewport can still be scrolled programmatically, by setting its
* horizontalScrollPosition property.
* </li>
* <li>
* <code>ScrollPolicy.AUTO</code> ("auto") - the scroll bar is displayed when
* the viewport's contentWidth is larger than its width.
* </li>
* </ul>
*
* <p>
* The scroll policy affects the measured size of the Scroller component.
* </p>
*
* @default ScrollPolicy.AUTO
*
* @see mx.core.ScrollPolicy
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="horizontalScrollPolicy", type="String", inherit="no", enumeration="off,on,auto")]
/**
* A proxy for the <code>liveDragging</code> style of the scrollbars
* used by the Scroller component.
*
* <p>If this style is set to <code>true</code>, then the
* <code>liveDragging</code> styles are set to <code>true</code> (the default).
* That means dragging a scrollbar thumb immediately updates the viewport's scroll position.
* If this style is set to <code>false</code>, then the <code>liveDragging</code> styles
* are set to <code>false</code>.
* That means when a scrollbar thumb is dragged the viewport's scroll position is only updated
* then the mouse button is released.</p>
*
* <p>Setting this style to <code>false</code> can be helpful
* when updating the viewport's display is so
* expensive that "liveDragging" performs poorly.</p>
*
* <p>By default this style is <code>undefined</code>, which means that
* the <code>liveDragging</code> styles are not modified.</p>
*
* @default undefined
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="liveScrolling", type="Boolean", inherit="no")]
/**
* @copy spark.components.supportClasses.GroupBase#style:rollOverColor
*
* @default 0xCEDBEF
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="rollOverColor", type="uint", format="Color", inherit="yes", theme="spark")]
/**
* @copy spark.components.supportClasses.GroupBase#style:symbolColor
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="symbolColor", type="uint", format="Color", inherit="yes", theme="spark, mobile")]
/**
* @copy spark.components.supportClasses.GroupBase#style:touchDelay
*
* @langversion 3.0
* @playerversion Flash 10.1
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
[Style(name="touchDelay", type="Number", format="Time", inherit="yes", minValue="0.0")]
/**
* Indicates under what conditions the vertical scroll bar is displayed.
*
* <ul>
* <li>
* <code>ScrollPolicy.ON</code> ("on") - the scroll bar is always displayed.
* </li>
* <li>
* <code>ScrollPolicy.OFF</code> ("off") - the scroll bar is never displayed.
* The viewport can still be scrolled programmatically, by setting its
* verticalScrollPosition property.
* </li>
* <li>
* <code>ScrollPolicy.AUTO</code> ("auto") - the scroll bar is displayed when
* the viewport's contentHeight is larger than its height.
* </li>
* </ul>
*
* <p>
* The scroll policy affects the measured size of the Scroller component.
* </p>
*
* @default ScrollPolicy.AUTO
*
* @see mx.core.ScrollPolicy
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="verticalScrollPolicy", type="String", inherit="no", enumeration="off,on,auto")]
//--------------------------------------
// Other metadata
//--------------------------------------
[ResourceBundle("components")]
[DefaultProperty("viewport")]
[IconFile("Scroller.png")]
/**
* The Scroller component displays a single scrollable component,
* called a viewport, and horizontal and vertical scroll bars.
* The viewport must implement the IViewport interface. Its skin
* must be a derivative of the Group class.
*
* <p>The Spark Group, DataGroup, and RichEditableText components implement
* the IViewport interface and can be used as the children of the Scroller control,
* as the following example shows:</p>
*
* <pre>
* &lt;s:Scroller width="100" height="100"&gt;
* &lt;s:Group&gt;
* &lt;mx:Image width="300" height="400"
* source="&#64;Embed(source='assets/logo.jpg')"/&gt;
* &lt;/s:Group&gt;
* &lt;/s:Scroller&gt;</pre>
*
* <p>The size of the Image control is set larger than that of its parent Group container.
* By default, the child extends past the boundaries of the parent container.
* Rather than allow the child to extend past the boundaries of the parent container,
* the Scroller specifies to clip the child to the boundaries and display scroll bars.</p>
*
* <p>Not all Spark containers implement the IViewPort interface.
* Therefore, those containers, such as the BorderContainer and SkinnableContainer containers,
* cannot be used as the direct child of the Scroller component.
* However, all Spark containers can have a Scroller component as a child component.
* For example, to use scroll bars on a child of the Spark BorderContainer,
* wrap the child in a Scroller component. </p>
*
* <p>To make the entire BorderContainer scrollable, wrap it in a Group container.
* Then, make the Group container the child of the Scroller component,
* For skinnable Spark containers that do not implement the IViewport interface,
* you can also create a custom skin for the container that
* includes the Scroller component. </p>
*
* <p>The IViewport interface defines a viewport for the components that implement it.
* A viewport is a rectangular subset of the area of a container that you want to display,
* rather than displaying the entire container.
* The scroll bars control the viewport's <code>horizontalScrollPosition</code> and
* <code>verticalScrollPosition</code> properties.
* scroll bars make it possible to view the area defined by the viewport's
* <code>contentWidth</code> and <code>contentHeight</code> properties.</p>
*
* <p>You can directly set properties on the component wrapped by the Scroller by
* using the <code>Scroller.viewport</code> property.
* For example, you can modify the viewport's <code>horizontalScrollPosition</code> and
* <code>verticalScrollPosition</code> properties.</p>
*
* <p>To directly access the scroll bar instances, either HScrollBar or VScrollBar,
* created by the Scroller, use the <code>Scroller.horizontalScrollBar</code> and
* <code>Scroller.verticalScrollBar</code> properties.</p>
*
* <p>You can combine scroll bars with explicit settings for the container's viewport.
* The viewport settings determine the initial position of the viewport,
* and then you can use the scroll bars to move it, as the following example shows: </p>
*
* <pre>
* &lt;s:Scroller width="100" height="100"&gt;
* &lt;s:Group
* horizontalScrollPosition="50" verticalScrollPosition="50"&gt;
* &lt;mx:Image width="300" height="400"
* source="&#64;Embed(source='assets/logo.jpg')"/&gt;
* &lt;/s:Group&gt;
* &lt;/s:Scroller&gt;</pre>
*
* <p>The scroll bars are displayed according to the vertical and horizontal scroll bar
* policy, which can be <code>auto</code>, <code>on</code>, or <code>off</code>.
* The <code>auto</code> policy means that the scroll bar will be visible and included
* in the layout when the viewport's content is larger than the viewport itself.</p>
*
* <p>The Scroller skin layout cannot be changed. It is unconditionally set to a
* private layout implementation that handles the scroll policies. Scroller skins
* can only provide replacement scroll bars. To gain more control over the layout
* of a viewport and its scroll bars, instead of using Scroller, just add them
* to a <code>Group</code> and use the scroll bar <code>viewport</code> property
* to link them together.</p>
*
* <p>The Scroller control has the following default characteristics:</p>
* <table class="innertable">
* <tr>
* <th>Characteristic</th>
* <th>Description</th>
* </tr>
* <tr>
* <td>Default size</td>
* <td>0</td>
* </tr>
* <tr>
* <td>Minimum size</td>
* <td>0</td>
* </tr>
* <tr>
* <td>Maximum size</td>
* <td>10000 pixels wide and 10000 pixels high</td>
* </tr>
* <tr>
* <td>Default skin class</td>
* <td>spark.skins.spark.ScrollerSkin</td>
* </tr>
* </table>
*
* @mxml
*
* <p>The <code>&lt;s:Scroller&gt;</code> tag inherits all of the tag
* attributes of its superclass and adds the following tag attributes:</p>
*
* <pre>
* &lt;s:Scroller
* <strong>Properties</strong>
* measuredSizeIncludesScrollBars="true"
* minViewportInset="0"
* pageScrollingEnabled="false"
* scrollSnappingMode="none"
* viewport="null"
*
* <strong>Styles</strong>
* alignmentBaseline="use_dominant_baseline"
* alternatingItemColors=""
* baselineShift="0.0"
* blockProgression="TB"
* breakOpportunity="auto"
* cffHinting="horizontal_stem"
* clearFloats="none"
* color="0"
* contentBackgroundAlpha=""
* contentBackgroundColor=""
* digitCase="default"
* digitWidth="default"
* direction="LTR"
* dominantBaseline="auto"
* downColor=""
* firstBaselineOffset="auto"
* focusColor=""
* focusedTextSelectionColor=""
* fontFamily="Arial"
* fontLookup="device"
* fontSize="12"
* fontStyle="normal"
* fontWeight="normal"
* horizontalScrollPolicy="auto"
* inactiveTextSelection=""
* justificationRule="auto"
* justificationStyle="auto"
* kerning="auto"
* leadingModel="auto"
* ligatureLevel="common"
* lineHeight="120%"
* lineThrough="false"
* listAutoPadding="40"
* listStylePosition="outside"
* listStyleType="disc"
* locale="en"
* paragraphEndIndent="0"
* paragraphSpaceAfter="0"
* paragraphSpaceBefore="0"
* paragraphStartIndent="0"
* renderingMode="CFF"
* rollOverColor=""
* symbolColor=""
* tabStops="null"
* textAlign="start"
* textAlignLast="start"
* textAlpha="1"
* textDecoration="none"
* textIndent="0"
* textJustify="inter_word"
* textRotation="auto"
* trackingLeft="0"
* trackingRight="0"
* typographicCase="default"
* unfocusedTextSelectionColor=""
* verticalScrollPolicy="auto"
* whiteSpaceCollapse="collapse"
* wordSpacing="100%,50%,150%"
* /&gt;
* </pre>
*
* @see spark.core.IViewport
* @see spark.components.DataGroup
* @see spark.components.Group
* @see spark.components.RichEditableText
* @see spark.skins.spark.ScrollerSkin
*
* @includeExample examples/ScrollerExample.mxml
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public class Scroller extends SkinnableComponent
implements IFocusManagerComponent, IVisualElementContainer
{
include "../core/Version.as";
//--------------------------------------------------------------------------
//
// Class constants
//
//--------------------------------------------------------------------------
/**
* @private
* The ratio that determines how far the list scrolls when pulled past its end.
*/
private static const PULL_TENSION_RATIO:Number = 0.5;
/**
* @private
* Used so we don't have to keep allocating Point(0,0) to do coordinate conversions
* while draggingg
*/
private static const ZERO_POINT:Point = new Point(0,0);
/**
* @private
* The name of the viewport's horizontal scroll position property
*/
private static const HORIZONTAL_SCROLL_POSITION:String = "horizontalScrollPosition";
/**
* @private
* The name of the viewport's vertical scroll position property
*/
private static const VERTICAL_SCROLL_POSITION:String = "verticalScrollPosition";
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function Scroller()
{
super();
hasFocusableChildren = true;
focusEnabled = false;
addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
addEventListener(Event.REMOVED_FROM_STAGE, removedFromStageHandler);
}
//--------------------------------------------------------------------------
//
// Variables: Touch Scrolling
//
//--------------------------------------------------------------------------
/**
* @private
* Threshold for screen distance they must move to count as a scroll
* Based on 20 pixels on a 252ppi device.
*/
mx_internal var minSlopInches:Number = 0.079365; // 20.0/252.0
/**
* @private
* The amount of deceleration to apply to the velocity for each effect period
* For a faster deceleration, you can switch this to 0.990.
*/
mx_internal var throwEffectDecelFactor:Number = 0.998;
/**
* @private
* When pageScrollingEnabled is true, this var specifies the minimum distance
* (as a percentage of the viewport size) that the content needs to be dragged
* in order to switch to an adjacent page.
*/
mx_internal var pageDragDistanceThreshold:Number = 0.5;
/**
* @private
* When pageScrollingEnabled is true, this var specifies the minimum velocity
* (in inches/second) that a throw needs in order to switch to an adjacent page.
*/
mx_internal var pageThrowVelocityThreshold:Number = 0.8;
/**
* @private
*/
private var scrollRangesChanged:Boolean = false;
/**
* @private
*/
private var pageScrollingChanged:Boolean = false;
/**
* @private
*/
private var snappingModeChanged:Boolean = false;
/**
* @private
*/
private var _pullEnabled:Boolean = true;
/**
* @private
*/
mx_internal function get pullEnabled():Boolean
{
return _pullEnabled;
}
/**
* @private
*/
mx_internal function set pullEnabled(value:Boolean):void
{
if (_pullEnabled == value)
return;
_pullEnabled = value;
scrollRangesChanged = true;
invalidateProperties();
}
/**
* @private
*/
private var _bounceEnabled:Boolean = true;
/**
* @private
*/
mx_internal function get bounceEnabled():Boolean
{
return _bounceEnabled;
}
/**
* @private
*/
mx_internal function set bounceEnabled(value:Boolean):void
{
if (_bounceEnabled == value)
return;
_bounceEnabled = value;
scrollRangesChanged = true;
invalidateProperties();
}
/**
* @private
* Touch Scroll Helper -- used to help figure out
* scrolling velocity and other information
*/
private var touchScrollHelper:TouchScrollHelper;
/**
* @private
* Keeps track of the horizontal scroll position
* before scrolling started, so we can figure out
* how to related it to the dragX that are
* associated with the touchScrollDrag events.
*/
private var hspBeforeTouchScroll:Number;
/**
* @private
* Keeps track of the vertical scroll position
* before scrolling started, so we can figure out
* how to related it to the dragY that are
* associated with the touchScrollDrag events.
*/
private var vspBeforeTouchScroll:Number;
/**
* @private
* Effect used for touch scroll throwing
*/
mx_internal var throwEffect:ThrowEffect;
/**
* @private
* The final position in the throw effect's vertical motion path
*/
private var throwFinalVSP:Number;
/**
* @private
* The final position in the throw effect's horizontal motion path
*/
private var throwFinalHSP:Number;
/**
* @private
* Indicates whether the previous throw reached one of the maximum
* scroll positions (vsp or hsp) that was in effect at the time.
*/
private var throwReachedMaximumScrollPosition:Boolean;
/**
* @private
* Used to keep track of whether the throw animation
* was stopped pre-emptively. We stop propogation of
* the mouse event, but in the throwEffect.EFFECT_END
* event handler, we need to tell it not to exit the
* scrolling state.
*/
private var stoppedPreemptively:Boolean = false;
/**
* @private
* Used to keep track of whether we should capture the next
* click event that we receive or whether we should let it dispatch
* normally. We capture the click event if a scroll happened. We
* set this property in mouseDown and touchScrollStart.
*/
private var captureNextClick:Boolean = false;
/**
* @private
* Used to keep track of whether we should capture the next
* mousedown event that we receive or whether we should let it dispatch
* normally. We capture the mousedown event if a scroll-throw is
* currently happening. We set this property in mouseDown, touchInteractionStart,
* and touchInteractionEnd.
*/
private var captureNextMouseDown:Boolean = false;
/**
* @private
* Animation to fade the scrollbars out when we are done
* throwing or dragging
*/
private var hideScrollBarAnimation:Animate;
/**
* @private
* Use to figure out whether the animation ended naturally and finished or
* whether we called stop() on it. Unfortunately, we get an EFFECT_END in
* both cases, so we must keep track of it ourselves.
*/
private var hideScrollBarAnimationPrematurelyStopped:Boolean;
/**
* @private
* Keeps track of whether a touch interaction is in progress.
*/
mx_internal var inTouchInteraction:Boolean = false;
/**
* @private
* These are the minimum and maximum scroll possitions allowed
* for both axes. They determine the points at which bounce and
* pull occur.
*/
private var minVerticalScrollPosition:Number = 0;
private var maxVerticalScrollPosition:Number = 0;
private var minHorizontalScrollPosition:Number = 0;
private var maxHorizontalScrollPosition:Number = 0;
/**
* @private
* The animation used by the snapElement function.
*/
private var snapElementAnimation:Animate;
/**
* @private
* When pageScrollingEnabled is true, this contains the
* scroll position of the current page.
*/
private var currentPageScrollPosition:Number;
/**
* @private
* Keeps track of the most recently snapped item, or -1 if none.
* This value is set as a side-effect of calling getSnappedPosition.
*/
private var lastSnappedElement:int = -1;
/**
* @private
* Remembers which part of the content is snapped at the
* time an orientation change begins. For paging without
* item snapping, this value is a page number. For item
* snapping, the value is an element number.
*/
private var orientationChangeSnapElement:int = -1;
/**
* @private
* Remembers the number of pages right before an orientation
* change occurs.
*/
private var previousOrientationPageCount:int = 0;
//--------------------------------------------------------------------------
//
// Variables: SoftKeyboard Support
//
//--------------------------------------------------------------------------
/**
* @private
*
* Some devices do not support a hardware keyboard.
* Instead, these devices use a keyboard that opens on
* the screen when necessary.
* A value of <code>true</code> means that when a component in
* the container wrapped by the scroller receives focus,
* the Scroller scrolls that component into view if the keyboard is
* opening
*/
mx_internal var ensureElementIsVisibleForSoftKeyboard:Boolean = true;
/**
* @private
*/
private var lastFocusedElement:IVisualElement;
/**
* @private
* Used to detect when the device orientation (landscape/portrait) has changed
*/
private var aspectRatio:String;
/**
* @private
*/
private var oldSoftKeyboardHeight:Number = NaN;
/**
* @private
*/
private var oldSoftKeyboardWidth:Number = NaN;
/**
* @private
*/
mx_internal var preventThrows:Boolean = false;
/**
* @private
*/
private var lastFocusedElementCaretBounds:Rectangle;
/**
* @private
*/
private var captureNextCaretBoundsChange:Boolean = false;
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// horizontalScrollBar
//----------------------------------
[SkinPart(required="false")]
[Bindable]
/**
* A skin part that defines the horizontal scroll bar.
*
* This property should be considered read-only. It is only
* set by the Scroller's skin.
*
* This property is Bindable.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public var horizontalScrollBar:HScrollBar;
//----------------------------------
// horizontalScrollBarFactory
//----------------------------------
[SkinPart(required="false", type="spark.components.HScrollBar")]
/**
* A skin part that defines the horizontal scroll bar component.
*
* The <code>horizontalScrollBar</code> skin part takes precedence over this
* skin part.
*
* When Scroller creates an instance of this part, it will set the
* <code>horizontalScrollBar</code> skin part to that instance.
*
* This property should be considered read-only. It is only
* set by the Scroller's skin.
* To access the HScrollBar instance, use <code>horizontalScrollBar</code>.
*/
public var horizontalScrollBarFactory:IFactory;
/**
* Creates the horizontalScrollBar part from the horizontalScrollBarFactory part.
*/
private function ensureDeferredHScrollBarCreated():void
{
if (!horizontalScrollBar && horizontalScrollBarFactory)
{
horizontalScrollBar = HScrollBar(createDynamicPartInstance("horizontalScrollBarFactory"));
Group(this.skin).addElement(horizontalScrollBar);
partAdded("horizontalScrollBar", horizontalScrollBar);
}
}
//----------------------------------
// horizontalScrollInProgress
//----------------------------------
/**
* Storage for the horizontalScrollInProgress property
*/
private var _horizontalScrollInProgress:Boolean = false;
/**
* @private
* Property used to communicate with ScrollerLayout to let it
* know when a horizontal scroll is in progress or not (and when
* the horizontal scroll bar should be hidden or not)
*/
mx_internal function get horizontalScrollInProgress():Boolean
{
return _horizontalScrollInProgress;
}
/**
* @private
*/
mx_internal function set horizontalScrollInProgress(value:Boolean):void
{
_horizontalScrollInProgress = value;
if (value && getStyle("interactionMode") == InteractionMode.TOUCH)
ensureDeferredHScrollBarCreated();
}
//----------------------------------
// verticalScrollBar
//----------------------------------
[SkinPart(required="false")]
[Bindable]
/**
* A skin part that defines the vertical scroll bar.
*
* This property should be considered read-only. It is only
* set by the Scroller's skin.
*
* This property is Bindable.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public var verticalScrollBar:VScrollBar;
//----------------------------------
// verticalScrollBarFactory
//----------------------------------
[SkinPart(required="false", type="spark.components.VScrollBar")]
/**
* A skin part that defines the vertical scroll bar.
*
* The <code>verticalScrollBar</code> skin part takes precedence over this
* skin part.
*
* When Scroller creates an instance of this part, it will set the
* <code>verticalScrollBar</code> skin part to that instance.
*
* This property should be considered read-only. It is only
* set by the Scroller's skin.
* To access the VScrollBar instance, use <code>verticalScrollBar</code>.
*/
public var verticalScrollBarFactory:IFactory;
/**
* Creates the verticalScrollBar part from the verticalScrollBarFactory part.
*/
private function ensureDeferredVScrollBarCreated():void
{
if (!verticalScrollBar && verticalScrollBarFactory)
{
verticalScrollBar = VScrollBar(createDynamicPartInstance("verticalScrollBarFactory"));
Group(this.skin).addElement(verticalScrollBar);
partAdded("verticalScrollBar", verticalScrollBar);
}
}
//----------------------------------
// verticalScrollInProgress
//----------------------------------
/**
* Storage for the verticalScrollInProgress property
*/
private var _verticalScrollInProgress:Boolean = false;
/**
* @private
* Property used to communicate with ScrollerLayout to let it
* know when a vertical scroll is in progress or not (and when
* the vertical scroll bar should be hidden or not)
*/
mx_internal function get verticalScrollInProgress():Boolean
{
return _verticalScrollInProgress;
}
/**
* @private
*/
mx_internal function set verticalScrollInProgress(value:Boolean):void
{
_verticalScrollInProgress = value;
if (value && getStyle("interactionMode") == InteractionMode.TOUCH)
ensureDeferredVScrollBarCreated();
}
//----------------------------------
// viewport - default property
//----------------------------------
private var _viewport:IViewport;
[Bindable(event="viewportChanged")]
/**
* The viewport component to be scrolled.
*
* <p>
* The viewport is added to the Scroller component's skin,
* which lays out both the viewport and scroll bars.
*
* When the <code>viewport</code> property is set, the viewport's
* <code>clipAndEnableScrolling</code> property is
* set to true to enable scrolling.
*
* The Scroller does not support rotating the viewport directly. The viewport's
* contents can be transformed arbitrarily, but the viewport itself cannot.
* </p>
*
* This property is Bindable.
*
* @default null
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get viewport():IViewport
{
return _viewport;
}
/**
* @private
*/
public function set viewport(value:IViewport):void
{
if (value == _viewport)
return;
uninstallViewport();
_viewport = value;
installViewport();
dispatchEvent(new Event("viewportChanged"));
}
/**
* @private
* This is used to disable thinning for automated testing.
*/
mx_internal static var dragEventThinning:Boolean = true;
private function installViewport():void
{
if (skin && viewport)
{
viewport.clipAndEnableScrolling = true;
Group(skin).addElementAt(viewport, 0);
viewport.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, viewport_propertyChangeHandler);
viewport.addEventListener(Event.RESIZE, viewport_resizeHandler);
}
if (verticalScrollBar)
verticalScrollBar.viewport = viewport;
if (horizontalScrollBar)
horizontalScrollBar.viewport = viewport;
}
private function uninstallViewport():void
{
if (horizontalScrollBar)
horizontalScrollBar.viewport = null;
if (verticalScrollBar)
verticalScrollBar.viewport = null;
if (skin && viewport)
{
viewport.clipAndEnableScrolling = false;
Group(skin).removeElement(viewport);
viewport.removeEventListener(PropertyChangeEvent.PROPERTY_CHANGE, viewport_propertyChangeHandler);
viewport.removeEventListener(Event.RESIZE, viewport_resizeHandler);
}
}
//----------------------------------
// minViewportInset
//----------------------------------
private var _minViewportInset:Number = 0;
[Inspectable(category="General", defaultValue="0")]
/**
* The minimum space between the viewport and the edges of the Scroller.
*
* If neither of the scroll bars is visible, then the viewport is inset by
* <code>minViewportInset</code> on all four sides.
*
* If a scroll bar is visible then the viewport is inset by <code>minViewportInset</code>
* or by the scroll bar's size, whichever is larger.
*
* ScrollBars are laid out flush with the edges of the Scroller.
*
* @default 0
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function get minViewportInset():Number
{
return _minViewportInset;
}
/**
* @private
*/
public function set minViewportInset(value:Number):void
{
if (value == _minViewportInset)
return;
_minViewportInset = value;
invalidateSkin();
}
//----------------------------------
// measuredSizeIncludesScrollBars
//----------------------------------
private var _measuredSizeIncludesScrollBars:Boolean = true;
[Inspectable(category="General", defaultValue="true")]
/**
* If <code>true</code>, the Scroller's measured size includes the space required for
* the visible scroll bars, otherwise the Scroller's measured size depends
* only on its viewport.
*
* <p>Components like TextArea, which "reflow" their contents to fit the
* available width or height may use this property to stabilize their
* measured size. By default a TextArea's is defined by its <code>widthInChars</code>
* and <code>heightInChars</code> properties and in many applications it's preferable
* for the measured size to remain constant, event when scroll bars are displayed
* by the TextArea skin's Scroller.</p>
*
* <p>In components where the content does not reflow, like a typical List's
* items, the default behavior is preferable because it makes it less
* likely that the component's content will be obscured by a scroll bar.</p>
*
* @default true
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function get measuredSizeIncludesScrollBars():Boolean
{
return _measuredSizeIncludesScrollBars;
}
/**
* @private
*/
public function set measuredSizeIncludesScrollBars(value:Boolean):void
{
if (value == _measuredSizeIncludesScrollBars)
return;
_measuredSizeIncludesScrollBars = value;
invalidateSkin();
}
//----------------------------------
// pageScrollingEnabled
//----------------------------------
private var _pageScrollingEnabled:Boolean = false;
[Inspectable(category="General", defaultValue="false")]
/**
* By default, for mobile applications, scrolling is pixel based.
* The final scroll location is any pixel location based on
* the drag and throw gesture.
* Set <code>pageScrollingEnabled</code> to <code>true</code> to
* enable page scrolling.
*
* <p><b>Note: </b>This property is only valid when the <code>interactionMode</code> style
* is set to <code>touch</code>, indicating a mobile application.</p>
*
* <p>The size of the page is determined by the size of the viewport
* of the scrollable component.
* You can only scroll a single page at a time, regardless of the scroll gesture.</p>
*
* <p>You must scroll at least 50% of the visible area of the component
* to cause the page to change.
* If you scroll less than 50%, the component remains on the current page.
* Alternatively, if the velocity of the scroll is high enough, the next page display.
* If the velocity is not high enough, the component remains on the current page.</p>
*
* @default false
*
* @langversion 3.0
* @playerversion AIR 3
* @productversion Flex 4.6
*/
public function get pageScrollingEnabled():Boolean
{
return _pageScrollingEnabled;
}
/**
* @private
*/
public function set pageScrollingEnabled(value:Boolean):void
{
if (value == _pageScrollingEnabled)
return;
_pageScrollingEnabled = value;
if (getStyle("interactionMode") == InteractionMode.TOUCH)
{
if (canScrollHorizontally && canScrollVertically)
throw new Error(resourceManager.getString("components", "operationSupportedForOneAxisOnly"));
scrollRangesChanged = true;
pageScrollingChanged = true;
invalidateProperties();
}
}
//----------------------------------
// scrollSnappingMode
//----------------------------------
private var _scrollSnappingMode:String = ScrollSnappingMode.NONE;
[Inspectable(category="General", enumeration="none,leadingEdge,center,trailingEdge", defaultValue="none")]
/**
* By default, for mobile applications, scrolling is pixel based.
* The final scroll location is any pixel location based on
* the drag and throw gesture.
* Set <code>scrollSnappingMode</code> to other than <code>none</code> to
* enable scroll snapping.
* With scroll snapping enabled,
* the content snaps to a final position based on the value of <code>scrollSnappingMode</code>.
*
* <p><b>Note: </b>This property is only valid when the <code>interactionMode</code> style
* is set to <code>touch</code>, indicating a mobile application.</p>
*
* <p>For example, you scroll a List vertically with <code>scrollSnappingMode</code>
* set to a value of <code>leadingEdge</code>.
* The List control snaps to a final scroll position where the top list element
* is aligned to the top of the list.</p>
*
* <p>Changing this property to anything other than <code>none</code> can
* result in an immediate change in scroll position to ensure
* an element is correctly snapped into position.
* This change in scroll position is not animated</p>
*
* <p>in MXML, the possible values are <code>"leadingEdge"</code>, <code>"center"</code>,
* <code>"trailingEdge"</code>, and <code>"none"</code>.
* ActionScript values are defined by spark.components.ScrollSnappingMode. </p>
*
* @see spark.components.ScrollSnappingMode
*
* @default "none"
*
* @langversion 3.0
* @playerversion AIR 3
* @productversion Flex 4.6
*/
public function get scrollSnappingMode():String
{
return _scrollSnappingMode;
}
/**
* @private
*/
public function set scrollSnappingMode(value:String):void
{
if (value == _scrollSnappingMode)
return;
_scrollSnappingMode = value;
if (getStyle("interactionMode") == InteractionMode.TOUCH)
{
if (canScrollHorizontally && canScrollVertically)
throw new Error(resourceManager.getString("components", "operationSupportedForOneAxisOnly"));
scrollRangesChanged = true;
snappingModeChanged = true;
invalidateProperties();
}
}
//----------------------------------
// maxDragRate
//----------------------------------
private static var _maxDragRate:Number = 30;
[Inspectable(category="General", defaultValue="30")]
/**
*
* Maximum number of times per second the scroll position
* and the display will be updated while dragging.
*
* @default 30
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public static function get maxDragRate():Number
{
return _maxDragRate;
}
public static function set maxDragRate(value:Number):void
{
_maxDragRate = value;
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* Scrolls the viewport so the specified element is visible.
*
* @param element A child element of the container,
* or of a nested container, wrapped by the Scroller.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function ensureElementIsVisible(element:IVisualElement):void
{
ensureElementPositionIsVisible(element);
}
/**
* @private
*
* @param elementLocalBounds ensure that these bounds of the element are
* visible. The bounds are in the coordinate system of the element
* @param doValidateNow if true, call validateNow() at the end of the
* function
*/
private function ensureElementPositionIsVisible(element:IVisualElement,
elementLocalBounds:Rectangle = null,
entireElementVisible:Boolean = true,
doValidateNow:Boolean = true):void
{
// First check that the element is a descendant
// If we are a GraphicElement, use the element's parent
var possibleDescendant:DisplayObject = element as DisplayObject;
if (element is IGraphicElement)
possibleDescendant = IGraphicElement(element).parent as DisplayObject;
if (!possibleDescendant || !contains(possibleDescendant))
return;
var layout:LayoutBase = viewportLayout;
if (layout)
{
// Before we change the scroll position, make sure there is
// no throw effect playing.
if (throwEffect && throwEffect.isPlaying)
{
throwEffect.stop();
snapContentScrollPosition();
}
// Scroll the element into view
var delta:Point = layout.getScrollPositionDeltaToAnyElement(element,
elementLocalBounds, entireElementVisible);
// Compute new delta if element is visible in the viewport bounds but is
// clipped/obscured by the soft keyboard
var topLevelApp:Application = FlexGlobals.topLevelApplication as Application;
var eltBounds:Rectangle;
var adjustForSoftKeyboard:Boolean = topLevelApp &&
(!topLevelApp.resizeForSoftKeyboard) &&
(stage && stage.softKeyboardRect.height > 0);
if (adjustForSoftKeyboard)
{
eltBounds = layout.getChildElementBounds(element);
// Get keyboard y-position in the scroller's coordinates
var keyboardTopLocal:Number = this.globalToLocal(stage.softKeyboardRect.topLeft).y;
var scrollerHeight:Number = this.getLayoutBoundsHeight();
// Does the keyboard clip the scroller?
// Is the bottom of the element clipped or outside the visible
// scroller height?
if ((keyboardTopLocal >= 0) &&
(keyboardTopLocal < scrollerHeight) &&
((eltBounds.bottom - viewport.verticalScrollPosition) > keyboardTopLocal))
{
// Compute a new delta to accomodate the soft keyboard
var dy:Number = 0;
if (eltBounds.height > keyboardTopLocal)
{
// Top justify if the element is taller than the
// scroller's visible height
dy = eltBounds.top;
}
else
{
// Bottom justify the element
dy = eltBounds.bottom - keyboardTopLocal;
}
var dx:Number = (delta) ? delta.x : 0;
// account for current verticalScrollPosition
delta = new Point(dx, dy - viewport.verticalScrollPosition);
}
}
if (delta)
{
viewport.horizontalScrollPosition += delta.x;
viewport.verticalScrollPosition += delta.y;
// We only care about focusThickness if we are positioning the whole element
if (!elementLocalBounds)
{
if (!eltBounds)
eltBounds = layout.getChildElementBounds(element);
var focusThickness:Number = 0;
if (element is IStyleClient)
focusThickness = IStyleClient(element).getStyle("focusThickness");
// Make sure that the focus ring is visible. Top and left sides have priority
if (focusThickness)
{
if (viewport.verticalScrollPosition > eltBounds.top - focusThickness)
viewport.verticalScrollPosition = eltBounds.top - focusThickness;
else if (viewport.verticalScrollPosition + height < eltBounds.bottom + focusThickness)
viewport.verticalScrollPosition = eltBounds.bottom + focusThickness - height;
if (viewport.horizontalScrollPosition > eltBounds.left - focusThickness)
viewport.horizontalScrollPosition = eltBounds.left - focusThickness;
else if (viewport.horizontalScrollPosition + width < eltBounds.right + focusThickness)
viewport.horizontalScrollPosition = eltBounds.right + focusThickness - width;
}
}
if (doValidateNow && viewport is UIComponent)
UIComponent(viewport).validateNow();
}
}
}
/**
* @private
* Internal API for programmatically snapping to a particular element.
* Can optionally animate the position change.
*/
mx_internal function snapElement(elementIndex:int,animate:Boolean):Animate
{
var layout:LayoutBase = viewportLayout;
if (!layout)
throw new Error(resourceManager.getString("components", "operationRequiresViewportLayout"));
var elementBounds:Rectangle = layout.getElementBounds(elementIndex);
var snapScrollPosition:Number;
// Find the scroll position that puts the specified element into
// the appropriate snapped position.
switch (scrollSnappingMode)
{
case ScrollSnappingMode.NONE:
{
throw new Error(resourceManager.getString("components", "operationRequiresSnappingMode"));
}
case ScrollSnappingMode.LEADING_EDGE:
{
if (canScrollHorizontally)
snapScrollPosition = elementBounds.left;
else if (canScrollVertically)
snapScrollPosition = elementBounds.top;
break;
}
case ScrollSnappingMode.CENTER:
{
if (canScrollHorizontally)
snapScrollPosition = elementBounds.left + elementBounds.width/2 - viewport.width/2;
else if (canScrollVertically)
snapScrollPosition = elementBounds.top + elementBounds.height/2 - viewport.height/2;
break;
}
case ScrollSnappingMode.TRAILING_EDGE:
{
if (canScrollHorizontally)
snapScrollPosition = elementBounds.right - viewport.width;
else if (canScrollVertically)
snapScrollPosition = elementBounds.bottom - viewport.height;
break;
}
}
var scrollProperty:String;
if (canScrollHorizontally)
scrollProperty = HORIZONTAL_SCROLL_POSITION;
else if (canScrollVertically)
scrollProperty = VERTICAL_SCROLL_POSITION;
// If there's an animation playing, we need
// to stop it before we snap the element into
// position.
stopAnimations();
if (animate)
{
if (!snapElementAnimation)
{
snapElementAnimation = new Animate();
snapElementAnimation.duration = 300;
snapElementAnimation.target = viewport;
}
var snapMotionPath:Vector.<MotionPath> = Vector.<MotionPath>([new SimpleMotionPath(scrollProperty, null, snapScrollPosition)]);
snapElementAnimation.motionPaths = snapMotionPath;
snapElementAnimation.play();
// If paging is enabled, make sure the destination snap position
// also becomes the current page.
if (pageScrollingEnabled)
currentPageScrollPosition = snapScrollPosition;
return snapElementAnimation;
}
else
{
if (scrollProperty)
viewport[scrollProperty] = snapScrollPosition;
return null;
}
}
/**
* @private
*/
mx_internal function stopAnimations():void
{
if (throwEffect && throwEffect.isPlaying)
throwEffect.stop();
if (snapElementAnimation && snapElementAnimation.isPlaying)
snapElementAnimation.stop();
}
//--------------------------------------------------------------------------
//
// Event Handlers
//
//--------------------------------------------------------------------------
/**
* @private
*/
private function getCurrentPageCount():int
{
var viewportWidth:Number = isNaN(viewport.width) ? 0 : viewport.width;
var viewportHeight:Number = isNaN(viewport.height) ? 0 : viewport.height;
var pageCount:int = 0;
if (canScrollHorizontally && viewportWidth != 0)
{
pageCount = Math.ceil(viewport.contentWidth / viewportWidth);
}
else if (canScrollVertically && viewportHeight != 0)
{
pageCount = Math.ceil(viewport.contentHeight/ viewportHeight);
}
return pageCount;
}
/**
* @private
*/
private function checkScrollPosition():void
{
// TODO (eday): This function is a mess. It needs to be refactored and simplified.
// It does too many things and has too many subtle behaviors. But as I'm
// writing this we're too late in the release (4.6) schedule to make any
// changes of that size. This should be revisited during 5.0 development.
// If the content size has changed, we may need to recalculate
// the minimum and maximum scroll positions.
determineScrollRanges();
// Determine whether there's been a device orientation change
// Note: the first time this code runs it may falsely appear as though an orientation
// change has occurred (aspectRatio is null). This is okay since there will be no
// throw animation playing, so orientationChange will not be acted upon.
var orientationChange:Boolean = aspectRatio != FlexGlobals.topLevelApplication.aspectRatio;
aspectRatio = FlexGlobals.topLevelApplication.aspectRatio;
// See whether we possibly need to re-throw because of changed max positions.
var needRethrow:Boolean = false;
// Here we check to see whether the current throw has maybe not gone far enough
// given the new content size.
// We don't rethrow for this reason in paging mode, as we don't want to go any further
// than to the adjacent page.
if (!pageScrollingEnabled)
{
if (throwReachedMaximumScrollPosition && (throwFinalVSP < maxVerticalScrollPosition || throwFinalHSP < maxHorizontalScrollPosition))
needRethrow = true;
if (throwFinalVSP > maxVerticalScrollPosition || throwFinalHSP > maxHorizontalScrollPosition)
needRethrow = true;
}
// See whether we possibly need to re-throw because the final snapped position is
// no longer snapped. This can occur when the snapped position was estimated due to virtual
// layout, and the actual snapped position (i.e. once the relevent elements have been measured)
// turns out to be different.
// We also do this when pageScrolling is enabled to make sure we snap to a valid page position
// after an orientation change - since an orientation change necessarily moves all the page
// boundaries.
if (scrollSnappingMode != ScrollSnappingMode.NONE || pageScrollingEnabled)
{
// NOTE: a lighter-weight way of doing this would be to retain the element
// at the end of the throw and see whether its bounds have changed.
if (canScrollHorizontally)
if (getSnappedPosition(throwFinalHSP, HORIZONTAL_SCROLL_POSITION) != throwFinalHSP)
needRethrow = true;
if (canScrollVertically)
if (getSnappedPosition(throwFinalVSP, VERTICAL_SCROLL_POSITION) != throwFinalVSP)
needRethrow = true;
}
if (throwEffect && throwEffect.isPlaying && needRethrow)
{
// There's currently a throw animation playing, and it's throwing to a
// now-incorrect position.
if (orientationChange)
{
// The throw end position became invalid because the device
// orientation changed. In this case, we just want to stop
// the throw animation and snap to valid positions. We don't
// want to animate to the final position because this may
// require changing directions relative to the current throw,
// which looks strange.
throwEffect.stop();
snapContentScrollPosition();
}
else
{
// The size of the content may have changed during the throw.
// In this case, we'll stop the current animation and start
// a new one that gets us to the correct position.
// Get the effect's current velocity
var velocity:Point = throwEffect.getCurrentVelocity();
// Stop the existing throw animation now that we've determined its current velocities.
stoppedPreemptively = true;
throwEffect.stop();
stoppedPreemptively = false;
// Now perform a new throw to get us to the right position.
if (setUpThrowEffect(-velocity.x, -velocity.y))
throwEffect.play();
}
}
else if (!inTouchInteraction)
{
// No touch interaction is in effect, but the content may be sitting at
// a scroll position that is now invalid. If so, snap the content to
// a valid position. The most likely reason we get here is that the
// device orientation changed while the content is stationary (i.e. not
// in an animated throw)
// If the orientation changed and orientationChangeSnapElement is set to a
// valid value, then we will attempt to snap to the same item/page that
// was snapped prior to the orientation change.
if (orientationChange && orientationChangeSnapElement != -1)
{
if (scrollSnappingMode == ScrollSnappingMode.NONE && pageScrollingEnabled)
{
// Paging without item snapping. We want to snap to the same page, as
// long as the number of pages is the same.
// The number of pages being different indicates that the relationship
// between pages and content is unknown, and it makes no sense to try and
// retain the same page.
if (previousOrientationPageCount == getCurrentPageCount())
{
var viewportWidth:Number = isNaN(viewport.width) ? 0 : viewport.width;
var viewportHeight:Number = isNaN(viewport.height) ? 0 : viewport.height;
if (canScrollHorizontally)
{
viewport.horizontalScrollPosition = orientationChangeSnapElement * viewportWidth;
currentPageScrollPosition = viewport.horizontalScrollPosition;
}
else if (canScrollVertically)
{
viewport.verticalScrollPosition = orientationChangeSnapElement * viewportHeight;
currentPageScrollPosition = viewport.verticalScrollPosition;
}
}
}
else
{
// Snap directly to the item that was snapped before the orientation changed.
// If this results in an invalid scroll position for the new orientation, the
// call to snapContentScrollPosition below will fix this.
snapElement(orientationChangeSnapElement,false);
}
orientationChangeSnapElement = -1;
}
snapContentScrollPosition();
}
}
/**
* @private
*/
private function handleSizeChange():void
{
// The content size has changed, so the current scroll
// position and/or any in-progress throw may need to be adjusted.
checkScrollPosition();
// See whether the current page scroll position still needs to be initialized.
if (pageScrollingEnabled && isNaN(currentPageScrollPosition))
determineCurrentPageScrollPosition();
}
/**
* @private
* Determines the minimum/maximum allowed scroll positions
* when in leading-edge snapping mode
*/
private function determineLeadingEdgeSnappingScrollRanges():void
{
var layout:LayoutBase = viewportLayout;
var maxPositionItemIndex:int;
var maxPositionItemBounds:Rectangle;
// Locate the element nearest the leading edge: top for vertical scrolling, left for horizontal.
var firstItemIndex:int = layout.getElementNearestScrollPosition(new Point(0, 0), "topLeft");
var firstItemBounds:Rectangle = layout.getElementBounds(firstItemIndex);
if (canScrollHorizontally)
{
// The minimum scroll position aligns the first element's leading (left) edge
// with the left edge of the viewport.
minHorizontalScrollPosition = firstItemBounds.left;
// The maximum scroll position is one which aligns an element's leading edge
// with the leading edge of the viewport, but also leaves the last element
// fully visible.
var viewportWidth:Number = isNaN(viewport.width) ? 0 : viewport.width;
maxPositionItemIndex = layout.getElementNearestScrollPosition(new Point(viewport.contentWidth-viewportWidth, 0), "topLeft");
do
{
maxPositionItemBounds = layout.getElementBounds(maxPositionItemIndex);
if ((viewport.contentWidth - maxPositionItemBounds.left) <= viewportWidth)
break;
}
while (++maxPositionItemIndex < layout.target.numElements);
maxHorizontalScrollPosition = maxPositionItemBounds.left;
}
else if (canScrollVertically)
{
// The minimum scroll position aligns the first element's leading (left) edge
// with the left edge of the viewport.
minVerticalScrollPosition = firstItemBounds.top;
// The maximum scroll position is one which aligns an element's leading edge
// with the leading edge of the viewport, but also leaves the last element
// fully visible.
var viewportHeight:Number = isNaN(viewport.height) ? 0 : viewport.height;
maxPositionItemIndex = layout.getElementNearestScrollPosition(new Point(0, viewport.contentHeight-viewportHeight), "topLeft");
do
{
maxPositionItemBounds = layout.getElementBounds(maxPositionItemIndex);
if ((viewport.contentHeight - maxPositionItemBounds.top) <= viewportHeight)
break;
}
while (++maxPositionItemIndex < layout.target.numElements);
maxVerticalScrollPosition = maxPositionItemBounds.top;
}
}
/**
* @private
* Determines the minimum/maximum allowed scroll positions
* when in center snapping mode
*/
private function determineCenterSnappingScrollRanges():void
{
var layout:LayoutBase = viewportLayout;
var leadingItemIndex:int;
var leadingItemBounds:Rectangle;
var trailingItemIndex:int;
var trailingItemBounds:Rectangle;
// For center snapping mode, the min/max positions must be set such that
// any element in the layout can be scrolled into the center position.
// Find the element nearest the zero point.
leadingItemIndex = layout.getElementNearestScrollPosition(new Point(0, 0), "center");
leadingItemBounds = layout.getElementBounds(leadingItemIndex);
if (canScrollHorizontally)
{
var viewportWidth:Number = isNaN(viewport.width) ? 0 : viewport.width;
trailingItemIndex = layout.getElementNearestScrollPosition(new Point(viewport.contentWidth, 0), "center");
trailingItemBounds = layout.getElementBounds(trailingItemIndex);
minVerticalScrollPosition = maxVerticalScrollPosition = 0;
// Calculate the scroll position that puts the first element into the center.
minHorizontalScrollPosition = leadingItemBounds.left + (leadingItemBounds.width/2) - (viewportWidth/2);
// Calculate the scroll position that puts the last element into the center.
maxHorizontalScrollPosition = trailingItemBounds.left + (trailingItemBounds.width/2) - (viewportWidth/2);
}
else if (canScrollVertically)
{
var viewportHeight:Number = isNaN(viewport.height) ? 0 : viewport.height;
trailingItemIndex = layout.getElementNearestScrollPosition(new Point(0, viewport.contentHeight), "center");
trailingItemBounds = layout.getElementBounds(trailingItemIndex);
minHorizontalScrollPosition = maxHorizontalScrollPosition = 0;
// Calculate the scroll position that puts the first element into the center.
minVerticalScrollPosition = leadingItemBounds.top + (leadingItemBounds.height/2) - (viewportHeight/2);
// Calculate the scroll position that puts the last element into the center.
maxVerticalScrollPosition = trailingItemBounds.top + (trailingItemBounds.height/2) - (viewportHeight/2);
}
}
/**
* @private
* Determines the minimum/maximum allowed scroll positions
* when in trailing-edge snapping mode
*/
private function determineTrailingEdgeSnappingScrollRanges():void
{
var layout:LayoutBase = viewportLayout;
var snappedItemIndex:int;
var snappedItemBounds:Rectangle;
var lastItemIndex:int;
var lastItemBounds:Rectangle;
if (canScrollHorizontally)
{
// The max scroll position is the one which aligns the last element's right edge
// with the viewport's right edge
var viewportWidth:Number = isNaN(viewport.width) ? 0 : viewport.width;
lastItemIndex = layout.getElementNearestScrollPosition(new Point(viewport.contentWidth, 0), "bottomRight");
lastItemBounds = layout.getElementBounds(lastItemIndex);
maxHorizontalScrollPosition = lastItemBounds.right - viewportWidth;
// The minimum scroll position is the one which aligns an element's right edge with the
// right edge of the viewport, but also leaves the first element fully visible.
snappedItemIndex = layout.getElementNearestScrollPosition(new Point(viewportWidth, 0), "bottomRight");
do
{
snappedItemBounds = layout.getElementBounds(snappedItemIndex);
if (snappedItemBounds.right <= viewportWidth)
break;
}
while (--snappedItemIndex >= 0);
minHorizontalScrollPosition = snappedItemBounds.right - viewportWidth;
}
else if (canScrollVertically)
{
// The max scroll position is the one which aligns the last element's bottom edge
// with the viewport's bottom edge
var viewportHeight:Number = isNaN(viewport.height) ? 0 : viewport.height;
lastItemIndex = layout.getElementNearestScrollPosition(new Point(0, viewport.contentHeight), "bottomRight");
lastItemBounds = layout.getElementBounds(lastItemIndex);
maxVerticalScrollPosition = lastItemBounds.bottom - viewportHeight;
// The minimum scroll position is the one which aligns an element's right edge with the
// right edge of the viewport, but also leaves the first element fully visible.
snappedItemIndex = layout.getElementNearestScrollPosition(new Point(0, viewportHeight), "bottomRight");
do
{
snappedItemBounds = layout.getElementBounds(snappedItemIndex);
if (snappedItemBounds.bottom <= viewportHeight)
break;
}
while (--snappedItemIndex >= 0);
minVerticalScrollPosition = snappedItemBounds.bottom - viewportHeight;
}
}
/**
* @private
* Determines the minimum/maximum allowed scroll positions.
*/
private function determineScrollRanges():void
{
minVerticalScrollPosition = maxVerticalScrollPosition = 0;
minHorizontalScrollPosition = maxHorizontalScrollPosition = 0;
if (viewport)
{
var viewportHeight:Number = isNaN(viewport.height) ? 0 : viewport.height;
var viewportWidth:Number = isNaN(viewport.width) ? 0 : viewport.width;
// For now, having both bounce and pull disabled puts us into a sort of
// "endless" scrolling mode, in which there are practically no minimum/maximum
// edges to bounce/pull against.
// TODO (eday): bounce and pull probably don't need to be controlled separately. These
// should be combined into a single property.
if (!bounceEnabled && !pullEnabled)
{
minVerticalScrollPosition = minHorizontalScrollPosition = -Number.MAX_VALUE;
maxVerticalScrollPosition = maxHorizontalScrollPosition = Number.MAX_VALUE;
}
else if (scrollSnappingMode == ScrollSnappingMode.NONE)
{
var remaining:Number;
maxVerticalScrollPosition = viewport.contentHeight > viewportHeight ?
viewport.contentHeight-viewportHeight : 0;
if (pageScrollingEnabled && canScrollVertically && viewportHeight != 0)
{
// If the content height isn't an exact multiple of the viewport height,
// then we make sure the max scroll position allows for a full page (including
// padding) at the end.
remaining = viewport.contentHeight % viewportHeight;
if (remaining)
maxVerticalScrollPosition += viewportHeight - remaining;
}
maxHorizontalScrollPosition = viewport.contentWidth > viewportWidth ?
viewport.contentWidth-viewportWidth : 0;
if (pageScrollingEnabled && canScrollHorizontally && viewportWidth != 0)
{
// If the content width isn't an exact multiple of the viewport width,
// then we make sure the max scroll position allows for a full page (including
// padding) at the end.
remaining = viewport.contentWidth % viewportWidth;
if (remaining)
maxHorizontalScrollPosition += viewportWidth - remaining;
}
}
else
{
var layout:LayoutBase = viewportLayout;
// Nothing to do if there is no layout or no layout elements
if (!layout || layout.target.numElements == 0)
return;
// Nothing to do if the viewport dimensions have not been set yet
if ((canScrollHorizontally && viewportWidth == 0) || (canScrollVertically && viewportHeight == 0))
return;
switch (scrollSnappingMode)
{
case ScrollSnappingMode.LEADING_EDGE:
determineLeadingEdgeSnappingScrollRanges();
break;
case ScrollSnappingMode.CENTER:
determineCenterSnappingScrollRanges();
break;
case ScrollSnappingMode.TRAILING_EDGE:
determineTrailingEdgeSnappingScrollRanges();
break;
}
}
}
if (verticalScrollBar)
{
verticalScrollBar.contentMinimum = minVerticalScrollPosition;
verticalScrollBar.contentMaximum = maxVerticalScrollPosition;
}
if (horizontalScrollBar)
{
horizontalScrollBar.contentMinimum = minHorizontalScrollPosition;
horizontalScrollBar.contentMaximum = maxHorizontalScrollPosition;
}
}
/**
* @private
*/
private function determineCurrentPageScrollPosition():void
{
if (canScrollHorizontally)
{
viewport.horizontalScrollPosition = getSnappedPosition(viewport.horizontalScrollPosition,HORIZONTAL_SCROLL_POSITION);
currentPageScrollPosition = viewport.horizontalScrollPosition;
}
else if (canScrollVertically)
{
viewport.verticalScrollPosition = getSnappedPosition(viewport.verticalScrollPosition,VERTICAL_SCROLL_POSITION);
currentPageScrollPosition = viewport.verticalScrollPosition;
}
}
/**
* @private
*/
private function handleSizeChangeOnUpdateComplete(event:FlexEvent):void
{
viewport.removeEventListener(FlexEvent.UPDATE_COMPLETE,
handleSizeChangeOnUpdateComplete);
handleSizeChange();
}
/**
* @private
*/
private function viewport_resizeHandler(event:Event):void
{
if (getStyle("interactionMode") == InteractionMode.TOUCH)
{
// If the viewport dimensions have changed, then we may need to update the
// scroll ranges and snap the scroll position per the new viewport size.
viewport.addEventListener(FlexEvent.UPDATE_COMPLETE,
handleSizeChangeOnUpdateComplete);
}
}
/**
* @private
*/
private function viewport_propertyChangeHandler(event:PropertyChangeEvent):void
{
switch(event.property)
{
case "contentWidth":
case "contentHeight":
invalidateSkin();
if (getStyle("interactionMode") == InteractionMode.TOUCH)
{
// If the content size changed, then the valid scroll position ranges
// may have changed. In this case, we need to schedule an updateComplete
// handler to check and potentially correct the scroll positions.
viewport.addEventListener(FlexEvent.UPDATE_COMPLETE,
handleSizeChangeOnUpdateComplete);
}
break;
case VERTICAL_SCROLL_POSITION:
case HORIZONTAL_SCROLL_POSITION:
if (getStyle("interactionMode") == InteractionMode.TOUCH)
{
// Determine whether the scroll position is being modified programmatically (i.e.
// not due to a touch interaction or animation)
if (!inTouchInteraction && (!snapElementAnimation || !snapElementAnimation.isPlaying))
{
// We need to ensure the scroll position is always an appropriately snapped value.
if (!settingScrollPosition)
{
settingScrollPosition = true;
viewport[event.property] = getSnappedPosition(Number(event.newValue), String(event.property));
settingScrollPosition = false;
}
// Reset the page scroll position from the programmatically-changed scroll position.
if (canScrollHorizontally && event.property == HORIZONTAL_SCROLL_POSITION)
currentPageScrollPosition = viewport.horizontalScrollPosition;
if (canScrollVertically && event.property == VERTICAL_SCROLL_POSITION)
currentPageScrollPosition = viewport.verticalScrollPosition;
}
else if (throwEffect && throwEffect.isPlaying && throwEffect.isSnapping)
{
// If a throw animation is playing just to snap an element into position,
// then we want to stop the animation as soon as the final position is reached
// to avoid very short snaps taking a relatively long time to complete.
if (Math.abs(viewport.horizontalScrollPosition - throwEffect.finalPosition.x) < 1 &&
Math.abs(viewport.verticalScrollPosition - throwEffect.finalPosition.y) < 1)
{
throwEffect.stop();
snapContentScrollPosition();
}
}
}
break;
}
}
// This keeps us from infinitely recursing while changing a scroll position from
// within the scroll position change handler.
private var settingScrollPosition:Boolean = false;
/**
* @private
* Listens for any focusIn events from descendants
*/
override protected function focusInHandler(event:FocusEvent):void
{
super.focusInHandler(event);
var fm:IFocusManager = focusManager;
// When we gain focus, make sure the focused element is visible
if (fm && viewport && ensureElementIsVisibleForSoftKeyboard)
{
var elt:IVisualElement = fm.getFocus() as IVisualElement;
lastFocusedElement = elt;
}
}
/**
* @private
*/
override protected function focusOutHandler(event:FocusEvent):void
{
super.focusOutHandler(event);
lastFocusedElement = null;
}
/**
* @private
*/
private function orientationChangingHandler(event:Event):void
{
orientationChangeSnapElement = -1;
// The orientation is about to change, so we see which item/page is currently snapped
// and remember it so we can snap to it again when the orientation change is complete.
if (scrollSnappingMode == ScrollSnappingMode.NONE && pageScrollingEnabled)
{
// For paging without item snapping, we remember the number of the current page.
var viewportWidth:Number = isNaN(viewport.width) ? 0 : viewport.width;
var viewportHeight:Number = isNaN(viewport.height) ? 0 : viewport.height;
if (canScrollHorizontally && viewportWidth != 0)
orientationChangeSnapElement = currentPageScrollPosition / viewportWidth;
else if (canScrollVertically && viewportHeight != 0)
orientationChangeSnapElement = currentPageScrollPosition / viewportHeight;
// Remember the page count so we'll know whether it changed.
previousOrientationPageCount = getCurrentPageCount();
}
else if (scrollSnappingMode != ScrollSnappingMode.NONE)
{
// For item snapping, we remember which specific element is currently snapped.
if (canScrollHorizontally)
getSnappedPosition(viewport.horizontalScrollPosition, HORIZONTAL_SCROLL_POSITION);
else if (canScrollVertically)
getSnappedPosition(viewport.verticalScrollPosition, VERTICAL_SCROLL_POSITION);
// lastSnappedElement was set as a side-effect of the call to getSnappedPosition above.
orientationChangeSnapElement = lastSnappedElement;
}
// Force the viewport layout to clear its cache of element
// dimensions so it can be repopulated with correct values
// after the orientation change is complete.
if (viewportLayout)
viewportLayout.clearVirtualLayoutCache();
}
//--------------------------------------------------------------------------
//
// Methods: IVisualElementContainer
//
//--------------------------------------------------------------------------
/**
* Returns 1 if there is a viewport, 0 otherwise.
*
* @return The number of visual elements in this visual container
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get numElements():int
{
return viewport ? 1 : 0;
}
/**
* Returns the viewport if there is a viewport and the
* index passed in is 0. Otherwise, it throws a RangeError.
*
* @param index The index of the element to retrieve.
*
* @return The element at the specified index.
*
* @throws RangeError If the index position does not exist in the child list.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function getElementAt(index:int):IVisualElement
{
if (viewport && index == 0)
return viewport;
else
throw new RangeError(resourceManager.getString("components", "indexOutOfRange", [index]));
}
/**
* Returns 0 if the element passed in is the viewport.
* Otherwise, it throws an ArgumentError.
*
* @param element The element to identify.
*
* @return The index position of the element to identify.
*
* @throws ArgumentError If the element is not a child of this object.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function getElementIndex(element:IVisualElement):int
{
if (element != null && element == viewport)
return 0;
else
throw ArgumentError(resourceManager.getString("components", "elementNotFoundInScroller", [element]));
}
/**
*
* This operation is not supported in Scroller.
* A Scroller control has only one child.
* Use the <code>viewport</code> property to manipulate
* it.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function addElement(element:IVisualElement):IVisualElement
{
throw new ArgumentError(resourceManager.getString("components", "operationNotSupported"));
}
/**
* This operation is not supported in Scroller.
* A Scroller control has only one child. Use the <code>viewport</code> property to manipulate
* it.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function addElementAt(element:IVisualElement, index:int):IVisualElement
{
throw new ArgumentError(resourceManager.getString("components", "operationNotSupported"));
}
/**
*
* This operation is not supported in Scroller.
* A Scroller control has only one child. Use the <code>viewport</code> property to manipulate
* it.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function removeElement(element:IVisualElement):IVisualElement
{
throw new ArgumentError(resourceManager.getString("components", "operationNotSupported"));
}
/**
*
* This operation is not supported in Scroller.
* A Scroller control has only one child. Use the <code>viewport</code> property to manipulate
* it.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function removeElementAt(index:int):IVisualElement
{
throw new ArgumentError(resourceManager.getString("components", "operationNotSupported"));
}
/**
*
* This operation is not supported in Scroller.
* A Scroller control has only one child. Use the <code>viewport</code> property to manipulate
* it.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function removeAllElements():void
{
throw new ArgumentError(resourceManager.getString("components", "operationNotSupported"));
}
/**
*
* This operation is not supported in Scroller.
* A Scroller control has only one child. Use the <code>viewport</code> property to manipulate
* it.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function setElementIndex(element:IVisualElement, index:int):void
{
throw new ArgumentError(resourceManager.getString("components", "operationNotSupported"));
}
/**
*
* This operation is not supported in Scroller.
* A Scroller control has only one child. Use the <code>viewport</code> property to manipulate
* it.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function swapElements(element1:IVisualElement, element2:IVisualElement):void
{
throw new ArgumentError(resourceManager.getString("components", "operationNotSupported"));
}
/**
*
* This operation is not supported in Scroller.
* A Scroller control has only one child. Use the <code>viewport</code> property to manipulate
* it.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function swapElementsAt(index1:int, index2:int):void
{
throw new ArgumentError(resourceManager.getString("components", "operationNotSupported"));
}
//--------------------------------------------------------------------------
//
// Private Helper Methods
//
//--------------------------------------------------------------------------
/**
* @private
* Helper method to easily invalidate the skins's size and display list.
*/
private function invalidateSkin():void
{
if (skin)
{
skin.invalidateSize()
skin.invalidateDisplayList();
}
}
/**
* @private
* Helper method to grab the ScrollerLayout.
*/
mx_internal function get scrollerLayout():ScrollerLayout
{
if (skin)
return Group(skin).layout as ScrollerLayout;
return null;
}
/**
* @private
* Helper method to grab scrollerLayout.canScrollHorizontally
*/
private function get canScrollHorizontally():Boolean
{
var layout:ScrollerLayout = scrollerLayout;
if (layout)
return layout.canScrollHorizontally;
return false;
}
/**
* @private
* Helper method to grab scrollerLayout.canScrollVertically
*/
private function get canScrollVertically():Boolean
{
var layout:ScrollerLayout = scrollerLayout;
if (layout)
return layout.canScrollVertically;
return false;
}
/**
* @private
* Helper method to grab viewport.layout
*/
private function get viewportLayout():LayoutBase
{
if (viewport is GroupBase)
return GroupBase(viewport).layout;
else if (viewport is SkinnableContainer)
return SkinnableContainer(viewport).layout;
return null;
}
//--------------------------------------------------------------------------
//
// Touch scrolling methods
//
//--------------------------------------------------------------------------
/**
* @private
* Add touch listeners
*/
private function installTouchListeners():void
{
addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
addEventListener(TouchInteractionEvent.TOUCH_INTERACTION_STARTING, touchInteractionStartingHandler);
addEventListener(TouchInteractionEvent.TOUCH_INTERACTION_START, touchInteractionStartHandler);
addEventListener(TouchInteractionEvent.TOUCH_INTERACTION_END, touchInteractionEndHandler);
// capture mouse listeners to help block click and mousedown events.
// mousedown is blocked when a scroll is in progress
// click is blocked when a scroll is in progress (or just finished)
addEventListener(MouseEvent.CLICK, touchScrolling_captureMouseHandler, true);
addEventListener(MouseEvent.MOUSE_DOWN, touchScrolling_captureMouseHandler, true);
}
/**
* @private
*/
private function uninstallTouchListeners():void
{
removeEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
removeEventListener(TouchInteractionEvent.TOUCH_INTERACTION_STARTING, touchInteractionStartingHandler);
removeEventListener(TouchInteractionEvent.TOUCH_INTERACTION_START, touchInteractionStartHandler);
removeEventListener(TouchInteractionEvent.TOUCH_INTERACTION_END, touchInteractionEndHandler);
removeEventListener(MouseEvent.CLICK, touchScrolling_captureMouseHandler, true);
removeEventListener(MouseEvent.MOUSE_DOWN, touchScrolling_captureMouseHandler, true);
}
/**
* @private
* This function determines whether a switch to an adjacent page is warranted, given
* the distance dragged and/or the velocity thrown.
*/
private function determineNewPageScrollPosition(velocityX:Number, velocityY:Number):void
{
// Convert the paging velocity threshold from inches/second to pixels/millisecond
var minVelocityPixels:Number = pageThrowVelocityThreshold * Capabilities.screenDPI / 1000;
if (canScrollHorizontally)
{
// Check both the throw velocity and the drag distance. If either exceeds our threholds, then we switch to the next page.
if (velocityX < -minVelocityPixels || viewport.horizontalScrollPosition >= currentPageScrollPosition + viewport.width * pageDragDistanceThreshold)
{
// Go to the next horizontal page
// Set the new page scroll position so the throw effect animates the page into place
currentPageScrollPosition = Math.min(currentPageScrollPosition + viewport.width, maxHorizontalScrollPosition);
}
else if (velocityX > minVelocityPixels || viewport.horizontalScrollPosition <= currentPageScrollPosition - viewport.width * pageDragDistanceThreshold)
{
// Go to the previous horizontal page
currentPageScrollPosition = Math.max(currentPageScrollPosition - viewport.width, minHorizontalScrollPosition);
}
// Ensure the new page position is snapped appropriately
currentPageScrollPosition = getSnappedPosition(currentPageScrollPosition, HORIZONTAL_SCROLL_POSITION);
}
else if (canScrollVertically)
{
// Check both the throw velocity and the drag distance. If either exceeds our threholds, then we switch to the next page.
if (velocityY < -minVelocityPixels || viewport.verticalScrollPosition >= currentPageScrollPosition + viewport.height * pageDragDistanceThreshold)
{
// Go to the next vertical page
// Set the new page scroll position so the throw effect animates the page into place
currentPageScrollPosition = Math.min(currentPageScrollPosition + viewport.height, maxVerticalScrollPosition);
}
else if (velocityY > minVelocityPixels || viewport.verticalScrollPosition <= currentPageScrollPosition - viewport.height * pageDragDistanceThreshold)
{
// Go to the previous vertical page
currentPageScrollPosition = Math.max(currentPageScrollPosition - viewport.height, minVerticalScrollPosition);
}
// Ensure the new page position is snapped appropriately
currentPageScrollPosition = getSnappedPosition(currentPageScrollPosition, VERTICAL_SCROLL_POSITION);
}
}
/**
* @private
* Set up the effect to be used for the throw animation
*/
private function setUpThrowEffect(velocityX:Number, velocityY:Number):Boolean
{
if (!throwEffect)
{
throwEffect = new ThrowEffect();
throwEffect.target = viewport;
throwEffect.addEventListener(EffectEvent.EFFECT_END, throwEffect_effectEndHandler);
}
var minHSP:Number = minHorizontalScrollPosition;
var minVSP:Number = minVerticalScrollPosition;
var maxHSP:Number = maxHorizontalScrollPosition;
var maxVSP:Number = maxVerticalScrollPosition;
if (pageScrollingEnabled)
{
// See whether a page switch is warranted for this touch gesture.
determineNewPageScrollPosition(velocityX, velocityY);
// The throw velocity is greatly attenuated in paging mode.
// Note that this must be done after the call above to
// determineNewPageScrollPosition which compares the velocity
// to our threshold.
const PAGING_VELOCITY_FACTOR:Number = 0.25;
velocityX *= PAGING_VELOCITY_FACTOR;
velocityY *= PAGING_VELOCITY_FACTOR;
// Make the scroller "lock" to the current page
if (canScrollHorizontally)
minHSP = maxHSP = currentPageScrollPosition;
else if (canScrollVertically)
minVSP = maxVSP = currentPageScrollPosition;
}
throwEffect.propertyNameX = canScrollHorizontally ? HORIZONTAL_SCROLL_POSITION : null;
throwEffect.propertyNameY = canScrollVertically ? VERTICAL_SCROLL_POSITION : null;
throwEffect.startingVelocityX = velocityX;
throwEffect.startingVelocityY = velocityY;
throwEffect.startingPositionX = viewport.horizontalScrollPosition;
throwEffect.startingPositionY = viewport.verticalScrollPosition;
throwEffect.minPositionX = minHSP;
throwEffect.minPositionY = minVSP;
throwEffect.maxPositionX = maxHSP;
throwEffect.maxPositionY = maxVSP;
throwEffect.decelerationFactor = throwEffectDecelFactor;
// In snapping mode, we need to ensure that the final throw position is snapped appropriately.
throwEffect.finalPositionFilterFunction = scrollSnappingMode == ScrollSnappingMode.NONE ? null : getSnappedPosition;
throwReachedMaximumScrollPosition = false;
if (throwEffect.setup())
{
throwFinalHSP = throwEffect.finalPosition.x;
if (canScrollHorizontally && bounceEnabled && throwFinalHSP == maxHorizontalScrollPosition)
throwReachedMaximumScrollPosition = true;
throwFinalVSP = throwEffect.finalPosition.y;
if (canScrollVertically && bounceEnabled && throwFinalVSP == maxVerticalScrollPosition)
throwReachedMaximumScrollPosition = true;
}
else
{
touchScrollHelper.endTouchScroll();
return false;
}
return true;
}
/**
* @private
* This function takes a scroll position and the associated property name, and finds
* the nearest snapped position (i.e. one that satifises the current scrollSnappingMode).
*/
private function getSnappedPosition(position:Number, propertyName:String):Number
{
var layout:LayoutBase = viewportLayout;
var nearestElementIndex:int = -1;
var nearestElementBounds:Rectangle;
var viewportWidth:Number = isNaN(viewport.width) ? 0 : viewport.width;
var viewportHeight:Number = isNaN(viewport.height) ? 0 : viewport.height;
if (scrollSnappingMode == ScrollSnappingMode.NONE && pageScrollingEnabled)
{
// If we're in paging mode and no snapping is enabled, then we must snap
// the position to the beginning of a page. i.e. a multiple of the
// viewport size.
var offset:Number;
if (canScrollHorizontally && propertyName == HORIZONTAL_SCROLL_POSITION &&
viewportWidth != 0 && viewport.contentWidth != 0)
{
// Get the offset into the current page. If less than half way, snap
// to the beginning of the page. Otherwise, snap to the beginning
// of the next page
offset = position % viewportWidth;
if (offset < viewportWidth / 2)
position -= offset;
else
position += viewportWidth - offset;
// Clip the position to the valid min/max range
position = Math.min(Math.max(minHorizontalScrollPosition, position), maxHorizontalScrollPosition);
}
else if (canScrollVertically && propertyName == VERTICAL_SCROLL_POSITION &&
viewportHeight != 0 && viewport.contentHeight != 0)
{
offset = position % viewportHeight;
if (offset < viewportHeight / 2)
position -= offset;
else
position += viewportHeight - offset;
// Clip the position to the valid min/max range
position = Math.min(Math.max(minVerticalScrollPosition, position), maxVerticalScrollPosition);
}
}
if (layout && layout.target.numElements > 0)
{
switch (_scrollSnappingMode)
{
case ScrollSnappingMode.LEADING_EDGE:
if (canScrollHorizontally && propertyName == HORIZONTAL_SCROLL_POSITION)
{
nearestElementIndex = layout.getElementNearestScrollPosition(new Point(position, 0), "topLeft");
nearestElementBounds = layout.getElementBounds(nearestElementIndex);
position = nearestElementBounds.left;
}
else if (canScrollVertically && propertyName == VERTICAL_SCROLL_POSITION)
{
nearestElementIndex = layout.getElementNearestScrollPosition(new Point(0, position), "topLeft");
nearestElementBounds = layout.getElementBounds(nearestElementIndex);
position = nearestElementBounds.top;
}
break;
case ScrollSnappingMode.CENTER:
if (canScrollHorizontally && propertyName == HORIZONTAL_SCROLL_POSITION)
{
nearestElementIndex = layout.getElementNearestScrollPosition(new Point(position + viewportWidth/2, 0), "center");
nearestElementBounds = layout.getElementBounds(nearestElementIndex);
position = nearestElementBounds.left + (nearestElementBounds.width / 2) - (viewportWidth / 2);
}
else if (canScrollVertically && propertyName == VERTICAL_SCROLL_POSITION)
{
nearestElementIndex = layout.getElementNearestScrollPosition(new Point(0, position + viewportHeight/2), "center");
nearestElementBounds = layout.getElementBounds(nearestElementIndex);
position = nearestElementBounds.top + (nearestElementBounds.height / 2) - (viewportHeight / 2);
}
break;
case ScrollSnappingMode.TRAILING_EDGE:
if (canScrollHorizontally && propertyName == HORIZONTAL_SCROLL_POSITION)
{
nearestElementIndex = layout.getElementNearestScrollPosition(new Point(position + viewportWidth, 0), "bottomRight");
nearestElementBounds = layout.getElementBounds(nearestElementIndex);
position = nearestElementBounds.right - viewportWidth;
}
else if (canScrollVertically && propertyName == VERTICAL_SCROLL_POSITION)
{
nearestElementIndex = layout.getElementNearestScrollPosition(new Point(0, position + viewportHeight), "bottomRight");
nearestElementBounds = layout.getElementBounds(nearestElementIndex);
position = nearestElementBounds.bottom - viewportHeight;
}
break;
}
}
lastSnappedElement = nearestElementIndex;
return Math.round(position);
}
/**
* @private
* When the throw or drag scroll is over, we should play a nice
* animation to hide the scrollbars.
*/
private function hideScrollBars():void
{
if (!hideScrollBarAnimation)
{
hideScrollBarAnimation = new Animate();
hideScrollBarAnimation.addEventListener(EffectEvent.EFFECT_END, hideScrollBarAnimation_effectEndHandler);
hideScrollBarAnimation.duration = 500;
var alphaMP:Vector.<MotionPath> = Vector.<MotionPath>([new SimpleMotionPath("alpha", 1, 0)]);
hideScrollBarAnimation.motionPaths = alphaMP;
}
// set up the target scrollbars (hsb and/or vsb)
var targets:Array = [];
if (horizontalScrollBar && horizontalScrollBar.visible)
{
targets.push(horizontalScrollBar);
}
if (verticalScrollBar && verticalScrollBar.visible)
{
targets.push(verticalScrollBar);
}
// we keep track of hideScrollBarAnimationPrematurelyStopped so that we know
// if the effect ended naturally or if we prematurely called stop()
hideScrollBarAnimationPrematurelyStopped = false;
hideScrollBarAnimation.play(targets);
}
//--------------------------------------------------------------------------
//
// Overridden methods
//
//--------------------------------------------------------------------------
/**
* @private
*/
override protected function createChildren():void
{
super.createChildren();
// Only listen for softKeyboardEvents if the
// softKeyboardBehavior attribute in the application descriptor equals "none"
if (Application.softKeyboardBehavior == "none")
{
addEventListener(SoftKeyboardEvent.SOFT_KEYBOARD_ACTIVATE,
softKeyboardActivateHandler, false,
EventPriority.DEFAULT, true);
addEventListener(SoftKeyboardEvent.SOFT_KEYBOARD_ACTIVATE,
softKeyboardActivateCaptureHandler, true,
EventPriority.DEFAULT, true);
addEventListener(SoftKeyboardEvent.SOFT_KEYBOARD_DEACTIVATE,
softKeyboardDeactivateHandler, false,
EventPriority.DEFAULT, true);
addEventListener(CaretBoundsChangeEvent.CARET_BOUNDS_CHANGE,
caretBoundsChangeHandler);
}
}
/**
* @private
*/
override public function styleChanged(styleProp:String):void
{
super.styleChanged(styleProp);
var allStyles:Boolean = (styleProp == null || styleProp == "styleName");
if (allStyles || styleProp == "horizontalScrollPolicy" ||
styleProp == "verticalScrollPolicy")
{
invalidateSkin();
}
if (allStyles || styleProp == "interactionMode")
{
if (getStyle("interactionMode") == InteractionMode.TOUCH)
{
installTouchListeners();
// Need to make sure the scroll ranges are updated now, since they may
// not have been if the scroller was in non-touch mode when the content
// was created/changed.
scrollRangesChanged = true;
invalidateProperties();
if (!touchScrollHelper)
{
touchScrollHelper = new TouchScrollHelper();
touchScrollHelper.target = this;
// Install callbacks with the helper
// The dragFunction is called repeatedly during dragging/scrolling.
touchScrollHelper.dragFunction = performDrag;
// The throwFunction is called once when dragging is done and the finger is released.
touchScrollHelper.throwFunction = performThrow;
}
// We don't support directly interacting with the scrollbars in touch mode
if (horizontalScrollBar)
{
horizontalScrollBar.mouseEnabled = false;
horizontalScrollBar.mouseChildren = false;
}
if (verticalScrollBar)
{
verticalScrollBar.mouseEnabled = false;
verticalScrollBar.mouseChildren = false;
}
}
else
{
// In case we're not in touch mode, we need to instantiate our deferred skin parts immediately
// TODO (egeorgie): support deferred scrollbar parts in non-touch mode
ensureDeferredHScrollBarCreated();
ensureDeferredVScrollBarCreated();
uninstallTouchListeners();
if (horizontalScrollBar)
{
horizontalScrollBar.mouseEnabled = true;
horizontalScrollBar.mouseChildren = true;
}
if (verticalScrollBar)
{
verticalScrollBar.mouseEnabled = true;
verticalScrollBar.mouseChildren = true;
}
}
}
// If the liveScrolling style was set, set the scrollbars' liveDragging styles
if (allStyles || styleProp == "liveScrolling")
{
const liveScrolling:* = getStyle("liveScrolling");
if ((liveScrolling === true) || (liveScrolling === false))
{
if (verticalScrollBar)
verticalScrollBar.setStyle("liveDragging", Boolean(liveScrolling));
if (horizontalScrollBar)
horizontalScrollBar.setStyle("liveDragging", Boolean(liveScrolling));
}
}
}
/**
* @private
*/
override protected function attachSkin():void
{
super.attachSkin();
if (getStyle("interactionMode") != InteractionMode.TOUCH)
{
// TODO (egeorgie): support deferred scrollbar parts in non-touch mode
// In case we're not in touch mode, we need to instantiate our deferred skin parts immediately
ensureDeferredHScrollBarCreated();
ensureDeferredVScrollBarCreated();
}
Group(skin).layout = new ScrollerLayout();
installViewport();
skin.addEventListener(MouseEvent.MOUSE_WHEEL, skin_mouseWheelHandler);
}
/**
* @private
*/
override protected function detachSkin():void
{
uninstallViewport();
Group(skin).layout = null;
skin.removeEventListener(MouseEvent.MOUSE_WHEEL, skin_mouseWheelHandler);
super.detachSkin();
}
/**
* @private
*/
override protected function partAdded(partName:String, instance:Object):void
{
super.partAdded(partName, instance);
const liveScrolling:* = getStyle("liveScrolling");
const liveScrollingSet:Boolean = (liveScrolling === true) || (liveScrolling === false);
const inTouchMode:Boolean = (getStyle("interactionMode") == InteractionMode.TOUCH);
if (instance == verticalScrollBar)
{
verticalScrollBar.viewport = viewport;
if (liveScrollingSet)
verticalScrollBar.setStyle("liveDragging", Boolean(liveScrolling));
verticalScrollBar.contentMinimum = minVerticalScrollPosition;
verticalScrollBar.contentMaximum = maxVerticalScrollPosition;
// We don't support directly interacting with the scrollbars in touch mode
if (inTouchMode)
{
verticalScrollBar.mouseEnabled = false;
verticalScrollBar.mouseChildren = false;
}
}
else if (instance == horizontalScrollBar)
{
horizontalScrollBar.viewport = viewport;
if (liveScrollingSet)
horizontalScrollBar.setStyle("liveDragging", Boolean(liveScrolling));
horizontalScrollBar.contentMinimum = minHorizontalScrollPosition;
horizontalScrollBar.contentMaximum = maxHorizontalScrollPosition;
// We don't support directly interacting with the scrollbars in touch mode
if (inTouchMode)
{
horizontalScrollBar.mouseEnabled = false;
horizontalScrollBar.mouseChildren = false;
}
}
}
/**
* @private
*/
override protected function partRemoved(partName:String, instance:Object):void
{
super.partRemoved(partName, instance);
if (instance == verticalScrollBar)
verticalScrollBar.viewport = null;
else if (instance == horizontalScrollBar)
horizontalScrollBar.viewport = null;
}
/**
* @private
*/
override protected function commitProperties():void
{
super.commitProperties();
if (scrollRangesChanged)
{
determineScrollRanges();
scrollRangesChanged = false;
}
if (pageScrollingChanged)
{
stopAnimations();
determineCurrentPageScrollPosition();
pageScrollingChanged = false;
}
if (snappingModeChanged)
{
stopAnimations();
snapContentScrollPosition();
snappingModeChanged = false;
}
}
//--------------------------------------------------------------------------
//
// Event handlers
//
//--------------------------------------------------------------------------
/**
* @private
*/
override protected function keyDownHandler(event:KeyboardEvent):void
{
super.keyDownHandler(event);
var vp:IViewport = viewport;
if (!vp || event.isDefaultPrevented())
return;
// If a TextField has the focus, then assume it will handle all keyboard
// events, and that it will not use Event.preventDefault().
if (getFocus() is TextField)
return;
if (verticalScrollBar && verticalScrollBar.visible)
{
var vspDelta:Number = NaN;
switch (event.keyCode)
{
case Keyboard.UP:
vspDelta = vp.getVerticalScrollPositionDelta(NavigationUnit.UP);
break;
case Keyboard.DOWN:
vspDelta = vp.getVerticalScrollPositionDelta(NavigationUnit.DOWN);
break;
case Keyboard.PAGE_UP:
vspDelta = vp.getVerticalScrollPositionDelta(NavigationUnit.PAGE_UP);
break;
case Keyboard.PAGE_DOWN:
vspDelta = vp.getVerticalScrollPositionDelta(NavigationUnit.PAGE_DOWN);
break;
case Keyboard.HOME:
vspDelta = vp.getVerticalScrollPositionDelta(NavigationUnit.HOME);
break;
case Keyboard.END:
vspDelta = vp.getVerticalScrollPositionDelta(NavigationUnit.END);
break;
}
if (!isNaN(vspDelta))
{
vp.verticalScrollPosition += vspDelta;
event.preventDefault();
}
}
if (horizontalScrollBar && horizontalScrollBar.visible)
{
var hspDelta:Number = NaN;
switch (event.keyCode)
{
case Keyboard.LEFT:
hspDelta = (layoutDirection == LayoutDirection.LTR) ?
vp.getHorizontalScrollPositionDelta(NavigationUnit.LEFT) :
vp.getHorizontalScrollPositionDelta(NavigationUnit.RIGHT);
break;
case Keyboard.RIGHT:
hspDelta = (layoutDirection == LayoutDirection.LTR) ?
vp.getHorizontalScrollPositionDelta(NavigationUnit.RIGHT) :
vp.getHorizontalScrollPositionDelta(NavigationUnit.LEFT);
break;
case Keyboard.HOME:
hspDelta = vp.getHorizontalScrollPositionDelta(NavigationUnit.HOME);
break;
case Keyboard.END:
hspDelta = vp.getHorizontalScrollPositionDelta(NavigationUnit.END);
break;
// If there's no vertical scrollbar, then map page up/down to
// page left,right
case Keyboard.PAGE_UP:
if (!verticalScrollBar || !(verticalScrollBar.visible))
{
hspDelta = (LayoutDirection.LTR) ?
vp.getHorizontalScrollPositionDelta(NavigationUnit.LEFT) :
vp.getHorizontalScrollPositionDelta(NavigationUnit.RIGHT);
}
break;
case Keyboard.PAGE_DOWN:
if (!verticalScrollBar || !(verticalScrollBar.visible))
{
hspDelta = (LayoutDirection.LTR) ?
vp.getHorizontalScrollPositionDelta(NavigationUnit.RIGHT) :
vp.getHorizontalScrollPositionDelta(NavigationUnit.LEFT);
}
break;
}
if (!isNaN(hspDelta))
{
vp.horizontalScrollPosition += hspDelta;
event.preventDefault();
}
}
}
private function skin_mouseWheelHandler(event:MouseEvent):void
{
const vp:IViewport = viewport;
if (event.isDefaultPrevented() || !vp || !vp.visible)
return;
// Dispatch the "mouseWheelChanging" event. If preventDefault() is called
// on this event, the event will be cancelled. Otherwise if the delta
// is modified the new value will be used.
var changingEvent:FlexMouseEvent = MouseEventUtil.createMouseWheelChangingEvent(event);
if (!dispatchEvent(changingEvent))
{
event.preventDefault();
return;
}
const delta:int = changingEvent.delta;
var nSteps:uint = Math.abs(event.delta);
var navigationUnit:uint;
// Scroll delta "steps". If the VSB is up, scroll vertically,
// if -only- the HSB is up then scroll horizontally.
// TODO: The problem is that viewport.validateNow() doesn’t necessarily
// finish the job, see http://bugs.adobe.com/jira/browse/SDK-25740.
// Since some imprecision in mouse-wheel scrolling is tolerable this is
// ok for now. For 4.next we should add Scroller API for (reliably)
// scrolling in different increments and refactor code like this to
// depend on it. Also applies to VScroller and HScroller mouse
// handlers.
if (verticalScrollBar && verticalScrollBar.visible)
{
navigationUnit = (delta < 0) ? NavigationUnit.DOWN : NavigationUnit.UP;
for (var vStep:int = 0; vStep < nSteps; vStep++)
{
var vspDelta:Number = vp.getVerticalScrollPositionDelta(navigationUnit);
if (!isNaN(vspDelta))
{
vp.verticalScrollPosition += vspDelta;
if (vp is IInvalidating)
IInvalidating(vp).validateNow();
}
}
event.preventDefault();
}
else if (horizontalScrollBar && horizontalScrollBar.visible)
{
navigationUnit = (delta < 0) ? NavigationUnit.RIGHT : NavigationUnit.LEFT;
for (var hStep:int = 0; hStep < nSteps; hStep++)
{
var hspDelta:Number = vp.getHorizontalScrollPositionDelta(navigationUnit);
if (!isNaN(hspDelta))
{
vp.horizontalScrollPosition += hspDelta;
if (vp is IInvalidating)
IInvalidating(vp).validateNow();
}
}
event.preventDefault();
}
}
//--------------------------------------------------------------------------
//
// Event handlers: Touch Scrolling
//
//--------------------------------------------------------------------------
/**
* @private
* Event handler dispatched when someone is about to start scrolling.
*/
private function touchInteractionStartingHandler(event:TouchInteractionEvent):void
{
// if it's us, don't do anything
// if it's someone else and we've started scrolling, cancel this event
// if it's someone else and we haven't started scrolling, don't do anything
// here yet. Worry about it in the touchInteractionStartHandler().
if (event.relatedObject != this && inTouchInteraction)
{
event.preventDefault();
}
}
/**
* @private
* Event handler dispatched when someone has started scrolling.
*/
private function touchInteractionStartHandler(event:TouchInteractionEvent):void
{
if (event.relatedObject != this)
{
// if it's not us scrolling, abort our scrolling attempt
touchScrollHelper.stopScrollWatch();
}
else
{
// we are scrolling
captureNextClick = true;
captureNextMouseDown = true;
preventThrows = false;
hspBeforeTouchScroll = viewport.horizontalScrollPosition;
vspBeforeTouchScroll = viewport.verticalScrollPosition;
// TODO (rfrishbe): should the ScrollerLayout just listen to
// Scroller events to determine this rather than doing it here.
// Also should figure out who's in charge of fading the alpha of the
// scrollbars...Scroller or ScrollerLayout (or even HScrollbar/VScrollbar)?
if (canScrollHorizontally)
horizontalScrollInProgress = true;
if (canScrollVertically)
verticalScrollInProgress = true;
// need to invaliadte the ScrollerLayout object so it'll update the
// scrollbars in overlay mode
skin.invalidateDisplayList();
// make sure our alpha is set back to normal from hideScrollBarAnimation
if (hideScrollBarAnimation && hideScrollBarAnimation.isPlaying)
{
// stop the effect, but make sure our code for EFFECT_END doesn't actually
// run since the effect didn't end on its own.
hideScrollBarAnimationPrematurelyStopped = true;
hideScrollBarAnimation.stop();
}
// We only show want the scroll bars to be visible if some content might actually be
// off screen. We determine this by looking at the min/max scroll positions.
if (horizontalScrollBar)
horizontalScrollBar.alpha = (maxHorizontalScrollPosition == 0 && minHorizontalScrollPosition == 0) ? 0.0 : 1.0;
if (verticalScrollBar)
verticalScrollBar.alpha = (maxVerticalScrollPosition == 0 && minVerticalScrollPosition == 0) ? 0.0 : 1.0;
inTouchInteraction = true;
}
}
/**
* @private
* Snap the scroll positions to valid values.
*/
private function snapContentScrollPosition(snapHorizontal:Boolean = true, snapVertical:Boolean = true):void
{
// Note that we only snap the scroll position if content is present. This allows existing scroll position
// values to be retained before content is added or when it is removed/readded.
if (snapHorizontal && viewport.contentWidth != 0)
{
viewport.horizontalScrollPosition = getSnappedPosition(
Math.min(Math.max(minHorizontalScrollPosition, viewport.horizontalScrollPosition), maxHorizontalScrollPosition),
HORIZONTAL_SCROLL_POSITION);
}
if (snapVertical && viewport.contentHeight != 0)
{
viewport.verticalScrollPosition = getSnappedPosition(
Math.min(Math.max(minVerticalScrollPosition, viewport.verticalScrollPosition), maxVerticalScrollPosition),
VERTICAL_SCROLL_POSITION);
}
}
/**
* @private
* Stop the effect if it's currently playing and prepare for a possible scroll
*/
private function stopThrowEffectOnMouseDown():void
{
if (throwEffect && throwEffect.isPlaying)
{
// stop the effect. we don't want to move it to its final value...we want to stop it in place
stoppedPreemptively = true;
throwEffect.stop();
// Snap the scroll position to the content in case the empty space beyond the edge was visible
// due to bounce/pull.
snapContentScrollPosition();
// get new values in case we start scrolling again
hspBeforeTouchScroll = viewport.horizontalScrollPosition;
vspBeforeTouchScroll = viewport.verticalScrollPosition;
}
}
/**
* @private
* Event listeners added while a scroll/throw animation is in effect
*/
private function touchScrolling_captureMouseHandler(event:MouseEvent):void
{
switch(event.type)
{
case MouseEvent.MOUSE_DOWN:
// If we get a mouse down when the throw animation is within a few
// pixels of its final destination, we'll go ahead and stop the
// touch interaction and allow the event propogation to continue
// so other handlers can see it. Otherwise, we'll capture the
// down event and start watching for the next scroll.
// 5 pixels at 252dpi worked fairly well for this heuristic.
const THRESHOLD_INCHES:Number = 0.01984; // 5/252
var captureThreshold:Number = Math.round(THRESHOLD_INCHES * Capabilities.screenDPI);
// Need to convert the pixel delta to the local coordinate system in
// order to compare it to a scroll position delta.
captureThreshold = globalToLocal(
new Point(captureThreshold,0)).subtract(globalToLocal(ZERO_POINT)).x;
if (captureNextMouseDown &&
(Math.abs(viewport.verticalScrollPosition - throwFinalVSP) > captureThreshold ||
Math.abs(viewport.horizontalScrollPosition - throwFinalHSP) > captureThreshold))
{
// Capture the down event.
stopThrowEffectOnMouseDown();
// Watch for a scroll to begin. The helper object will call our
// performDrag and performThrow callbacks as appropriate.
touchScrollHelper.startScrollWatch(
event,
canScrollHorizontally,
canScrollVertically,
Math.round(minSlopInches * Capabilities.screenDPI),
dragEventThinning ? _maxDragRate : NaN);
event.stopImmediatePropagation();
}
else
{
// Stop the current throw and allow the down event
// to propogate normally.
if (throwEffect && throwEffect.isPlaying)
{
throwEffect.stop();
snapContentScrollPosition();
}
}
break;
case MouseEvent.CLICK:
if (!captureNextClick)
return;
event.stopImmediatePropagation();
break;
}
}
/**
* @private
* Mousedown listener that adds the other listeners to watch for a scroll.
*/
private function mouseDownHandler(event:MouseEvent):void
{
stopThrowEffectOnMouseDown();
// If the snap animation is playing, we need to stop it
// before watching for a scroll and potentially beginning
// a new touch interaction.
if (snapElementAnimation && snapElementAnimation.isPlaying)
{
snapElementAnimation.stop();
// If paging is enabled and the user interrupted the snap animation,
// we need to set the current page to where the animation was stopped.
if (pageScrollingEnabled)
determineCurrentPageScrollPosition();
}
captureNextClick = false;
// Watch for a scroll to begin. The helper object will call our
// performDrag and performThrow callbacks as appropriate.
touchScrollHelper.startScrollWatch(
event,
canScrollHorizontally,
canScrollVertically,
Math.round(minSlopInches * Capabilities.screenDPI),
dragEventThinning ? _maxDragRate : NaN);
}
/**
* @private
*/
mx_internal function performDrag(dragX:Number, dragY:Number):void
{
if (textSelectionAutoScrollEnabled)
{
setUpTextSelectionAutoScroll();
return;
}
// dragX and dragY are delta value in the global coordinate space.
// In order to use them to change the scroll position we must convert
// them to the scroller's local coordinate space first.
// This code converts the deltas from global to local.
var localDragDeltas:Point =
globalToLocal(new Point(dragX,dragY)).subtract(globalToLocal(ZERO_POINT));
dragX = localDragDeltas.x;
dragY = localDragDeltas.y;
var xMove:int = 0;
var yMove:int = 0;
if (canScrollHorizontally)
xMove = dragX;
if (canScrollVertically)
yMove = dragY;
var newHSP:Number = hspBeforeTouchScroll - xMove;
var newVSP:Number = vspBeforeTouchScroll - yMove;
var viewportWidth:Number = isNaN(viewport.width) ? 0 : viewport.width;
// If we're pulling the list past its end, we want it to move
// only a portion of the finger distance to simulate tension.
if (pullEnabled)
{
if (newHSP < minHorizontalScrollPosition)
newHSP = Math.round(minHorizontalScrollPosition + ((newHSP-minHorizontalScrollPosition) * PULL_TENSION_RATIO));
if (newHSP > maxHorizontalScrollPosition)
newHSP = Math.round(maxHorizontalScrollPosition + ((newHSP-maxHorizontalScrollPosition) * PULL_TENSION_RATIO));
var viewportHeight:Number = isNaN(viewport.height) ? 0 : viewport.height;
if (newVSP < minVerticalScrollPosition)
newVSP = Math.round(minVerticalScrollPosition + ((newVSP-minVerticalScrollPosition) * PULL_TENSION_RATIO));
if (newVSP > maxVerticalScrollPosition)
newVSP = Math.round(maxVerticalScrollPosition + ((newVSP-maxVerticalScrollPosition) * PULL_TENSION_RATIO));
// clamp the values here
newHSP = Math.min(Math.max(newHSP, -viewportWidth), maxHorizontalScrollPosition+viewportWidth);
newVSP = Math.min(Math.max(newVSP, -viewportHeight), maxVerticalScrollPosition+viewportHeight);
}
viewport.horizontalScrollPosition = newHSP;
viewport.verticalScrollPosition = newVSP;
}
/**
* @private
*/
private function throwEffect_effectEndHandler(event:EffectEvent):void
{
// if we stopped the effect ourself (because someone pressed down), then let's not consider
// this the end
if (stoppedPreemptively)
return;
touchScrollHelper.endTouchScroll();
}
/**
* @private
*/
mx_internal function performThrow(velocityX:Number, velocityY:Number):void
{
// Don't throw if we're doing a text selection auto scroll
if (textSelectionAutoScrollEnabled)
{
stopTextSelectionAutoScroll();
touchScrollHelper.endTouchScroll();
return;
}
// If the soft keyboard is up (or about to come up), or
// we're offscreen for some reason, don't start a throw.
if (preventThrows || !stage)
{
touchScrollHelper.endTouchScroll();
return;
}
stoppedPreemptively = false;
// The velocity values are deltas in the global coordinate space.
// In order to use them to change the scroll position we must convert
// them to the scroller's local coordinate space first.
// This code converts the deltas from global to local.
//
// Note that we scale the velocity values up and then back down around the
// calls to globalToLocal. This is because the runtime only returns values
// rounded to the nearest 0.05. The velocities are small number (<4.0) with
// lots of precision that we don't want to lose. The scaling preserves
// a sufficient level of precision for our purposes.
var throwVelocity:Point = new Point(velocityX, velocityY);
throwVelocity.x *= 100000;
throwVelocity.y *= 100000;
// Because we subtract out the difference between the two coordinate systems' origins,
// This is essentially just multiplying by a scaling factor.
throwVelocity =
this.globalToLocal(throwVelocity).subtract(this.globalToLocal(new Point(0, 0)));
throwVelocity.x /= 100000;
throwVelocity.y /= 100000;
if (setUpThrowEffect(throwVelocity.x, throwVelocity.y))
throwEffect.play();
}
/**
* @private
* When the throw is over, no need to listen for mouse events anymore.
* Also, use this to hide the scrollbars.
*/
private function touchInteractionEndHandler(event:TouchInteractionEvent):void
{
if (event.relatedObject == this)
{
captureNextMouseDown = false;
// don't reset captureNextClick here because touchScrollEnd
// may be invoked on mouseUp and mouseClick occurs immediately
// after that, so we want to block this next mouseClick
hideScrollBars();
inTouchInteraction = false;
}
}
/**
* @private
* Called when the effect finishes playing on the scrollbars. This is so ScrollerLayout
* can hide the scrollbars completely and go back to controlling its visibility.
*/
private function hideScrollBarAnimation_effectEndHandler(event:EffectEvent):void
{
// distinguish between if we called stop() and if the effect ended naturally
if (hideScrollBarAnimationPrematurelyStopped)
return;
// now get rid of the scrollbars visibility
horizontalScrollInProgress = false;
verticalScrollInProgress = false;
// need to invalidate the ScrollerLayout object so it'll update the
// scrollbars in overlay mode
skin.invalidateDisplayList();
}
//--------------------------------------------------------------------------
//
// Text selection auto scroll
//
//--------------------------------------------------------------------------
/**
* @private
* When true, use the text selection scroll behavior instead of the
* typical "throw" behavior. This is only used when interactionMode="touch"
*/
mx_internal var textSelectionAutoScrollEnabled:Boolean = false;
private var textSelectionAutoScrollTimer:Timer;
private var minTextSelectionVScrollPos:int = 0;
private var maxTextSelectionVScrollPos:int = -1;
private var minTextSelectionHScrollPos:int = 0;
private var maxTextSelectionHScrollPos:int = -1;
private static const TEXT_SELECTION_AUTO_SCROLL_FPS:int = 10;
/**
* @private
* Change scroll behavior when selecting text.
*/
mx_internal function enableTextSelectionAutoScroll(enable:Boolean,
minHScrollPosition:int = 0, maxHScrollPosition:int = -1,
minVScrollPosition:int = 0, maxVScrollPosition:int = -1):void
{
if (getStyle("interactionMode") == InteractionMode.TOUCH)
{
this.textSelectionAutoScrollEnabled = enable;
this.minTextSelectionHScrollPos = minHScrollPosition;
this.maxTextSelectionHScrollPos = maxHScrollPosition;
this.minTextSelectionVScrollPos = minVScrollPosition;
this.maxTextSelectionVScrollPos = maxVScrollPosition;
}
}
/**
* @private
*/
mx_internal function setUpTextSelectionAutoScroll():void
{
if (!textSelectionAutoScrollTimer)
{
textSelectionAutoScrollTimer = new Timer(1000 / TEXT_SELECTION_AUTO_SCROLL_FPS);
textSelectionAutoScrollTimer.addEventListener(TimerEvent.TIMER,
textSelectionAutoScrollTimerHandler);
textSelectionAutoScrollTimer.start();
}
}
/**
* @private
*/
mx_internal function stopTextSelectionAutoScroll():void
{
if (textSelectionAutoScrollTimer)
{
textSelectionAutoScrollTimer.stop();
textSelectionAutoScrollTimer.removeEventListener(TimerEvent.TIMER,
textSelectionAutoScrollTimerHandler);
textSelectionAutoScrollTimer = null;
}
}
/**
* @private
*/
private function textSelectionAutoScrollTimerHandler(event:TimerEvent):void
{
const SLOW_SCROLL_THRESHOLD:int = 12; // Distance from edge to trigger a slow scroll
const SLOW_SCROLL_SPEED:int = 20; // Pixels per timer callback to scroll
const FAST_SCROLL_THRESHOLD:int = 3; // Distance from edge to trigger a fast scroll
const FAST_SCROLL_DELTA:int = 30; // Added to SLOW_SCROLL_SPEED to determine fast speed
var newVSP:Number = viewport.verticalScrollPosition;
var newHSP:Number = viewport.horizontalScrollPosition;
if (canScrollHorizontally)
{
if (mouseX > width - SLOW_SCROLL_THRESHOLD)
{
newHSP += SLOW_SCROLL_SPEED;
if (mouseX > width - FAST_SCROLL_THRESHOLD)
newHSP += FAST_SCROLL_DELTA;
if (maxTextSelectionHScrollPos != -1 && newHSP > maxTextSelectionHScrollPos)
newHSP = maxTextSelectionHScrollPos;
}
if (mouseX < SLOW_SCROLL_THRESHOLD)
{
newHSP -= SLOW_SCROLL_SPEED;
if (mouseX < FAST_SCROLL_THRESHOLD)
newHSP -= FAST_SCROLL_DELTA;
if (newHSP < minTextSelectionHScrollPos)
newHSP = minTextSelectionHScrollPos;
}
}
if (canScrollVertically)
{
if (mouseY > height - SLOW_SCROLL_THRESHOLD)
{
newVSP += SLOW_SCROLL_SPEED;
if (mouseY > height - FAST_SCROLL_THRESHOLD)
newVSP += FAST_SCROLL_DELTA;
if (maxTextSelectionVScrollPos != -1 && newVSP > maxTextSelectionVScrollPos)
newVSP = maxTextSelectionVScrollPos;
}
if (mouseY < SLOW_SCROLL_THRESHOLD)
{
newVSP -= SLOW_SCROLL_SPEED;
if (mouseY < FAST_SCROLL_THRESHOLD)
newVSP -= FAST_SCROLL_DELTA;
if (newVSP < minTextSelectionVScrollPos)
newVSP = minTextSelectionVScrollPos;
}
}
if (newHSP != viewport.horizontalScrollPosition)
viewport.horizontalScrollPosition = newHSP;
if (newVSP != viewport.verticalScrollPosition)
viewport.verticalScrollPosition = newVSP;
}
//--------------------------------------------------------------------------
//
// Event handlers: SoftKeyboard Interaction
//
//--------------------------------------------------------------------------
/**
* @private
*/
private function addedToStageHandler(event:Event):void
{
if (getStyle("interactionMode") == InteractionMode.TOUCH)
{
// Note that we listen for orientationChanging in the capture phase. This is done so we get the event
// before Application does to ensure that the pre-orientation-change dimensions are still in effect.
// On iOS, Application swaps the dimensions and forces a validation in its orientationChanging handler.
systemManager.stage.addEventListener("orientationChanging", orientationChangingHandler, true);
}
}
/**
* @private
*/
private function removedFromStageHandler(event:Event):void
{
if (getStyle("interactionMode") == InteractionMode.TOUCH)
systemManager.stage.removeEventListener("orientationChanging", orientationChangingHandler, true);
}
/**
* @private
* Called when the soft keyboard is activated.
*
* There are three use cases for Scroller and text component interaction
*
* A. Pressing a TextInput to open up the soft keyboard
* B. Pressing in the middle of a TextArea to open up the soft keyboard
* C. Pressing in a text component on a device that doesn't support soft keyboard
*
* For use case A, lastFocusedElementCaretBounds is never set, so we just
* call ensureElementIsVisible on the TextInput
*
* For use case B, we first get a softKeyboard active event in the
* capture phase. We then receive a caretBoundsChange event from the
* TextArea skin. We store the bounds in lastFocusedElementCaretBounds
* and use that value in the call to ensureElementPositionIsVisible in
* the softKeyboard activate bubble phase.
*
* For use case C, we never receive a soft keyboard activate event, so
* we just listen for caretBoundsChange.
*/
private function softKeyboardActivateHandler(event:SoftKeyboardEvent):void
{
preventThrows = true;
// Size of app has changed, so run this logic again
var keyboardRect:Rectangle = stage.softKeyboardRect;
if (keyboardRect.width > 0 && keyboardRect.height > 0)
{
if (lastFocusedElement && ensureElementIsVisibleForSoftKeyboard &&
(keyboardRect.height != oldSoftKeyboardHeight ||
keyboardRect.width != oldSoftKeyboardWidth))
{
// lastFocusedElementCaretBounds might have been set in the
// caretBoundsChange event handler
if (lastFocusedElementCaretBounds == null)
{
ensureElementIsVisible(lastFocusedElement);
}
else
{
// Only show entire element if we just activated the soft keyboard
// If the predictive text bar showed up, we don't want the
// the element to jump
var isSoftKeyboardActive:Boolean = oldSoftKeyboardHeight > 0 || oldSoftKeyboardWidth > 0;
ensureElementPositionIsVisible(lastFocusedElement, lastFocusedElementCaretBounds, !isSoftKeyboardActive);
lastFocusedElementCaretBounds = null;
}
}
oldSoftKeyboardHeight = keyboardRect.height;
oldSoftKeyboardWidth = keyboardRect.width;
}
}
/**
* @private
* Listen for softKeyboard activate in the capture phase so we know if
* we need to delay calling ensureElementPositionIsVisible if we get
* a caretBoundsChange event
*/
private function softKeyboardActivateCaptureHandler(event:SoftKeyboardEvent):void
{
var keyboardRect:Rectangle = stage.softKeyboardRect;
if (keyboardRect.width > 0 && keyboardRect.height > 0)
{
captureNextCaretBoundsChange = true;
}
}
/**
* @private
* Called when the soft keyboard is deactivated. Tells the top level
* application to resize itself and fix the scroll position if necessary
*/
private function softKeyboardDeactivateHandler(event:SoftKeyboardEvent):void
{
// Adjust the scroll position after the application's size is restored.
adjustScrollPositionAfterSoftKeyboardDeactivate();
oldSoftKeyboardHeight = NaN;
oldSoftKeyboardWidth = NaN;
preventThrows = false;
}
/**
* @private
*/
mx_internal function adjustScrollPositionAfterSoftKeyboardDeactivate():void
{
// If the throw animation is still playing, stop it.
if (throwEffect && throwEffect.isPlaying)
throwEffect.stop();
// Fix the scroll position in case we're off the end from the animation
snapContentScrollPosition();
}
/**
* @private
*
* If we just received a softKeyboardActivate event in the capture phase,
* we will wait until the bubble phase to call ensureElementPositionIsVisible
* For now, store the caret bounds to be used.
*/
private function caretBoundsChangeHandler(event:CaretBoundsChangeEvent):void
{
if (event.isDefaultPrevented())
return;
event.preventDefault();
if (captureNextCaretBoundsChange)
{
lastFocusedElementCaretBounds = event.newCaretBounds;
captureNextCaretBoundsChange = false;
return;
}
// If caretBounds is changing, minimize the scroll
ensureElementPositionIsVisible(lastFocusedElement, event.newCaretBounds, false, false);
}
}
}