blob: 79c5c1b6adc44d4574f2da96635b41f5d729301c [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package spark.components.supportClasses
{
import flash.events.Event;
import flash.events.MouseEvent;
import flash.events.TimerEvent;
import flash.geom.Point;
import flash.utils.Timer;
import mx.core.mx_internal;
import mx.events.FlexEvent;
import mx.events.PropertyChangeEvent;
import mx.events.ResizeEvent;
import mx.events.SandboxMouseEvent;
import spark.components.Button;
import spark.core.IViewport;
import spark.effects.animation.Animation;
import spark.effects.animation.MotionPath;
import spark.effects.animation.SimpleMotionPath;
import spark.effects.easing.IEaser;
import spark.effects.easing.Linear;
import spark.effects.easing.Sine;
import spark.events.TrackBaseEvent;
use namespace mx_internal;
/**
* The inactive state.
* This is the state when there is no content to scroll,
* which means <code>maximum</code> &lt;= <code>minimum</code>.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[SkinState("inactive")]
/**
* @copy spark.components.supportClasses.GroupBase#style:symbolColor
*
* @default 0x000000
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="symbolColor", type="uint", format="Color", inherit="yes", theme="spark")]
/**
* If true, the thumb's size along the scrollbar's axis will be
* its preferred size.
*
* @default false
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="fixedThumbSize", type="Boolean", inherit="no")]
/**
* If true (the default), the thumb's visibility will be reset
* whenever its size is updated.
*
* Overrides of <code>updateSkinDisplayList()</code> in
* <code>HScrollBar</code> and <code>VScrollBar</code>
* make the thumb visible if it's smaller than the track,
* unless this style is false.
*
* Set this style to false to control thumb visibility directly.
*
* @default true
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="autoThumbVisibility", type="Boolean", inherit="no")]
/**
* If true (the default) then dragging the scrollbar's thumb with the mouse immediately
* updates the scrollbar's value. If false, the scrollbar's value is only updated when
* the mouse button is released.
*
* @default true
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="liveDragging", type="Boolean", inherit="no")]
/**
* Number of milliseconds after the first page event
* until subsequent page events occur.
*
* @default 500
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="repeatDelay", type="Number", format="Time", inherit="no", minValue="0.0")]
/**
* Number of milliseconds between page events
* if the user presses and holds the mouse on the track.
*
* @default 35
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="repeatInterval", type="Number", format="Time", inherit="no", minValueExclusive="0.0")]
/**
* This style determines whether the scrollbar will animate
* smoothly when paging and stepping. When false, page and step
* operations will jump directly to the paged/stepped locations.
* When true, the scrollbar, and any content it is scrolling, will
* animate to that location.
*
* @default true
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="smoothScrolling", type="Boolean", inherit="no")]
//--------------------------------------
// Excluded APIs
//--------------------------------------
[Exclude(name="focusBlendMode", kind="style")]
[Exclude(name="focusThickness", kind="style")]
/**
* The ScrollBarBase class helps to position
* the portion of data that is displayed when there is too much data
* to fit in a display area.
* The ScrollBarBase class displays a pair of scrollbars and a viewport.
* A viewport is a UIComponent that implements IViewport, such as Group.
*
* <p>This control extends the TrackBase class and
* is the base class for the HScrollBar and VScrollBar
* controls.</p>
*
* <p>A scroll bar consists of a track, a variable-size scroll thumb, and
* two optional arrow buttons. The ScrollBarBase class uses four parameters
* to calculate its display state:</p>
*
* <ul>
* <li><code>minimum</code>: Minimum range value.</li>
* <li><code>maximum</code>:Maximum range value.</li>
* <li><code>value</code>: Current position, which must be within the
* minimum and maximum range values.</li>
* <li>Viewport size: Represents the number of items
* in the range that you can display at one time. The
* number of items must be less than or equal to the
* range, where the range is the set of values between
* the minimum range value and the maximum range value.</li>
* </ul>
*
*
* @mxml
*
* <p>The <code>&lt;s:ScrollBarBase&gt;</code> tag inherits all of the tag
* attributes of its superclass and adds the following tag attributes:</p>
*
* <pre>
* &lt;s:ScrollBarBase
* <strong>Properties</strong>
* pageSize="20"
* snapInterval="1"
* viewport="null"
*
* <strong>Styles</strong>
* autoThumbVisibility="true"
* fixedThumbSize="false"
* repeatDelay="500"
* repeatInterval="35"
* smoothScrolling="true"
* symbolColor="0x000000"
* /&gt;
* </pre>
* @see spark.core.IViewport
* @see spark.skins.spark.ScrollerSkin
* @see spark.skins.spark.ScrollBarDownButtonSkin
* @see spark.skins.spark.ScrollBarLeftButtonSkin
* @see spark.skins.spark.ScrollBarRightButtonSkin
* @see spark.skins.spark.ScrollBarUpButtonSkin
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public class ScrollBarBase extends TrackBase
{
include "../../core/Version.as";
//--------------------------------------------------------------------------
//
// Class constants
//
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function ScrollBarBase():void
{
super();
}
//--------------------------------------------------------------------------
//
// Skins
//
//--------------------------------------------------------------------------
[SkinPart(required="false")]
/**
* An optional skin part that defines a button
* that, when pressed, steps the scrollbar up.
* This is equivalent to a decreasing step to the <code>value</code> property.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public var decrementButton:Button;
[SkinPart(required="false")]
/**
* An optional skin part that defines a button
* that, when pressed, steps the scrollbar down.
* This is equivalent
* to an increasing step to the <code>value</code> property.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public var incrementButton:Button;
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* @private
* this one animator is used by both paging and stepping animations. It
* is responsible for running the repeated operation (animating from the beginning
* of the repeat to whenever it ends or the user stops the repeating action).
*/
private var _animator:Animation = null;
private function get animator():Animation
{
if (_animator)
return _animator;
_animator = new Animation();
var animTarget:AnimationTarget = new AnimationTarget(animationUpdateHandler);
animTarget.endFunction = animationEndHandler;
_animator.animationTarget = animTarget;
return _animator;
}
/**
* @private
* These variables track whether we are currently involved in a stepping
* animation, and which direction we are stepping
*/
private var steppingDown:Boolean;
private var steppingUp:Boolean;
/**
* @private
* This variable tracks whether we are currently stepping the ScrollBarBase
*/
private var isStepping:Boolean;
/**
* @private
* This variable tracks whether we are currently running an animation to
* do a single changeValueByPage() operation. This is used to end that operation properly
* if another operation interrupts it.
*/
private var animatingOnce:Boolean;
/**
* @private
* Location of the mouse down event on the thumb, relative to the thumb's origin.
* Used to update the value property when the mouse is dragged.
*/
private var clickOffset:Point;
/**
* @private
* Easers used in animated scrolling operations
*/
private static var linearEaser:IEaser = new Linear();
private static var easyInLinearEaser:IEaser = new Linear(.1);
private static var deceleratingSineEaser:IEaser = new Sine(0);
// TODO (hmuller): transient?
// Direction indicator for current track-scrolling operations
private var trackScrollDown:Boolean;
// Timer used for repeated scrolling when mouse is held down on track
private var trackScrollTimer:Timer;
// TODO (hmuller): transient?
// Cache current position on track for scrolling operations
private var trackPosition:Point = new Point();
// TODO (hmuller): transient?
// Flag to indicate whether track-scrolling is in process
private var trackScrolling:Boolean = false;
//--------------------------------------------------------------------------
//
// Overridden properties: Range
//
//--------------------------------------------------------------------------
[Inspectable(category="General", defaultValue="0.0")]
/**
* @private
* Invalidate the skin state when minimum is changed.
*/
override public function set minimum(value:Number):void
{
if (value == super.minimum)
return;
super.minimum = value;
invalidateSkinState();
}
[Inspectable(category="General", defaultValue="100.0")]
/**
* @private
* Invalidate the skin state when maximum is changed.
*/
override public function set maximum(value:Number):void
{
if (value == super.maximum)
return;
super.maximum = value;
invalidateSkinState();
}
[Inspectable(category="General", defaultValue="1.0", minValue="0.0")]
/**
* @inheritDoc
*
* @default 1
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
override public function set snapInterval(value:Number):void
{
super.snapInterval = value;
// setting snapInterval may change the pageSize
pageSizeChanged = true;
}
/**
* @private
* Keep the pendingValue in sync with the actual value so that updateSkinDisplayList()
* overrides can just use pendingValue.
*/
override protected function setValue(value:Number):void
{
_pendingValue = value;
super.setValue(value);
}
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//---------------------------------
// pageSize
//---------------------------------
private var _pageSize:Number = 20;
private var pageSizeChanged:Boolean = false;
[Inspectable(category="General", defaultValue="20", minValue="0.0")]
/**
* The change in the value of the <code>value</code> property
* when you call the <code>changeValueByPage()</code> method.
*
* @default 20
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get pageSize():Number
{
return _pageSize;
}
/**
* @private
*/
public function set pageSize(value:Number):void
{
if (value == _pageSize)
return;
_pageSize = value;
pageSizeChanged = true;
invalidateProperties();
invalidateDisplayList();
}
//----------------------------------
// pendingValue
//----------------------------------
/**
* @private
*/
private var _pendingValue:Number = 0;
/**
* The value the scrollbar will have when the mouse button is released.
*
* <p>If the <code>liveDragging</code> style is false, then the scrollbar's value is only set
* when the mouse button is released. The value is not updated while the slider thumb is
* being dragged.</p>
*
* <p>This property is updated when the scrollbar thumb moves, even if
* <code>liveDragging</code> is false.</p>
*
* @default 0
* @return The value implied by the thumb position.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
protected function get pendingValue():Number
{
return _pendingValue;
}
/**
* @private
*/
protected function set pendingValue(value:Number):void
{
if (value == _pendingValue)
return;
_pendingValue = value;
invalidateDisplayList();
}
//----------------------------------
// viewport
//----------------------------------
private var _viewport:IViewport;
/**
* The viewport controlled by this scrollbar.
*
* If a viewport is specified, then changes to its actual size, content
* size, and scroll position cause the corresponding ScrollBarBase methods to
* run:
* <ul>
* <li><code>viewportResizeHandler()</code></li>
* <li><code>contentWidthChangeHandler()</code></li>
* <li><code>contentHeightChangeHandler()</code></li>
* <li><code>viewportVerticalScrollPositionChangeHandler()</code></li>
* <li><code>viewportHorizontalScrollPositionChangeHandler()</code></li>
* </ul>
*
* <p>The VScrollBar and HScrollBar classes override these methods to
* keep their <code>pageSize</code>, <code>maximum</code>, and <code>value</code> properties in sync with the
* viewport. Similarly, they override their <code>changeValueByPage()</code> and <code>changeValueByStep()</code> methods to
* use the viewport's <code>scrollPositionDelta</code> methods to compute page and
* and step offsets.</p>
*
* @default null
* @see spark.components.VScrollBar
* @see spark.components.HScrollBar
*
*
* @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;
if (_viewport) // old _viewport
{
_viewport.removeEventListener(PropertyChangeEvent.PROPERTY_CHANGE, viewport_propertyChangeHandler);
_viewport.removeEventListener(ResizeEvent.RESIZE, viewportResizeHandler);
_viewport.clipAndEnableScrolling = false;
}
_viewport = value;
if (_viewport) // new _viewport
{
_viewport.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, viewport_propertyChangeHandler);
_viewport.addEventListener(ResizeEvent.RESIZE, viewportResizeHandler);
_viewport.clipAndEnableScrolling = true;
}
}
//----------------------------------
// content minimum/maximum
//----------------------------------
private var _contentMinimum:Number = NaN;
/**
* @private
*/
mx_internal function get contentMinimum():Number
{
return _contentMinimum;
}
/**
* @private
*/
mx_internal function set contentMinimum(value:Number):void
{
if (value == _contentMinimum)
return;
_contentMinimum = value;
invalidateDisplayList();
}
private var _contentMaximum:Number = NaN;
/**
* @private
*/
mx_internal function get contentMaximum():Number
{
return _contentMaximum;
}
/**
* @private
*/
mx_internal function set contentMaximum(value:Number):void
{
if (value == _contentMaximum)
return;
_contentMaximum = value;
invalidateDisplayList();
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* @private
*/
private function startAnimation(duration:Number, valueTo:Number,
easer:IEaser, startDelay:Number = 0):void
{
animator.stop();
animator.duration = duration;
animator.easer = easer;
animator.motionPaths = new <MotionPath>[
new SimpleMotionPath("value", value, valueTo)];
animator.startDelay = startDelay;
animator.play();
}
/**
* @private
* Returns the integer multiple of snapInterval that's closest to size.
*
* <p>If snapInterval is 0, which means that values are only constrained
* by the minimum and maximum properties, then size is returned unchanged.</p>
*
* <p>This method is used by commitProperties() to validate the
* pageSize. There's a copy of this method in Range.as</p>
*
* @param size The input size.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
private function nearestValidSize(size:Number):Number
{
var interval:Number = snapInterval;
if (interval == 0)
return size;
var validSize:Number = Math.round(size / interval) * interval
return (Math.abs(validSize) < interval) ? interval : validSize;
}
/**
* @private
*/
override protected function commitProperties():void
{
super.commitProperties();
if (pageSizeChanged)
{
_pageSize = nearestValidSize(_pageSize);
pageSizeChanged = false;
}
}
/**
* @private
*/
override protected function getCurrentSkinState():String
{
if (maximum <= minimum)
return "inactive";
return super.getCurrentSkinState();
}
/**
* @private
*/
override protected function partAdded(partName:String, instance:Object):void
{
super.partAdded(partName, instance);
if (instance == decrementButton)
{
decrementButton.addEventListener(FlexEvent.BUTTON_DOWN,
button_buttonDownHandler);
decrementButton.addEventListener(MouseEvent.ROLL_OVER,
button_rollOverHandler);
decrementButton.addEventListener(MouseEvent.ROLL_OUT,
button_rollOutHandler);
decrementButton.autoRepeat = true;
}
else if (instance == incrementButton)
{
incrementButton.addEventListener(FlexEvent.BUTTON_DOWN,
button_buttonDownHandler);
incrementButton.addEventListener(MouseEvent.ROLL_OVER,
button_rollOverHandler);
incrementButton.addEventListener(MouseEvent.ROLL_OUT,
button_rollOutHandler);
incrementButton.autoRepeat = true;
}
else if (instance == track)
{
track.addEventListener(MouseEvent.ROLL_OVER,
track_rollOverHandler);
track.addEventListener(MouseEvent.ROLL_OUT,
track_rollOutHandler);
}
}
/**
* @private
*/
override protected function partRemoved(partName:String, instance:Object):void
{
super.partRemoved(partName, instance);
if (instance == decrementButton)
{
decrementButton.removeEventListener(FlexEvent.BUTTON_DOWN,
button_buttonDownHandler);
decrementButton.removeEventListener(MouseEvent.ROLL_OVER,
button_rollOverHandler);
decrementButton.removeEventListener(MouseEvent.ROLL_OUT,
button_rollOutHandler);
}
else if (instance == incrementButton)
{
incrementButton.removeEventListener(FlexEvent.BUTTON_DOWN,
button_buttonDownHandler);
incrementButton.removeEventListener(MouseEvent.ROLL_OVER,
button_rollOverHandler);
incrementButton.removeEventListener(MouseEvent.ROLL_OUT,
button_rollOutHandler);
}
else if (instance == track)
{
track.removeEventListener(MouseEvent.ROLL_OVER,
track_rollOverHandler);
track.removeEventListener(MouseEvent.ROLL_OUT,
track_rollOutHandler);
}
}
/**
* @private
*/
override public function styleChanged(styleProp:String):void
{
super.styleChanged(styleProp);
if (styleProp == "autoThumbVisibility")
invalidateDisplayList();
}
/**
* @private
*/
override protected function system_mouseMoveHandler(event:MouseEvent):void
{
if (!track)
return;
var p:Point = track.globalToLocal(new Point(event.stageX, event.stageY));
var newValue:Number = pointToValue(p.x - clickOffset.x, p.y - clickOffset.y);
newValue = nearestValidValue(newValue, snapInterval);
if (newValue != pendingValue)
{
dispatchEvent(new TrackBaseEvent(TrackBaseEvent.THUMB_DRAG));
if (getStyle("liveDragging") === true)
{
setValue(newValue);
dispatchEvent(new Event(Event.CHANGE));
}
else
{
pendingValue = newValue;
}
}
event.updateAfterEvent();
}
/**
* @private
*/
override protected function system_mouseUpHandler(event:Event):void
{
if ((getStyle("liveDragging") === false) && (value != pendingValue))
{
setValue(pendingValue);
dispatchEvent(new Event(Event.CHANGE));
}
super.system_mouseUpHandler(event);
}
/**
* Adds or subtracts <code>pageSize</code> from <code>value</code>.
* For an addition, the new <code>value</code> is the closest multiple of <code>pageSize</code>
* that is larger than the current <code>value</code>.
* For a subtraction, the new <code>value</code>
* is the closest multiple of <code>pageSize</code> that is
* smaller than the current value.
* The minimum value of <code>value</code> is <code>pageSize</code>.
*
* @param increase Whether the paging action adds (<code>true</code>) or
* decreases (<code>false</code>) <code>value</code>.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function changeValueByPage(increase:Boolean = true):void
{
var val:Number;
if (increase)
val = Math.min(value + pageSize, maximum);
else
val = Math.max(value - pageSize, minimum);
if (getStyle("smoothScrolling"))
{
startAnimation(getStyle("repeatInterval"), val, linearEaser);
}
else
{
setValue(val);
dispatchEvent(new Event(Event.CHANGE));
}
}
//--------------------------------------------------------------------------
//
// Event Handlers
//
//--------------------------------------------------------------------------
//---------------------------------
// Viewport property changes
//---------------------------------
private function viewport_propertyChangeHandler(event:PropertyChangeEvent):void
{
switch(event.property)
{
case "contentWidth":
viewportContentWidthChangeHandler(event);
break;
case "contentHeight":
viewportContentHeightChangeHandler(event);
break;
case "horizontalScrollPosition":
viewportHorizontalScrollPositionChangeHandler(event);
break;
case "verticalScrollPosition":
viewportVerticalScrollPositionChangeHandler(event);
break;
}
}
/**
* @private
* Called when the viewport's width or height value changes. Does nothing by default.
*/
mx_internal function viewportResizeHandler(event:ResizeEvent):void
{
}
/**
* @private
* Called when the viewport's <code>contentWidth</code> value changes. Does nothing by default.
*/
mx_internal function viewportContentWidthChangeHandler(event:PropertyChangeEvent):void
{
}
/**
* @private
* Called when the viewport's <code>contentHeight</code> value changes. Does nothing by default.
*/
mx_internal function viewportContentHeightChangeHandler(event:PropertyChangeEvent):void
{
}
/**
* @private
* Called when the viewport's <code>horizontalScrollPosition</code> value changes. Does nothing by default.
*/
mx_internal function viewportHorizontalScrollPositionChangeHandler(event:PropertyChangeEvent):void
{
}
/**
* @private
* Called when the viewport's <code>verticalScrollPosition</code> value changes. Does nothing by default.
*/
mx_internal function viewportVerticalScrollPositionChangeHandler(event:PropertyChangeEvent):void
{
}
//---------------------------------
// Thumb dragging handlers
//---------------------------------
/**
* @private
*/
override protected function thumb_mouseDownHandler(event:MouseEvent) : void
{
// Stop animation before thumb dragging
stopAnimation();
super.thumb_mouseDownHandler(event);
clickOffset = thumb.globalToLocal(new Point(event.stageX, event.stageY));
}
//---------------------------------
// Mouse up/down handlers
//---------------------------------
/**
* Handles a click on the increment or decrement button of the scroll bar.
* This should cause a stepping operation, which is repeated if held down.
* The delay before repetition begins and the delay between repeated events
* are determined by the <code>repeatDelay</code> and
* <code>repeatInterval</code> styles of the underlying Button objects.
*
* @param event The event object.
*
* @see spark.components.Button
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
protected function button_buttonDownHandler(event:Event):void
{
// Make sure we finish any running page animation before starting
// to step.
if (!isStepping)
stopAnimation();
var increment:Boolean = (event.target == incrementButton);
// Dispatch changeStart for the first step if we can make a step.
if (!isStepping &&
((increment && value < maximum) ||
(!increment && value > minimum)))
{
dispatchEvent(new FlexEvent(FlexEvent.CHANGE_START));
isStepping = true;
systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_UP,
button_buttonUpHandler, true);
systemManager.getSandboxRoot().addEventListener(
SandboxMouseEvent.MOUSE_UP_SOMEWHERE, button_buttonUpHandler);
}
// Noop if we're currently running a stepping animation. We get
// called repeatedly here due to the button's autoRepeat
if (!steppingDown && !steppingUp)
{
// TODO (chaase): first step is non-animated, just to simplify the delayed
// start of the animated stepping. Seems okay, but worth thinking
// about whether we should animate the first step too
changeValueByStep(increment);
// Only animate if smoothScrolling enabled and we're not at the end already
if (getStyle("smoothScrolling") &&
((increment && pendingValue < maximum) ||
(!increment && pendingValue > minimum)))
{
// Default stepSize may be too small to be useful here; use fraction of
// pageSize if it's larger
animateStepping(increment ? maximum : minimum,
Math.max(pageSize/10, stepSize));
}
return;
}
}
/**
* Handles releasing the increment or decrement button of the scrollbar.
* This ends the stepping operation started by the original buttonDown
* event on the button.
*
* @param event The event object.
*
* @see spark.components.Button
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
protected function button_buttonUpHandler(event:Event):void
{
if (steppingDown || steppingUp)
{
// stopAnimation will not dispatch a changeEnd.
stopAnimation();
dispatchEvent(new FlexEvent(FlexEvent.CHANGE_END));
steppingUp = steppingDown = false;
isStepping = false;
}
else if (isStepping)
{
// Dispatch changeEnd event for no animation case
dispatchEvent(new FlexEvent(FlexEvent.CHANGE_END));
isStepping = false;
}
systemManager.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_UP,
button_buttonUpHandler, true);
systemManager.getSandboxRoot().removeEventListener(
SandboxMouseEvent.MOUSE_UP_SOMEWHERE, button_buttonUpHandler);
}
//---------------------------------
// Track dragging handlers
//---------------------------------
/**
* @private
* Handle mouse-down events for the scroll track. In our handler,
* we figure out where the event occurred on the track and begin
* paging the scroll position toward that location. We start a
* timer to handle repeating events if the user keeps the button
* pressed on the track.
*/
override protected function track_mouseDownHandler(event:MouseEvent):void
{
// TODO (chaase): We might want a different event mechanism eventually
// which would push this enabled check into the child/skin components
if (!enabled)
return;
// Make sure we finish any running page animation before starting
// a new one.
stopAnimation();
// Cache original event location for use on later repeating events
trackPosition = track.globalToLocal(new Point(event.stageX, event.stageY));
// If the user shift-clicks on the track, then offset the event coordinates so
// that the thumb ends up centered under the mouse.
if (event.shiftKey)
{
var thumbW:Number = (thumb) ? thumb.getLayoutBoundsWidth() : 0;
var thumbH:Number = (thumb) ? thumb.getLayoutBoundsHeight() : 0;
trackPosition.x -= (thumbW / 2);
trackPosition.y -= (thumbH / 2);
}
var newScrollValue:Number = pointToValue(trackPosition.x, trackPosition.y);
trackScrollDown = (newScrollValue > pendingValue);
if (event.shiftKey)
{
// shift-click positions jumps to the clicked location instead
// of incrementally paging
var slideDuration:Number = getStyle("slideDuration");
var adjustedValue:Number = nearestValidValue(newScrollValue, snapInterval);
if (getStyle("smoothScrolling") &&
slideDuration != 0 &&
(maximum - minimum) != 0)
{
dispatchEvent(new FlexEvent(FlexEvent.CHANGE_START));
// Animate the shift-click operation
startAnimation(slideDuration *
(Math.abs(value - newScrollValue) / (maximum - minimum)),
adjustedValue, deceleratingSineEaser);
animatingOnce = true;
}
else
{
setValue(adjustedValue);
dispatchEvent(new Event(Event.CHANGE));
}
return;
}
dispatchEvent(new FlexEvent(FlexEvent.CHANGE_START));
// Assume we're repeating unless user releases
animatingOnce = false;
changeValueByPage(trackScrollDown);
trackScrolling = true;
// Add event handlers for drag and up events
systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_MOVE,
track_mouseMoveHandler, true);
systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_UP,
track_mouseUpHandler, true);
systemManager.getSandboxRoot().addEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE,
track_mouseUpHandler);
// TODO (chaase): consider using the repeat behavior of Button
// to handle track-down repetition, instead of doing it with a
// custom Timer. As long as we can distinguish the first
// down event from subsequent ones, we may be able to just let
// Button do most of this work.
// Start a timer to handle repeating events if the user
// continues to hold the mouse button down
if (!trackScrollTimer)
{
trackScrollTimer = new Timer(getStyle("repeatDelay"), 1);
trackScrollTimer.addEventListener(TimerEvent.TIMER,
trackScrollTimerHandler);
}
else
{
// Note that this behavior, resetting the initial delay, differs
// from Flex3 but is more consistent with general application
// scrollbar behavior
trackScrollTimer.delay = getStyle("repeatDelay");
trackScrollTimer.repeatCount = 1;
}
trackScrollTimer.start();
}
/**
* Animates the operation to move to <code>newValue</code>.
* The <code>pageSize</code> parameter is used to compute the amount
* of time taken to get to that value, so that the time taken to animate
* a paging operation is roughly the same as the non-animated version;
* both operations should end up at the same place at about the same time.
*
* @param newValue The final value being paged to.
* @param pageSize The amount of horizontal or vertical movement requested.
* This value is used to compute, with the <code>repeatInterval</code> style,
* the total time taken to move to the new value. <code>pageSize</code>
* is usually set dynamically by the containing Scroller to the value required
* to view content at a logical content boundary.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
protected function animatePaging(newValue:Number, pageSize:Number):void
{
animatingOnce = false;
// TODO (chaase): hard-coding easing behavior, how to style it?
startAnimation(
getStyle("repeatInterval") * (Math.abs(newValue - pendingValue) / pageSize),
newValue, linearEaser);
}
/**
* Animates the operation to step to <code>newValue</code>.
* The <code>stepSize</code> parameter is used to compute the amount
* of time taken to get to that value, so that the time taken to animate
* a stepping operation is roughly the same as the non-animated version;
* both operations should end up at the same place at about the same time.
*
* @param newValue The final value being stepped to.
* @param stepSize The amount of stepping requested.
* This value is used to compute, with the <code>repeatInterval</code> style,
* the total time taken to step to the new value. <code>stepSize</code>
* is usually set dynamically by the containing Scroller to the value required
* to view content at a logical content boundary.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
protected function animateStepping(newValue:Number, stepSize:Number):void
{
steppingDown = (newValue > pendingValue);
steppingUp = !steppingDown;
var denominator:Number = (stepSize != 0) ? stepSize : 1; // avoid div-by-0 below
var duration:Number = getStyle("repeatInterval") *
(Math.abs(newValue - pendingValue) / denominator);
// Cap ease-in factor to 500 ms on long-duration animations
var easer:IEaser;
if (duration > 5000)
easer = new Linear(500/duration);
else
easer = easyInLinearEaser;
// TODO (chaase): we're using ScrollBarBase's repeatInterval for animated
// stepping, but Button's repeatInterval for non-animated stepping
// TODO (chaase): think about the total duration for the animation.
// TODO (chaase): hard-coding easing behavior, how to style it?
startAnimation(duration, newValue, easer, getStyle("repeatDelay"));
}
/**
* @private
* Handles events from the Animation that runs the page, step,
* and shift-click smooth-scrolling operations.
* Just call setValue() with the current animated value.
*/
private function animationUpdateHandler(animation:Animation):void
{
// TODO (klin): Add support to send change events at the right intervals.
setValue(animation.currentValue["value"]);
}
/**
* @private
* Handles end event from the Animation that runs the page, step,
* and shift-click animations.
* We dispatch the "change" event at this time, after the animation
* is done.
*/
private function animationEndHandler(animation:Animation):void
{
if (trackScrolling)
trackScrolling = false;
// End stepping animation
if (steppingDown || steppingUp)
{
// If we're animating stepping, end on a final real step call in the
// appropriate direction, ensuring that we stop on a content
// item boundary
changeValueByStep(steppingDown);
animator.startDelay = 0;
return;
}
// End paging or shift-click animation.
setValue(nearestValidValue(pendingValue, snapInterval));
dispatchEvent(new Event(Event.CHANGE));
// We only dispatch the changeEnd event in the endHandler
// for paging when we are not repeating.
if (animatingOnce)
{
dispatchEvent(new FlexEvent(FlexEvent.CHANGE_END));
animatingOnce = false;
}
}
/**
* @private
* Stops a running animation prematurely and calls the
* animationEndHandler.
*/
private function stopAnimation():void
{
if (animator.isPlaying)
animationEndHandler(animator);
// Stop it regardless, in case the animation is startDelayed and
// thus not 'playing', but still active
animator.stop();
}
/**
* @private
* This gets called at certain intervals to repeat the scroll
* event when the user is still holding the mouse button
* down on the track.
*/
private function trackScrollTimerHandler(event:Event):void
{
// Only repeat the scrolling if the current scroll position
// (represented by fraction) is not past the current
// mouse position on the track
var newScrollValue:Number = pointToValue(trackPosition.x, trackPosition.y);
var fixedThumbSize:Boolean = getStyle("fixedThumbSize") !== false;
// The end result we want, with either animated or non-animated paging,
// is for the thumb to end up under the click point.
// For the fixedThumbSize case, where the thumb may be much smaller
// than the pageSize, we instead want the thumb to end up
// where it would in the variable size case (on a lower value than the
// clicked value), but to end up at the end of the track if it is
// "close enough" to the end. The heuristic for "close enough" is
// if the end of the track is the nearestValidValue on pageSize
// boundaries.
if (trackScrollDown)
{
var range:Number = maximum - minimum;
if (range == 0)
return;
if ((value + pageSize) > newScrollValue &&
(!fixedThumbSize || nearestValidValue(newScrollValue, pageSize) != maximum))
return;
}
else if (newScrollValue > value)
{
return;
}
if (getStyle("smoothScrolling"))
{
// This gets called after an initial repeateDelay on a paging
// operation, but after that we're just running the animation. This
// function is only called repeatedly in the non-smoothScrolling case.
var valueDelta:Number = Math.abs(value - newScrollValue);
var pages:int;
var pageToVal:Number;
if (newScrollValue > value)
{
pages = pageSize != 0 ?
int(valueDelta / pageSize) :
valueDelta;
if (fixedThumbSize && nearestValidValue(newScrollValue, pageSize) == maximum)
pageToVal = maximum;
else
pageToVal = value + (pages * pageSize);
}
else
{
pages = pageSize != 0 ?
int(Math.ceil(valueDelta / pageSize)) :
valueDelta;
pageToVal = Math.max(minimum, value - (pages * pageSize));
}
animatePaging(pageToVal, pageSize);
return;
}
var oldValue:Number = value;
changeValueByPage(trackScrollDown);
if (trackScrollTimer && trackScrollTimer.repeatCount == 1)
{
// If this was the first time repeating, set the Timer to
// repeat indefinitely with an appropriate interval delay
trackScrollTimer.delay = getStyle("repeatInterval");
trackScrollTimer.repeatCount = 0;
}
}
/**
* @private
* Record a new trackPosition, which is the location of the
* mouse on the track, relative to the stage. This is used
* in the ongoing Timer events for track scrolling. Note
* that the timer will be stopped when the mouse is not over
* the track, so although we are setting new trackPosition
* values, we are not actually stepping the scroll if the mouse
* is outside of the track area.
*/
private function track_mouseMoveHandler(event:MouseEvent):void
{
if (trackScrolling)
{
var pt:Point = new Point(event.stageX, event.stageY);
// Cache original event location for use on later repeating events
trackPosition = track.globalToLocal(pt);
}
}
/**
* @private
* Stop scrolling the track if the mouse leaves the stage
* area. Remove the listeners and stop the Timer.
*/
private function track_mouseUpHandler(event:Event):void
{
trackScrolling = false;
systemManager.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_MOVE,
track_mouseMoveHandler, true);
systemManager.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_UP,
track_mouseUpHandler, true);
systemManager.getSandboxRoot().removeEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE,
track_mouseUpHandler);
// First, we check for smoothScrolling and also if we are
// in the non-repeating case.
if (getStyle("smoothScrolling"))
{
if (!animatingOnce)
{
// We check the timer to see if the user released the mouse
// before the repeat delay has expired.
if (trackScrollTimer && trackScrollTimer.running)
{
// If the animation has not yet finished before the repeat delay
// we set animatingOnce to true. Otherwise, the animation
// is done but repeating has not begun so we dispatch a changeEnd
// event.
if (animator.isPlaying)
animatingOnce = true;
else
dispatchEvent(new FlexEvent(FlexEvent.CHANGE_END));
}
else
{
// repeating case
stopAnimation();
dispatchEvent(new FlexEvent(FlexEvent.CHANGE_END));
}
}
}
else
{
// Dispatch changeEnd if there's no animation.
dispatchEvent(new FlexEvent(FlexEvent.CHANGE_END));
}
if (trackScrollTimer)
trackScrollTimer.reset();
}
/**
* @private
* If we are still in the middle of track-scrolling, restart the
* timer when the mouse re-enters the track area.
*/
private function track_rollOverHandler(event:MouseEvent):void
{
// TODO (klin): Fix up roll over/roll out handling so that
// it works with animation.
if (trackScrolling && trackScrollTimer)
trackScrollTimer.start();
}
/**
* @private
* Stop the track-scrolling repeat events if the mouse leaves
* the track area.
*/
private function track_rollOutHandler(event:MouseEvent):void
{
if (trackScrolling && trackScrollTimer)
trackScrollTimer.stop();
}
/**
* @private
* Resume the increment/decrement animation if the mouse enters the
* button area
*/
private function button_rollOverHandler(event:MouseEvent):void
{
if (steppingUp || steppingDown)
animator.resume();
}
/**
* @private
* Pause the increment/decrement animation if the mouse leaves the
* button area
*/
private function button_rollOutHandler(event:MouseEvent):void
{
if (steppingUp || steppingDown)
animator.pause();
}
}
}