1  /**
     2   * com.sekati.ui.Scroll
     3   * @version 5.2.0
     4   * @author jason m horwitz | sekati.com
     5   * Copyright (c) 2007  jason m horwitz | Sekat LLC. All Rights Reserved.
     6   * Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
     7   */
     8  import com.sekati.core.CoreObject;
     9  import com.sekati.events.FramePulse;
    10  import com.sekati.except.FatalArgumentException;
    11  import com.sekati.external.MouseWheel;
    12  import com.sekati.ui.IScrollable;
    13  import com.sekati.utils.Delegate;
    14  
    15  import mx.transitions.easing.Strong;
    16  import mx.transitions.Tween;
    17  
    18  /**
    19   * Scroll handles mouseWheel (PC & Mac), dynamic resizing of content, external size tracking for accordian 
    20   * style content scrolling, slideContent method, modal ui states, proportional bar, gutter and more. 
    21   * 
    22   * {@code Usage:
    23   *  var _scroll:Scroll = Scroll("_y", contentMc, maskMc, gutterMc, barMc, true, true, true, true);
    24   *  var _scroll:Scroll = Scroll ("_y", contentMc, maskMc, gutter, bar, true, true, true, true, contentMc, .8, .5);
    25   * }
    26   */
    27  class com.sekati.ui.Scroll extends CoreObject implements IScrollable {
    28  
    29  	private var _this:Scroll;
    30  	private var _axis:String;
    31  	private var _prop:String;
    32  	private var _content:Object;
    33  	private var _mask:MovieClip;	
    34  	private var _gutter:MovieClip;
    35  	private var _bar:MovieClip;
    36  	private var _isDrag:Boolean;
    37  	private var _isResizeGutter:Boolean;
    38  	private var _isResizeBar:Boolean;
    39  	private var _isMouseWheel:Boolean;
    40  	private var _contentSizeTracker:Object;
    41  	private var _contentTop:Number;
    42  	private var _contentBot:Number;
    43  	private var _gutterTop:Number;
    44  	private var _gutterBot:Number;
    45  	private var _oldPos:Number;
    46  	private var _newPos:Number;
    47  	private var _friction:Number;
    48  	private var _ratio:Number;
    49  	private var _speed:Number;
    50  	private var _slide:Tween;
    51  
    52  	/**
    53  	 * Constructor
    54  	 * @param axis (String) axis the scroller will scroll on: must be "_x" or "_y"
    55  	 * @param content (Object) content object
    56  	 * @param mask (MovieClip) content mask
    57  	 * @param gutter (MovieClip) gutter object
    58  	 * @param bar (MovieClip) scroller bar object
    59  	 * @param isResizeGutter (Boolean) optional resize gutter to content mask
    60  	 * @param isResizeBar (Boolean) optional resize bar proportional to content
    61  	 * @param isMouseWheel (Boolean) optional enable mouseWheel scrolling (one may choose to use 2 instances of Scroll for hscroll & vscroll and only want vscroll in which case mouseWheel should only be enabled for one instance)
    62  	 * @param isInit (Boolean) optional init scroller upon instantiation, else {@link #init()} must be called manually
    63  	 * @param contentSizeTracker (Object) optional object used to track content size. Useful in accordian content where visibility or masking is used to hide partial content [default: content]
    64  	 * @param friction (Number) optional scroll motion friction [default 0.8] 
    65  	 * @param ratio (Number) optional scroll motion ratio [default 0.5]
    66  	 * @return Void
    67  	 * @throws Error if axis is not "_x" or "_y" & returns without proper instantiation
    68  	 * {@code Usage:
    69  	 * 	var scroll:Scroll = new Scroll("_x", contentMc, maskMc, gutterMc, barMc, true, true, true, true, contentMc, 0.7, 0.4);
    70  	 * }
    71  	 */
    72  	public function Scroll(axis:String, content:Object, mask:MovieClip, gutter:MovieClip, bar:MovieClip, isResizeGutter:Boolean, isResizeBar:Boolean, isMouseWheel:Boolean, isInit:Boolean, contentSizeTracker:Object, friction:Number, ratio:Number) {
    73  		super( );
    74  		// scrolling axis is critical - throw an error and force breakage to gain attention
    75  		if (axis != "_x" && axis != "_y") {
    76  			//throw new Error ("@@@ com.sekati.ui.Scroll Error: constructor expects axis param: '_x' or '_y'.");
    77  			throw new FatalArgumentException( this, "Scroll Constructor expects axis param: '_x' or '_y'.", arguments );
    78  			return;
    79  		}
    80  		// user defined                                 
    81  		_this = this;
    82  		_axis = axis;
    83  		_content = content;
    84  		_gutter = gutter;
    85  		_bar = bar;
    86  		_mask = mask;
    87  		_contentSizeTracker = (!contentSizeTracker) ? content : contentSizeTracker;
    88  		_isResizeBar = (!isResizeBar) ? false : true;
    89  		_isResizeGutter = (!isResizeGutter) ? false : true;
    90  		_isMouseWheel = (!isMouseWheel) ? false : true;
    91  		_friction = (friction) ? friction : 0.8;
    92  		_ratio = (ratio) ? ratio : 0.5;
    93  		// class defined
    94  		_speed = 0;		
    95  		_prop = (_axis == "_y") ? "_height" : "_width";
    96  		_isDrag = false;
    97  		
    98  		// auto init on request ...
    99  		if (isInit) {
   100  			init( );
   101  		}
   102  	}
   103  
   104  	/**
   105  	 * Initialize scroll behavior.
   106  	 * @return Void
   107  	 */
   108  	public function init():Void {
   109  		// define confines, mouseWheel, and set scroller to rollout color state
   110  		setConfines( );
   111  		setMouseWheel( );
   112  		// define event handlers
   113  		_bar.onPress = Delegate.create( _this, bar_onPress );
   114  		_bar.onRelease = _bar.onReleaseOutside = Delegate.create( _this, bar_onRelease );
   115  		_gutter.onPress = Delegate.create( _this, gutter_onPress );
   116  		// begin _onEnterFrame
   117  		FramePulse.getInstance( ).addFrameListener( _this );
   118  	}
   119  
   120  	/**
   121  	 * Tween content to position, scroller will reposition accordingly.
   122  	 * @param pos (Number) position to slide content on axis
   123  	 * @param sec (Number) optional tween duration in seconds [default: 0.5]
   124  	 * @return Void
   125  	 */
   126  	public function slideContent(pos:Number, sec:Number):Void {
   127  		var newScrollPos:Number = (_contentTop - pos) * (_gutterBot - _gutterTop) / (_gutterBot - _contentBot + _bar[_prop]) + _gutterTop;
   128  		slideScroller( newScrollPos, sec );
   129  	}
   130  
   131  	/**
   132  	 * Tween bar to position, content will reposition accordingly.
   133  	 * @param pos (Number) position to slide scroller on axis
   134  	 * @param sec (Number) optional tween duration in seconds [default: 0.5]
   135  	 * @return Void
   136  	 */
   137  	public function slideScroller(pos:Number, sec:Number):Void {
   138  		stopScroller( );
   139  		updateConfines( );
   140  		var s:Number = (!sec) ? 0.5 : sec;
   141  		_slide = new Tween( _bar, _axis, Strong.easeOut, _bar[_axis], resolveScrollerPos( pos ), s, true );
   142  	}
   143  
   144  	/**
   145  	 * Move the scroller bar by a certain amount
   146  	 * @param amount (Number) pixels to move: positive or negative
   147  	 * @return Void
   148  	 * {@code
   149  	 * 	_myScroll.moveScroll( -100 );
   150  	 * }
   151  	 */
   152  	public function moveScroller(amount:Number):Void {
   153  		stopScroller( );
   154  		updateConfines( );
   155  		_bar._y = resolveScrollerPos( _bar._y + amount );
   156  	}	
   157  
   158  	/**
   159  	 * Check if content is scrollable.
   160  	 * @return Boolean
   161  	 */
   162  	public function isScrollable():Boolean {
   163  		// check if we need a scroller at all
   164  		var _isScrollable:Boolean = (_content[_axis] <= _mask[_axis] && _contentSizeTracker[_prop] < _mask[_prop]) ? false : true;
   165  		return _isScrollable;
   166  	}
   167  
   168  	/**
   169  	 * Check if bar is being dragged.
   170  	 * @return Boolean
   171  	 */
   172  	public function isDragging():Boolean {
   173  		return _isDrag;	
   174  	}
   175  
   176  	// MOUSE WHEEL HANDLER
   177  	
   178  	/**
   179  	 * Check if Mouse is in scrollable area.
   180  	 * @return Boolean
   181  	 */
   182  	public function isMouseInArea():Boolean {
   183  		var x:Number = _content._parent._xmouse;
   184  		var y:Number = _content._parent._ymouse;
   185  		return (x >= 0 && x <= _mask._width && y >= 0 && y <= _mask._height);
   186  	}
   187  
   188  	private function setMouseWheel():Void {
   189  		if (_isMouseWheel) {
   190  			//deactivate mousewheel on textfield & activate mouse listener
   191  			_content.mouseWheelEnabled = false;
   192  			// setup Mac/PC MouseWheel support
   193  			MouseWheel.init( _this );
   194  		}
   195  	}
   196  
   197  	private function onMouseWheel(delta:Number):Void {
   198  		if (isMouseInArea( ) && isScrollable( )) {
   199  			var m:Number = _bar[_axis] - ((_gutter[_prop] / 5) * (delta / 3));
   200  			slideScroller( m );
   201  		}
   202  	}
   203  
   204  	// TOOLS
   205  	private function setConfines():Void {
   206  		_contentTop = _content[_axis];
   207  		_gutterTop = _gutter[_axis];
   208  		_gutterBot = _gutter[_axis] + _gutter[_prop];
   209  		if (!_isResizeBar) {
   210  			_gutterBot -= _bar[_prop];
   211  		}
   212  		// scale gutter to content mask
   213  		_gutter[_prop] = (_isResizeGutter) ? _mask[_prop] : _gutter[_prop];
   214  		// set content related confines (isloated since may need to be called on size change)
   215  		updateConfines( );
   216  	}
   217  
   218  	private function updateConfines():Void {
   219  		_contentBot = _contentTop + _contentSizeTracker[_prop];
   220  		//scale scroller bar in proportion to the content to scroll
   221  		if (_isResizeBar) {
   222  			
   223  			// new proportioning
   224  			var percent:Number = Math.ceil( (_mask[_prop] / _contentSizeTracker[_prop]) * 100 );			
   225  			_bar[_prop] = _gutter[_prop] * percent / 100;
   226  			
   227  			//get new gutterBot with new size of scrollerbar                               
   228  			_gutterBot = _gutter[_axis] + _gutter[_prop] - _bar[_prop];
   229  		}
   230  	}
   231  
   232  	private function resolveScrollerPos(pos:Number):Number {
   233  		//makes sure scroller moves within boundaries
   234  		return Math.max( Math.min( pos, _gutterBot ), _gutterTop );
   235  	}
   236  
   237  	private function stopScroller():Void {
   238  		_speed = 0;
   239  		_slide.stop( );
   240  	}
   241  
   242  	// UI EVENT HANDLERS
   243  	private function _onEnterFrame():Void {
   244  		_bar._visible = isScrollable( );
   245  		// height changed, adjust scroller
   246  		if (_contentBot != _contentTop + _contentSizeTracker[_prop]) {
   247  			_contentBot = _contentTop + _contentSizeTracker[_prop];
   248  			updateConfines( );
   249  			_bar[_axis] = resolveScrollerPos( (_contentTop + _content[_axis]) * (_gutterBot - _gutterTop) / (_gutterBot - _contentBot + _bar[_prop]) + _gutterTop );
   250  		}
   251  		//                                                                                                   
   252  		if (!_isDrag) {
   253  			_oldPos = _bar[_axis];
   254  			_newPos = _oldPos + _speed;
   255  			if (_newPos > _gutterBot || _newPos < _gutterTop) {
   256  				//bounce - reverse movement
   257  				_speed = -_speed;
   258  				_newPos = _oldPos;
   259  			}
   260  			_speed = Math.round( _speed * _friction * 100 ) / 100;
   261  		} else {
   262  			_oldPos = _newPos;
   263  			_newPos = _bar[_axis];
   264  		}
   265  		//update scroller
   266  		_bar[_axis] = _newPos;
   267  		//update content
   268  		var percent:Number = (_bar[_axis] - _gutterTop) / (_gutterBot - _gutterTop);
   269  		_content[_axis] = Math.round( (percent * (_gutterBot - _contentBot + _bar[_prop])) + _contentTop );
   270  		//weird throw bug fix - gets stuck on speed 0.05 when scroller is thrown upwards and decelerates
   271  		if (_speed < 0.1 && _speed > -0.1) {
   272  			_speed = 0;
   273  		}
   274  	}
   275  
   276  	private function bar_onPress():Void {
   277  		stopScroller( );
   278  		_isDrag = true;
   279  		if(_axis == "_y") {
   280  			_bar.startDrag( false, _bar._x, _gutterTop, _bar._x, _gutterBot );
   281  		} else {
   282  			_bar.startDrag( false, _gutterTop, _bar._y, _gutterBot, _bar._y );
   283  		}
   284  	}
   285  
   286  	private function bar_onRelease():Void {
   287  		_bar.stopDrag( );
   288  		_isDrag = false;
   289  		//throw
   290  		_speed = (_newPos - _oldPos) * _ratio;
   291  	}
   292  
   293  	private function gutter_onPress():Void {
   294  		if (isScrollable( )) {
   295  			_gutter.useHandCursor = true;
   296  			var mousePos:Number = (_axis == "_y") ? _gutter._parent._ymouse : _gutter._parent._xmouse;
   297  			slideScroller( mousePos - (_bar[_prop] / 2) );
   298  		} else {
   299  			_gutter.useHandCursor = false;
   300  		}
   301  	}
   302  
   303  	/**
   304  	 * Destroy the Scroll.
   305  	 * @return Void
   306  	 */
   307  	public function destroy():Void {
   308  		FramePulse.getInstance( ).removeFrameListener( _this );
   309  		super.destroy( );
   310  	}	
   311  }