/** * com.sekati.ui.Scroll * @version 5.2.0 * @author jason m horwitz | sekati.com * Copyright (c) 2007 jason m horwitz | Sekat LLC. All Rights Reserved. * Released under the MIT License: http://www.opensource.org/licenses/mit-license.php */ import com.sekati.core.CoreObject; import com.sekati.events.FramePulse; import com.sekati.except.FatalArgumentException; import com.sekati.external.MouseWheel; import com.sekati.ui.IScrollable; import com.sekati.utils.Delegate; import mx.transitions.easing.Strong; import mx.transitions.Tween; /** * Scroll handles mouseWheel (PC & Mac), dynamic resizing of content, external size tracking for accordian * style content scrolling, slideContent method, modal ui states, proportional bar, gutter and more. * * {@code Usage: * var _scroll:Scroll = Scroll("_y", contentMc, maskMc, gutterMc, barMc, true, true, true, true); * var _scroll:Scroll = Scroll ("_y", contentMc, maskMc, gutter, bar, true, true, true, true, contentMc, .8, .5); * } */ class com.sekati.ui.Scroll extends CoreObject implements IScrollable { private var _this:Scroll; private var _axis:String; private var _prop:String; private var _content:Object; private var _mask:MovieClip; private var _gutter:MovieClip; private var _bar:MovieClip; private var _isDrag:Boolean; private var _isResizeGutter:Boolean; private var _isResizeBar:Boolean; private var _isMouseWheel:Boolean; private var _contentSizeTracker:Object; private var _contentTop:Number; private var _contentBot:Number; private var _gutterTop:Number; private var _gutterBot:Number; private var _oldPos:Number; private var _newPos:Number; private var _friction:Number; private var _ratio:Number; private var _speed:Number; private var _slide:Tween; /** * Constructor * @param axis (String) axis the scroller will scroll on: must be "_x" or "_y" * @param content (Object) content object * @param mask (MovieClip) content mask * @param gutter (MovieClip) gutter object * @param bar (MovieClip) scroller bar object * @param isResizeGutter (Boolean) optional resize gutter to content mask * @param isResizeBar (Boolean) optional resize bar proportional to content * @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) * @param isInit (Boolean) optional init scroller upon instantiation, else {@link #init()} must be called manually * @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] * @param friction (Number) optional scroll motion friction [default 0.8] * @param ratio (Number) optional scroll motion ratio [default 0.5] * @return Void * @throws Error if axis is not "_x" or "_y" & returns without proper instantiation * {@code Usage: * var scroll:Scroll = new Scroll("_x", contentMc, maskMc, gutterMc, barMc, true, true, true, true, contentMc, 0.7, 0.4); * } */ 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) { super( ); // scrolling axis is critical - throw an error and force breakage to gain attention if (axis != "_x" && axis != "_y") { //throw new Error ("@@@ com.sekati.ui.Scroll Error: constructor expects axis param: '_x' or '_y'."); throw new FatalArgumentException( this, "Scroll Constructor expects axis param: '_x' or '_y'.", arguments ); return; } // user defined _this = this; _axis = axis; _content = content; _gutter = gutter; _bar = bar; _mask = mask; _contentSizeTracker = (!contentSizeTracker) ? content : contentSizeTracker; _isResizeBar = (!isResizeBar) ? false : true; _isResizeGutter = (!isResizeGutter) ? false : true; _isMouseWheel = (!isMouseWheel) ? false : true; _friction = (friction) ? friction : 0.8; _ratio = (ratio) ? ratio : 0.5; // class defined _speed = 0; _prop = (_axis == "_y") ? "_height" : "_width"; _isDrag = false; // auto init on request ... if (isInit) { init( ); } } /** * Initialize scroll behavior. * @return Void */ public function init():Void { // define confines, mouseWheel, and set scroller to rollout color state setConfines( ); setMouseWheel( ); // define event handlers _bar.onPress = Delegate.create( _this, bar_onPress ); _bar.onRelease = _bar.onReleaseOutside = Delegate.create( _this, bar_onRelease ); _gutter.onPress = Delegate.create( _this, gutter_onPress ); // begin _onEnterFrame FramePulse.getInstance( ).addFrameListener( _this ); } /** * Tween content to position, scroller will reposition accordingly. * @param pos (Number) position to slide content on axis * @param sec (Number) optional tween duration in seconds [default: 0.5] * @return Void */ public function slideContent(pos:Number, sec:Number):Void { var newScrollPos:Number = (_contentTop - pos) * (_gutterBot - _gutterTop) / (_gutterBot - _contentBot + _bar[_prop]) + _gutterTop; slideScroller( newScrollPos, sec ); } /** * Tween bar to position, content will reposition accordingly. * @param pos (Number) position to slide scroller on axis * @param sec (Number) optional tween duration in seconds [default: 0.5] * @return Void */ public function slideScroller(pos:Number, sec:Number):Void { stopScroller( ); updateConfines( ); var s:Number = (!sec) ? 0.5 : sec; _slide = new Tween( _bar, _axis, Strong.easeOut, _bar[_axis], resolveScrollerPos( pos ), s, true ); } /** * Move the scroller bar by a certain amount * @param amount (Number) pixels to move: positive or negative * @return Void * {@code * _myScroll.moveScroll( -100 ); * } */ public function moveScroller(amount:Number):Void { stopScroller( ); updateConfines( ); _bar._y = resolveScrollerPos( _bar._y + amount ); } /** * Check if content is scrollable. * @return Boolean */ public function isScrollable():Boolean { // check if we need a scroller at all var _isScrollable:Boolean = (_content[_axis] <= _mask[_axis] && _contentSizeTracker[_prop] < _mask[_prop]) ? false : true; return _isScrollable; } /** * Check if bar is being dragged. * @return Boolean */ public function isDragging():Boolean { return _isDrag; } // MOUSE WHEEL HANDLER /** * Check if Mouse is in scrollable area. * @return Boolean */ public function isMouseInArea():Boolean { var x:Number = _content._parent._xmouse; var y:Number = _content._parent._ymouse; return (x >= 0 && x <= _mask._width && y >= 0 && y <= _mask._height); } private function setMouseWheel():Void { if (_isMouseWheel) { //deactivate mousewheel on textfield & activate mouse listener _content.mouseWheelEnabled = false; // setup Mac/PC MouseWheel support MouseWheel.init( _this ); } } private function onMouseWheel(delta:Number):Void { if (isMouseInArea( ) && isScrollable( )) { var m:Number = _bar[_axis] - ((_gutter[_prop] / 5) * (delta / 3)); slideScroller( m ); } } // TOOLS private function setConfines():Void { _contentTop = _content[_axis]; _gutterTop = _gutter[_axis]; _gutterBot = _gutter[_axis] + _gutter[_prop]; if (!_isResizeBar) { _gutterBot -= _bar[_prop]; } // scale gutter to content mask _gutter[_prop] = (_isResizeGutter) ? _mask[_prop] : _gutter[_prop]; // set content related confines (isloated since may need to be called on size change) updateConfines( ); } private function updateConfines():Void { _contentBot = _contentTop + _contentSizeTracker[_prop]; //scale scroller bar in proportion to the content to scroll if (_isResizeBar) { // new proportioning var percent:Number = Math.ceil( (_mask[_prop] / _contentSizeTracker[_prop]) * 100 ); _bar[_prop] = _gutter[_prop] * percent / 100; //get new gutterBot with new size of scrollerbar _gutterBot = _gutter[_axis] + _gutter[_prop] - _bar[_prop]; } } private function resolveScrollerPos(pos:Number):Number { //makes sure scroller moves within boundaries return Math.max( Math.min( pos, _gutterBot ), _gutterTop ); } private function stopScroller():Void { _speed = 0; _slide.stop( ); } // UI EVENT HANDLERS private function _onEnterFrame():Void { _bar._visible = isScrollable( ); // height changed, adjust scroller if (_contentBot != _contentTop + _contentSizeTracker[_prop]) { _contentBot = _contentTop + _contentSizeTracker[_prop]; updateConfines( ); _bar[_axis] = resolveScrollerPos( (_contentTop + _content[_axis]) * (_gutterBot - _gutterTop) / (_gutterBot - _contentBot + _bar[_prop]) + _gutterTop ); } // if (!_isDrag) { _oldPos = _bar[_axis]; _newPos = _oldPos + _speed; if (_newPos > _gutterBot || _newPos < _gutterTop) { //bounce - reverse movement _speed = -_speed; _newPos = _oldPos; } _speed = Math.round( _speed * _friction * 100 ) / 100; } else { _oldPos = _newPos; _newPos = _bar[_axis]; } //update scroller _bar[_axis] = _newPos; //update content var percent:Number = (_bar[_axis] - _gutterTop) / (_gutterBot - _gutterTop); _content[_axis] = Math.round( (percent * (_gutterBot - _contentBot + _bar[_prop])) + _contentTop ); //weird throw bug fix - gets stuck on speed 0.05 when scroller is thrown upwards and decelerates if (_speed < 0.1 && _speed > -0.1) { _speed = 0; } } private function bar_onPress():Void { stopScroller( ); _isDrag = true; if(_axis == "_y") { _bar.startDrag( false, _bar._x, _gutterTop, _bar._x, _gutterBot ); } else { _bar.startDrag( false, _gutterTop, _bar._y, _gutterBot, _bar._y ); } } private function bar_onRelease():Void { _bar.stopDrag( ); _isDrag = false; //throw _speed = (_newPos - _oldPos) * _ratio; } private function gutter_onPress():Void { if (isScrollable( )) { _gutter.useHandCursor = true; var mousePos:Number = (_axis == "_y") ? _gutter._parent._ymouse : _gutter._parent._xmouse; slideScroller( mousePos - (_bar[_prop] / 2) ); } else { _gutter.useHandCursor = false; } } /** * Destroy the Scroll. * @return Void */ public function destroy():Void { FramePulse.getInstance( ).removeFrameListener( _this ); super.destroy( ); } }