3 function X_UI_ScrollBox_momentum( current, start, time, lowerMargin, wrapperSize, deceleration ){
\r
4 var distance = current - start,
\r
5 speed = Math.abs( distance ) / time,
\r
9 deceleration = deceleration === undefined ? 0.0006 : deceleration;
\r
11 destination = current + ( speed * speed ) / ( 2 * deceleration ) * ( distance < 0 ? -1 : 1 );
\r
12 duration = speed / deceleration;
\r
14 if( destination < lowerMargin ){
\r
15 destination = wrapperSize ? lowerMargin - ( wrapperSize / 2.5 * ( speed / 8 ) ) : lowerMargin;
\r
16 distance = Math.abs( destination - current );
\r
17 duration = distance / speed;
\r
19 if ( destination > 0 ) {
\r
20 destination = wrapperSize ? wrapperSize / 2.5 * ( speed / 8 ) : 0;
\r
21 distance = Math.abs( current ) + destination;
\r
22 duration = distance / speed;
\r
26 destination : Math.round( destination ),
\r
31 var X_UI_ScrollBox_SUPPORT_ATTRS = {
\r
32 // スクロール開始するために必要な移動距離、縦か横、どちらか制限する場合、より重要
\r
33 directionLockThreshold : [ 10, X.UI.Dirty.CLEAN, X.UI.Attr.USER.UINODE, X.UI.Attr.Type.LENGTH ],
\r
34 scrollXEnabled : [ true, X.UI.Dirty.CLEAN, X.UI.Attr.USER.UINODE, X.UI.Attr.Type.BOOLEAN ],
\r
35 scrollYEnabled : [ true, X.UI.Dirty.CLEAN, X.UI.Attr.USER.UINODE, X.UI.Attr.Type.BOOLEAN ],
\r
36 scrollEnabled : [ true, X.UI.Dirty.CLEAN, X.UI.Attr.USER.UINODE, X.UI.Attr.Type.BOOLEAN ],
\r
37 bounceEnabled : [ true, X.UI.Dirty.CLEAN, X.UI.Attr.USER.UINODE, X.UI.Attr.Type.BOOLEAN ],
\r
38 bounceTime : [ 600, X.UI.Dirty.CLEAN, X.UI.Attr.USER.UINODE, X.UI.Attr.Type.TIME ],
\r
39 useWheel : [ true, X.UI.Dirty.CLEAN, X.UI.Attr.USER.UINODE, X.UI.Attr.Type.BOOLEAN ],
\r
40 useKey : [ true, X.UI.Dirty.CLEAN, X.UI.Attr.USER.UINODE, X.UI.Attr.Type.BOOLEAN ],
\r
41 hasScrollShadow : [ true, X.UI.Dirty.CLEAN, X.UI.Attr.USER.UINODE, X.UI.Attr.Type.BOOLEAN ],
\r
42 scrollShadowColor : [ '#000', X.UI.Dirty.CLEAN, X.UI.Attr.USER.UINODE, X.UI.Attr.Type.COLOR ]
\r
46 X.UI._ScrollBox = X.UI._ChromeBox.inherits(
\r
48 X_Class.PRIVATE_DATA | X_Class.SUPER_ACCESS,
\r
51 //elmScroller : null,
\r
52 //elmScrollbar : null,
\r
54 directionLockThreshold : 10,
\r
55 scrollXEnabled : true,
\r
56 scrollYEnabled : true,
\r
57 scrollEnabled : true,
\r
58 bounceEnabled : true,
\r
59 momentumEnabled : true,
\r
63 hasScrollShadow : true,
\r
64 scrollShadowColor : '#000',
\r
66 supportAttrs : XUI_Attr_createAttrDef( X.UI.Attr.Support, X_UI_ScrollBox_SUPPORT_ATTRS ),
\r
72 directionLocked : '',
\r
75 isAnimating : false,
\r
76 isInTransition : false,
\r
89 maxScrollX : 0, // px
\r
90 maxScrollY : 0, // px
\r
93 absStartX : 0, // px
\r
94 absStartY : 0, // px
\r
99 directionX : 0, // px
\r
100 directionY : 0, // px
\r
101 scrollXPercent : 0, // この値は scroll 不要になっても保持される。 scroll 必要時に参照される
\r
102 scrollYPercent : 0,
\r
104 lastScrollWidth : 0,
\r
105 lastScrollHeight : 0,
\r
109 _containerNode : null,
\r
110 scrollManager : null,
\r
112 Constructor : function( layout, args ){
\r
113 this[ 'Super' ]( layout, args );
\r
114 this._containerNode = X_Class_getPrivate( this.containerNode );
\r
115 this._containerXNode = this._containerNode.xnode[ 'className' ]( 'ScrollSlider' ).listen( X_EVENT_ANIME_END, this, X_UI_ScrollBox_onAnimeEnd );
\r
116 this.xnode[ 'className' ]( 'ScrollBox' );
\r
119 creationComplete : function(){
\r
120 X.UI._Box.prototype.creationComplete.apply( this, arguments );
\r
121 this.scrollManager;
\r
124 calculate : function(){
\r
125 this.lastScrollWidth = this._containerNode.boxWidth;
\r
126 this.lastScrollHeight = this._containerNode.boxHeight;
\r
127 this.lastBoxWidth = this.boxWidth;
\r
128 this.lastBoxHeight = this.boxHeight;
\r
130 X.UI._Box.prototype.calculate.apply( this, arguments );
\r
132 // TODO root の layout_complete 後に。
\r
133 // TODO calculate 前に scroll の解放。
\r
136 this.lastScrollWidth !== this._containerNode.boxWidth ||
\r
137 this.lastScrollHeight !== this._containerNode.boxHeight ||
\r
138 this.lastBoxWidth !== this.boxWidth || this.lastBoxHeight !== this.boxHeight
\r
140 X_UI_rootData[ 'listenOnce' ]( X.UI.Event.LAYOUT_COMPLETE, this, X_UI_ScrollBox_onLayoutComplete );
\r
144 scrollTo : function( x, y, opt_time, opt_easing, opt_release ){
\r
145 //if( this.scrollX === x && this.scrollY === y ) return;
\r
147 opt_time = 0 <= opt_time ? opt_time : 0;
\r
148 opt_easing = opt_easing || 'circular';
\r
149 opt_release = 0 <= opt_release ? opt_release : 300;
\r
151 this.isInTransition = 0 < opt_time;
\r
153 X_UI_ScrollBox_translate( this, x, y, opt_time, opt_easing, opt_release );
\r
156 _remove : function(){
\r
157 X.UI._AbstractUINode.prototype._remove.apply( this, arguments );
\r
158 if( this.scrolling ){
\r
166 function X_UI_ScrollBox_onLayoutBefore( e ){
\r
167 //this._containerXNode.stop();
\r
170 function X_UI_ScrollBox_onLayoutComplete( e ){
\r
171 // scroll の停止、GPU の解除
\r
172 var font = this.fontSize = this._containerXNode.call( 'fontSize' );
\r
174 this.maxScrollX = ( this.boxWidth - this._containerNode.boxWidth ) * font;
\r
175 this.maxScrollY = ( this.boxHeight - this._containerNode.boxHeight ) * font;
\r
177 this.hasHScroll = this.scrollXEnabled && this.maxScrollX < 0;
\r
178 this.hasVScroll = this.scrollYEnabled && this.maxScrollY < 0;
\r
180 if( !this.hasHScroll ){
\r
181 this.maxScrollX = 0;
\r
182 //this.scrollWidth = this.boxWidth;
\r
185 if( !this.hasVScroll ){
\r
186 this.maxScrollY = 0;
\r
187 //this.scrollHeight = this.boxHeight;
\r
190 delete this.endTime;
\r
191 delete this.directionX;
\r
192 delete this.directionY;
\r
194 //this.wrapperOffset = this.xnodeWrapper[ 'offset' ]();
\r
196 //this[ 'dispatch' ]('refresh');
\r
198 X_UI_ScrollBox_resetPosition( this, 0 );
\r
200 if( this.hasHScroll || this.hasVScroll ){
\r
202 if( this.scrolling ){
\r
203 this.scrollTo( this.maxScrollX * this.scrollXPercent, this.maxScrollY * this.scrollYPercent, 100 );
\r
206 this[ 'listen' ]( X.UI.Event._POINTER_DOWN, this, X_UI_ScrollBox_onStart );
\r
207 X_UI_rootData[ 'listen' ]( X.UI.Event.LAYOUT_BEFORE, this, X_UI_ScrollBox_onLayoutBefore );
\r
209 this.scrollTo( this.maxScrollX * this.scrollXPercent, this.maxScrollY * this.scrollYPercent );
\r
210 this.scrolling = true;
\r
214 if( this.scrolling ){
\r
216 this[ 'unlisten' ]( X.UI.Event._POINTER_DOWN, this, X_UI_ScrollBox_onStart );
\r
217 X_UI_rootData[ 'unlisten' ]( X.UI.Event.LAYOUT_BEFORE, this, X_UI_ScrollBox_onLayoutBefore );
\r
219 ( this.scrollX !== 0 || this.scrollY !== 0 ) && this.scrollTo( 0, 0 );
\r
221 delete this.scrolling;
\r
225 function X_UI_ScrollBox_translate( that, x, y, opt_time, opt_easing, opt_release ){
\r
227 opt_time = 0 <= opt_time ? opt_time : 0;
\r
228 opt_easing = opt_easing === '' ? '' : opt_easing || 'circular';
\r
229 opt_release = 0 <= opt_release ? opt_release : 300;
\r
231 that._containerXNode.animate(
\r
240 opt_time, opt_easing, opt_release
\r
246 if( that.indicators ){
\r
247 for( i = that.indicators.length; i--; ){
\r
248 that.indicators[ i ].updatePosition();
\r
253 function X_UI_ScrollBox_onStart( e ){
\r
254 var ret = X_Callback_NONE;
\r
256 // React to left mouse button only
\r
257 if( e.pointerType === 'mouse' && e.button !== 0 ){
\r
261 if( !this.scrollEnabled || ( this.initiated && e.pointerType !== this.initiated ) ){
\r
265 this.initiated = e.pointerType;
\r
266 this.moved = false;
\r
269 this.directionX = 0;
\r
270 this.directionY = 0;
\r
271 this.directionLocked = '';
\r
272 this.startTime = X_Timer_now();
\r
275 if( this.isInTransition || this.isAnimating ){
\r
276 this.isInTransition = this.isAnimating = false;
\r
277 this[ 'dispatch' ]( X.UI.Event.SCROLL_END );
\r
280 this.startX = this.scrollX;
\r
281 this.startY = this.scrollY;
\r
282 this.absStartX = this.scrollX;
\r
283 this.absStartY = this.scrollY;
\r
284 this.pointX = e.pageX;
\r
285 this.pointY = e.pageY;
\r
287 this[ 'listen' ]( X.UI.Event._POINTER_MOVE, this, X_UI_ScrollBox_onMove );
\r
288 this[ 'listen' ]( [ X.UI.Event._POINTER_UP, X.UI.Event._POINTER_CANCEL ], this, X_UI_ScrollBox_onEnd );
\r
290 return ret | X_Callback_PREVENT_DEFAULT;
\r
293 function X_UI_ScrollBox_onMove( e ){
\r
294 var ret = X_Callback_NONE,
\r
295 deltaX, deltaY, timestamp,
\r
297 absDistX, absDistY;
\r
298 // 規定以上の move でスクロール開始
\r
300 if( !this.scrollEnabled || e.pointerType !== this.initiated ){
\r
304 if( e.buttons !== 1 ){
\r
305 return X_UI_ScrollBox_onEnd.call( this, e );
\r
309 if( !this._containerXNode[ '_anime' ] ){
\r
310 //console.log( 'gpuレイヤーの用意 ' + e.pageY );
\r
311 //console.log( 'mov1 x:' + this.scrollX + ' y:' + this.scrollY );
\r
312 X_UI_ScrollBox_translate( this, this.scrollX, this.scrollY, 0, '', 300 );
\r
316 deltaX = e.pageX - this.pointX;
\r
317 deltaY = e.pageY - this.pointY;
\r
318 timestamp = X_Timer_now();
\r
320 this.pointX = e.pageX;
\r
321 this.pointY = e.pageY;
\r
323 this.distX += deltaX;
\r
324 this.distY += deltaY;
\r
325 absDistX = Math.abs(this.distX);
\r
326 absDistY = Math.abs(this.distY);
\r
328 // We need to move at least 10 pixels for the scrolling to initiate
\r
329 if( 300 < timestamp - this.endTime && ( absDistX < 10 && absDistY < 10 ) ){
\r
333 // If you are scrolling in one direction lock the other
\r
334 if( !this.directionLocked ){
\r
335 if( absDistX > absDistY + this.directionLockThreshold ){
\r
336 this.directionLocked = 'h'; // lock horizontally
\r
338 if( absDistY >= absDistX + this.directionLockThreshold ){
\r
339 this.directionLocked = 'v'; // lock vertically
\r
341 this.directionLocked = 'n'; // no lock
\r
345 if( this.directionLocked === 'h' ){
\r
348 if( this.directionLocked === 'v' ){
\r
352 deltaX = this.hasHScroll ? deltaX : 0;
\r
353 deltaY = this.hasVScroll ? deltaY : 0;
\r
356 this[ 'dispatch' ]( X.UI.Event.SCROLL_BEFORE_MOVE );
\r
358 this.minusX = deltaX;
\r
359 this.minusY = deltaY;
\r
361 this[ 'dispatch' ]( X.UI.Event.SCROLL_MOVE );
\r
364 newX = this.scrollX + deltaX;// - this.minusX;
\r
365 newY = this.scrollY + deltaY;// - this.minusY;
\r
367 // Slow down if outside of the boundaries
\r
368 if( 0 < newX || newX < this.maxScrollX ){
\r
369 newX = this.bounceEnabled ? this.scrollX + ( deltaX ) / 3 : 0 < newX ? 0 : this.maxScrollX;
\r
372 if( 0 < newY || newY < this.maxScrollY ){
\r
373 //console.log( 'slow... ' + newY + ' ' + this.maxScrollY );
\r
374 newY = this.bounceEnabled ? this.scrollY + ( deltaY ) / 3 : 0 < newY ? 0 : this.maxScrollY;
\r
377 this.directionX = 0 < deltaX ? -1 : deltaX < 0 ? 1 : 0;
\r
378 this.directionY = 0 < deltaY ? -1 : deltaY < 0 ? 1 : 0;
\r
380 //console.log( 'mov2 x:' + newX + ' y:' + newY );
\r
381 X_UI_ScrollBox_translate( this, newX, newY, 0, '', 300 );
\r
383 if( 300 < timestamp - this.startTime ){
\r
384 this.startTime = timestamp;
\r
385 this.startX = this.scrollX;
\r
386 this.startY = this.scrollY;
\r
389 return ret | X_Callback_PREVENT_DEFAULT | X_Callback_MONOPOLY;
\r
392 function X_UI_ScrollBox_onEnd( e ){
\r
393 var ret = X_Callback_NONE,
\r
397 momentumX, momentumY,
\r
398 duration, distanceX, distanceY;
\r
400 this[ 'unlisten' ]( X.UI.Event._POINTER_MOVE, this, X_UI_ScrollBox_onMove );
\r
401 this[ 'unlisten' ]( [ X.UI.Event._POINTER_UP, X.UI.Event._POINTER_CANCEL ], this, X_UI_ScrollBox_onEnd );
\r
403 if( !this.scrollEnabled || e.pointerType !== this.initiated ){
\r
407 delete this.isInTransition;
\r
408 delete this.initiated;
\r
409 this.endTime = X_Timer_now();
\r
411 duration = this.endTime - this.startTime;
\r
412 newX = Math.round( this.scrollX );
\r
413 newY = Math.round( this.scrollY );
\r
414 distanceX = Math.abs(newX - this.startX);
\r
415 distanceY = Math.abs(newY - this.startY);
\r
417 // reset if we are outside of the boundaries
\r
418 if( X_UI_ScrollBox_resetPosition( this, this.bounceTime ) ){
\r
422 // we scrolled less than 10 pixels
\r
424 // this[ 'dispatch' ]( X_EVENT_CANCELED );
\r
425 console.log( 'we scrolled less than 10 pixels' );
\r
429 // start momentum animation if needed
\r
430 if( this.momentumEnabled && duration < 300 ){
\r
431 momentumX = this.hasHScroll ?
\r
432 X_UI_ScrollBox_momentum( this.scrollX, this.startX, duration, this.maxScrollX, this.bounceEnabled ? this.boxWidth * this.fontSize : 0, this.deceleration ) :
\r
433 { destination: newX, duration: 0 };
\r
434 momentumY = this.hasVScroll ?
\r
435 X_UI_ScrollBox_momentum( this.scrollY, this.startY, duration, this.maxScrollY, this.bounceEnabled ? this.boxHeight * this.fontSize : 0, this.deceleration ) :
\r
436 { destination: newY, duration: 0 };
\r
437 newX = momentumX.destination;
\r
438 newY = momentumY.destination;
\r
439 time = Math.max( momentumX.duration, momentumY.duration ) | 0;
\r
440 this.isInTransition = true;
\r
442 console.log( '慣性無し' );
\r
445 if( newX != this.scrollX || newY != this.scrollY ){
\r
446 // change easing function when scroller goes out of the boundaries
\r
447 if( 0 < newX || newX < this.maxScrollX || 0 < newY || newY < this.maxScrollY ){
\r
448 easing = 'quadratic';
\r
451 console.log( 'end2 x:' + newX + ' y:' + newY + ' t:' + time );
\r
452 this.scrollTo( newX, newY, time, easing, 1000 );
\r
456 console.log( 'end1 x:' + newX + ' y:' + newY );
\r
457 this.scrollTo( newX, newY, 0, '', 1000 ); // ensures that the last position is rounded
\r
459 this[ 'dispatch' ]( X.UI.Event.SCROLL_END );
\r
464 function X_UI_ScrollBox_resetPosition( that, time ){
\r
465 var x = that.scrollX,
\r
470 if( !that.hasHScroll || 0 < that.scrollX ){
\r
473 if( that.scrollX < that.maxScrollX ){
\r
474 x = that.maxScrollX;
\r
477 if( !that.hasVScroll || 0 < that.scrollY ){
\r
480 if( that.scrollY < that.maxScrollY ){
\r
481 y = that.maxScrollY;
\r
484 if( x === that.scrollX && y === that.scrollY ){
\r
485 console.log( 'no バウンド y:' + y + ' max:' + that.maxScrollY );
\r
489 //console.log( 'バウンド!' );
\r
490 //console.log( 'rese x:' + x + ' y:' + y );
\r
491 that.scrollTo( x, y, time, that.bounceEasing, 1000 );
\r
496 function X_UI_ScrollBox_onAnimeEnd( e ){
\r
497 if( e.target !== this._containerXNode || !this.isInTransition ){
\r
498 return X_Callback_NONE;
\r
500 if( !X_UI_ScrollBox_resetPosition( this, this.bounceTime ) ){
\r
501 this.isInTransition = false;
\r
502 this.dispatch( X.UI.Event.SCROLL_END );
\r
504 return X_Callback_NONE;
\r
507 X.UI.ScrollBox = X.UI.ChromeBox.inherits(
\r
509 X_Class.SUPER_ACCESS,
\r
512 Constructor : function(){
\r
516 name : 'ScrollBox-Scroller',
\r
517 role : 'container',
\r
524 i = arguments.length,
\r
528 arg = arguments[ --i ];
\r
529 if( arg[ 'instanceOf' ] && arg[ 'instanceOf' ]( X.UI.Layout.Base ) ){
\r
532 args[ args.length ] = arg;
\r
536 args[ 0 ] = X.UI.Layout.Vertical;
\r
538 X_Class_newPrivate(
\r
540 X.UI.Layout.Canvas,
\r
546 X.UI.VBox.apply( 0, args )
\r
550 scrollX : function(){
\r
553 scrollY : function(){
\r
556 scrollWidth : function(){
\r
559 scrollHeight : function(){
\r
562 scrollTo : function( nodeOrX, y ){
\r