source: Dev/branches/rest-dojo-ui/client/dojox/mobile/app/ImageView.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

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.