├── .gitignore ├── README ├── src └── com │ └── freshplanet │ └── lib │ ├── util │ └── pool │ │ └── IPool.as │ └── ui │ └── scroll │ ├── mobile │ ├── ScrollListEvent.as │ ├── ScrollList.as │ ├── AlphabetizedScrollList.as │ ├── HorizontalScrollController.as │ └── ScrollController.as │ └── web │ └── ScrollController.as ├── NOTICE ├── example ├── src │ ├── ScrollControllerExampleApp.as │ ├── com │ │ └── freshplanet │ │ │ └── lib │ │ │ └── ui │ │ │ ├── example │ │ │ └── util │ │ │ │ └── RectangleSprite.as │ │ │ └── scroll │ │ │ └── mobile │ │ │ └── example │ │ │ └── ScrollControllerExample.as │ └── ScrollControllerExampleApp-app.xml ├── .project └── .actionScriptProperties └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | # Air Mobile Scroll Controller 2 | 3 | Everything you need to have scrolling in your Adobe Air application that feels native on mobile. 4 | 5 | 6 | # How to use 7 | 8 | Add ScrollController.as, create a new ScrollController object, and call its addScrollControll() method. 9 | See the documentation for the details. 10 | 11 | 12 | # Note 13 | 14 | Don't hesitate to play around with the scrolling parameters (static constants) and suggest values that feel 15 | more native to you. 16 | -------------------------------------------------------------------------------- /src/com/freshplanet/lib/util/pool/IPool.as: -------------------------------------------------------------------------------- 1 | package com.freshplanet.lib.util.pool 2 | { 3 | 4 | /** 5 | * @author Renaud Bardet 6 | * 7 | * this is the prototype of a Pool, for storing reusable elements 8 | * you can either use the DynamicPool implementation or implement this Interface for better performances on a specific case 9 | * 10 | */ 11 | public interface IPool 12 | { 13 | 14 | function alloc(size:int):void ; 15 | 16 | function pop():* ; 17 | 18 | function push(element:*):void ; 19 | 20 | function dealloc():void ; 21 | 22 | function close():void ; 23 | 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2012 Freshplanet (http://freshplanet.com | opensource@freshplanet.com) 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /src/com/freshplanet/lib/ui/scroll/mobile/ScrollListEvent.as: -------------------------------------------------------------------------------- 1 | package com.freshplanet.lib.ui.scroll.mobile 2 | { 3 | import flash.display.DisplayObject; 4 | import flash.events.Event; 5 | 6 | public class ScrollListEvent extends Event 7 | { 8 | 9 | public static const CLICK : String = "ui.scroll.ScrollListEvent.CLICK" ; 10 | 11 | public var listElement : DisplayObject ; 12 | public var data : * ; 13 | 14 | public function ScrollListEvent(type:String, listElement:DisplayObject, data:*, bubbles:Boolean=false, cancelable:Boolean=false) 15 | { 16 | 17 | super(type, bubbles, cancelable); 18 | 19 | this.listElement = listElement ; 20 | this.data = data ; 21 | 22 | } 23 | 24 | } 25 | } -------------------------------------------------------------------------------- /example/src/ScrollControllerExampleApp.as: -------------------------------------------------------------------------------- 1 | package 2 | { 3 | import com.freshplanet.lib.ui.scroll.mobile.example.ScrollControllerExample; 4 | 5 | import flash.display.Sprite; 6 | import flash.display.StageAlign; 7 | import flash.display.StageScaleMode; 8 | import flash.utils.setTimeout; 9 | 10 | public class ScrollControllerExampleApp extends Sprite 11 | { 12 | public function ScrollControllerExampleApp() 13 | { 14 | super(); 15 | 16 | // support autoOrients 17 | stage.align = StageAlign.TOP_LEFT; 18 | stage.scaleMode = StageScaleMode.NO_SCALE; 19 | 20 | setTimeout(showScrollControllerExample, 100); 21 | } 22 | 23 | private function showScrollControllerExample():void 24 | { 25 | this.removeChildren(); 26 | this.addChild(new ScrollControllerExample()); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /example/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | ScrollController 4 | 5 | 6 | 7 | 8 | 9 | com.adobe.flexbuilder.project.flexbuilder 10 | 11 | 12 | 13 | 14 | com.adobe.flexbuilder.project.apollobuilder 15 | 16 | 17 | 18 | 19 | 20 | com.adobe.flexide.project.multiplatform.multiplatformasnature 21 | com.adobe.flexide.project.multiplatform.multiplatformnature 22 | com.adobe.flexbuilder.project.apollonature 23 | com.adobe.flexbuilder.project.actionscriptnature 24 | 25 | 26 | 27 | [source path] src 28 | 2 29 | /Users/arno/Projects/Freshplanet/Air-Mobile-ScrollController/src 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /example/src/com/freshplanet/lib/ui/example/util/RectangleSprite.as: -------------------------------------------------------------------------------- 1 | package com.freshplanet.lib.ui.example.util 2 | { 3 | import flash.display.Shape; 4 | import flash.display.Sprite; 5 | import flash.text.TextField; 6 | 7 | public class RectangleSprite extends Sprite 8 | { 9 | public function RectangleSprite(color:uint, rsX:int, rsY:int, rsWidth:int, rsHeight:int, segmentHeight:int = -1) 10 | { 11 | super(); 12 | 13 | this.x = rsX; 14 | this.y = rsY; 15 | 16 | var shape:Shape; 17 | if(segmentHeight > 0) 18 | { 19 | var segmentCount:int = rsHeight / segmentHeight; 20 | segmentHeight = rsHeight / segmentCount; 21 | for (var i:int = 0; i < segmentCount; i++) 22 | { 23 | shape = new Shape(); 24 | shape.y = i*segmentHeight; 25 | shape.graphics.beginFill(i % 2 == 0 ? color : color * 2); 26 | shape.graphics.drawRect(0, 0, rsWidth, segmentHeight); 27 | shape.graphics.endFill(); 28 | this.addChild(shape); 29 | 30 | var textfield:TextField = new TextField(); 31 | textfield.textColor = 0xffffff; 32 | textfield.y = i*segmentHeight; 33 | textfield.height = segmentHeight - 1; 34 | textfield.text = String(i); 35 | this.addChild(textfield); 36 | } 37 | } 38 | else 39 | { 40 | shape = new Shape(); 41 | shape.graphics.beginFill(color); 42 | shape.graphics.drawRect(0, 0, rsWidth, rsHeight); 43 | shape.graphics.endFill(); 44 | this.addChild(shape); 45 | } 46 | 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /example/src/com/freshplanet/lib/ui/scroll/mobile/example/ScrollControllerExample.as: -------------------------------------------------------------------------------- 1 | package com.freshplanet.lib.ui.scroll.mobile.example 2 | { 3 | import com.freshplanet.lib.ui.example.util.RectangleSprite; 4 | import com.freshplanet.lib.ui.scroll.mobile.ScrollController; 5 | 6 | import flash.display.Sprite; 7 | import flash.events.Event; 8 | import flash.geom.Rectangle; 9 | 10 | 11 | public class ScrollControllerExample extends Sprite 12 | { 13 | private var _scroll:ScrollController; 14 | 15 | public function ScrollControllerExample() 16 | { 17 | super(); 18 | 19 | this.addEventListener(Event.ADDED_TO_STAGE, this.onAddedToStage); 20 | this.addEventListener(Event.REMOVED_FROM_STAGE, this.onRemovedFromStage); 21 | } 22 | 23 | private function onAddedToStage(e:Event):void 24 | { 25 | this.removeEventListener(Event.ADDED_TO_STAGE, this.onAddedToStage); 26 | 27 | var container:RectangleSprite = new RectangleSprite(0x440000, 50, 50, this.stage.stageWidth - 100, this.stage.stageHeight - 100);//red background 28 | this.addChild(container); 29 | 30 | var content:RectangleSprite = new RectangleSprite(0x444477, 0, 0, this.stage.stageWidth - 100, this.stage.stageHeight * 2, 30);//blue foreground 31 | container.addChild(content); 32 | 33 | var containerViewport:Rectangle = new Rectangle(0, 0, this.stage.stageWidth - 100, this.stage.stageHeight - 100); 34 | 35 | this._scroll = new ScrollController(); 36 | this._scroll.horizontalScrollingEnabled = false; 37 | this._scroll.addScrollControll(content, container, containerViewport); 38 | } 39 | 40 | private function onRemovedFromStage(e:Event):void 41 | { 42 | this.removeEventListener(Event.REMOVED_FROM_STAGE, this.onRemovedFromStage); 43 | 44 | this._scroll.removeScrollControll(); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /example/.actionScriptProperties: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | pache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /src/com/freshplanet/lib/ui/scroll/web/ScrollController.as: -------------------------------------------------------------------------------- 1 | package com.freshplanet.lib.ui.scroll.web 2 | { 3 | import flash.display.DisplayObject; 4 | import flash.display.DisplayObjectContainer; 5 | import flash.display.InteractiveObject; 6 | import flash.events.MouseEvent; 7 | import flash.geom.Point; 8 | import flash.geom.Rectangle; 9 | 10 | public class ScrollController 11 | { 12 | private var _content:DisplayObject; 13 | private var _container:DisplayObjectContainer; 14 | /** ViewPort in coordinates of the container. */ 15 | private var _containerViewport:Rectangle; 16 | private var _scrollBarThumb:InteractiveObject; 17 | private var _scrollBarViewPort:Rectangle; 18 | 19 | /** ViewPort in coordinates of the content. */ 20 | private var _contentViewport:Rectangle; 21 | 22 | /** Level in the screen stack. */ 23 | private static var level:int = 0; 24 | 25 | public static const JS_SCROLLING_EVENT:String = "JSScrollingEvent"; 26 | 27 | public function ScrollController():void{}; 28 | 29 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 30 | // INTERFACE 31 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 32 | /** 33 | * Add a scroll controller 34 | * @param content the display object that will be scrolled 35 | * @param container the object containing the content 36 | * @param containerViewPort mask for the content 37 | * @param scrollBarThumb the thumb (clickable moving part) of the scrollbar 38 | * @param scrollBarViewPort mask for the scrollbar 39 | */ 40 | public function addScrollControll(content:DisplayObject, container:DisplayObjectContainer, containerViewport:Rectangle, scrollBarThumb:InteractiveObject, scrollBarViewPort:Rectangle = null):void 41 | { 42 | _content = content; 43 | _container = container; 44 | _containerViewport = containerViewport.clone(); 45 | _scrollBarThumb = scrollBarThumb; 46 | if(scrollBarViewPort == null) 47 | _scrollBarViewPort = containerViewport.clone(); 48 | else 49 | _scrollBarViewPort = scrollBarViewPort.clone(); 50 | 51 | // compute the viewport in the content coordinates 52 | var viewportTopLeft:Point = _content.globalToLocal( _container.localToGlobal( _containerViewport.topLeft )); 53 | var viewportBottomRight:Point = _content.globalToLocal( _container.localToGlobal( _containerViewport.bottomRight )); 54 | 55 | _contentViewport = new Rectangle( 0, 0, viewportBottomRight.x - viewportTopLeft.x, viewportBottomRight.y - viewportTopLeft.y ); 56 | if(_contentViewport.height >= getContentHeight()) 57 | { 58 | scrollBarThumb.visible = false; 59 | _content = null; 60 | _container = null; 61 | _containerViewport = null; 62 | _scrollBarThumb = null; 63 | _scrollBarViewPort = null; 64 | return; 65 | } 66 | scrollBarThumb.visible = true; 67 | scrollBarThumb.y = _scrollBarViewPort.y; 68 | _content.scrollRect = _contentViewport.clone(); 69 | 70 | setupListeners(); 71 | level++; 72 | } 73 | 74 | public function removeScrollControll():void 75 | { 76 | if(_content != null) 77 | { 78 | removeListeners(); 79 | _content = null; 80 | _container = null; 81 | _containerViewport = null; 82 | _scrollBarThumb = null; 83 | _scrollBarViewPort = null; 84 | level--; 85 | } 86 | } 87 | 88 | /** Scroll to a specific position. Fraction should be between 0.0 and 1.0. */ 89 | public function scrollTo(fraction:Number):void 90 | { 91 | if(_content != null) 92 | { 93 | fraction = fraction > 1.0 ? 1.0 : (fraction < 0.0 ? 0.0 : fraction); 94 | 95 | // bounds of the scrollbar 96 | var minY:Number = _scrollBarViewPort.y; 97 | var maxY:Number = _scrollBarViewPort.height - _scrollBarThumb.height + _scrollBarViewPort.y; 98 | 99 | // update scrollbar position 100 | _scrollBarThumb.y = maxY + (minY - maxY) * (1.0 - fraction); 101 | 102 | updateContentPositionFromScrollBar(); 103 | } 104 | } 105 | 106 | public function getCurrentScrollPosition():Number 107 | { 108 | return _content != null ? _content.scrollRect.y : 0; 109 | } 110 | 111 | public function setCurrentScrollPosition(position:Number):void 112 | { 113 | if(_content == null) 114 | return; 115 | moveContentTo(position); 116 | updateScrollBarPositionFromContent(); 117 | } 118 | 119 | public function getReversedScrollPosition():Number 120 | { 121 | return getContentHeight() - _content.scrollRect.y; 122 | } 123 | 124 | public function setReversedScrollPosition(position:Number):void 125 | { 126 | moveContentTo(getContentHeight() - position); 127 | updateScrollBarPositionFromContent(); 128 | } 129 | 130 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 131 | // LISTENERS MANAGEMENT 132 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 133 | private function setupListeners():void 134 | { 135 | if (_scrollBarThumb) _scrollBarThumb.addEventListener(MouseEvent.MOUSE_DOWN, onScrollBarThumbMouseDown); 136 | _container.stage.addEventListener(JS_SCROLLING_EVENT, handleMouseWheel, false, level); 137 | } 138 | 139 | private function removeListeners():void 140 | { 141 | _scrollBarThumb.removeEventListener(MouseEvent.MOUSE_DOWN, onScrollBarThumbMouseDown); 142 | _scrollBarThumb.removeEventListener(MouseEvent.MOUSE_UP, onScrollBarThumbMouseUp); 143 | _container.stage.removeEventListener(MouseEvent.MOUSE_MOVE, onDragThumb); 144 | _container.stage.removeEventListener(JS_SCROLLING_EVENT, handleMouseWheel); 145 | } 146 | 147 | private function onScrollBarThumbMouseDown(event:MouseEvent):void 148 | { 149 | _scrollBarThumb.removeEventListener(MouseEvent.MOUSE_DOWN, onScrollBarThumbMouseDown); 150 | _container.stage.addEventListener(MouseEvent.MOUSE_UP, onScrollBarThumbMouseUp); 151 | _container.stage.addEventListener(MouseEvent.MOUSE_MOVE, onDragThumb); 152 | 153 | // stores the initial positions 154 | _firstTouchY = event.stageY; 155 | _initialScrollY = _scrollBarThumb.y; 156 | } 157 | 158 | private function onScrollBarThumbMouseUp(event:MouseEvent):void 159 | { 160 | _scrollBarThumb.addEventListener(MouseEvent.MOUSE_DOWN, onScrollBarThumbMouseDown); 161 | 162 | if (_container && _container.stage) 163 | { 164 | _container.stage.removeEventListener(MouseEvent.MOUSE_UP, onScrollBarThumbMouseUp); 165 | _container.stage.removeEventListener(MouseEvent.MOUSE_MOVE, onDragThumb); 166 | } 167 | } 168 | 169 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 170 | // UPDATE HANDLERS 171 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 172 | private var _firstTouchY:Number; 173 | private var _initialScrollY:Number; 174 | private function onDragThumb(event:MouseEvent):void 175 | { 176 | var newTouchY:Number = event.stageY; 177 | var stageDeltaY:Number = newTouchY - _firstTouchY; 178 | 179 | // convert to scrollbar coordinates 180 | var deltaY:Number = _scrollBarThumb.globalToLocal( new Point( 0, stageDeltaY )).y - _scrollBarThumb.globalToLocal( new Point( 0, 0 )).y; 181 | 182 | moveScrollBarTo(_initialScrollY + deltaY); 183 | updateContentPositionFromScrollBar(); 184 | } 185 | 186 | private function handleMouseWheel(event:MouseEvent):void { 187 | // arbitrary conversion of delta 188 | var atBound:Boolean = moveContentBy(-event.delta*6); 189 | updateScrollBarPositionFromContent(); 190 | event.stopImmediatePropagation(); 191 | if(!atBound) 192 | event.preventDefault(); 193 | } 194 | 195 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 196 | // POSITION UPDATE 197 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 198 | // Scrollbar 199 | private function moveScrollBarTo(posY:Number):void 200 | { 201 | // bounds of the scrollbar 202 | var minY:Number = _scrollBarViewPort.y; 203 | var maxY:Number = _scrollBarViewPort.height - _scrollBarThumb.height + _scrollBarViewPort.y; 204 | 205 | // update scrollbar position 206 | _scrollBarThumb.y = posY > maxY ? maxY : (posY < minY ? minY:posY); 207 | } 208 | 209 | private function updateContentPositionFromScrollBar():void 210 | { 211 | // get the percent from the scrollbar position 212 | // percent = (val - min)/(max - min) 213 | var percent:Number = (_scrollBarThumb.y - _scrollBarViewPort.y) / ( _scrollBarViewPort.height - _scrollBarThumb.height ); 214 | 215 | // bounds of the content 216 | var minY:Number = _contentViewport.y; 217 | var maxY:Number = _contentViewport.y - _contentViewport.height + getContentHeight(); 218 | 219 | var newViewPort:Rectangle = _content.scrollRect.clone(); 220 | newViewPort.y = maxY + (minY - maxY) * (1.0 - percent); 221 | 222 | // update content position 223 | _content.scrollRect = newViewPort; 224 | } 225 | 226 | private function moveContentTo(position:Number):void 227 | { 228 | // bounds of the content 229 | var minY:Number = _contentViewport.y; 230 | var maxY:Number = _contentViewport.y - _contentViewport.height + getContentHeight(); 231 | 232 | var newViewPort:Rectangle = _content.scrollRect.clone(); 233 | newViewPort.y = position > maxY ? maxY : (position < minY ? minY:position); 234 | 235 | // update content position 236 | _content.scrollRect = newViewPort; 237 | } 238 | 239 | // Mouse wheel 240 | private function moveContentBy(deltaY:Number):Boolean 241 | { 242 | // bounds of the content 243 | var minY:Number = _contentViewport.y; 244 | var maxY:Number = _contentViewport.y - _contentViewport.height + getContentHeight(); 245 | 246 | var newViewPort:Rectangle = _content.scrollRect.clone(); 247 | 248 | var atBound:Boolean = newViewPort.y == maxY || newViewPort.y == minY; 249 | 250 | var newY:Number = newViewPort.y + deltaY; 251 | newY = newY > maxY ? maxY : (newY < minY ? minY:newY); 252 | newViewPort.y = newY; 253 | 254 | // update content position 255 | _content.scrollRect = newViewPort; 256 | 257 | atBound = atBound && (newY == maxY || newY == minY); 258 | 259 | return atBound; 260 | } 261 | 262 | private function updateScrollBarPositionFromContent():void 263 | { 264 | // get the percent from the content position 265 | // percent = (val - min)/(max - min) 266 | var percent:Number = (_content.scrollRect.y - _contentViewport.y) / ( getContentHeight() - _contentViewport.height ); 267 | 268 | // bounds of the scrollbar 269 | var minY:Number = _scrollBarViewPort.y; 270 | var maxY:Number = _scrollBarViewPort.height - _scrollBarThumb.height + _scrollBarViewPort.y; 271 | 272 | // update scrollbar position 273 | _scrollBarThumb.y = maxY + (minY - maxY) * (1.0 - percent); 274 | } 275 | 276 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 277 | // UTIL 278 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 279 | private function getContentHeight():Number 280 | { 281 | if (!_content.stage) return 0; 282 | 283 | var originalHeightOnStage:Number = _content.transform.pixelBounds.height; 284 | var originalHeightOnContent:Number = _content.globalToLocal( new Point( 0, originalHeightOnStage )).y - _content.globalToLocal( new Point( 0, 0 )).y; 285 | originalHeightOnContent += 20; 286 | // handle the browser zoom out which scales the pixelBounds 287 | originalHeightOnContent *= _content.stage.getChildAt(0).height/_content.stage.getChildAt(0).transform.pixelBounds.height; 288 | return originalHeightOnContent; 289 | } 290 | } 291 | } -------------------------------------------------------------------------------- /src/com/freshplanet/lib/ui/scroll/mobile/ScrollList.as: -------------------------------------------------------------------------------- 1 | package com.freshplanet.lib.ui.scroll.mobile 2 | { 3 | 4 | import flash.display.DisplayObject; 5 | import flash.display.Sprite; 6 | import flash.events.Event; 7 | import flash.events.MouseEvent; 8 | import flash.geom.Point; 9 | import flash.geom.Rectangle; 10 | 11 | 12 | /** 13 | * @author Renaud Bardet 14 | * 15 | * This Class handles a scrollable list of any displayable items 16 | * it is optimised for long lists 17 | */ 18 | public class ScrollList extends Sprite 19 | { 20 | 21 | public static const SCROLL_LIST_ORIENTATION_HORIZONTAL:String = "horizontal" ; 22 | public static const SCROLL_LIST_ORIENTATION_VERTICAL:String = "vertical" ; 23 | 24 | private var _orientation:String ; 25 | 26 | private var _dataProvider : Vector.<*>; 27 | 28 | private var _content : Sprite; 29 | private var _extraContent:Sprite; 30 | private var _listContent:Sprite; 31 | 32 | private var _scrollController : ScrollController; 33 | 34 | private var _getElementBounds : Function ; 35 | 36 | private var _createElement : Function ; // function(elementData):DisplayObject 37 | 38 | private var _releaseElement : Function ; // function(DisplayObject):void 39 | 40 | private var _upperIndex : int ; // index of data wich is currently represented by the upper effective element 41 | 42 | private var _cacheElementsBounds : Vector. ; 43 | private var _cacheContentBounds : Rectangle ; 44 | 45 | private var _currentElements : Array ; // used as a int-hash 46 | 47 | // --------------------------------------- 48 | // CONSTRUCTOR 49 | // --------------------------------------- 50 | 51 | public function ScrollList( 52 | orientation:String, 53 | dataProvider:Vector.<*>, // elementData 54 | boundsRect:Rectangle, 55 | getElementBoundsFct:Function, // function(elementData):Rectangle 56 | createElementFct:Function, // function(elemenetData):DisplayObject 57 | releaseElementFct:Function = null // function(DisplayObject):void 58 | ) 59 | { 60 | 61 | if ( orientation != SCROLL_LIST_ORIENTATION_HORIZONTAL && orientation != SCROLL_LIST_ORIENTATION_VERTICAL ) 62 | throw new ArgumentError( "Orientation should be one of refered Strings SCROLL_LIST_ORIENTATION_HORIZONTAL or SCROLL_LIST_ORIENTATION_VERTICAL" ) ; 63 | 64 | _orientation = orientation ; 65 | 66 | if ( !dataProvider ) 67 | throw new ArgumentError( "You should provide a dataProvider even if it is empty" ) ; 68 | 69 | this._getElementBounds = getElementBoundsFct ; 70 | this._createElement = createElementFct ; 71 | this._releaseElement = releaseElementFct ; 72 | 73 | this._content = new Sprite(); 74 | _extraContent = new Sprite(); 75 | _listContent = new Sprite(); 76 | 77 | addChild( this._content ); 78 | _content.addChild(_extraContent); 79 | _content.addChild(_listContent); 80 | 81 | _scrollController = new ScrollController() ; 82 | _scrollController.horizontalScrollingEnabled = _orientation == SCROLL_LIST_ORIENTATION_HORIZONTAL ; 83 | _scrollController.verticalScrollingEnabled = _orientation == SCROLL_LIST_ORIENTATION_VERTICAL ; 84 | _scrollController.displayVerticalScrollbar = true; 85 | _scrollController.addScrollControll( _content, this, boundsRect, null, 1 ) ; 86 | 87 | this.dataProvider = dataProvider ; 88 | 89 | _scrollController.addEventListener( ScrollController.SCROLL_POSITION_CHANGE, onScrollChanged, false, 0, true ) ; 90 | 91 | addEventListener( Event.ADDED_TO_STAGE, onAddedToStage, false, 0, true ) ; 92 | 93 | } 94 | 95 | // --------------------------------------- 96 | // PUBLIC 97 | // --------------------------------------- 98 | 99 | public function addExtraContent(object:DisplayObject):void 100 | { 101 | _extraContent.addChild(object); 102 | } 103 | 104 | public function setListMask(mask:DisplayObject):void 105 | { 106 | _extraContent.addChild(mask); 107 | _listContent.mask = mask; 108 | } 109 | /** 110 | * scroll to a specified element in the list 111 | * if the element is duplicated, the first element from the top will be considered 112 | * @param data an element present in dataProvider 113 | */ 114 | public function scrollTo( data:*, animated:Boolean = false ):void 115 | { 116 | 117 | for( var i:int = 0 ; i < _dataProvider.length ; ++i ) 118 | { 119 | 120 | if ( _dataProvider[i] == data ) 121 | { 122 | 123 | var elBounds:Rectangle = _cacheElementsBounds[i] ; 124 | var to:Point = elBounds.topLeft.clone(); 125 | 126 | if ( _orientation == SCROLL_LIST_ORIENTATION_VERTICAL ) 127 | to.y = Math.min( to.y, _cacheContentBounds.height - bounds.height ) ; 128 | else 129 | to.x = Math.min( to.x, _cacheContentBounds.width - bounds.width ) ; 130 | 131 | _scrollController.scrollTo( to, animated ) ; 132 | redraw() ; 133 | 134 | break; 135 | } 136 | 137 | } 138 | 139 | } 140 | 141 | public function dispose():void 142 | { 143 | 144 | _scrollController.removeScrollControll() ; 145 | 146 | for each ( var el:DisplayObject in _currentElements ) 147 | { 148 | 149 | _listContent.removeChild( el ) ; 150 | el.removeEventListener( MouseEvent.MOUSE_DOWN, onElementMouseDown ) ; 151 | el.removeEventListener( MouseEvent.MOUSE_UP, onElementMouseUp ) ; 152 | _releaseElement( el ) ; 153 | 154 | } 155 | 156 | _currentElements = null ; 157 | 158 | } 159 | 160 | // --------------------------------------- 161 | // PRIVATE 162 | // --------------------------------------- 163 | 164 | private function onScrollChanged( e: Event ):void 165 | { 166 | 167 | redraw() ; 168 | 169 | } 170 | 171 | private function onAddedToStage( e : Event ):void 172 | { 173 | 174 | redraw() ; 175 | 176 | } 177 | 178 | private function redraw():void 179 | { 180 | 181 | var displayedBounds:Rectangle = bounds.clone() ; 182 | if ( _orientation == SCROLL_LIST_ORIENTATION_VERTICAL ) 183 | displayedBounds.y += _scrollController.scrollPosition.y ; 184 | else 185 | displayedBounds.x += _scrollController.scrollPosition.x ; 186 | 187 | for ( var i:int = 0 ; i < _currentElements.length ; ++i ) 188 | { 189 | 190 | if ( _currentElements[i] == undefined ) 191 | continue ; 192 | 193 | var elBounds:Rectangle = _currentElements[i].getBounds( _listContent ) ; 194 | 195 | // if the element is not visible anymore 196 | if ( 197 | _orientation == SCROLL_LIST_ORIENTATION_VERTICAL && ( elBounds.bottom < displayedBounds.top || elBounds.top > displayedBounds.bottom ) 198 | || 199 | _orientation == SCROLL_LIST_ORIENTATION_HORIZONTAL && ( elBounds.right < displayedBounds.left || elBounds.left > displayedBounds.right ) 200 | ) 201 | { 202 | 203 | _listContent.removeChild( _currentElements[i] ) ; 204 | _currentElements[i].removeEventListener( MouseEvent.MOUSE_DOWN, onElementMouseDown ) ; 205 | _currentElements[i].removeEventListener( MouseEvent.MOUSE_UP, onElementMouseUp ) ; 206 | _releaseElement( _currentElements[i] ) ; 207 | delete _currentElements[i] ; // delete the reference in the array but keep the indexes of other elements intact 208 | 209 | } 210 | 211 | } 212 | 213 | for ( i = 0 ; i < _cacheElementsBounds.length ; ++i ) 214 | { 215 | 216 | if ( _currentElements[i] == undefined ) // if it's not currently displayed 217 | { 218 | 219 | elBounds = _cacheElementsBounds[i] ; 220 | 221 | // check if it's visible 222 | if ( _orientation == SCROLL_LIST_ORIENTATION_VERTICAL ) 223 | { 224 | if ( elBounds.bottom < displayedBounds.top ) // + _scrollController.speed 225 | continue ; // too high, skip to the next 226 | else if ( elBounds.top > displayedBounds.bottom ) 227 | break ; // too low, next are not relevant 228 | } 229 | else 230 | { 231 | if ( elBounds.right < displayedBounds.left ) 232 | continue ; // the el is left of the viewport, skip to the next 233 | else if ( elBounds.left > displayedBounds.right ) 234 | break ; // the el is right of the viewport, next els are not relevant 235 | } 236 | 237 | var el:DisplayObject = _createElement( _dataProvider[i] ) ; 238 | el.addEventListener( MouseEvent.MOUSE_DOWN, onElementMouseDown ) ; 239 | el.addEventListener( MouseEvent.MOUSE_UP, onElementMouseUp ) ; 240 | el.y = elBounds.y ; 241 | el.x = elBounds.x ; 242 | _listContent.addChild( el ) ; 243 | _currentElements[i] = el ; 244 | 245 | } 246 | 247 | } 248 | 249 | } 250 | 251 | private function estimateContentBounds():Rectangle 252 | { 253 | 254 | var estBounds:Rectangle = new Rectangle( 0, 0, 0, 0 ) ; 255 | 256 | for ( var i:int = 0 ; i < _dataProvider.length ; ++i ) 257 | { 258 | 259 | var elBounds:Rectangle = _getElementBounds( _dataProvider[i] ) ; 260 | if ( _orientation == SCROLL_LIST_ORIENTATION_VERTICAL ) 261 | { 262 | estBounds.width = Math.max( elBounds.width, estBounds.width ) ; 263 | elBounds.y += estBounds.height ; 264 | estBounds.height += elBounds.height ; 265 | } 266 | else 267 | { 268 | estBounds.height = Math.max( elBounds.height, estBounds.height ) ; 269 | elBounds.x += estBounds.width ; 270 | estBounds.width += elBounds.width ; 271 | } 272 | _cacheElementsBounds.push( elBounds ) ; 273 | 274 | } 275 | 276 | return estBounds ; 277 | 278 | } 279 | 280 | private var _lastDownMousePos:Point = new Point(0, 0); 281 | private function onElementMouseDown(e:Event):void 282 | { 283 | 284 | _lastDownMousePos = new Point( this.mouseX, this.mouseY ) ; 285 | 286 | } 287 | 288 | private function onElementMouseUp(e:MouseEvent):void 289 | { 290 | 291 | // check if there was no significant delta Y between the down and the up 292 | // if so it's a click 293 | if ( 294 | _orientation == SCROLL_LIST_ORIENTATION_VERTICAL && Math.abs(this.mouseY - _lastDownMousePos.y) < 10 295 | || _orientation == SCROLL_LIST_ORIENTATION_HORIZONTAL && Math.abs(this.mouseX - _lastDownMousePos.x) < 10 296 | ) 297 | { 298 | 299 | var element:DisplayObject = DisplayObject(e.currentTarget) ; 300 | var data:* = _currentElements.indexOf(element) > -1 ? _dataProvider[ _currentElements.indexOf(element) ] : null ; 301 | dispatchEvent( new ScrollListEvent( ScrollListEvent.CLICK, element, data ) ) ; 302 | 303 | } 304 | 305 | } 306 | 307 | // --------------------------------------- 308 | // GETTERS AND SETTERS 309 | // --------------------------------------- 310 | 311 | public function get bounds():Rectangle 312 | { 313 | return _scrollController.containerViewport ; 314 | } 315 | 316 | public function set bounds(value:Rectangle):void 317 | { 318 | _scrollController.containerViewport = value ; 319 | redraw() ; 320 | } 321 | 322 | public function get dataProvider():Vector.<*> 323 | { 324 | return _dataProvider; 325 | } 326 | 327 | public function set dataProvider(value:Vector.<*>):void 328 | { 329 | 330 | for each ( var el:DisplayObject in _currentElements ) 331 | { 332 | 333 | _listContent.removeChild( el ) ; 334 | _releaseElement( el ) ; 335 | 336 | } 337 | 338 | _dataProvider = value; 339 | 340 | _cacheElementsBounds = new [] ; 341 | _currentElements = [] ; 342 | 343 | _cacheContentBounds = estimateContentBounds() ; 344 | 345 | _scrollController.setContentRect( _cacheContentBounds ) ; 346 | 347 | // if scroll is out of new bounds go to end 348 | if ( _orientation == SCROLL_LIST_ORIENTATION_VERTICAL ) 349 | { 350 | if ( _scrollController.scrollPosition.y > _cacheContentBounds.height - _scrollController.containerViewport.height ) 351 | _scrollController.scrollToBottom() ; 352 | } 353 | else 354 | { 355 | if ( _scrollController.scrollPosition.x > _cacheContentBounds.width - _scrollController.containerViewport.width ) 356 | _scrollController.scrollToRight() ; 357 | } 358 | 359 | redraw() ; 360 | 361 | } 362 | 363 | public function get scrollController():ScrollController 364 | { 365 | 366 | return _scrollController ; 367 | 368 | } 369 | 370 | } 371 | 372 | } -------------------------------------------------------------------------------- /src/com/freshplanet/lib/ui/scroll/mobile/AlphabetizedScrollList.as: -------------------------------------------------------------------------------- 1 | package com.freshplanet.lib.ui.scroll.mobile 2 | { 3 | import com.freshplanet.lib.util.pool.IPool; 4 | 5 | import flash.display.DisplayObject; 6 | import flash.display.Shape; 7 | import flash.display.Sprite; 8 | import flash.events.MouseEvent; 9 | import flash.geom.Rectangle; 10 | import flash.text.TextField; 11 | import flash.text.TextFormat; 12 | import flash.text.TextFormatAlign; 13 | 14 | public class AlphabetizedScrollList extends Sprite 15 | { 16 | 17 | private static var SYMBOLS : String = 'AlphabetizedScrollList.SYMBOLS' ; 18 | 19 | private static var ALPHABET : Array = [ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', { label : '1', data : '123456789' } ]; 20 | 21 | // this array of Strings determine how keys will be grouped, 22 | // by default it's letter by letter 23 | // but you could also define it as ABC - DEF - HIJ ... 24 | // it also defines in wich order the keys will be presented, so you can define numbers first or last 25 | // any element that cannot be affiliated to one of those key will be displayed at the end 26 | // it is case insensitive 27 | private var _groupBy:Array = ALPHABET; 28 | 29 | private var _dataProvider:Vector.<*> ; 30 | 31 | private var _alphaIndex:Function ; // returns a string on wich the alphabetical sort will be based 32 | 33 | private var _alphaHash:Object ; 34 | 35 | private var _scrollList:ScrollList ; 36 | private var _scrollListData:Vector.<*> ; 37 | 38 | private var _anchorPool:IPool ; 39 | private var _initAnchor:Function ; // function(DisplayObject, String):void 40 | private var _elementPool:IPool ; 41 | private var _initElement:Function ; // function(DisplayObject, elementData):void 42 | 43 | private var _anchors:Vector. ; 44 | 45 | private var _alphabetSelector:Sprite ; 46 | private var _alphabetSelectorBackground:Shape ; 47 | private var _alphabetSelectorScrollController:ScrollController ; 48 | 49 | private var _getListItemBounds:Function ; 50 | 51 | public function AlphabetizedScrollList( 52 | dataProvider:Vector.<*>, 53 | viewport:Rectangle, 54 | alphaIndex:Function, // function(elementData):String 55 | elementPool:IPool, 56 | initElementFct:Function, // function(DisplayObject, elementData):void 57 | anchorPool:IPool, 58 | initAnchorFct:Function // function(DisplayObject, String):void 59 | ) 60 | { 61 | 62 | super() ; 63 | 64 | _alphaIndex = alphaIndex ; 65 | 66 | _elementPool = elementPool ; 67 | _initElement = initElementFct ; 68 | 69 | _anchorPool = anchorPool ; 70 | _initAnchor = initAnchorFct ; 71 | 72 | _getListItemBounds = defaultGetBounds ; 73 | _anchors = new Vector.() ; 74 | 75 | _scrollList = new ScrollList( 76 | ScrollList.SCROLL_LIST_ORIENTATION_VERTICAL, 77 | new <*>[], 78 | viewport, 79 | getElementBounds, 80 | createListElement, 81 | disposeListElement 82 | ) ; 83 | 84 | _scrollList.addEventListener( ScrollListEvent.CLICK, onElementClicked ) ; 85 | 86 | addChild( _scrollList ) ; 87 | 88 | this.dataProvider = dataProvider ; 89 | 90 | } 91 | 92 | // --------------------------------------- 93 | // PUBLIC 94 | // --------------------------------------- 95 | 96 | public function refresh():void 97 | { 98 | 99 | // reinit current list with the same data 100 | dataProvider = dataProvider ; 101 | 102 | } 103 | 104 | public function dispose():void 105 | { 106 | 107 | _scrollList.dispose() ; 108 | this.removeChild( _alphabetSelector ) ; 109 | 110 | } 111 | 112 | // --------------------------------------- 113 | // PRIVATE 114 | // --------------------------------------- 115 | 116 | private function onElementClicked( e:ScrollListEvent ):void 117 | { 118 | 119 | dispatchEvent( new ScrollListEvent( e.type, e.listElement, e.data ) ) ; 120 | 121 | } 122 | 123 | private function getElementBounds( data:* ):Rectangle 124 | { 125 | 126 | var el:DisplayObject = createListElement( data ) ; 127 | var bounds:Rectangle = _getListItemBounds(el) ; 128 | disposeListElement( el ) ; 129 | return bounds ; 130 | 131 | } 132 | 133 | private function defaultGetBounds(el:DisplayObject):Rectangle 134 | { 135 | 136 | return el.getBounds( el ) ; 137 | 138 | } 139 | 140 | private function createListElement( data:* ):DisplayObject 141 | { 142 | 143 | if( !data.hasOwnProperty( "type" ) ) 144 | throw "unexpected data" ; 145 | 146 | if( data['type'] == "anchor" ) 147 | { 148 | var a:DisplayObject = _anchorPool.pop() ; 149 | _anchors.push(a) ; 150 | var anchorName:String = data.data ; 151 | if ( anchorName == SYMBOLS ) 152 | anchorName = '' ; 153 | _initAnchor( a, anchorName ) ; 154 | return a ; 155 | }else{ 156 | var el:DisplayObject = _elementPool.pop() ; 157 | _initElement( el, data.data ) ; 158 | return el ; 159 | } 160 | 161 | } 162 | 163 | private function disposeListElement( element:DisplayObject ):void 164 | { 165 | 166 | var anchorIndex:int = _anchors.indexOf( element ) 167 | if ( anchorIndex > -1 ) 168 | { 169 | 170 | _anchors.splice( anchorIndex, 1 ) ; 171 | _anchorPool.push( element ) ; 172 | 173 | } 174 | else 175 | { 176 | 177 | _elementPool.push( element ) ; 178 | 179 | } 180 | 181 | } 182 | 183 | private function createAlphabetSelector( _bounds:Rectangle ):void 184 | { 185 | 186 | if ( _alphabetSelector ) 187 | this.removeChild( _alphabetSelector ) ; 188 | 189 | _alphabetSelector = new Sprite(); 190 | _alphabetSelectorBackground = new Shape(); 191 | _alphabetSelectorBackground.graphics.beginFill(0x000000, 1.0); 192 | _alphabetSelectorBackground.graphics.drawRoundRect(0, 0, 37, _bounds.height - 16, 37); 193 | _alphabetSelectorBackground.graphics.endFill(); 194 | _alphabetSelectorBackground.y = 8 ; 195 | _alphabetSelector.addChild(_alphabetSelectorBackground); 196 | _alphabetSelectorBackground.alpha = 0.1; 197 | 198 | _alphabetSelector.x = _bounds.width - _alphabetSelector.width - 5 ; 199 | 200 | var letterHeight:Number = (_bounds.height - 32) / _groupBy.length ; 201 | var letterContainer:Sprite = new Sprite() ; 202 | var textfield:TextField; 203 | var currentY:Number = 16; 204 | var defaultFormat:TextFormat = new TextFormat(); 205 | defaultFormat.align = flash.text.TextFormatAlign.CENTER; 206 | defaultFormat.size = 20; 207 | defaultFormat.font = "Futura Medium"; 208 | defaultFormat.color = 0x4c626d; 209 | for each (var key:* in _groupBy) 210 | { 211 | var letter:String = '' 212 | if ( key is String ) 213 | letter = key ; 214 | else 215 | letter = key.label ; 216 | 217 | textfield = new TextField; 218 | textfield.defaultTextFormat = defaultFormat; 219 | textfield.text = letter ; 220 | textfield.height = 25 ; 221 | textfield.width = 37; 222 | textfield.y = currentY; 223 | textfield.x = 0; 224 | textfield.selectable = false; 225 | letterContainer.addChild(textfield); 226 | textfield.addEventListener(MouseEvent.ROLL_OVER, onLetterOver, false, 0, true); 227 | textfield.addEventListener(MouseEvent.ROLL_OUT, onLetterOut, false, 0, true); 228 | currentY += letterHeight ; 229 | } 230 | _alphabetSelector.addChild( letterContainer ) ; 231 | 232 | _alphabetSelector.addEventListener(MouseEvent.ROLL_OVER, onAlphabetOver, false, 0, true); 233 | _alphabetSelector.addEventListener(MouseEvent.ROLL_OUT, onAlphabetOut, false, 0, true); 234 | 235 | this.addChild(_alphabetSelector); 236 | 237 | } 238 | 239 | private function onAlphabetOver(event:MouseEvent):void 240 | { 241 | _alphabetSelectorBackground.alpha = 0.3; 242 | } 243 | 244 | private function onAlphabetOut(event:MouseEvent):void 245 | { 246 | _alphabetSelectorBackground.alpha = 0.1; 247 | } 248 | 249 | private function onLetterClicked(event:MouseEvent):void 250 | { 251 | var textfield:TextField = event.target as TextField; 252 | gotoAnchor(textfield.text); 253 | } 254 | 255 | private function onLetterOver(event:MouseEvent):void 256 | { 257 | var textfield:TextField = event.target as TextField; 258 | textfield.textColor = 0xffffff; 259 | gotoAnchor(textfield.text); 260 | } 261 | 262 | private function onLetterOut(event:MouseEvent):void 263 | { 264 | var textfield:TextField = event.target as TextField; 265 | textfield.textColor = 0x4c626d ; 266 | } 267 | 268 | private function gotoAnchor( anchorName:String ):void 269 | { 270 | 271 | _scrollList.scrollTo( _alphaHash[ anchorName ].anchor ) ; 272 | 273 | } 274 | 275 | // --------------------------------------- 276 | // GETTERS AND SETTERS 277 | // --------------------------------------- 278 | 279 | public function get dataProvider():Vector.<*> 280 | { 281 | 282 | return _dataProvider ; 283 | 284 | } 285 | 286 | public function set dataProvider(value:Vector.<*>):void 287 | { 288 | 289 | _dataProvider = value ; 290 | 291 | _alphaHash = new Object() ; 292 | 293 | // construct the hash wich consist in a dictionary of 294 | // groupingKey -> { 295 | // data : dictionary of 296 | // elementHash -> element 297 | // anchor : reference to the scrollList data associated with this grouping Key 298 | // } 299 | for ( var i:int=0 ; i<_dataProvider.length ; ++i ) 300 | { 301 | 302 | var elementHash:String = _alphaIndex(_dataProvider[i]) ; 303 | 304 | var hashKey:String = SYMBOLS ; // default key, means the element will be displayed at the end if no other key can be found in the grouping funciton 305 | 306 | // look for the first letter of the hash in the grouping function and determine the hashKey for that element 307 | for ( var j:int=0 ; j < _groupBy.length ; ++j ) 308 | { 309 | 310 | var keyLabel : String = '' ; 311 | var keyData : String = '' ; 312 | if ( _groupBy[j] is String ) 313 | { 314 | keyLabel = _groupBy[j] ; 315 | keyData = _groupBy[j] ; 316 | } else { 317 | keyLabel = _groupBy[j].label ; 318 | keyData = _groupBy[j].data ; 319 | } 320 | 321 | // use case incensitive keys 322 | if ( keyData.toUpperCase().indexOf( elementHash.substr(0,1).toUpperCase() ) > -1 ) 323 | { 324 | 325 | hashKey = keyLabel ; 326 | 327 | break ; 328 | } 329 | } 330 | 331 | if ( !_alphaHash.hasOwnProperty( hashKey ) ) 332 | { 333 | _alphaHash[ hashKey ] = { 334 | data : new Object(), 335 | anchor : { type : "anchor", data : hashKey } 336 | } ; 337 | } 338 | 339 | _alphaHash[ hashKey ].data[ elementHash ] = _dataProvider[i] ; 340 | 341 | } 342 | 343 | // construct the dataProvider that will be passed to the scrollList 344 | _scrollListData = new Vector.<*>() ; 345 | var keys:Array = _groupBy.concat( [ SYMBOLS ] ) ; 346 | for ( j=0 ; j < keys.length ; ++j ) 347 | { 348 | 349 | var label:String = ''; 350 | if ( keys[j] is String ) 351 | label = keys[j] ; 352 | else 353 | label = keys[j].label ; 354 | 355 | if ( !_alphaHash.hasOwnProperty( label ) ) 356 | { 357 | _alphaHash[ label ] = { 358 | data : new Object(), 359 | anchor : { type : "anchor", data : label } 360 | } ; 361 | } 362 | 363 | _scrollListData.push( _alphaHash[label].anchor ) ; 364 | 365 | var sortedElements : Vector.<*> = new <*>[] ; 366 | for ( var key : * in _alphaHash[label].data ) 367 | sortedElements.push(key) ; 368 | sortedElements.sort( Array.CASEINSENSITIVE ) ; 369 | 370 | for ( var k:int = 0 ; k < sortedElements.length ; ++k ) 371 | { 372 | 373 | _scrollListData.push( 374 | { 375 | type : "element", 376 | data : _alphaHash[label].data[ sortedElements[k] ] 377 | } ) ; 378 | 379 | } 380 | 381 | } 382 | 383 | if( _alphaHash[ SYMBOLS ].data.length == 0 ) 384 | _scrollListData.splice( _alphaHash[SYMBOLS].anchor, 1 ) ; 385 | 386 | _scrollList.dataProvider = _scrollListData ; 387 | 388 | createAlphabetSelector( _scrollList.bounds ) ; 389 | 390 | } 391 | 392 | public function get groupBy():Array 393 | { 394 | return _groupBy; 395 | } 396 | 397 | public function set groupBy(value:Array):void 398 | { 399 | _groupBy = value; 400 | dataProvider = dataProvider ; // reset the content 401 | } 402 | 403 | public function set getListItemBounds(value:Function):void 404 | { 405 | _getListItemBounds = value; 406 | dataProvider = dataProvider ; 407 | } 408 | 409 | 410 | } 411 | } -------------------------------------------------------------------------------- /example/src/ScrollControllerExampleApp-app.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 18 | ScrollController 19 | 20 | 21 | ScrollController 22 | 23 | 25 | ScrollController 26 | 27 | 30 | 0.0.0 31 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | [This value will be overwritten by Flash Builder in the output app.xml] 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | true 115 | false 116 | true 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 138 | 162 | 163 | 166 | 167 | 168 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 217 | 218 | 227 | 228 | 229 | 230 | 231 | 234 | 235 | 236 | 237 | 238 | 239 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 267 | 268 | 270 | 271 | 272 | 273 | 274 | 276 | 277 | 278 | 279 | 280 | 282 | 283 | 284 | 285 | 286 | ]]> 287 | 288 | 289 | UIDeviceFamily 291 | 292 | 1 293 | 2 294 | 295 | ]]> 296 | high 297 | 298 | 299 | -------------------------------------------------------------------------------- /src/com/freshplanet/lib/ui/scroll/mobile/HorizontalScrollController.as: -------------------------------------------------------------------------------- 1 | package com.freshplanet.lib.ui.scroll.mobile 2 | { 3 | import flash.display.DisplayObject; 4 | import flash.display.DisplayObjectContainer; 5 | import flash.display.Shape; 6 | import flash.events.Event; 7 | import flash.events.MouseEvent; 8 | import flash.geom.Point; 9 | import flash.geom.Rectangle; 10 | import flash.utils.getTimer; 11 | 12 | public class HorizontalScrollController 13 | { 14 | 15 | 16 | // ------------------------------------------------------------------------------------------ 17 | // 18 | // Static Vars 19 | // 20 | // ------------------------------------------------------------------------------------------ 21 | 22 | public static var FRICTION_COEFFICIENT:Number = 0.9; 23 | public static var MAX_SPEED_ALLOWED_WHEN_BOUNCING:Number = 0.5; 24 | public static var MAX_SPEED_ALLOWED_WHEN_RUNNING_FREE:Number = 2.0; 25 | public static var SPEED_REDUCTION_WHEN_BOUNCING:Number = 0.3; 26 | public static var SCROLLBAR_FIRST_POSITION:Number = 0; 27 | 28 | // ------------------------------------------------------------------------------------------ 29 | // 30 | // Private Vars 31 | // 32 | // ------------------------------------------------------------------------------------------ 33 | 34 | // state 35 | private var _paused : Boolean = true; 36 | 37 | // config scroll 38 | private var _content : DisplayObject; 39 | private var _container : DisplayObjectContainer; 40 | 41 | private var _contentViewport : Rectangle; 42 | private var _containerViewport : Rectangle; 43 | 44 | private var _contentInitialBounds : Rectangle; // full size of the content before we put it in the scroll Rect, don't seem to be able to find it after so cache it here 45 | 46 | // scroll movement cache 47 | private var _lastTouch : Object; 48 | private var _touches : Vector.; 49 | private var _trajectories : Vector.; 50 | private var _touchesCleanup:Boolean = false; // do we need to clean _touches after processing them this frame ? 51 | 52 | public function HorizontalScrollController() {} 53 | 54 | private function addTouch( event : MouseEvent ) : void 55 | { 56 | if ( !_touches ) 57 | _touches = new Vector.(); 58 | 59 | var time : Number = getTimer(); 60 | this._touches.push( { 'time' : time, 'stageX' : event.stageX } ); 61 | 62 | } 63 | 64 | /** 65 | * trajectory are in y in coordinate of content and represent the scrollRect.y over frames 66 | */ 67 | private function addTrajectory( currentX : Number, time : Number = 0 ) : void 68 | { 69 | if ( !_trajectories ) 70 | _trajectories = new Vector.(); 71 | 72 | var trajectoryTime : Number = time != 0 ? time : getTimer(); 73 | this._trajectories.push( { 'time' : trajectoryTime, 'localX' : currentX } ); 74 | 75 | } 76 | 77 | private function getDeltaTime() : Number 78 | { 79 | if ( _trajectories != null && _trajectories.length != 0 ) 80 | return getTimer() - _trajectories[_trajectories.length - 1]['time']; 81 | else 82 | return 1000 / 24; // 1 frame in ms 83 | } 84 | 85 | private function getContentWidth() : Number 86 | { 87 | var originalWidthOnStage : Number = _content.transform.pixelBounds.width; 88 | var originalWidthOnContent : Number = _content.globalToLocal( new Point( originalWidthOnStage, 0 )).x - _content.globalToLocal( new Point( 0, 0 )).x; 89 | return originalWidthOnContent + 10; // margin bottom (!!!!! warning, drawDebug messup this function) 90 | } 91 | 92 | private function processTouchInteraction( touch : Object ) : void 93 | { 94 | if ( touch == null || touch == _lastTouch ) 95 | return; 96 | 97 | var deltaX : int = 0; 98 | var coef : Number; // how much do we follow touch movement, 1 = we follow completelly 99 | 100 | if ( ( _content.scrollRect.x + _contentViewport.width < this.getContentWidth() ) && _contentViewport.left < _content.scrollRect.x ) // if inside scroll area 101 | { 102 | //trace( 'scrolling from interaction inside area' ); 103 | coef = 1; 104 | } 105 | else // if outside 106 | { 107 | //trace( 'scrolling from interaction outside area' ); 108 | coef = 0.3; 109 | } 110 | 111 | var stageDeltaX : int = _lastTouch['stageX'] - touch['stageX']; // delta in event position y in stage space 112 | var contentDeltaX : int = _content.globalToLocal( new Point( stageDeltaX, 0 )).x - _content.globalToLocal( new Point( 0, 0 )).x; 113 | 114 | deltaX = contentDeltaX * coef; 115 | 116 | _lastTouch = touch; 117 | 118 | moveContentFromDelta( deltaX, touch['time'] ); 119 | } 120 | 121 | 122 | private function moveContentFromDelta( deltaX : Number, time : Number = 0 ) : void 123 | { 124 | var newScrollRect : Rectangle = _content.scrollRect.clone(); 125 | newScrollRect.x += deltaX; 126 | _content.scrollRect = newScrollRect; 127 | this.addTrajectory( newScrollRect.x, time ); 128 | } 129 | 130 | 131 | 132 | // ------------------------------------------------------------------------------------------ 133 | // 134 | // Private Event Handler 135 | // 136 | // ------------------------------------------------------------------------------------------ 137 | 138 | private function mouseDownHandler( event : MouseEvent ) : void 139 | { 140 | hasTouchContainer = true; 141 | this.addTouch( event ); 142 | _container.addEventListener( MouseEvent.MOUSE_MOVE, onMoveDrag, false, 0, true ); 143 | _container.addEventListener( MouseEvent.MOUSE_UP, onStopDrag, false, 0, true ); 144 | _container.addEventListener( Event.ENTER_FRAME, onEnterFrame, false, 0, true ); 145 | 146 | } 147 | 148 | private function onMoveDrag( event : MouseEvent ) : void 149 | { 150 | addTouch( event ); 151 | _container.addEventListener( Event.ENTER_FRAME, onEnterFrame, false, 0, true ); // we re-add it because we may demove it since we stopped moving for a while and restart 152 | } 153 | 154 | private function onStopDrag( event : MouseEvent ) : void 155 | { 156 | _container.removeEventListener( MouseEvent.MOUSE_MOVE, onMoveDrag ); 157 | _container.removeEventListener( MouseEvent.MOUSE_UP, onStopDrag ); 158 | _touchesCleanup = true; 159 | } 160 | 161 | private function onContentResize( event : Event ) : void 162 | { 163 | if ( _contentViewport.width >= this.getContentWidth() ) 164 | pause(); 165 | else 166 | { 167 | if (_paused) 168 | { 169 | var newViewPort:Rectangle = _contentViewport.clone(); 170 | //newViewPort.x += SCROLLBAR_FIRST_POSITION; 171 | _content.scrollRect = newViewPort; 172 | //resume(); 173 | } 174 | } 175 | } 176 | 177 | 178 | private function onEnterFrame( event : Event ) : void 179 | { 180 | if (_content.scrollRect == null) 181 | { 182 | return; 183 | } 184 | 185 | // setting up first touch 186 | if ( _lastTouch == null && _touches && _touches.length != 0 ) 187 | { 188 | //trace( 'first touch' ); 189 | _lastTouch = _touches[0]; 190 | 191 | this.addTrajectory( _content.scrollRect.x, _lastTouch['time'] ); 192 | 193 | if ( _touches.length == 1) 194 | return; 195 | // no return otherwise, can be different from current touch, sometimes we get several touch per frame 196 | } 197 | 198 | var pendingTouches : Vector.; 199 | 200 | if ( _touches && _touches.length != 0 && _touches.indexOf( _lastTouch ) != _touches.length - 1 ) 201 | { 202 | pendingTouches = _touches.slice( _touches.indexOf( _lastTouch ) + 1 ); 203 | //trace( 'pending touches lenght =', pendingTouches.length ); 204 | } 205 | 206 | 207 | var deltaX : int = 0; 208 | var stopAfterThisOne : Boolean = false; 209 | 210 | if ( pendingTouches ) // there is new touches 211 | { 212 | //trace( 'onEnterFrame with interaction' ); 213 | 214 | // if so for each pending touch, process them and add them to the trajectory 215 | for each ( var touch : Object in pendingTouches ) 216 | { 217 | this.processTouchInteraction( touch ); 218 | } 219 | } 220 | else 221 | { 222 | //trace( 'no interaction this frame' ); 223 | var currentSpeed : Number = this.getLastSpeed(); 224 | 225 | if ( ( _content.scrollRect.x + _contentViewport.width < this.getContentWidth() ) && _contentViewport.left < _content.scrollRect.x ) // if inside scroll area 226 | { 227 | //trace( 'scrolling freely inside area' ); 228 | if ( currentSpeed ) 229 | { 230 | currentSpeed = currentSpeed * Math.min( MAX_SPEED_ALLOWED_WHEN_RUNNING_FREE, Math.abs( currentSpeed )) / Math.abs( currentSpeed ); // current speed maxed out at twice what's possible when boncing 231 | //trace( 'currentSpeed =', currentSpeed ); 232 | deltaX = currentSpeed * FRICTION_COEFFICIENT * this.getDeltaTime(); // friction 233 | } 234 | else 235 | deltaX = 0; // because when blocking free move with click, there is not trajectories so no speed yet 236 | 237 | var absDeltaX:Number = deltaX > 0 ? deltaX : -deltaX; 238 | if ( absDeltaX < 2 ) // Math.abs( deltaX ) < 2 239 | { 240 | //trace( 'natural scroll too slow, stopping interaction now' ); 241 | stopAfterThisOne = true; 242 | } 243 | } 244 | else // if outside 245 | { 246 | //trace( 'scrolling freely outside area' ); 247 | // get closer to border at constant speed linear from initial delta 248 | var v : Number; 249 | // if on top 250 | if ( _contentViewport.left >= _content.scrollRect.x ) 251 | { 252 | //trace( 'on top' ); 253 | 254 | // var v : Number = currentSpeed - 0.0001 * ( _content.scrollRect.y - _contentViewport.top ) * this.getDeltaTime(); // speed 255 | v = -MAX_SPEED_ALLOWED_WHEN_BOUNCING > currentSpeed + SPEED_REDUCTION_WHEN_BOUNCING ? -MAX_SPEED_ALLOWED_WHEN_BOUNCING : currentSpeed + SPEED_REDUCTION_WHEN_BOUNCING //Math.max( -MAX_SPEED_ALLOWED_WHEN_BOUNCING, currentSpeed + SPEED_REDUCTION_WHEN_BOUNCING ); 256 | //trace( 'v =', v ); 257 | deltaX = v * this.getDeltaTime(); 258 | 259 | if ( deltaX > _contentViewport.left - _content.scrollRect.x ) // going back to scrolling freeling inside area, we want to stop 260 | { 261 | //trace('going back to free after this one, time to stop'); 262 | deltaX = _contentViewport.left - _content.scrollRect.x; 263 | stopAfterThisOne = true; 264 | } 265 | } 266 | else 267 | { 268 | //trace( 'on bottom' ); 269 | 270 | //var v : Number = currentSpeed - 0.0001 * ( (_content.scrollRect.y + _contentViewport.height) - this.getContentHeight() ) * this.getDeltaTime(); // speed 271 | v = MAX_SPEED_ALLOWED_WHEN_BOUNCING < currentSpeed - SPEED_REDUCTION_WHEN_BOUNCING ? MAX_SPEED_ALLOWED_WHEN_BOUNCING : currentSpeed - SPEED_REDUCTION_WHEN_BOUNCING// Math.min( MAX_SPEED_ALLOWED_WHEN_BOUNCING, currentSpeed - SPEED_REDUCTION_WHEN_BOUNCING ); 272 | //trace( 'v =', v ); 273 | deltaX = v * this.getDeltaTime(); 274 | 275 | if ( deltaX < this.getContentWidth() - (_content.scrollRect.x + _contentViewport.width) ) // going back to scrolling freeling inside area, we want to stop 276 | { 277 | //trace('going back to free after this one, time to stop'); 278 | deltaX = this.getContentWidth() - (_content.scrollRect.x + _contentViewport.width); 279 | stopAfterThisOne = true; 280 | } 281 | } 282 | } 283 | } 284 | 285 | if ( deltaX != 0 ) 286 | { 287 | this.moveContentFromDelta( deltaX ); 288 | } 289 | 290 | if ( stopAfterThisOne ) 291 | { 292 | //trace( 'stopAfterThisOne' ); 293 | _container.removeEventListener( Event.ENTER_FRAME, onEnterFrame ); 294 | //trace('remove onEnterFrame'); 295 | 296 | var last : Object = _trajectories[ _trajectories.length - 1]; 297 | this._trajectories = new Vector.; 298 | _trajectories.push( last ); // keep last one 299 | } 300 | 301 | if ( _touchesCleanup ) 302 | { 303 | //trace( 'touches cleanup' ); 304 | _touchesCleanup = false; 305 | _touches = null; 306 | _lastTouch = null; 307 | } 308 | 309 | 310 | //trace( '\\ exit onEnterFrame -----------------------------------------------------------' ); 311 | } 312 | 313 | 314 | 315 | 316 | // ------------------------------------------------------------------------------------------ 317 | // 318 | // Public API 319 | // 320 | // ------------------------------------------------------------------------------------------ 321 | 322 | /** 323 | * add scroll logic for this content, in this container, displayed in this viewport area 324 | * @param content stuff to scroll 325 | * @param container We will listen the mouse event on this guy 326 | * @param containerViewPort we will mask the content outside the viewport 327 | * 328 | */ 329 | public function addScrollControll( content : DisplayObject, container : DisplayObjectContainer, containerViewPort : Rectangle ) : void 330 | { 331 | if ( _content != null || _container != null ) 332 | throw new Error( "This Scroll Controller Already manage some content and container, find another one for you!" ); 333 | 334 | if ( content == null || container == null ) 335 | throw new Error( "Content or Container are null, I cannot manage that!" ); 336 | 337 | if ( containerViewPort == null || containerViewPort.width == 0 || containerViewPort.height == 0 ) 338 | throw new Error( "Incorrect viewport information, what do you really want to see ? viewport = " + containerViewPort.toString() ); 339 | 340 | _container = container; 341 | _content = content; 342 | 343 | _containerViewport = containerViewPort.clone(); // don't touch my viewport 344 | 345 | var viewportTopLeft : Point = _content.globalToLocal( _container.localToGlobal( containerViewPort.topLeft )); 346 | var viewportBottomRight : Point = _content.globalToLocal( _container.localToGlobal( containerViewPort.bottomRight )); 347 | 348 | 349 | _contentViewport = new Rectangle( 0, 0, viewportBottomRight.x - viewportTopLeft.x, viewportBottomRight.y ); // we want viewport in content coords too 350 | 351 | //trace('y position', _content.y, _contentViewport.y); 352 | 353 | //_content.y = viewportTopLeft.y; 354 | 355 | _container.addEventListener( MouseEvent.MOUSE_DOWN, mouseDownHandler, false, 0, true ); 356 | 357 | onContentResize( null ); 358 | 359 | } 360 | 361 | 362 | /** 363 |         * Draw the viewport area on top of the container 364 |         */ 365 |     public function drawDebug() : void 366 |     { 367 |            if ( !_container || !_containerViewport ) 368 |                return; 369 |             370 |             371 |            // debug container 372 |            var shape : Shape = new Shape(); 373 |            shape.graphics.lineStyle( 3, 0xFF0000 ); 374 |            shape.graphics.beginFill( 0xFFFFFF, 0.4 ); 375 |            shape.graphics.drawRect( _containerViewport.x, _containerViewport.y, _containerViewport.width, _containerViewport.height ); 376 |            shape.graphics.endFill(); 377 |            shape.cacheAsBitmap = true; 378 |             379 |            _container.addChild( shape ); 380 |             381 |            // trace to debug content 382 |            trace( '---------- SCROLL CONTROLLER : DEBUG CONTENT --------------'); 383 |             384 |            var contentBounds : Rectangle = _content.getBounds( _content ); 385 |            trace( 'content bounds   (x, y, w, h) :', int( contentBounds.x ), int( contentBounds.y ), int( contentBounds.width ), int( contentBounds.height )); 386 |             387 |            trace( 'content position (x, y, w, h) :', int( _content.x ), int( _content.y ), int( _content.width ), int( _content.height )); 388 |             389 |            trace( 'content viewport (x, y, w, h) :', int( _contentViewport.x ), int( _contentViewport.y ), int( _contentViewport.width ), int( _contentViewport.height )); 390 |             391 |            if ( _content is DisplayObjectContainer ) 392 |            { 393 |                shape = new Shape(); 394 |                shape.graphics.lineStyle( 3, 0x00FF00 ); 395 |                shape.graphics.beginFill( 0xFF0000, 0.4 ); 396 |                shape.graphics.drawRect( _contentViewport.x, _contentViewport.y, _contentViewport.width, _contentViewport.height ); 397 |                shape.graphics.endFill(); 398 |                shape.cacheAsBitmap = true; 399 |                 400 |                DisplayObjectContainer( _content ).addChild( shape ); 401 |                 402 |                shape = new Shape(); 403 |                shape.graphics.lineStyle( 3, 0x0000FF ); 404 |                shape.graphics.beginFill( 0x000000, 0.4 ); 405 |                shape.graphics.drawCircle( 0, 0, 50 ); // see the origin of the content in parent 406 |                shape.graphics.endFill(); 407 |                shape.cacheAsBitmap = true; 408 |                 409 |                DisplayObjectContainer( _content ).addChild( shape ); 410 |            } 411 |            else 412 |            { 413 |                trace( _content ); 414 |            } 415 |        } 416 | 417 | public function removeScrollControll() : void 418 | { 419 | if (_content) 420 | { 421 | _content.removeEventListener( Event.RESIZE, onContentResize ); 422 | _content.removeEventListener( Event.ADDED, onContentResize ); 423 | _content.removeEventListener( Event.ADDED_TO_STAGE, onContentResize ) 424 | _content.scrollRect = null; 425 | 426 | } 427 | 428 | if (_container) 429 | { 430 | _container.removeEventListener( MouseEvent.MOUSE_MOVE, onMoveDrag ); 431 | _container.removeEventListener( MouseEvent.MOUSE_UP, onStopDrag ); 432 | _container.removeEventListener( MouseEvent.MOUSE_DOWN, mouseDownHandler ); 433 | _container.removeEventListener( Event.ENTER_FRAME, onEnterFrame ); 434 | } 435 | 436 | _content = null; 437 | _container = null; 438 | _contentViewport = null; 439 | _containerViewport = null; 440 | _contentInitialBounds = null; 441 | } 442 | 443 | 444 | public function pause() : void 445 | { 446 | _paused = true; 447 | //trace('pause scroll controll'); 448 | _container.removeEventListener( MouseEvent.MOUSE_MOVE, onMoveDrag ); 449 | _container.removeEventListener( MouseEvent.MOUSE_UP, onStopDrag ); 450 | _container.removeEventListener( MouseEvent.MOUSE_DOWN, mouseDownHandler ); 451 | _container.removeEventListener( Event.ENTER_FRAME, onEnterFrame ); 452 | 453 | //trace('remove onEnterFrame'); 454 | } 455 | 456 | public function resume() : void 457 | { 458 | if (_contentViewport.height >= this.getContentWidth()) 459 | { 460 | return; 461 | } 462 | 463 | _paused = false; 464 | //trace('resume scroll controll'); 465 | _container.addEventListener( MouseEvent.MOUSE_DOWN, mouseDownHandler, false, 0, true ); 466 | } 467 | 468 | private function getLastSpeed() : Number 469 | { 470 | if ( this._trajectories == null || this._trajectories.length < 2 ) 471 | return 0; 472 | 473 | var a : Object = _trajectories[ this._trajectories.length - 2 ]; 474 | var b : Object = _trajectories[ this._trajectories.length - 1 ]; 475 | 476 | if ( this._trajectories.length > 2400 ) // no need to keep that much, cleaning up if movement last very long only 477 | { 478 | this._trajectories = new Vector.(); 479 | this._trajectories.push( a, b ); 480 | } 481 | 482 | // speed = delta_D / delta_t 483 | return ( b['localX'] - a['localX'] ) / ( b['time'] - a['time'] ); 484 | } 485 | 486 | private var hasTouchContainer:Boolean = false; 487 | 488 | public function autoSlide():void 489 | { 490 | if (!hasTouchContainer) 491 | { 492 | if (_content && _content.scrollRect) 493 | { 494 | var newScrollRect : Rectangle = _content.scrollRect.clone(); 495 | newScrollRect.x += 1; 496 | _content.scrollRect = newScrollRect; 497 | 498 | if (newScrollRect.x + newScrollRect.width >= this.getContentWidth()) 499 | { 500 | hasTouchContainer = true; 501 | } 502 | } 503 | } 504 | } 505 | 506 | } 507 | } -------------------------------------------------------------------------------- /src/com/freshplanet/lib/ui/scroll/mobile/ScrollController.as: -------------------------------------------------------------------------------- 1 | package com.freshplanet.lib.ui.scroll.mobile 2 | { 3 | import flash.display.DisplayObject; 4 | import flash.display.DisplayObjectContainer; 5 | import flash.display.Shape; 6 | import flash.events.Event; 7 | import flash.events.EventDispatcher; 8 | import flash.events.MouseEvent; 9 | import flash.events.TimerEvent; 10 | import flash.geom.Point; 11 | import flash.geom.Rectangle; 12 | import flash.utils.Timer; 13 | import flash.utils.getTimer; 14 | 15 | public class ScrollController extends EventDispatcher 16 | { 17 | // --------------------------------------------------------------------------------------// 18 | // // 19 | // CONSTANTS // 20 | // // 21 | // --------------------------------------------------------------------------------------// 22 | 23 | /** 24 | * Event dispatched every time the scroll position changes. 25 | * Read scrollPosition to know the current scroll position. 26 | * 27 | * @see #scrollPosition 28 | */ 29 | public static const SCROLL_POSITION_CHANGE : String = "ScrollPositionChange"; 30 | 31 | /** 32 | * Event dispatched every time the page changes. 33 | * Read currentPage to know the current page. 34 | * 35 | * @see #currentPage 36 | * @see #pagingEnabled 37 | */ 38 | public static const PAGE_CHANGE : String = "PageChange"; 39 | 40 | // Toggle debug statements. 41 | private static const DEBUG : Boolean = false; 42 | 43 | // Scrolling parameters 44 | private static const SCROLLING_MIN_AMPLITUDE : Number = 1; // in pixels 45 | private static const SCROLLING_MIN_SPEED : Number = 0.01; // in pixels/ms 46 | private static const SCROLLING_MAX_SPEED : Number = 4; // in pixels/ms 47 | private static const FREE_SCROLLING_FRICTION_COEF : Number = 0.0015; 48 | private static const BOUNCING_FRICTION_COEF : Number = 3; 49 | private static const BOUNCING_SPRING_RATE : Number = 0.03; 50 | private static const BOUNCING_FINGER_SLOW_DOWN_COEF : Number = 0.5; 51 | private static const FORCED_ANIMATED_SCROLLING_MIN_DURATION : Number = 150; // in ms 52 | private static const FORCED_ANIMATED_SCROLLING_MAX_SPEED : Number = 8; // in pixels/ms 53 | 54 | // Scroll bars appearance 55 | private static const SCROLLBAR_THICKNESS : Number = 10; 56 | private static const SCROLLBAR_MIN_LENGTH_WHEN_FREE : Number = 100; 57 | private static const SCROLLBAR_MIN_LENGTH_WHEN_BOUNCING : Number = 10; 58 | private static const SCROLLBAR_CORNER_RADIUS : Number = 10; 59 | private static const SCROLLBAR_SIDE_MARGIN : Number = 2; 60 | private static const SCROLLBAR_COLOR : uint = 0x000000; 61 | private static const SCROLLBAR_ALPHA : Number = 0.5; 62 | private static const SCROLLBAR_FADEOUT_DELAY : Number = 250; // in ms 63 | 64 | 65 | // --------------------------------------------------------------------------------------// 66 | // // 67 | // PUBLIC API // 68 | // // 69 | // --------------------------------------------------------------------------------------// 70 | 71 | /** Indicate if scrolling is enabled in the vertical direction */ 72 | public var verticalScrollingEnabled : Boolean = true; 73 | 74 | /** Indicate if scrolling is enabled in the horizontal direction */ 75 | public var horizontalScrollingEnabled : Boolean = true; 76 | 77 | /** 78 | * Indicate if the vertical scroll bar should be displayed. 79 | * This has no effect if vertical scrolling is disabled. 80 | */ 81 | public var displayVerticalScrollbar : Boolean = true; 82 | 83 | /** 84 | * Indicate if the horizontal scroll bar should be displayed. 85 | * This has no effect if horizontal scrolling is disabled. 86 | */ 87 | public var displayHorizontalScrollbar : Boolean = true; 88 | 89 | /** 90 | * Indicate if the scrolling should scroll only to integer increments 91 | * of the viewport width (like iOS homepage). 92 | */ 93 | public var pagingEnabled : Boolean = false; 94 | 95 | /** The current scrolling page (in horizontal and vertical directions) */ 96 | public var currentPage : Point = new Point(); 97 | 98 | public function ScrollController() 99 | { 100 | super(); 101 | } 102 | 103 | /** 104 | * Add scroll logic to a given view. 105 | * 106 | * @param content The content that should be scrollable. 107 | * @param container The container on which we will listen to mouse events. 108 | * @param containerViewport A rectangle (in container coordinates) outside of which the 109 | * content should be masked. The scroll bar will be displayed on the right side of the 110 | * container viewport. If null, we default to the container bounds. 111 | * @param contentRect A rectangle (in content coordinates) outside of which the scrolling 112 | * shouldn't go. If null, the whole content will be scrollable. 113 | * @param scaleRatio The scale ratio between the content and the container (scaleRatio > 0). 114 | * Default: 1. 115 | */ 116 | public function addScrollControll( content : DisplayObject, 117 | container : DisplayObjectContainer, 118 | containerViewport : Rectangle = null, 119 | contentRect : Rectangle = null, 120 | scaleRatio : Number = 1) : void 121 | { 122 | if (scaleRatio > 0) 123 | { 124 | _scaleRatio = scaleRatio; 125 | } 126 | 127 | if (_content || _container) 128 | { 129 | trace("ScrollController Error - This controller is already in use."); 130 | return; 131 | } 132 | 133 | if (!content || !container) 134 | { 135 | trace("ScrollController Error - content and container can't be null."); 136 | return; 137 | } 138 | 139 | // Save the parameters 140 | _container = container; 141 | _content = content; 142 | _cachedContentBounds = null; 143 | 144 | // Setup content rect 145 | setContentRect(contentRect); 146 | 147 | // Setup container viewport 148 | this.containerViewport = containerViewport; 149 | 150 | // Initialize the scroll bars 151 | initScrollBars(); 152 | 153 | // Start listening to touch events 154 | _container.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown, false, 0, true); 155 | } 156 | 157 | /** Remove scroll logic of this controller. */ 158 | public function removeScrollControll() : void 159 | { 160 | if (_content) 161 | _content.scrollRect = null; 162 | 163 | if (_container) 164 | { 165 | _container.removeEventListener(MouseEvent.MOUSE_DOWN, onMouseDown); 166 | _container.removeEventListener(MouseEvent.MOUSE_MOVE, onMouseMove); 167 | _container.removeEventListener(MouseEvent.MOUSE_OUT, onMouseOut); 168 | _container.removeEventListener(MouseEvent.MOUSE_UP, onMouseUp); 169 | _container.removeEventListener(Event.ENTER_FRAME, onEnterFrame); 170 | _container.mouseChildren = true; 171 | } 172 | 173 | scrollBarFadeOutTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, onScrollBarFadeOutTimerComplete); 174 | 175 | if (_verticalScrollBar && _container && _container.contains(_verticalScrollBar)) 176 | _container.removeChild(_verticalScrollBar); 177 | 178 | if (_horizontalScrollBar && _container && _container.contains(_horizontalScrollBar)) 179 | _container.removeChild(_horizontalScrollBar); 180 | 181 | _content = null; 182 | _container = null; 183 | _verticalScrollBar = null; 184 | _horizontalScrollBar = null; 185 | 186 | _containerViewport = null; 187 | _contentRect = null; 188 | 189 | _forcedAnimatedScrolling = false; 190 | _scrollingLockedForPaging = false; 191 | } 192 | 193 | /** 194 | * Scroll to the origin (top-left) of the scrollable area. 195 | * 196 | * @param animated If true, the scrolling will be animated at the maximum scrolling speed. 197 | * If false, the scrolling will happen instantely. 198 | * 199 | * @see #scrollTo() 200 | */ 201 | public function scrollToOrigin( animated : Boolean = false ) : void 202 | { 203 | scrollTo(contentRect.topLeft, animated); 204 | } 205 | 206 | /** 207 | * Scroll to the top of the scrollable area. 208 | * 209 | * @param animated If true, the scrolling will be animated at the maximum scrolling speed. 210 | * If false, the scrolling will happen instantely. 211 | * 212 | * @see #scrollTo() 213 | */ 214 | public function scrollToTop( animated : Boolean = false ) : void 215 | { 216 | var newPosition:Point = new Point(scrollPosition.x, contentRect.top); 217 | scrollTo(newPosition, animated); 218 | } 219 | 220 | /** 221 | * Scroll to the bottom of the scrollable area. 222 | * 223 | * @param animated If true, the scrolling will be animated at the maximum scrolling speed. 224 | * If false, the scrolling will happen instantely. 225 | * 226 | * @see #scrollTo() 227 | */ 228 | public function scrollToBottom( animated : Boolean = false ) : void 229 | { 230 | _cachedContentBounds = null; 231 | var newPosition:Point = new Point(scrollPosition.x, contentRect.bottom - _content.scrollRect.height); 232 | scrollTo(newPosition, animated); 233 | } 234 | 235 | /** 236 | * Scroll to the left of the scrollable area. 237 | * 238 | * @param animated If true, the scrolling will be animated at the maximum scrolling speed. 239 | * If false, the scrolling will happen instantely. 240 | * 241 | * @see #scrollTo() 242 | */ 243 | public function scrollToLeft( animated : Boolean = false ) : void 244 | { 245 | var newPosition:Point = new Point(contentRect.left, scrollPosition.y); 246 | scrollTo(newPosition, animated); 247 | } 248 | 249 | /** 250 | * Scroll to the right of the scrollable area. 251 | * 252 | * @param animated If true, the scrolling will be animated at the maximum scrolling speed. 253 | * If false, the scrolling will happen instantely. 254 | * 255 | * @see #scrollTo() 256 | */ 257 | public function scrollToRight( animated : Boolean = false ) : void 258 | { 259 | var newPosition:Point = new Point(contentRect.right - _content.scrollRect.width, scrollPosition.y); 260 | scrollTo(newPosition, animated); 261 | } 262 | 263 | /** 264 | * Scroll to a given position (in content coordinates). 265 | * 266 | * @param position A point representing the target scrolling position. 267 | * @param animated If true, the scrolling will be animated at the maximum scrolling speed. 268 | * If false, the scrolling will happen instantely. 269 | * 270 | * @see #scrollToTop() 271 | */ 272 | public function scrollTo( position : Point, animated : Boolean = false ) : void 273 | { 274 | if (animated) 275 | { 276 | _forcedAnimatedScrollingTarget = position; 277 | var forcedAnimatedScrollingSpeedX:Number = Math.min(FORCED_ANIMATED_SCROLLING_MAX_SPEED, Math.abs((position.x - scrollPosition.x) / FORCED_ANIMATED_SCROLLING_MIN_DURATION)); 278 | var forcedAnimatedScrollingSpeedY:Number = Math.min(FORCED_ANIMATED_SCROLLING_MAX_SPEED, Math.abs((position.y - scrollPosition.y) / FORCED_ANIMATED_SCROLLING_MIN_DURATION)); 279 | _forcedAnimatedScrollingSpeed = new Point(forcedAnimatedScrollingSpeedX, forcedAnimatedScrollingSpeedY); 280 | _forcedAnimatedScrolling = true; 281 | _timeOfLastFrame = getTimer(); 282 | _container.addEventListener(Event.ENTER_FRAME, onEnterFrame, false, 0, true); 283 | } 284 | else if (_content && _content.scrollRect) 285 | { 286 | var scrollRect:Rectangle = _content.scrollRect; 287 | scrollRect.x = position.x; 288 | scrollRect.y = position.y; 289 | _content.scrollRect = scrollRect; 290 | _previousScrollPositions['t-1'] = _previousScrollPositions['t-2'] = scrollPosition; 291 | updateScrollBars(); 292 | } 293 | } 294 | 295 | /** The current scroll position (in content coordinates). */ 296 | public function get scrollPosition() : Point 297 | { 298 | return _content && _content.scrollRect ? _content.scrollRect.topLeft : new Point(); 299 | } 300 | 301 | /** Indicates if the user is touching the scrolling area at the moment. */ 302 | public function get touchingScreen() : Boolean 303 | { 304 | return _touchingScreen; 305 | } 306 | 307 | /** The current scrolling speed (in pixel/ms in content coordinates). */ 308 | public function get speed() : Point 309 | { 310 | return _speed; 311 | } 312 | 313 | private var _cachedContentBounds:Rectangle = null; 314 | 315 | /** The initial bounds of the content in its own coordinates system. */ 316 | public function get contentBounds() : Rectangle 317 | { 318 | if (!_content) 319 | return null; 320 | 321 | if (_cachedContentBounds) 322 | return _cachedContentBounds.clone(); 323 | 324 | var originalWidthOnStage : Number = _content.transform.pixelBounds.width; 325 | var originalWidthOnContent : Number = _content.globalToLocal(new Point(originalWidthOnStage, 0)).x - _content.globalToLocal(new Point()).x; 326 | 327 | var originalHeightOnStage : Number = _content.transform.pixelBounds.height; 328 | var originalHeightOnContent : Number = _content.globalToLocal(new Point( 0, originalHeightOnStage)).y - _content.globalToLocal(new Point()).y; 329 | 330 | _cachedContentBounds = new Rectangle(0, 0, int(originalWidthOnContent), int(originalHeightOnContent)); 331 | 332 | return _cachedContentBounds.clone(); 333 | } 334 | 335 | /** A rectangle (in content coordinates) outside of which the scrolling won't go. */ 336 | public function get contentRect() : Rectangle 337 | { 338 | if (!_content) 339 | return null; 340 | 341 | return _contentRect ? _contentRect : contentBounds; 342 | } 343 | 344 | /** 345 | * Set the contentRect property. 346 | * 347 | * @param rect The desired scrollable area. 348 | * @param animated If true, and the current scrolling position is out of the new rectangle, 349 | * we will animate a scrolling movement to get back in the new rectangle. If false, the 350 | * scrolling movement happens instantly. 351 | */ 352 | public function setContentRect( rect : Rectangle, animated : Boolean = false ) : void 353 | { 354 | // Content rect accept only integer values 355 | if (rect) 356 | _contentRect = new Rectangle(int(rect.x), int(rect.y), int(rect.width), int(rect.height)); 357 | else 358 | _contentRect = null; 359 | 360 | 361 | _cachedContentBounds = null; 362 | 363 | // Content rect can't be smaller than viewport 364 | contentRect.width = Math.max(contentRect.width, containerViewport.width/_scaleRatio); 365 | contentRect.height = Math.max(contentRect.height, containerViewport.height/_scaleRatio); 366 | 367 | if (animated) 368 | { 369 | _timeOfLastFrame = getTimer(); 370 | _container.addEventListener(Event.ENTER_FRAME, onEnterFrame, false, 0, true); 371 | } 372 | else 373 | { 374 | if (!contentRect.containsPoint(scrollPosition)) 375 | scrollToOrigin(); 376 | } 377 | } 378 | 379 | /** A rectangle (in container coordinates) outside of which the content is masked. */ 380 | public function get containerViewport() : Rectangle 381 | { 382 | if (!_container) 383 | return null; 384 | 385 | if (!_containerViewport) 386 | { 387 | var bounds:Rectangle = _container.getBounds(_container); 388 | _containerViewport = new Rectangle(int(bounds.x), int(bounds.y), int(bounds.width), int(bounds.height)); 389 | } 390 | 391 | return _containerViewport; 392 | } 393 | 394 | public function set containerViewport( viewport : Rectangle ) : void 395 | { 396 | var oldViewport:Rectangle = _containerViewport; 397 | 398 | _containerViewport = viewport ? viewport.clone() : null; 399 | 400 | // Content rect can't be smaller than viewport 401 | contentRect.width = Math.max(contentRect.width, containerViewport.width); 402 | contentRect.height = Math.max(contentRect.height, containerViewport.height); 403 | 404 | if (!_content.scrollRect || !containerViewport.equals(oldViewport)) 405 | { 406 | // Update the scroll rect 407 | var topLeft:Point = _content.globalToLocal(_container.localToGlobal(containerViewport.topLeft)); 408 | var bottomRight:Point = _content.globalToLocal(_container.localToGlobal(containerViewport.bottomRight)); 409 | _content.scrollRect = new Rectangle(contentRect.x, contentRect.y, int(bottomRight.x-topLeft.x), int(bottomRight.y-topLeft.y)); 410 | } 411 | } 412 | 413 | 414 | public function pauseScrolling():void 415 | { 416 | if (_container) 417 | { 418 | _container.removeEventListener(MouseEvent.MOUSE_DOWN, onMouseDown); 419 | } 420 | } 421 | 422 | public function resumeScrolling():void 423 | { 424 | if (_container) 425 | { 426 | _container.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown, false, 0, true); 427 | } 428 | } 429 | 430 | // --------------------------------------------------------------------------------------// 431 | // // 432 | // PRIVATE VARS // 433 | // // 434 | // --------------------------------------------------------------------------------------// 435 | 436 | // Managed objects 437 | private var _content : DisplayObject; 438 | private var _container : DisplayObjectContainer; 439 | 440 | // Scale ratio 441 | private var _scaleRatio : Number = 1; 442 | 443 | // Config rectangles 444 | private var _contentRect : Rectangle; 445 | private var _containerViewport : Rectangle; 446 | 447 | // Scroll bars 448 | private var _verticalScrollBar : Shape; 449 | private var _horizontalScrollBar : Shape; 450 | private var _scrollBarFadeOutTimer : Timer; 451 | 452 | // Flag indicating if the user is touching the screen 453 | private var _touchingScreen : Boolean = false; 454 | 455 | // Flag indicating that we receive a touch event between two frames, and that it has not 456 | // been processed yet. 457 | private var _pendingTouch : Boolean = false; 458 | 459 | // Last 2 finger positions (in stage coordinates) 460 | private var _previousFingerPosition : Point = new Point(); 461 | private var _currentFingerPosition : Point = new Point(); 462 | 463 | // Time-related vars 464 | private var _timeOfLastFrame : Number = 0; 465 | private var _timeOfLastMouseDown : Number = 0; 466 | 467 | // Current scrolling speed (in pixels/ms in content coordinates) 468 | private var _speed : Point = new Point(); 469 | 470 | // Last 2 scrolling positions (in content coordinates) 471 | private var _previousScrollPositions : Object = { 't-1': new Point(), 't-2': new Point() }; 472 | 473 | // Forced animated scrolling (cf. scrollTo()) 474 | private var _forcedAnimatedScrolling : Boolean = false; 475 | private var _forcedAnimatedScrollingSpeed : Point = new Point(); 476 | private var _forcedAnimatedScrollingTarget : Point = new Point(); 477 | 478 | // Flag indicating if the scrolling is locked for paging purpose 479 | private var _scrollingLockedForPaging : Boolean = false; 480 | 481 | 482 | // --------------------------------------------------------------------------------------// 483 | // // 484 | // PRIVATE FUNCTIONS // 485 | // // 486 | // --------------------------------------------------------------------------------------// 487 | 488 | /** Create the scroll bars and add them to the display list */ 489 | private function initScrollBars() : void 490 | { 491 | _verticalScrollBar = new Shape(); 492 | _verticalScrollBar.cacheAsBitmap = true; 493 | _verticalScrollBar.alpha = 0; 494 | 495 | _horizontalScrollBar = new Shape(); 496 | _horizontalScrollBar.cacheAsBitmap = true; 497 | _horizontalScrollBar.alpha = 0; 498 | 499 | updateScrollBars(); 500 | } 501 | 502 | /** Redraw vertical scroll bar with a new length (can't use scaling because of the rounded corners) */ 503 | private function set verticalScrollBarLength( value : Number ) : void 504 | { 505 | var currentAlpha:Number = _verticalScrollBar.alpha; 506 | _verticalScrollBar.transform = _content.transform; 507 | _verticalScrollBar.alpha = currentAlpha; 508 | _verticalScrollBar.graphics.clear(); 509 | _verticalScrollBar.graphics.beginFill(SCROLLBAR_COLOR, SCROLLBAR_ALPHA); 510 | _verticalScrollBar.graphics.drawRoundRect(0, 0, SCROLLBAR_THICKNESS, value, SCROLLBAR_CORNER_RADIUS, SCROLLBAR_CORNER_RADIUS); 511 | _verticalScrollBar.graphics.endFill(); 512 | } 513 | 514 | /** Redraw horizontal scroll bar with a new length (can't use scaling because of the rounded corners) */ 515 | private function set horizontalScrollBarLength( value : Number ) : void 516 | { 517 | var currentAlpha:Number = _horizontalScrollBar.alpha; 518 | _horizontalScrollBar.transform = _content.transform; 519 | _horizontalScrollBar.alpha = currentAlpha; 520 | _horizontalScrollBar.graphics.clear(); 521 | _horizontalScrollBar.graphics.beginFill(SCROLLBAR_COLOR, SCROLLBAR_ALPHA); 522 | _horizontalScrollBar.graphics.drawRoundRect(0, 0, value, SCROLLBAR_THICKNESS, SCROLLBAR_CORNER_RADIUS, SCROLLBAR_CORNER_RADIUS); 523 | _horizontalScrollBar.graphics.endFill(); 524 | } 525 | 526 | /** 527 | * Update the size and position of the scroll bars depending on the current state of the 528 | * controller. 529 | */ 530 | private function updateScrollBars() : void 531 | { 532 | // Check if the scroll bars are on the display list. If not, add them. 533 | // We do it at every update because they are added on the container, which might clean its 534 | // children without us knowing. 535 | if (!_container.contains(_verticalScrollBar)) _container.addChild(_verticalScrollBar); 536 | if (!_container.contains(_horizontalScrollBar)) _container.addChild(_horizontalScrollBar); 537 | 538 | // We hide a scroll bar if one of the following is true: 539 | // - scrolling in this direction is disabled 540 | // - the scroll bar in this direction is disabled 541 | // - the content is smaller than the container in this direction 542 | _verticalScrollBar.visible = verticalScrollingEnabled && displayVerticalScrollbar && contentRect.height > _content.scrollRect.height; 543 | _horizontalScrollBar.visible = horizontalScrollingEnabled && displayHorizontalScrollbar && contentRect.width > _content.scrollRect.width; 544 | 545 | // If the content is smaller than the container in both directions, no need to continue 546 | if (!_verticalScrollBar.visible && !_horizontalScrollBar.visible) return; 547 | 548 | // Same as scrolling relative position, but constrained between 0 and 1 because the scroll bar 549 | // has to stay within the container viewport. 550 | var verticalScrollBarRelativePosition:Number = Math.min(1, Math.max(0, scrollingRelativePosition.y)); 551 | var horizontalScrollBarRelativePosition:Number = Math.min(1, Math.max(0, scrollingRelativePosition.x)); 552 | 553 | // Compute new scroll bars length 554 | var newVerticalScrollBarLength:Number = Math.max(SCROLLBAR_MIN_LENGTH_WHEN_FREE, _content.scrollRect.height * (_content.scrollRect.height / contentRect.height)); 555 | var newHorizontalScrollBarLength:Number = Math.max(SCROLLBAR_MIN_LENGTH_WHEN_FREE, _content.scrollRect.width * (_content.scrollRect.width / contentRect.width)); 556 | 557 | // Absolute number of pixels currently in the bounce areas (0 if we are not bouncing). 558 | // Used to reduce the height of the scroll bars when bouncing. 559 | var bounceAbsoluteSize:Point = new Point(); 560 | if (bouncingAboveContent) bounceAbsoluteSize.y = _content.scrollRect.height * Math.abs(scrollingRelativePosition.y); 561 | else if (bouncingUnderContent) bounceAbsoluteSize.y = _content.scrollRect.height * (scrollingRelativePosition.y - 1); 562 | if (bouncingLeftFromContent) bounceAbsoluteSize.x = _content.scrollRect.width * Math.abs(scrollingRelativePosition.x); 563 | if (bouncingRightFromContent) bounceAbsoluteSize.x = _content.scrollRect.width * Math.abs(scrollingRelativePosition.x - 1); 564 | if (bounceAbsoluteSize.y) newVerticalScrollBarLength = Math.max(SCROLLBAR_MIN_LENGTH_WHEN_BOUNCING, newVerticalScrollBarLength - bounceAbsoluteSize.y); 565 | if (bounceAbsoluteSize.x) newHorizontalScrollBarLength = Math.max(SCROLLBAR_MIN_LENGTH_WHEN_BOUNCING, newHorizontalScrollBarLength - bounceAbsoluteSize.x); 566 | 567 | // Apply new scroll bars length (redraws the scroll bars) 568 | verticalScrollBarLength = newVerticalScrollBarLength; 569 | horizontalScrollBarLength = newHorizontalScrollBarLength; 570 | 571 | // Update scroll bars position 572 | _verticalScrollBar.x = containerViewport.right - _verticalScrollBar.width - SCROLLBAR_SIDE_MARGIN; 573 | _verticalScrollBar.y = _containerViewport.top + (_containerViewport.height - newVerticalScrollBarLength*_verticalScrollBar.scaleY) * verticalScrollBarRelativePosition; 574 | _horizontalScrollBar.x = _containerViewport.left + (_containerViewport.width - newHorizontalScrollBarLength*_horizontalScrollBar.scaleX) * horizontalScrollBarRelativePosition; 575 | _horizontalScrollBar.y = containerViewport.bottom - _horizontalScrollBar.height - SCROLLBAR_SIDE_MARGIN; 576 | } 577 | 578 | /** 579 | * Timer used to fade out the scroll bar after a given delay when no scrolling is 580 | * happening. When the speed reaches zero in the content area, the timer starts. 581 | * When the timer fires, the fade out animation starts. If the user starts scrolling, 582 | * the timer is reset. 583 | */ 584 | private function get scrollBarFadeOutTimer() : Timer 585 | { 586 | // Lazy creation 587 | if (!_scrollBarFadeOutTimer) 588 | { 589 | _scrollBarFadeOutTimer = new Timer(SCROLLBAR_FADEOUT_DELAY, 1); 590 | _scrollBarFadeOutTimer.addEventListener(TimerEvent.TIMER_COMPLETE, onScrollBarFadeOutTimerComplete, false, 0, true); 591 | } 592 | 593 | return _scrollBarFadeOutTimer; 594 | } 595 | 596 | /** Display the scroll bar and cancel any fade out animation. */ 597 | private function showScrollBars() : void 598 | { 599 | if (!_verticalScrollBar || !_horizontalScrollBar) 600 | return; 601 | 602 | // Reset the fade out timer 603 | scrollBarFadeOutTimer.reset(); 604 | 605 | // Stop the fade out action if started 606 | _verticalScrollBar.removeEventListener(Event.ENTER_FRAME, fadeOutScrollBar); 607 | _horizontalScrollBar.removeEventListener(Event.ENTER_FRAME, fadeOutScrollBar); 608 | 609 | // Show the scroll bars 610 | _verticalScrollBar.alpha = 1; 611 | _horizontalScrollBar.alpha = 1; 612 | } 613 | 614 | /** 615 | * Start the fade out timer to hide the scroll bars after a given delay. 616 | * 617 | * @see #scrollBarFadeOutTimer() 618 | */ 619 | private function hideScrollBars() : void 620 | { 621 | if (!_verticalScrollBar || !_horizontalScrollBar) 622 | return; 623 | 624 | // Start the fade out timer 625 | scrollBarFadeOutTimer.start(); 626 | } 627 | 628 | /** 629 | * Scroll bar fade out animation. This function is called every frame while the scroll bars 630 | * fade out. 631 | */ 632 | private function fadeOutScrollBar( event : Event ) : void 633 | { 634 | var scrollBar:Shape = event.target as Shape; 635 | if (!scrollBar) return; 636 | 637 | // Decrease the scroll bar alpha 638 | scrollBar.alpha = Math.max(0, scrollBar.alpha - 0.2); 639 | 640 | // Stop the fade out if finished 641 | if (scrollBar.alpha == 0) scrollBar.removeEventListener(Event.ENTER_FRAME, fadeOutScrollBar); 642 | } 643 | 644 | /** Set the current page and update the scrolling */ 645 | private function setCurrentPage( page : Point, animated:Boolean = true ) : void 646 | { 647 | if (!page || contentRect == null) return; 648 | 649 | if (page.x != currentPage.x || page.y != currentPage.y) 650 | { 651 | currentPage = page; 652 | 653 | // Dispatch a change event 654 | dispatchEvent(new Event(PAGE_CHANGE)); 655 | } 656 | 657 | // Scroll to the new page 658 | var targetX:Number = contentRect.left + currentPage.x * _content.scrollRect.width; 659 | var targetY:Number = contentRect.top + currentPage.y * _content.scrollRect.height; 660 | var target:Point = new Point(targetX, targetY); 661 | scrollTo(target, animated); 662 | } 663 | 664 | /** 665 | * Follow the finger when touching the screen. 666 | * 667 | * @param deltaTime The time (in seconds) since the last frame 668 | */ 669 | private function manageFingerScrolling( deltaTime : Number ) : void 670 | { 671 | // DEBUG INFO 672 | if (DEBUG) 673 | { 674 | trace('---- Start managing finger scrolling'); 675 | } 676 | 677 | // Compute the scrolling amplitude 678 | var delta:Point = _content.globalToLocal(_previousFingerPosition).subtract(_content.globalToLocal(_currentFingerPosition)); 679 | if (!horizontalScrollingEnabled) delta.x = 0; 680 | if (!verticalScrollingEnabled) delta.y = 0; 681 | if (bouncingLeftFromContent || bouncingRightFromContent) delta.x *= BOUNCING_FINGER_SLOW_DOWN_COEF; 682 | if (bouncingAboveContent || bouncingUnderContent) delta.y *= BOUNCING_FINGER_SLOW_DOWN_COEF; 683 | 684 | // Apply the scrolling movement 685 | moveContentFromDelta(delta, deltaTime); 686 | 687 | // Save the finger position 688 | _previousFingerPosition = _currentFingerPosition; 689 | 690 | // Indicate that finger touch has been processed 691 | _pendingTouch = false; 692 | 693 | // DEBUG INFO 694 | if (DEBUG) 695 | { 696 | trace('------ _currentFingerPosition = ' + _currentFingerPosition + ' | speed = ' + speed + ' | delta = ' + delta + ' | deltaTime = ' + deltaTime); 697 | trace('---- Stop managing finger scrolling'); 698 | } 699 | } 700 | 701 | /** 702 | * Apply physics laws when scrolling freely. 703 | * 704 | * @param deltaTime The time (in seconds) since the last frame 705 | */ 706 | private function manageFreeScrolling( deltaTime : Number ) : void 707 | { 708 | // DEBUG INFO 709 | if (DEBUG) 710 | { 711 | trace('---- Start managing free scrolling'); 712 | } 713 | 714 | // Physics without paging 715 | var f:Point = new Point(); 716 | var k:Point = new Point(); 717 | var x0:Number = 0; 718 | var y0:Number = 0; 719 | 720 | // Update the physics laws in X axis 721 | if (bouncingLeftFromContent || bouncingRightFromContent) 722 | { 723 | f.x = 3; // friction coefficient 724 | k.x = BOUNCING_SPRING_RATE; // spring rate 725 | x0 = bouncingLeftFromContent ? contentRect.left+10 : contentRect.right-_content.scrollRect.width-10; // spring origin 726 | } 727 | else 728 | { 729 | f.x = FREE_SCROLLING_FRICTION_COEF; // friction coefficient 730 | k.x = 0; // spring rate (no spring on content) 731 | x0 = 0; // spring origin (no spring on content) 732 | } 733 | 734 | // Update the physics laws in Y axis 735 | if (bouncingAboveContent || bouncingUnderContent) 736 | { 737 | f.y = 3; // friction coefficient 738 | k.y = BOUNCING_SPRING_RATE; // spring rate 739 | y0 = bouncingAboveContent ? contentRect.top+10 : contentRect.bottom-_content.scrollRect.height-10; // spring origin 740 | } 741 | else 742 | { 743 | f.y = FREE_SCROLLING_FRICTION_COEF; // friction coefficient 744 | k.y = 0; // spring rate (no spring on content) 745 | y0 = 0; // spring origin (no spring on content) 746 | } 747 | 748 | // Compute the new scrolling position after deltaTime. 749 | // The movement equation is one of a mobile moving on a plane surface with a friction coefficient, maybe attached to a spring (if in the bouncing area). 750 | // It is thus a second order differential equation: d2y/dt2 + f*dy/dt + k*(y-y0) = 0 751 | // Here we assume deltaTime is very short and thus discretize the equation and compute y based on y(t-1) and y(t-2). 752 | var x:Number = 1/(1/Math.pow(deltaTime,2)+f.x/deltaTime+k.x) * (_previousScrollPositions['t-1'].x*(f.x/deltaTime+2/Math.pow(deltaTime,2)) - _previousScrollPositions['t-2'].x/Math.pow(deltaTime,2) + k.x*x0); 753 | var y:Number = 1/(1/Math.pow(deltaTime,2)+f.y/deltaTime+k.y) * (_previousScrollPositions['t-1'].y*(f.y/deltaTime+2/Math.pow(deltaTime,2)) - _previousScrollPositions['t-2'].y/Math.pow(deltaTime,2) + k.y*y0); 754 | 755 | // When we go from the bouncing area to the content area, we force the position to be exactly on the top (or bottom) of the content area. 756 | // This prevents a vibration effect when the content is smaller than the screen (the elasticity would make the scroller go from one bouncing 757 | // area to the other). We then stop the movement completely. 758 | var stopAfter:Boolean = false; 759 | if (_previousScrollPositions['t-1'].x < contentRect.left && x > contentRect.left) 760 | { 761 | x = contentRect.left; 762 | stopAfter = true; 763 | } 764 | if (_previousScrollPositions['t-1'].x + _content.scrollRect.width > contentRect.right && x + _content.scrollRect.width < contentRect.right) 765 | { 766 | x = contentRect.right - _content.scrollRect.width; 767 | stopAfter = true; 768 | } 769 | if (_previousScrollPositions['t-1'].y < contentRect.top && y > contentRect.top) 770 | { 771 | y = contentRect.top; 772 | stopAfter = true; 773 | } 774 | if (_previousScrollPositions['t-1'].y + _content.scrollRect.height > contentRect.bottom && y + _content.scrollRect.height < contentRect.bottom) 775 | { 776 | y = contentRect.bottom - _content.scrollRect.height; 777 | stopAfter = true; 778 | } 779 | 780 | // Apply the movement 781 | var delta:Point = new Point(x-_content.scrollRect.x, y-_content.scrollRect.y); 782 | moveContentFromDelta(delta, deltaTime, SCROLLING_MAX_SPEED, SCROLLING_MAX_SPEED, stopAfter); 783 | 784 | // DEBUG INFO 785 | if (DEBUG) 786 | { 787 | trace('------ speed = ' + speed + ' | delta = ' + delta + ' | deltaTime = ' + deltaTime); 788 | trace('---- Stop managing free scrolling'); 789 | } 790 | } 791 | 792 | /** 793 | * Manage the forced animated scrolling. 794 | * 795 | * @param deltaTime The time (in seconds) since the last frame 796 | * 797 | * @see #scrollTo() 798 | */ 799 | private function manageForcedAnimatedScrolling( deltaTime : Number ) : void 800 | { 801 | // DEBUG INFO 802 | if (DEBUG) 803 | { 804 | trace('---- Start managing forced animated scrolling'); 805 | } 806 | 807 | // Compute the total distance remaining and let moveContentFromDelta()'s speed control 808 | // do the job. 809 | var delta:Point = _forcedAnimatedScrollingTarget.subtract(scrollPosition); 810 | 811 | if (!delta.x && !delta.y) 812 | { 813 | stopMovement(); 814 | _forcedAnimatedScrolling = false; 815 | } 816 | else 817 | { 818 | // If the delta makes us go in the bounce area, we limit it and then stop the movement 819 | var stopAfter:Boolean = false; 820 | if (_previousScrollPositions['t-1'].x > contentRect.left && scrollPosition.x + delta.x < contentRect.left) 821 | { 822 | delta.x = contentRect.left - scrollPosition.x; 823 | stopAfter = true; 824 | } 825 | if (_previousScrollPositions['t-1'].x + _content.scrollRect.width < contentRect.right && scrollPosition.x + delta.x + _content.scrollRect.width > contentRect.right) 826 | { 827 | delta.x = contentRect.right - _content.scrollRect.width - scrollPosition.x; 828 | stopAfter = true; 829 | } 830 | if (_previousScrollPositions['t-1'].y > contentRect.top && scrollPosition.y + delta.y < contentRect.top) 831 | { 832 | delta.y = contentRect.top - scrollPosition.y; 833 | stopAfter = true; 834 | } 835 | if (_previousScrollPositions['t-1'].y + _content.scrollRect.height < contentRect.bottom && scrollPosition.y + delta.y + _content.scrollRect.height > contentRect.bottom) 836 | { 837 | delta.y = contentRect.bottom - _content.scrollRect.height - scrollPosition.y; 838 | stopAfter = true; 839 | } 840 | 841 | 842 | moveContentFromDelta(delta, deltaTime, _forcedAnimatedScrollingSpeed.x, _forcedAnimatedScrollingSpeed.y, stopAfter); 843 | } 844 | 845 | // DEBUG INFO 846 | if (DEBUG) 847 | { 848 | trace('------ target = ' + _forcedAnimatedScrollingTarget + ' | speed = ' + speed + ' | delta = ' + delta + ' | deltaTime = ' + deltaTime); 849 | trace('---- Stop managing forced animated scrolling'); 850 | } 851 | } 852 | 853 | /** 854 | * Manage the paging behavior right after the user remove his finger from the screen. 855 | * 856 | * @see #pagingEnabled 857 | */ 858 | private function managePaging() : void 859 | { 860 | if (pagingEnabled && scrollingWithinContentArea && !_scrollingLockedForPaging) 861 | { 862 | _scrollingLockedForPaging = true; 863 | 864 | var pageX:int = Math.floor((scrollPosition.x - contentRect.left + _content.scrollRect.width/2) / _content.scrollRect.width); 865 | var pageY:int = Math.floor((scrollPosition.y - contentRect.top + _content.scrollRect.height/2) / _content.scrollRect.height); 866 | if (_speed.x > 0 && pageX == currentPage.x) pageX += 1; 867 | else if (_speed.x < 0 && pageX == currentPage.x) pageX -= 1; 868 | if (_speed.y > 0 && pageY == currentPage.y) pageY += 1; 869 | else if (_speed.y < 0 && pageY == currentPage.y) pageY -= 1; 870 | 871 | setCurrentPage(new Point(pageX, pageY)); 872 | 873 | return; 874 | } 875 | } 876 | 877 | /** 878 | * Scroll the content from a given distance. 879 | * 880 | * @param delta The scrolling distance in pixels in the content coordinates. 881 | * @param deltaTime The time interval corresponding to this movement (used to compute the current speed). 882 | */ 883 | private function moveContentFromDelta( delta : Point, deltaTime : Number, maxSpeedX : Number = SCROLLING_MAX_SPEED, maxSpeedY : Number = SCROLLING_MAX_SPEED, stopAfter : Boolean = false ) : void 884 | { 885 | 886 | // Avoid unintended tiny movement on single tap 887 | // We except the case where it brings the scrolling to the edges of the scrollable area 888 | // in order not to get stuck at the border of the bouncing area (and have the scroll bar not 889 | // disappearing. Also we except the forced animated scrolling 890 | if (Math.abs(delta.x) < SCROLLING_MIN_AMPLITUDE && (scrollPosition.x + delta.x) != contentRect.left && (scrollPosition.x + delta.x) != (contentRect.right - _content.scrollRect.width) && !_forcedAnimatedScrolling) 891 | delta.x = 0; 892 | if (Math.abs(delta.y) < SCROLLING_MIN_AMPLITUDE && (scrollPosition.y + delta.y) != contentRect.top && (scrollPosition.y + delta.y) != (contentRect.bottom - _content.scrollRect.height) && !_forcedAnimatedScrolling) 893 | delta.y = 0; 894 | 895 | // Limit the instant speed 896 | if (Math.abs(delta.x/deltaTime) > maxSpeedX) 897 | delta.x = delta.x/Math.abs(delta.x)*maxSpeedX*deltaTime; 898 | if (Math.abs(delta.y/deltaTime) > maxSpeedY) 899 | delta.y = delta.y/Math.abs(delta.y)*maxSpeedY*deltaTime; 900 | 901 | // Show the scroll bar if the movement is initiated by finger 902 | if ((delta.x || delta.y) && _touchingScreen) 903 | showScrollBars(); 904 | 905 | // Update the scroll rect 906 | var scrollRect : Rectangle = _content.scrollRect.clone(); 907 | scrollRect.x += delta.x; 908 | scrollRect.y += delta.y; 909 | if (delta.x || delta.y) 910 | { 911 | _content.scrollRect = scrollRect; 912 | } 913 | 914 | // Compute the speed and trajectory, or stop the movement if necessary 915 | if (stopAfter) 916 | { 917 | stopMovement(); 918 | } 919 | else 920 | { 921 | // Compute the speed 922 | var currentSpeedX:Number = 0.5*(delta.x/deltaTime+_speed.x); 923 | if (Math.abs(currentSpeedX) < SCROLLING_MIN_SPEED) currentSpeedX = 0; 924 | var currentSpeedY:Number = 0.5*(delta.y/deltaTime+_speed.y); 925 | if (Math.abs(currentSpeedY) < SCROLLING_MIN_SPEED) currentSpeedY = 0; 926 | _speed = new Point(currentSpeedX, currentSpeedY); 927 | 928 | _movementStopped = (_speed.y == 0.0); 929 | 930 | // Save the trajectory 931 | _previousScrollPositions['t-2'] = _previousScrollPositions['t-1']; 932 | _previousScrollPositions['t-1'] = new Point(scrollRect.x, scrollRect.y); 933 | } 934 | 935 | // Update the scroll bars 936 | updateScrollBars(); 937 | 938 | // Send an update event if the position has changed 939 | if (delta.x || delta.y) 940 | { 941 | var event:Event = new Event(SCROLL_POSITION_CHANGE); 942 | dispatchEvent(event); 943 | } 944 | } 945 | 946 | private var _movementStopped:Boolean = false; 947 | 948 | public function get movementStopped():Boolean 949 | { 950 | return _movementStopped; 951 | } 952 | 953 | /** 954 | * Completely stop the movement. 955 | */ 956 | private function stopMovement() : void 957 | { 958 | _movementStopped = true; 959 | _previousScrollPositions['t-2'] = _previousScrollPositions['t-1'] = scrollPosition; 960 | _speed = new Point(); 961 | } 962 | 963 | /** 964 | * The current relative scrolling position. Values (x,y) are: 965 | *
    966 | *
  • negative if bouncing above or left from the content area
  • 967 | *
  • between 0 and 1 if scrolling within the content area
  • 968 | *
  • greater than 1 if bouncing under or right from the content area
  • 969 | *
970 | */ 971 | private function get scrollingRelativePosition() : Point 972 | { 973 | // When the content is smaller than the viewport, we choose arbitrary values 974 | // to obtain the desired behavior. 975 | var result:Point = new Point(); 976 | 977 | if (contentRect.width > _content.scrollRect.width) 978 | result.x = (scrollPosition.x - contentRect.left) / (contentRect.width - _content.scrollRect.width); 979 | else if (scrollPosition.x == contentRect.left) 980 | result.x = 0; 981 | else if (scrollPosition.x < contentRect.left) 982 | result.x = -1; 983 | else 984 | result.x = 2; 985 | 986 | if (contentRect.height > _content.scrollRect.height) 987 | result.y = (scrollPosition.y - contentRect.top) / (contentRect.height - _content.scrollRect.height); 988 | else if (scrollPosition.y == contentRect.top) 989 | result.y = 0; 990 | else if (scrollPosition.y < contentRect.top) 991 | result.y = -1; 992 | else 993 | result.y = 2; 994 | 995 | return result; 996 | } 997 | 998 | /** Indicate if the scrolling position is within the content area. */ 999 | private function get scrollingWithinContentArea() : Boolean 1000 | { 1001 | return scrollingRelativePosition.x >= 0 && scrollingRelativePosition.x <= 1 && scrollingRelativePosition.y >= 0 && scrollingRelativePosition.y <= 1; 1002 | } 1003 | 1004 | /** Indicate if the scrolling position is in the left bouncing area. */ 1005 | private function get bouncingLeftFromContent() : Boolean 1006 | { 1007 | return scrollingRelativePosition.x < 0; 1008 | } 1009 | 1010 | /** Indicate if the scrolling position is in the right bouncing area. */ 1011 | private function get bouncingRightFromContent() : Boolean 1012 | { 1013 | return scrollingRelativePosition.x > 1; 1014 | } 1015 | 1016 | /** Indicate if the scrolling position is in the top bouncing area. */ 1017 | private function get bouncingAboveContent() : Boolean 1018 | { 1019 | return scrollingRelativePosition.y < 0; 1020 | } 1021 | 1022 | /** Indicate if the scrolling position is in the bottom bouncing area. */ 1023 | private function get bouncingUnderContent() : Boolean 1024 | { 1025 | return scrollingRelativePosition.y > 1; 1026 | } 1027 | 1028 | 1029 | // --------------------------------------------------------------------------------------// 1030 | // // 1031 | // PRIVATE EVENT LISTENERS // 1032 | // // 1033 | // --------------------------------------------------------------------------------------// 1034 | 1035 | 1036 | private function onMouseDown( event : MouseEvent ) : void 1037 | { 1038 | // If the scrolling is locked by the paging feature, ignore the touch 1039 | if (_scrollingLockedForPaging) 1040 | { 1041 | if (DEBUG) 1042 | { 1043 | trace('>> onMouseDown - scrollingLockedForPaging ', "returning..."); 1044 | } 1045 | return; 1046 | } 1047 | 1048 | // The user starts touching the screen 1049 | _touchingScreen = true; 1050 | _timeOfLastMouseDown = getTimer(); 1051 | 1052 | // Update finger position 1053 | _previousFingerPosition = new Point(event.stageX, event.stageY); 1054 | _currentFingerPosition = _previousFingerPosition; 1055 | 1056 | // Start listening to touch and frame events 1057 | _container.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove, false, 0, true); 1058 | _container.addEventListener(MouseEvent.MOUSE_OUT, onMouseOut, false, 0, true); 1059 | _container.addEventListener(MouseEvent.MOUSE_UP, onMouseUp, false, 0, true); 1060 | _container.addEventListener(Event.ENTER_FRAME, onEnterFrame, false, 0, true); 1061 | 1062 | // DEBUG INFO 1063 | if (DEBUG) 1064 | { 1065 | trace('>> onMouseDown - _currentFingerPosition = ' + _currentFingerPosition); 1066 | } 1067 | } 1068 | 1069 | private function onMouseUp( event : MouseEvent ) : void 1070 | { 1071 | // The user stops touching the screen 1072 | _touchingScreen = false; 1073 | 1074 | // Stop listening to touch events 1075 | _container.removeEventListener(MouseEvent.MOUSE_MOVE, onMouseMove); 1076 | _container.removeEventListener(MouseEvent.MOUSE_OUT, onMouseOut); 1077 | _container.removeEventListener(MouseEvent.MOUSE_UP, onMouseUp); 1078 | 1079 | // If we get a mouse down and a mouse up between two frames, we need to either stop the scrolling, 1080 | // either update the speed. 1081 | if (_timeOfLastMouseDown > _timeOfLastFrame) 1082 | { 1083 | if (_currentFingerPosition.equals(new Point(event.stageX, event.stageY))) 1084 | { 1085 | stopMovement(); 1086 | } 1087 | else 1088 | { 1089 | onEnterFrame(null); 1090 | } 1091 | } 1092 | 1093 | // If the paging is enabled, now is the time to check if we're in between two pages 1094 | if (pagingEnabled) 1095 | { 1096 | _pendingTouch = false; 1097 | managePaging(); 1098 | } 1099 | 1100 | // DEBUG INFO 1101 | if (DEBUG) 1102 | { 1103 | trace('<< onMouseUp - _currentFingerPosition = ' + _currentFingerPosition); 1104 | } 1105 | } 1106 | 1107 | private function onMouseOut( event : MouseEvent ) : void 1108 | { 1109 | // If it was a mouse out outside of the container, we consider it as a mouse up 1110 | if (!_container.getBounds(_container.stage).contains(event.stageX, event.stageY)) 1111 | onMouseUp(event); 1112 | } 1113 | 1114 | private function onMouseMove( event : MouseEvent) : void 1115 | { 1116 | // Indicate we received a touch and it hasn't been processed yet 1117 | _pendingTouch = true; 1118 | 1119 | // Update finger position 1120 | _currentFingerPosition = new Point(event.stageX, event.stageY); 1121 | 1122 | // DEBUG INFO 1123 | if (DEBUG) 1124 | { 1125 | trace('xx onMouseMove - currentFingerPosition = ' + _currentFingerPosition); 1126 | } 1127 | } 1128 | 1129 | private function onEnterFrame( event : Event ) : void 1130 | { 1131 | // DEBUG INFO 1132 | if (DEBUG) 1133 | { 1134 | trace('-- onEnterFrame | speed = ' + speed); 1135 | } 1136 | 1137 | // If content scroll rect not ready, can't do anything 1138 | if (!_content.scrollRect) 1139 | return; 1140 | 1141 | // Update time information (if deltaTime == 0, we wait for the next frame) 1142 | var deltaTime:Number = getTimer() - _timeOfLastFrame; 1143 | if (deltaTime == 0) return; 1144 | _timeOfLastFrame += deltaTime; 1145 | 1146 | // If a forced animated scrolling was requested, we perform it. 1147 | // Otherwise, if the user is touching the screen, or if we haven't processed the last touch yet, we follow the finger. 1148 | // Otherwise, we let the physics take care of the scrolling. 1149 | if (_forcedAnimatedScrolling) 1150 | manageForcedAnimatedScrolling(deltaTime) 1151 | else if (_touchingScreen || _pendingTouch) 1152 | manageFingerScrolling(deltaTime) 1153 | else 1154 | manageFreeScrolling(deltaTime); 1155 | 1156 | // If we are in the scrolling area, not touching the screen, not forcing an animation, 1157 | // and with no speed, we can stop refreshing the display and fade out the scroll bars. 1158 | if (!_forcedAnimatedScrolling && !_touchingScreen && scrollingWithinContentArea && speed.x == 0 && speed.y == 0) 1159 | { 1160 | _scrollingLockedForPaging = false; 1161 | _container.removeEventListener(Event.ENTER_FRAME, onEnterFrame); 1162 | hideScrollBars(); 1163 | } 1164 | 1165 | // If we are moving, we don't transmit mouse events to children 1166 | _container.mouseChildren = (speed.x == 0 && speed.y == 0); 1167 | 1168 | // DEBUG INFO 1169 | if (DEBUG) 1170 | { 1171 | trace('-- onExitFrame'); 1172 | } 1173 | } 1174 | 1175 | private function onScrollBarFadeOutTimerComplete( event : TimerEvent ) : void 1176 | { 1177 | if(_verticalScrollBar) 1178 | _verticalScrollBar.addEventListener(Event.ENTER_FRAME, fadeOutScrollBar, false, 0, true); 1179 | 1180 | if (_horizontalScrollBar) 1181 | _horizontalScrollBar.addEventListener(Event.ENTER_FRAME, fadeOutScrollBar, false, 0, true); 1182 | }; 1183 | 1184 | /** 1185 | * Scroll to a certain page. 1186 | * 1187 | * This method will take care of translating between a certain page number 1188 | * and the position it has to move across. 1189 | * 1190 | * @param position 1191 | * 1192 | */ 1193 | public function scrollToPage(position:int, animated:Boolean = true):void 1194 | { 1195 | var p : Point; 1196 | if (horizontalScrollingEnabled && !verticalScrollingEnabled) { 1197 | p = new Point( position, currentPage.y ); 1198 | } else if (!horizontalScrollingEnabled && verticalScrollingEnabled) { 1199 | p = new Point( currentPage.x, position ); 1200 | } 1201 | setCurrentPage(p, animated); 1202 | } 1203 | } 1204 | } 1205 | --------------------------------------------------------------------------------