source: Dev/trunk/src/client/dojox/mobile/app/ImageView.js @ 529

Last change on this file since 529 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 19.0 KB
Line 
1dojo.provide("dojox.mobile.app.ImageView");
2dojo.experimental("dojox.mobile.app.ImageView");
3dojo.require("dojox.mobile.app._Widget");
4
5dojo.require("dojo.fx.easing");
6
7dojo.declare("dojox.mobile.app.ImageView", dojox.mobile.app._Widget, {
8
9        // zoom: Number
10        //              The current level of zoom.  This should not be set manually.
11        zoom: 1,
12
13        // zoomCenterX: Number
14        //              The X coordinate in the image where the zoom is focused
15        zoomCenterX: 0,
16
17        // zoomCenterY: Number
18        //              The Y coordinate in the image where the zoom is focused
19        zoomCenterY: 0,
20
21        // maxZoom: Number
22        //              The highest degree to which an image can be zoomed.  For example,
23        //              a maxZoom of 5 means that the image will be 5 times larger than normal
24        maxZoom: 5,
25
26        // autoZoomLevel: Number
27        //              The degree to which the image is zoomed when auto zoom is invoked.
28        //              The higher the number, the more the image is zoomed in.
29        autoZoomLevel: 3,
30
31        // disableAutoZoom: Boolean
32        //              Disables auto zoom
33        disableAutoZoom: false,
34
35        // disableSwipe: Boolean
36        //              Disables the users ability to swipe from one image to the next.
37        disableSwipe: false,
38
39        // autoZoomEvent: String
40        //              Overrides the default event listened to which invokes auto zoom
41        autoZoomEvent: null,
42
43        // _leftImg: Node
44        //              The full sized image to the left
45        _leftImg: null,
46
47        // _centerImg: Node
48        //              The full sized image in the center
49        _centerImg: null,
50
51        // _rightImg: Node
52        //              The full sized image to the right
53        _rightImg: null,
54
55        // _leftImg: Node
56        //              The small sized image to the left
57        _leftSmallImg: null,
58
59        // _centerImg: Node
60        //              The small sized image in the center
61        _centerSmallImg: null,
62
63        // _rightImg: Node
64        //              The small sized image to the right
65        _rightSmallImg: null,
66
67        constructor: function(){
68
69                this.panX = 0;
70                this.panY = 0;
71
72                this.handleLoad = dojo.hitch(this, this.handleLoad);
73                this._updateAnimatedZoom = dojo.hitch(this, this._updateAnimatedZoom);
74                this._updateAnimatedPan = dojo.hitch(this, this._updateAnimatedPan);
75                this._onAnimPanEnd = dojo.hitch(this, this._onAnimPanEnd);
76        },
77
78        buildRendering: function(){
79                this.inherited(arguments);
80
81                this.canvas = dojo.create("canvas", {}, this.domNode);
82
83                dojo.addClass(this.domNode, "mblImageView");
84        },
85
86        postCreate: function(){
87                this.inherited(arguments);
88
89                this.size = dojo.marginBox(this.domNode);
90
91                dojo.style(this.canvas, {
92                        width: this.size.w + "px",
93                        height: this.size.h + "px"
94                });
95                this.canvas.height = this.size.h;
96                this.canvas.width = this.size.w;
97
98                var _this = this;
99
100                // Listen to the mousedown/touchstart event.  Record the position
101                // so we can use it to pan the image.
102                this.connect(this.domNode, "onmousedown", function(event){
103                        if(_this.isAnimating()){
104                                return;
105                        }
106                        if(_this.panX){
107                                _this.handleDragEnd();
108                        }
109
110                        _this.downX = event.targetTouches ? event.targetTouches[0].clientX : event.clientX;
111                        _this.downY = event.targetTouches ? event.targetTouches[0].clientY : event.clientY;
112                });
113
114                // record the movement of the mouse.
115                this.connect(this.domNode, "onmousemove", function(event){
116                        if(_this.isAnimating()){
117                                return;
118                        }
119                        if((!_this.downX && _this.downX !== 0) || (!_this.downY && _this.downY !== 0)){
120                                // If the touch didn't begin on this widget, ignore the movement
121                                return;
122                        }
123
124                        if((!_this.disableSwipe && _this.zoom == 1)
125                                || (!_this.disableAutoZoom && _this.zoom != 1)){
126                                var x = event.targetTouches ?
127                                                        event.targetTouches[0].clientX : event.pageX;
128                                var y = event.targetTouches ?
129                                                        event.targetTouches[0].clientY : event.pageY;
130
131                                _this.panX = x - _this.downX;
132                                _this.panY = y - _this.downY;
133
134                                if(_this.zoom == 1){
135                                        // If not zoomed in, then try to move to the next or prev image
136                                        // but only if the mouse has moved more than 10 pixels
137                                        // in the X direction
138                                        if(Math.abs(_this.panX) > 10){
139                                                _this.render();
140                                        }
141                                }else{
142                                        // If zoomed in, pan the image if the mouse has moved more
143                                        // than 10 pixels in either direction.
144                                        if(Math.abs(_this.panX) > 10 || Math.abs(_this.panY) > 10){
145                                                _this.render();
146                                        }
147                                }
148                        }
149                });
150
151                this.connect(this.domNode, "onmouseout", function(event){
152                        if(!_this.isAnimating() && _this.panX){
153                                _this.handleDragEnd();
154                        }
155                });
156
157                this.connect(this.domNode, "onmouseover", function(event){
158                        _this.downX = _this.downY = null;
159                });
160
161                // Set up AutoZoom, which zooms in a fixed amount when the user taps
162                // a part of the canvas
163                this.connect(this.domNode, "onclick", function(event){
164                        if(_this.isAnimating()){
165                                return;
166                        }
167                        if(_this.downX == null || _this.downY == null){
168                                return;
169                        }
170
171                        var x = (event.targetTouches ?
172                                                event.targetTouches[0].clientX : event.pageX);
173                        var y = (event.targetTouches ?
174                                                event.targetTouches[0].clientY : event.pageY);
175
176                        // If the mouse/finger has moved more than 14 pixels from where it
177                        // started, do not treat it as a click.  It is a drag.
178                        if(Math.abs(_this.panX) > 14 || Math.abs(_this.panY) > 14){
179                                _this.downX = _this.downY = null;
180                                _this.handleDragEnd();
181                                return;
182                        }
183                        _this.downX = _this.downY = null;
184
185                        if(!_this.disableAutoZoom){
186
187                                if(!_this._centerImg || !_this._centerImg._loaded){
188                                        // Do nothing until the image is loaded
189                                        return;
190                                }
191                                if(_this.zoom != 1){
192                                        _this.set("animatedZoom", 1);
193                                        return;
194                                }
195
196                                var pos = dojo._abs(_this.domNode);
197
198                                // Translate the clicked point to a point on the source image
199                                var xRatio = _this.size.w / _this._centerImg.width;
200                                var yRatio = _this.size.h / _this._centerImg.height;
201
202                                // Do an animated zoom to the point which was clicked.
203                                _this.zoomTo(
204                                        ((x - pos.x) / xRatio) - _this.panX,
205                                        ((y - pos.y) / yRatio) - _this.panY,
206                                        _this.autoZoomLevel);
207                        }
208                });
209
210                // Listen for Flick events
211                dojo.connect(this.domNode, "flick", this, "handleFlick");
212        },
213
214        isAnimating: function(){
215                // summary:
216                //              Returns true if an animation is in progress, false otherwise.
217                return this._anim && this._anim.status() == "playing";
218        },
219
220        handleDragEnd: function(){
221                // summary:
222                //              Handles the end of a dragging event. If not zoomed in, it
223                //              determines if the next or previous image should be transitioned
224                //              to.
225                this.downX = this.downY = null;
226                console.log("handleDragEnd");
227
228                if(this.zoom == 1){
229                        if(!this.panX){
230                                return;
231                        }
232
233                        var leftLoaded = (this._leftImg && this._leftImg._loaded)
234                                                        || (this._leftSmallImg && this._leftSmallImg._loaded);
235                        var rightLoaded = (this._rightImg && this._rightImg._loaded)
236                                                        || (this._rightSmallImg && this._rightSmallImg._loaded);
237
238                        // Check if the drag has moved the image more than half its length.
239                        // If so, move to either the previous or next image.
240                        var doMove =
241                                !(Math.abs(this.panX) < this._centerImg._baseWidth / 2) &&
242                                (
243                                        (this.panX > 0 && leftLoaded ? 1 : 0) ||
244                                        (this.panX < 0 && rightLoaded ? 1 : 0)
245                                );
246
247
248                        if(!doMove){
249                                // If not moving to another image, animate the sliding of the
250                                // image back into place.
251                                this._animPanTo(0, dojo.fx.easing.expoOut, 700);
252                        }else{
253                                // Move to another image.
254                                this.moveTo(this.panX);
255                        }
256                }else{
257                        if(!this.panX && !this.panY){
258                                return;
259                        }
260                        // Recenter the zoomed image based on where it was panned to
261                        // previously
262                        this.zoomCenterX -= (this.panX / this.zoom);
263                        this.zoomCenterY -= (this.panY / this.zoom);
264
265                        this.panX = this.panY = 0;
266                }
267
268        },
269
270        handleFlick: function(event){
271                // summary:
272                //              Handle a flick event.
273                if(this.zoom == 1 && event.duration < 500){
274                        // Only handle quick flicks here, less than 0.5 seconds
275
276                        // If not zoomed in, then check if we should move to the next photo
277                        // or not
278                        if(event.direction == "ltr"){
279                                this.moveTo(1);
280                        }else if(event.direction == "rtl"){
281                                this.moveTo(-1);
282                        }
283                        // If an up or down flick occurs, it means nothing so ignore it
284                        this.downX = this.downY = null;
285                }
286        },
287
288        moveTo: function(direction){
289                direction = direction > 0 ? 1 : -1;
290                var toImg;
291
292                if(direction < 1){
293                        if(this._rightImg && this._rightImg._loaded){
294                                toImg = this._rightImg;
295                        }else if(this._rightSmallImg && this._rightSmallImg._loaded){
296                                toImg = this._rightSmallImg;
297                        }
298                }else{
299                        if(this._leftImg && this._leftImg._loaded){
300                                toImg = this._leftImg;
301                        }else if(this._leftSmallImg && this._leftSmallImg._loaded){
302                                toImg = this._leftSmallImg;
303                        }
304                }
305
306                this._moveDir = direction;
307                var _this = this;
308
309                if(toImg && toImg._loaded){
310                        // If the image is loaded, make a linear animation to show it
311                        this._animPanTo(this.size.w * direction, null, 500, function(){
312                                _this.panX = 0;
313                                _this.panY = 0;
314
315                                if(direction < 0){
316                                        // Moving to show the right image
317                                        _this._switchImage("left", "right");
318                                }else{
319                                        // Moving to show the left image
320                                        _this._switchImage("right", "left");
321                                }
322
323                                _this.render();
324                                _this.onChange(direction * -1);
325                        });
326
327                }else{
328                        // If the next image is not loaded, make an animation to
329                        // move the center image to half the width of the widget and back
330                        // again
331
332                        console.log("moveTo image not loaded!", toImg);
333
334                        this._animPanTo(0, dojo.fx.easing.expoOut, 700);
335                }
336        },
337
338        _switchImage: function(toImg, fromImg){
339                var toSmallImgName = "_" + toImg + "SmallImg";
340                var toImgName = "_" + toImg + "Img";
341
342                var fromSmallImgName = "_" + fromImg + "SmallImg";
343                var fromImgName = "_" + fromImg + "Img";
344
345                this[toImgName] = this._centerImg;
346                this[toSmallImgName] = this._centerSmallImg;
347
348                this[toImgName]._type = toImg;
349
350                if(this[toSmallImgName]){
351                        this[toSmallImgName]._type = toImg;
352                }
353
354                this._centerImg = this[fromImgName];
355                this._centerSmallImg = this[fromSmallImgName];
356                this._centerImg._type = "center";
357
358                if(this._centerSmallImg){
359                        this._centerSmallImg._type = "center";
360                }
361                this[fromImgName] = this[fromSmallImgName] = null;
362        },
363
364        _animPanTo: function(to, easing, duration, callback){
365                this._animCallback = callback;
366                this._anim = new dojo.Animation({
367                        curve: [this.panX, to],
368                        onAnimate: this._updateAnimatedPan,
369                        duration: duration || 500,
370                        easing: easing,
371                        onEnd: this._onAnimPanEnd
372                });
373
374                this._anim.play();
375                return this._anim;
376        },
377
378        onChange: function(direction){
379                // summary:
380                //              Stub function that can be listened to in order to provide
381                //              new images when the displayed image changes
382        },
383
384        _updateAnimatedPan: function(amount){
385                this.panX = amount;
386                this.render();
387        },
388
389        _onAnimPanEnd: function(){
390                this.panX = this.panY = 0;
391
392                if(this._animCallback){
393                        this._animCallback();
394                }
395        },
396
397        zoomTo: function(centerX, centerY, zoom){
398                this.set("zoomCenterX", centerX);
399                this.set("zoomCenterY", centerY);
400
401                this.set("animatedZoom", zoom);
402        },
403
404        render: function(){
405                var cxt = this.canvas.getContext('2d');
406
407                cxt.clearRect(0, 0, this.canvas.width, this.canvas.height);
408
409                // Render the center image
410                this._renderImg(
411                        this._centerSmallImg,
412                        this._centerImg,
413                        this.zoom == 1 ? (this.panX < 0 ? 1 : this.panX > 0 ? -1 : 0) : 0);
414
415                if(this.zoom == 1 && this.panX != 0){
416                        if(this.panX > 0){
417                                // Render the left image, showing the right side of it
418                                this._renderImg(this._leftSmallImg, this._leftImg, 1);
419                        }else{
420                                // Render the right image, showing the left side of it
421                                this._renderImg(this._rightSmallImg, this._rightImg, -1);
422                        }
423                }
424        },
425
426        _renderImg: function(smallImg, largeImg, panDir){
427                // summary:
428                //              Renders a single image
429
430
431                // If zoomed, we just display the center img
432                var img = (largeImg && largeImg._loaded) ? largeImg : smallImg;
433
434                if(!img || !img._loaded){
435                        // If neither the large or small image is loaded, display nothing
436                        return;
437                }
438                var cxt = this.canvas.getContext('2d');
439
440                var baseWidth = img._baseWidth;
441                var baseHeight = img._baseHeight;
442
443                // Calculate the size the image would be if there were no bounds
444                var desiredWidth = baseWidth * this.zoom;
445                var desiredHeight = baseHeight * this.zoom;
446
447                // Calculate the actual size of the viewable image
448                var destWidth = Math.min(this.size.w, desiredWidth);
449                var destHeight = Math.min(this.size.h, desiredHeight);
450
451
452                // Calculate the size of the window on the original image to use
453                var sourceWidth = this.dispWidth = img.width * (destWidth / desiredWidth);
454                var sourceHeight = this.dispHeight = img.height * (destHeight / desiredHeight);
455
456                var zoomCenterX = this.zoomCenterX - (this.panX / this.zoom);
457                var zoomCenterY = this.zoomCenterY - (this.panY / this.zoom);
458
459                // Calculate where the center of the view should be
460                var centerX = Math.floor(Math.max(sourceWidth / 2,
461                                Math.min(img.width - sourceWidth / 2, zoomCenterX)));
462                var centerY = Math.floor(Math.max(sourceHeight / 2,
463                                Math.min(img.height - sourceHeight / 2, zoomCenterY)));
464
465
466                var sourceX = Math.max(0,
467                        Math.round((img.width - sourceWidth)/2 + (centerX - img._centerX)) );
468                var sourceY = Math.max(0,
469                        Math.round((img.height - sourceHeight) / 2 + (centerY - img._centerY))
470                                                );
471
472                var destX = Math.round(Math.max(0, this.canvas.width - destWidth)/2);
473                var destY = Math.round(Math.max(0, this.canvas.height - destHeight)/2);
474
475                var oldDestWidth = destWidth;
476                var oldSourceWidth = sourceWidth;
477
478                if(this.zoom == 1 && panDir && this.panX){
479
480                        if(this.panX < 0){
481                                if(panDir > 0){
482                                        // If the touch is moving left, and the right side of the
483                                        // image should be shown, then reduce the destination width
484                                        // by the absolute value of panX
485                                        destWidth -= Math.abs(this.panX);
486                                        destX = 0;
487                                }else if(panDir < 0){
488                                        // If the touch is moving left, and the left side of the
489                                        // image should be shown, then set the displayed width
490                                        // to the absolute value of panX, less some pixels for
491                                        // a padding between images
492                                        destWidth = Math.max(1, Math.abs(this.panX) - 5);
493                                        destX = this.size.w - destWidth;
494                                }
495                        }else{
496                                if(panDir > 0){
497                                        // If the touch is moving right, and the right side of the
498                                        // image should be shown, then set the destination width
499                                        // to the absolute value of the pan, less some pixels for
500                                        // padding
501                                        destWidth = Math.max(1, Math.abs(this.panX) - 5);
502                                        destX = 0;
503                                }else if(panDir < 0){
504                                        // If the touch is moving right, and the left side of the
505                                        // image should be shown, then reduce the destination width
506                                        // by the widget width minus the absolute value of panX
507                                        destWidth -= Math.abs(this.panX);
508                                        destX = this.size.w - destWidth;
509                                }
510                        }
511
512                        sourceWidth = Math.max(1,
513                                                Math.floor(sourceWidth * (destWidth / oldDestWidth)));
514
515                        if(panDir > 0){
516                                // If the right side of the image should be displayed, move
517                                // the sourceX to be the width of the image minus the difference
518                                // between the original sourceWidth and the new sourceWidth
519                                sourceX = (sourceX + oldSourceWidth) - (sourceWidth);
520                        }
521                        sourceX = Math.floor(sourceX);
522                }
523
524                try{
525
526                        // See https://developer.mozilla.org/en/Canvas_tutorial/Using_images
527                        cxt.drawImage(
528                                img,
529                                Math.max(0, sourceX),
530                                sourceY,
531                                Math.min(oldSourceWidth, sourceWidth),
532                                sourceHeight,
533                                destX,  // Xpos
534                                destY, // Ypos
535                                Math.min(oldDestWidth, destWidth),
536                                destHeight
537                        );
538                }catch(e){
539                        console.log("Caught Error",e,
540
541                                        "type=", img._type,
542                                        "oldDestWidth = ", oldDestWidth,
543                                        "destWidth", destWidth,
544                                        "destX", destX
545                                        , "oldSourceWidth=",oldSourceWidth,
546                                        "sourceWidth=", sourceWidth,
547                                        "sourceX = " + sourceX
548                        );
549                }
550        },
551
552        _setZoomAttr: function(amount){
553                this.zoom = Math.min(this.maxZoom, Math.max(1, amount));
554
555                if(this.zoom == 1
556                                && this._centerImg
557                                && this._centerImg._loaded){
558
559                        if(!this.isAnimating()){
560                                this.zoomCenterX = this._centerImg.width / 2;
561                                this.zoomCenterY = this._centerImg.height / 2;
562                        }
563                        this.panX = this.panY = 0;
564                }
565
566                this.render();
567        },
568
569        _setZoomCenterXAttr: function(value){
570                if(value != this.zoomCenterX){
571                        if(this._centerImg && this._centerImg._loaded){
572                                value = Math.min(this._centerImg.width, value);
573                        }
574                        this.zoomCenterX = Math.max(0, Math.round(value));
575                }
576        },
577
578        _setZoomCenterYAttr: function(value){
579                if(value != this.zoomCenterY){
580                        if(this._centerImg && this._centerImg._loaded){
581                                value = Math.min(this._centerImg.height, value);
582                        }
583                        this.zoomCenterY = Math.max(0, Math.round(value));
584                }
585        },
586
587        _setZoomCenterAttr: function(value){
588                if(value.x != this.zoomCenterX || value.y != this.zoomCenterY){
589                        this.set("zoomCenterX", value.x);
590                        this.set("zoomCenterY", value.y);
591                        this.render();
592                }
593        },
594
595        _setAnimatedZoomAttr: function(amount){
596                if(this._anim && this._anim.status() == "playing"){
597                        return;
598                }
599
600                this._anim = new dojo.Animation({
601                        curve: [this.zoom, amount],
602                        onAnimate: this._updateAnimatedZoom,
603                        onEnd: this._onAnimEnd
604                });
605
606                this._anim.play();
607        },
608
609        _updateAnimatedZoom: function(amount){
610                this._setZoomAttr(amount);
611        },
612
613        _setCenterUrlAttr: function(urlOrObj){
614                this._setImage("center", urlOrObj);
615        },
616        _setLeftUrlAttr: function(urlOrObj){
617                this._setImage("left", urlOrObj);
618        },
619        _setRightUrlAttr: function(urlOrObj){
620                this._setImage("right", urlOrObj);
621        },
622
623        _setImage: function(name, urlOrObj){
624                var smallUrl = null;
625
626                var largeUrl = null;
627
628                if(dojo.isString(urlOrObj)){
629                        // If the argument is a string, then just load the large url
630                        largeUrl = urlOrObj;
631                }else{
632                        largeUrl = urlOrObj.large;
633                        smallUrl = urlOrObj.small;
634                }
635
636                if(this["_" + name + "Img"] && this["_" + name + "Img"]._src == largeUrl){
637                        // Identical URL, ignore it
638                        return;
639                }
640
641                // Just do the large image for now
642                var largeImg = this["_" + name + "Img"] = new Image();
643                largeImg._type = name;
644                largeImg._loaded = false;
645                largeImg._src = largeUrl;
646                largeImg._conn = dojo.connect(largeImg, "onload", this.handleLoad);
647
648                if(smallUrl){
649                        // If a url to a small version of the image has been provided,
650                        // load that image first.
651                        var smallImg = this["_" + name + "SmallImg"] = new Image();
652                        smallImg._type = name;
653                        smallImg._loaded = false;
654                        smallImg._conn = dojo.connect(smallImg, "onload", this.handleLoad);
655                        smallImg._isSmall = true;
656                        smallImg._src = smallUrl;
657                        smallImg.src = smallUrl;
658                }
659
660                // It's important that the large url's src is set after the small image
661                // to ensure it's loaded second.
662                largeImg.src = largeUrl;
663        },
664
665        handleLoad: function(evt){
666                // summary:
667                //              Handles the loading of an image, both the large and small
668                //              versions.  A render is triggered as a result of each image load.
669
670                var img = evt.target;
671                img._loaded = true;
672
673                dojo.disconnect(img._conn);
674
675                var type = img._type;
676
677                switch(type){
678                        case "center":
679                                this.zoomCenterX = img.width / 2;
680                                this.zoomCenterY = img.height / 2;
681                                break;
682                }
683
684                var height = img.height;
685                var width = img.width;
686
687                if(width / this.size.w < height / this.size.h){
688                        // Fit the height to the height of the canvas
689                        img._baseHeight = this.canvas.height;
690                        img._baseWidth = width / (height / this.size.h);
691                }else{
692                        // Fix the width to the width of the canvas
693                        img._baseWidth = this.canvas.width;
694                        img._baseHeight = height / (width / this.size.w);
695                }
696                img._centerX = width / 2;
697                img._centerY = height / 2;
698
699                this.render();
700
701                this.onLoad(img._type, img._src, img._isSmall);
702        },
703
704        onLoad: function(type, url, isSmall){
705                // summary:
706                //              Dummy function that is called whenever an image loads.
707                // type: String
708                //              The position of the image that has loaded, either
709                //              "center", "left" or "right"
710                // url: String
711                //              The src of the image
712                // isSmall: Boolean
713                //              True if it is a small version of the image that has loaded,
714                //              false otherwise.
715        }
716});
Note: See TracBrowser for help on using the repository browser.