| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // 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> |
| * <s:Scroller width="100" height="100"> |
| * <s:Group> |
| * <mx:Image width="300" height="400" |
| * source="@Embed(source='assets/logo.jpg')"/> |
| * </s:Group> |
| * </s:Scroller></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> |
| * <s:Scroller width="100" height="100"> |
| * <s:Group |
| * horizontalScrollPosition="50" verticalScrollPosition="50"> |
| * <mx:Image width="300" height="400" |
| * source="@Embed(source='assets/logo.jpg')"/> |
| * </s:Group> |
| * </s:Scroller></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><s:Scroller></code> tag inherits all of the tag |
| * attributes of its superclass and adds the following tag attributes:</p> |
| * |
| * <pre> |
| * <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%" |
| * /> |
| * </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); |
| } |
| } |
| |
| } |