| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // Licensed to the Apache Software Foundation (ASF) under one or more |
| // contributor license agreements. See the NOTICE file distributed with |
| // this work for additional information regarding copyright ownership. |
| // The ASF licenses this file to You under the Apache License, Version 2.0 |
| // (the "License"); you may not use this file except in compliance with |
| // the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| package spark.components |
| { |
| import flash.events.Event; |
| import flash.events.MouseEvent; |
| import flash.geom.Point; |
| |
| import mx.core.EventPriority; |
| import mx.core.IInvalidating; |
| import mx.core.InteractionMode; |
| import mx.core.mx_internal; |
| import mx.events.FlexMouseEvent; |
| import mx.events.PropertyChangeEvent; |
| import mx.events.ResizeEvent; |
| |
| import spark.components.supportClasses.ScrollBarBase; |
| import spark.core.IViewport; |
| import spark.core.NavigationUnit; |
| import spark.utils.MouseEventUtil; |
| |
| use namespace mx_internal; |
| |
| //-------------------------------------- |
| // Events |
| //-------------------------------------- |
| |
| /** |
| * Dispatched when the <code>horizontalScrollPosition</code> is going |
| * to change due to a <code>mouseWheel</code> event. |
| * |
| * <p>The default behavior is to scroll horizontally by the event |
| * <code>delta</code> number of "steps". |
| * The viewport's <code>getHorizontalScrollPositionDelta</code> method using |
| * either <code>LEFT</code> or <code>RIGHT</code>, depending on the scroll |
| * direction, determines the width of the step.</p> |
| * |
| * <p>Calling the <code>preventDefault()</code> method |
| * on the event prevents the horizontal scroll position from changing. |
| * Otherwise if you modify the <code>delta</code> property of the event, |
| * that value will be used as the number horizontal 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")] |
| |
| //-------------------------------------- |
| // Other metadata |
| //-------------------------------------- |
| |
| [IconFile("HScrollBar.png")] |
| [DefaultTriggerEvent("change")] |
| |
| /** |
| * The HScrollBar (horizontal scrollbar) control lets you control |
| * the portion of data that is displayed when there is too much data |
| * to fit horizontally in a display area. |
| * |
| * <p>Although you can use the HScrollBar control as a stand-alone control, |
| * you usually combine it as part of another group of components to |
| * provide scrolling functionality.</p> |
| * |
| * <p>The HScrollBar control has the following default characteristics:</p> |
| * <table class="innertable"> |
| * <tr> |
| * <th>Characteristic</th> |
| * <th>Description</th> |
| * </tr> |
| * <tr> |
| * <td>Default size</td> |
| * <td>85 pixels wide by 15 pixels high</td> |
| * </tr> |
| * <tr> |
| * <td>Minimum size</td> |
| * <td>35 pixels wide and 35 pixels high</td> |
| * </tr> |
| * <tr> |
| * <td>Maximum size</td> |
| * <td>10000 pixels wide and 10000 pixels high</td> |
| * </tr> |
| * <tr> |
| * <td>Default skin classes</td> |
| * <td>spark.skins.spark.HScrollBarSkin |
| * <p>spark.skins.spark.HScrollBarThumbSkin</p> |
| * <p>spark.skins.spark.HScrollBarTrackSkin</p></td> |
| * </tr> |
| * </table> |
| * |
| * @mxml |
| * <p>The <code><s:HScrollBar></code> tag inherits all of the tag |
| * attributes of its superclass and adds the following tag attributes:</p> |
| * |
| * <pre> |
| * <s:HScrollBar |
| * |
| * <strong>Properties</strong> |
| * viewport="" |
| * /> |
| * </pre> |
| * |
| * @see spark.skins.spark.HScrollBarSkin |
| * @see spark.skins.spark.HScrollBarThumbSkin |
| * @see spark.skins.spark.HScrollBarTrackSkin |
| * |
| * @includeExample examples/HScrollBarExample.mxml |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @productversion Flex 4 |
| */ |
| public class HScrollBar extends ScrollBarBase |
| { |
| include "../core/Version.as"; |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Constructor |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * Constructor. |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @productversion Flex 4 |
| */ |
| public function HScrollBar() |
| { |
| super(); |
| } |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Overridden properties |
| // |
| //-------------------------------------------------------------------------- |
| |
| private var maxAndPageSizeInvalid:Boolean = false; |
| |
| private function updateMaximumAndPageSize():void |
| { |
| var hsp:Number = viewport.horizontalScrollPosition; |
| var viewportWidth:Number = isNaN(viewport.width) ? 0 : viewport.width; |
| // Special case: if contentWidth is 0, assume that it hasn't been |
| // updated yet. Making the maximum==hsp here avoids trouble later |
| // when Range constrains value |
| var cWidth:Number = viewport.contentWidth; |
| |
| if (getStyle("interactionMode") == InteractionMode.TOUCH) |
| { |
| // For mobile, we allow the min/max to extend a little beyond the ends so |
| // we can support bounce/pull kinetic effects. |
| minimum = -viewportWidth; |
| maximum = (cWidth == 0) ? hsp + viewportWidth : cWidth; |
| } |
| else |
| { |
| minimum = 0; |
| maximum = (cWidth == 0) ? hsp : cWidth - viewportWidth; |
| } |
| |
| |
| pageSize = viewportWidth; |
| } |
| |
| /** |
| * The viewport controlled by this scrollbar. |
| * |
| * @default null |
| * |
| * @see spark.core.IViewport |
| * @langversion 3.0 |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @productversion Flex 4 |
| * |
| */ |
| override public function set viewport(newViewport:IViewport):void |
| { |
| |
| const oldViewport:IViewport = super.viewport; |
| if (oldViewport == newViewport) |
| return; |
| |
| if (oldViewport) |
| { |
| oldViewport.removeEventListener(MouseEvent.MOUSE_WHEEL, mouseWheelHandler); |
| removeEventListener(MouseEvent.MOUSE_WHEEL, hsb_mouseWheelHandler, true); |
| } |
| |
| super.viewport = newViewport; |
| |
| if (newViewport) |
| { |
| updateMaximumAndPageSize() |
| value = newViewport.horizontalScrollPosition; |
| |
| // The HSB viewport mouse wheel listener is added at a low priority so that |
| // if a VSB installs a listener it will run first and cancel the event. |
| newViewport.addEventListener(MouseEvent.MOUSE_WHEEL, mouseWheelHandler, false, EventPriority.DEFAULT_HANDLER); |
| |
| // The HSB mouse wheel listener stops propagation and redispatches its event, |
| // so we listen during the capture phase. |
| addEventListener(MouseEvent.MOUSE_WHEEL, hsb_mouseWheelHandler, true); |
| } |
| } |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Methods |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * @private |
| */ |
| override protected function pointToValue(x:Number, y:Number):Number |
| { |
| if (!thumb || !track) |
| return 0; |
| |
| var r:Number = track.getLayoutBoundsWidth() - thumb.getLayoutBoundsWidth(); |
| return minimum + ((r != 0) ? (x / r) * (maximum - minimum) : 0); |
| } |
| |
| /** |
| * @private |
| */ |
| override protected function updateSkinDisplayList():void |
| { |
| if (!thumb || !track) |
| return; |
| |
| var trackSize:Number = track.getLayoutBoundsWidth(); |
| |
| var min:Number; |
| var max:Number; |
| if (getStyle("interactionMode") == InteractionMode.TOUCH && viewport) |
| { |
| // For calculating thumb position/size on mobile, we want to exclude |
| // the extra margin we added to minimum and maximum for bounce/pull. |
| var viewportWidth:Number = isNaN(viewport.width) ? 0 : viewport.width; |
| |
| // This code uses explicit values passed in from Scroller, since "item snapping" means |
| // there may be visible padding at either end of the content, and bounce/pull occurs beyond |
| // the padding |
| min = !isNaN(contentMinimum) ? contentMinimum : 0; |
| max = !isNaN(contentMaximum) ? contentMaximum : Math.max(0, maximum - viewportWidth); |
| } |
| else |
| { |
| min = minimum; |
| max = maximum; |
| } |
| var range:Number = max - min; |
| |
| var fixedThumbSize:Boolean = !(getStyle("fixedThumbSize") === false); |
| var thumbPos:Point; |
| var thumbPosTrackX:Number = 0; |
| var thumbPosParentX:Number = 0; |
| var thumbSize:Number = trackSize; |
| if (range > 0) |
| { |
| if (!fixedThumbSize) |
| { |
| thumbSize = Math.min((pageSize / (range + pageSize)) * trackSize, trackSize) |
| thumbSize = Math.max(thumb.minWidth, thumbSize); |
| } |
| else |
| { |
| thumbSize = thumb ? thumb.width : 0; |
| } |
| |
| // calculate new thumb position. |
| thumbPosTrackX = (pendingValue - min) * ((trackSize - thumbSize) / range); |
| } |
| |
| // Special thumb behavior for bounce/pull. When the component is positioned |
| // beyond its min/max, we want the scroll thumb to shink in size. |
| // Note: not checking interactionMode==TOUCH here because it is assumed that |
| // value will never exceed min/max unless in touch mode. |
| if (pendingValue < min) |
| { |
| if (!fixedThumbSize) |
| { |
| // The minimum size we'll shrink the thumb to is either thumb.minWidth or thumbSize: whichever is smaller. |
| thumbSize = Math.max(Math.min(thumb.minWidth, thumbSize), thumbSize + (pendingValue - min)); |
| } |
| thumbPosTrackX = 0; // thumb is always at position zero when content is being "pulled" |
| } |
| if (pendingValue > max) |
| { |
| if (!fixedThumbSize) |
| { |
| // The minimum size we'll shrink the thumb to is either thumb.minWidth or thumbSize: whichever is smaller. |
| thumbSize = Math.max(Math.min(thumb.minWidth, thumbSize), thumbSize - (pendingValue - max)); |
| } |
| thumbPosTrackX = trackSize - thumbSize; |
| } |
| |
| if (!fixedThumbSize) |
| thumb.setLayoutBoundsSize(thumbSize, NaN); |
| |
| if (getStyle("autoThumbVisibility") === true) |
| thumb.visible = thumbSize < trackSize; |
| |
| // convert thumb position to parent's coordinates. |
| thumbPos = track.localToGlobal(new Point(thumbPosTrackX, 0)); |
| if (thumb.parent) |
| thumbPosParentX = thumb.parent.globalToLocal(thumbPos).x; |
| |
| thumb.setLayoutBoundsPosition(Math.round(thumbPosParentX), thumb.getLayoutBoundsY()); |
| } |
| |
| /** |
| * Updates the <code>value</code> property and, if viewport is non-null, sets |
| * its <code>horizontalScrollPosition</code> to <code>value</code>. |
| * |
| * @param value The new value of the <code>value</code> property. |
| * @see #viewport |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @productversion Flex 4 |
| */ |
| override protected function setValue(value:Number):void |
| { |
| super.setValue(value); |
| if (viewport) |
| viewport.horizontalScrollPosition = value; |
| } |
| |
| |
| /** |
| * Increment <code>value</code> by a page if <code>increase</code> is <code>true</code>, |
| * or decrement <code>value</code> by a page if <code>increase</code> is <code>false</code>. |
| * Increasing the scrollbar's <code>value</code> scrolls the viewport to the right. |
| * Decreasing the <code>value</code> scrolls to the viewport to the left. |
| * |
| * <p>If the <code>viewport</code> property is set, then its |
| * <code>getHorizontalScrollPositionDelta()</code> method |
| * is used to compute the size of the page increment. |
| * If <code>viewport</code> is null, then the scrollbar's |
| * <code>pageSize</code> property is used.</p> |
| * |
| * @param increase Whether to increment (<code>true</code>) or |
| * decrement (<code>false</code>) <code>value</code>. |
| * |
| * @see spark.components.supportClasses.ScrollBarBase#changeValueByPage() |
| * @see spark.components.supportClasses.Range#setValue() |
| * @see spark.core.IViewport |
| * @see spark.core.IViewport#horizontalScrollPosition |
| * @see spark.core.IViewport#getHorizontalScrollPositionDelta() |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @productversion Flex 4 |
| */ |
| override public function changeValueByPage(increase:Boolean = true):void |
| { |
| var oldPageSize:Number; |
| if (viewport) |
| { |
| // Want to use ScrollBarBase's changeValueByPage() implementation to get the same |
| // animated behavior for scrollbars with and without viewports. |
| // For now, just change pageSize temporarily and call the superclass |
| // implementation. |
| oldPageSize = pageSize; |
| pageSize = Math.abs(viewport.getHorizontalScrollPositionDelta( |
| (increase) ? NavigationUnit.PAGE_RIGHT : NavigationUnit.PAGE_LEFT)); |
| } |
| super.changeValueByPage(increase); |
| if (viewport) |
| pageSize = oldPageSize; |
| } |
| |
| /** |
| * @private |
| */ |
| override protected function animatePaging(newValue:Number, pageSize:Number):void |
| { |
| if (viewport) |
| { |
| var vpPageSize:Number = Math.abs(viewport.getHorizontalScrollPositionDelta( |
| (newValue > value) ? NavigationUnit.PAGE_RIGHT : NavigationUnit.PAGE_LEFT)); |
| super.animatePaging(newValue, vpPageSize); |
| return; |
| } |
| super.animatePaging(newValue, pageSize); |
| } |
| |
| /** |
| * If <code>viewport</code> is not null, |
| * changes the horizontal scroll position for a line up |
| * or line down operation by |
| * scrolling the viewport. |
| * This method calculates the amount to scroll by calling the |
| * <code>IViewport.getHorizontalScrollPositionDelta()</code> method |
| * with either <code>flash.ui.Keyboard.RIGHT</code> |
| * or <code>flash.ui.Keyboard.LEFT</code>. |
| * It then calls the <code>setValue()</code> method to |
| * set the <code>IViewport.horizontalScrollPosition</code> property |
| * to the appropriate value. |
| * |
| * <p>If <code>viewport</code> is null, |
| * calling this method changes the scroll position for a line up |
| * or line down operation by calling |
| * the <code>changeValueByStep()</code> method.</p> |
| * |
| * @param increase Whether the line scoll is up (<code>true</code>) or |
| * down (<code>false</code>). |
| * |
| * @see spark.components.supportClasses.Range#changeValueByStep() |
| * @see spark.components.supportClasses.Range#setValue() |
| * @see spark.core.IViewport |
| * @see spark.core.IViewport#horizontalScrollPosition |
| * @see spark.core.IViewport#getHorizontalScrollPositionDelta() |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @productversion Flex 4 |
| */ |
| override public function changeValueByStep(increase:Boolean = true):void |
| { |
| var oldStepSize:Number; |
| if (viewport) |
| { |
| // Want to use ScrollBarBase's changeValueByStep() implementation to get the same |
| // animated behavior for scrollbars with and without viewports. |
| // For now, just change stepSize temporarily and call the superclass |
| // implementation. |
| oldStepSize = stepSize; |
| stepSize = Math.abs(viewport.getHorizontalScrollPositionDelta( |
| (increase) ? NavigationUnit.RIGHT : NavigationUnit.LEFT)); |
| } |
| super.changeValueByStep(increase); |
| if (viewport) |
| stepSize = oldStepSize; |
| } |
| |
| /** |
| * @private |
| */ |
| override protected function partAdded(partName:String, instance:Object):void |
| { |
| if (instance == thumb) |
| { |
| thumb.setConstraintValue("left", undefined); |
| thumb.setConstraintValue("right", undefined); |
| thumb.setConstraintValue("horizontalCenter", undefined); |
| } |
| |
| super.partAdded(partName, instance); |
| } |
| |
| /** |
| * @private |
| * Set this scrollbar's value to the viewport's current horizontalScrollPosition. |
| */ |
| override mx_internal function viewportHorizontalScrollPositionChangeHandler(event:PropertyChangeEvent):void |
| { |
| if (viewport) |
| value = viewport.horizontalScrollPosition; |
| } |
| |
| /** |
| * @private |
| * Set this scrollbar's maximum to the viewport's contentWidth |
| * less the viewport width and its pageSize to the viewport's width. |
| */ |
| override mx_internal function viewportResizeHandler(event:ResizeEvent):void |
| { |
| if (viewport) |
| updateMaximumAndPageSize(); |
| } |
| |
| /** |
| * @private |
| * Set this scrollbar's maximum to the viewport's contentWidth less the viewport width. |
| */ |
| override mx_internal function viewportContentWidthChangeHandler(event:PropertyChangeEvent):void |
| { |
| if (viewport) |
| { |
| if (getStyle("interactionMode") == InteractionMode.TOUCH) |
| { |
| updateMaximumAndPageSize(); |
| } |
| else |
| { |
| // SDK-28898: reverted previous behavior for desktop, resets |
| // scroll position to zero when all content is removed. |
| maximum = Math.max(0, viewport.contentWidth - viewport.width); |
| } |
| } |
| } |
| |
| /** |
| * @private |
| */ |
| override public function styleChanged(styleName:String):void |
| { |
| super.styleChanged(styleName); |
| |
| var allStyles:Boolean = !styleName || styleName == "styleName"; |
| |
| if (allStyles || styleName == "interactionMode") |
| { |
| if (viewport) |
| { |
| // Some of the information needed |
| // is calculated in measure() on a child |
| maxAndPageSizeInvalid = true; |
| invalidateSize(); |
| } |
| } |
| } |
| |
| /** |
| * @private |
| */ |
| override protected function measure():void |
| { |
| super.measure(); |
| if (maxAndPageSizeInvalid) |
| { |
| maxAndPageSizeInvalid = false; |
| updateMaximumAndPageSize(); |
| } |
| } |
| |
| /** |
| * @private |
| * Scroll horizontally by event.delta "steps". This listener is added to the viewport |
| * at a lower priority then the vertical scrollbar mouse wheel listener, so that vertical |
| * scrolling is preferred when both scrollbars exist. |
| */ |
| mx_internal function mouseWheelHandler(event:MouseEvent):void |
| { |
| const vp:IViewport = viewport; |
| if (event.isDefaultPrevented() || !vp || !vp.visible || !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(delta); |
| var navigationUnit:uint; |
| var scrollPositionChanged:Boolean; |
| |
| // Scroll delta "steps". |
| 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; |
| scrollPositionChanged = true; |
| if (vp is IInvalidating) |
| IInvalidating(vp).validateNow(); |
| } |
| } |
| |
| if (scrollPositionChanged) |
| dispatchEvent(new Event(Event.CHANGE)); |
| |
| event.preventDefault(); |
| } |
| |
| /** |
| * @private |
| * Redispatch HSB mouse wheel events to the viewport to give the VSB's listener, if any, |
| * an opportunity to handle/cancel them. If no VSB exists, mouseWheelHandler (see above) |
| * will process the event. |
| */ |
| private function hsb_mouseWheelHandler(event:MouseEvent):void |
| { |
| const vp:IViewport = viewport; |
| if (event.isDefaultPrevented() || !vp || !vp.visible) |
| return; |
| |
| event.stopImmediatePropagation(); |
| vp.dispatchEvent(event); |
| } |
| } |
| } |