PhotoView.java revision b3aab90bb37aa9cc60be32e05678ee55d6575ee8
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.gallery3d.ui;
18
19import com.android.gallery3d.R;
20import com.android.gallery3d.app.GalleryActivity;
21import com.android.gallery3d.common.Utils;
22import com.android.gallery3d.data.Path;
23import com.android.gallery3d.ui.PositionRepository.Position;
24
25import android.content.Context;
26import android.graphics.Bitmap;
27import android.graphics.Color;
28import android.graphics.RectF;
29import android.os.Message;
30import android.os.SystemClock;
31import android.view.GestureDetector;
32import android.view.MotionEvent;
33import android.view.ScaleGestureDetector;
34
35public class PhotoView extends GLView {
36    @SuppressWarnings("unused")
37    private static final String TAG = "PhotoView";
38
39    public static final int INVALID_SIZE = -1;
40
41    private static final int MSG_TRANSITION_COMPLETE = 1;
42    private static final int MSG_SHOW_LOADING = 2;
43
44    private static final long DELAY_SHOW_LOADING = 250; // 250ms;
45
46    private static final int TRANS_NONE = 0;
47    private static final int TRANS_SWITCH_NEXT = 3;
48    private static final int TRANS_SWITCH_PREVIOUS = 4;
49
50    public static final int TRANS_SLIDE_IN_RIGHT = 1;
51    public static final int TRANS_SLIDE_IN_LEFT = 2;
52    public static final int TRANS_OPEN_ANIMATION = 5;
53
54    private static final int LOADING_INIT = 0;
55    private static final int LOADING_TIMEOUT = 1;
56    private static final int LOADING_COMPLETE = 2;
57    private static final int LOADING_FAIL = 3;
58
59    private static final int ENTRY_PREVIOUS = 0;
60    private static final int ENTRY_NEXT = 1;
61
62    private static final int IMAGE_GAP = 96;
63    private static final int SWITCH_THRESHOLD = 256;
64    private static final float SWIPE_THRESHOLD = 300f;
65
66    private static final float DEFAULT_TEXT_SIZE = 20;
67
68    public interface PhotoTapListener {
69        public void onSingleTapUp(int x, int y);
70    }
71
72    // the previous/next image entries
73    private final ScreenNailEntry mScreenNails[] = new ScreenNailEntry[2];
74
75    private final ScaleGestureDetector mScaleDetector;
76    private final GestureDetector mGestureDetector;
77    private final DownUpDetector mDownUpDetector;
78
79    private PhotoTapListener mPhotoTapListener;
80
81    private final PositionController mPositionController;
82
83    private Model mModel;
84    private StringTexture mLoadingText;
85    private StringTexture mNoThumbnailText;
86    private int mTransitionMode = TRANS_NONE;
87    private final TileImageView mTileView;
88    private Texture mVideoPlayIcon;
89
90    private boolean mShowVideoPlayIcon;
91    private ProgressSpinner mLoadingSpinner;
92
93    private SynchronizedHandler mHandler;
94
95    private int mLoadingState = LOADING_COMPLETE;
96
97    private int mImageRotation;
98
99    private Path mOpenedItemPath;
100    private GalleryActivity mActivity;
101
102    public PhotoView(GalleryActivity activity) {
103        mActivity = activity;
104        mTileView = new TileImageView(activity);
105        addComponent(mTileView);
106        Context context = activity.getAndroidContext();
107        mLoadingSpinner = new ProgressSpinner(context);
108        mLoadingText = StringTexture.newInstance(
109                context.getString(R.string.loading),
110                DEFAULT_TEXT_SIZE, Color.WHITE);
111        mNoThumbnailText = StringTexture.newInstance(
112                context.getString(R.string.no_thumbnail),
113                DEFAULT_TEXT_SIZE, Color.WHITE);
114
115        mHandler = new SynchronizedHandler(activity.getGLRoot()) {
116            @Override
117            public void handleMessage(Message message) {
118                switch (message.what) {
119                    case MSG_TRANSITION_COMPLETE: {
120                        onTransitionComplete();
121                        break;
122                    }
123                    case MSG_SHOW_LOADING: {
124                        if (mLoadingState == LOADING_INIT) {
125                            // We don't need the opening animation
126                            mOpenedItemPath = null;
127
128                            mLoadingSpinner.startAnimation();
129                            mLoadingState = LOADING_TIMEOUT;
130                            invalidate();
131                        }
132                        break;
133                    }
134                    default: throw new AssertionError(message.what);
135                }
136            }
137        };
138
139        mGestureDetector = new GestureDetector(context,
140                new MyGestureListener(), null, true /* ignoreMultitouch */);
141        mScaleDetector = new ScaleGestureDetector(context, new MyScaleListener());
142        mDownUpDetector = new DownUpDetector(new MyDownUpListener());
143
144        for (int i = 0, n = mScreenNails.length; i < n; ++i) {
145            mScreenNails[i] = new ScreenNailEntry();
146        }
147
148        mPositionController = new PositionController(this, context);
149        mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play);
150    }
151
152
153    public void setModel(Model model) {
154        if (mModel == model) return;
155        mModel = model;
156        mTileView.setModel(model);
157        if (model != null) notifyOnNewImage();
158    }
159
160    public void setPhotoTapListener(PhotoTapListener listener) {
161        mPhotoTapListener = listener;
162    }
163
164    private boolean setTileViewPosition(int centerX, int centerY, float scale) {
165        int inverseX = mPositionController.getImageWidth() - centerX;
166        int inverseY = mPositionController.getImageHeight() - centerY;
167        TileImageView t = mTileView;
168        int rotation = mImageRotation;
169        switch (rotation) {
170            case 0: return t.setPosition(centerX, centerY, scale, 0);
171            case 90: return t.setPosition(centerY, inverseX, scale, 90);
172            case 180: return t.setPosition(inverseX, inverseY, scale, 180);
173            case 270: return t.setPosition(inverseY, centerX, scale, 270);
174            default: throw new IllegalArgumentException(String.valueOf(rotation));
175        }
176    }
177
178    public void setPosition(int centerX, int centerY, float scale) {
179        if (setTileViewPosition(centerX, centerY, scale)) {
180            layoutScreenNails();
181        }
182    }
183
184    private void updateScreenNailEntry(int which, ImageData data) {
185        if (mTransitionMode == TRANS_SWITCH_NEXT
186                || mTransitionMode == TRANS_SWITCH_PREVIOUS) {
187            // ignore screen nail updating during switching
188            return;
189        }
190        ScreenNailEntry entry = mScreenNails[which];
191        if (data == null) {
192            entry.set(false, null, 0);
193        } else {
194            entry.set(true, data.bitmap, data.rotation);
195        }
196    }
197
198    // -1 previous, 0 current, 1 next
199    public void notifyImageInvalidated(int which) {
200        switch (which) {
201            case -1: {
202                updateScreenNailEntry(
203                        ENTRY_PREVIOUS, mModel.getPreviousImage());
204                layoutScreenNails();
205                invalidate();
206                break;
207            }
208            case 1: {
209                updateScreenNailEntry(ENTRY_NEXT, mModel.getNextImage());
210                layoutScreenNails();
211                invalidate();
212                break;
213            }
214            case 0: {
215                // mImageWidth and mImageHeight will get updated
216                mTileView.notifyModelInvalidated();
217
218                mImageRotation = mModel.getImageRotation();
219                if (((mImageRotation / 90) & 1) == 0) {
220                    mPositionController.setImageSize(
221                            mTileView.mImageWidth, mTileView.mImageHeight);
222                } else {
223                    mPositionController.setImageSize(
224                            mTileView.mImageHeight, mTileView.mImageWidth);
225                }
226                updateLoadingState();
227                break;
228            }
229        }
230    }
231
232    private void updateLoadingState() {
233        // Possible transitions of mLoadingState:
234        //        INIT --> TIMEOUT, COMPLETE, FAIL
235        //     TIMEOUT --> COMPLETE, FAIL, INIT
236        //    COMPLETE --> INIT
237        //        FAIL --> INIT
238        if (mModel.getLevelCount() != 0 || mModel.getBackupImage() != null) {
239            mHandler.removeMessages(MSG_SHOW_LOADING);
240            mLoadingState = LOADING_COMPLETE;
241        } else if (mModel.isFailedToLoad()) {
242            mHandler.removeMessages(MSG_SHOW_LOADING);
243            mLoadingState = LOADING_FAIL;
244        } else if (mLoadingState != LOADING_INIT) {
245            mLoadingState = LOADING_INIT;
246            mHandler.removeMessages(MSG_SHOW_LOADING);
247            mHandler.sendEmptyMessageDelayed(
248                    MSG_SHOW_LOADING, DELAY_SHOW_LOADING);
249        }
250    }
251
252    public void notifyModelInvalidated() {
253        if (mModel == null) {
254            updateScreenNailEntry(ENTRY_PREVIOUS, null);
255            updateScreenNailEntry(ENTRY_NEXT, null);
256        } else {
257            updateScreenNailEntry(ENTRY_PREVIOUS, mModel.getPreviousImage());
258            updateScreenNailEntry(ENTRY_NEXT, mModel.getNextImage());
259        }
260        layoutScreenNails();
261
262        if (mModel == null) {
263            mTileView.notifyModelInvalidated();
264            mImageRotation = 0;
265            mPositionController.setImageSize(0, 0);
266            updateLoadingState();
267        } else {
268            notifyImageInvalidated(0);
269        }
270    }
271
272    @Override
273    protected boolean onTouch(MotionEvent event) {
274        mGestureDetector.onTouchEvent(event);
275        mScaleDetector.onTouchEvent(event);
276        mDownUpDetector.onTouchEvent(event);
277        return true;
278    }
279
280    @Override
281    protected void onLayout(
282            boolean changeSize, int left, int top, int right, int bottom) {
283        mTileView.layout(left, top, right, bottom);
284        if (changeSize) {
285            mPositionController.setViewSize(getWidth(), getHeight());
286            for (ScreenNailEntry entry : mScreenNails) {
287                entry.updateDrawingSize();
288            }
289        }
290    }
291
292    private static int gapToSide(int imageWidth, int viewWidth) {
293        return Math.max(0, (viewWidth - imageWidth) / 2);
294    }
295
296    /*
297     * Here is how we layout the screen nails
298     *
299     *  previous            current           next
300     *  ___________       ________________     __________
301     * |  _______  |     |   __________   |   |  ______  |
302     * | |       | |     |  |   right->|  |   | |      | |
303     * | |       |<-------->|<--left   |  |   | |      | |
304     * | |_______| |  |  |  |__________|  |   | |______| |
305     * |___________|  |  |________________|   |__________|
306     *                |  <--> gapToSide()
307     *                |
308     * IMAGE_GAP + Max(previous.gapToSide(), current.gapToSide)
309     */
310    private void layoutScreenNails() {
311        int width = getWidth();
312        int height = getHeight();
313
314        // Use the image width in AC, since we may fake the size if the
315        // image is unavailable
316        RectF bounds = mPositionController.getImageBounds();
317        int left = Math.round(bounds.left);
318        int right = Math.round(bounds.right);
319        int gap = gapToSide(right - left, width);
320
321        // layout the previous image
322        ScreenNailEntry entry = mScreenNails[ENTRY_PREVIOUS];
323
324        if (entry.isEnabled()) {
325            entry.layoutRightEdgeAt(left - (
326                    IMAGE_GAP + Math.max(gap, entry.gapToSide())));
327        }
328
329        // layout the next image
330        entry = mScreenNails[ENTRY_NEXT];
331        if (entry.isEnabled()) {
332            entry.layoutLeftEdgeAt(right + (
333                    IMAGE_GAP + Math.max(gap, entry.gapToSide())));
334        }
335    }
336
337    @Override
338    protected void render(GLCanvas canvas) {
339        PositionController p = mPositionController;
340
341        // Draw the current photo
342        if (mLoadingState == LOADING_COMPLETE) {
343            super.render(canvas);
344        }
345
346        // Draw the previous and the next photo
347        if (mTransitionMode != TRANS_SLIDE_IN_LEFT
348                && mTransitionMode != TRANS_SLIDE_IN_RIGHT
349                && mTransitionMode != TRANS_OPEN_ANIMATION) {
350            ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
351            ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
352
353            if (prevNail.mVisible) prevNail.draw(canvas);
354            if (nextNail.mVisible) nextNail.draw(canvas);
355        }
356
357        // Draw the progress spinner and the text below it
358        //
359        // (x, y) is where we put the center of the spinner.
360        // s is the size of the video play icon, and we use s to layout text
361        // because we want to keep the text at the same place when the video
362        // play icon is shown instead of the spinner.
363        int w = getWidth();
364        int h = getHeight();
365        int x = Math.round(mPositionController.getImageBounds().centerX());
366        int y = h / 2;
367        int s = Math.min(getWidth(), getHeight()) / 6;
368
369        if (mLoadingState == LOADING_TIMEOUT) {
370            StringTexture m = mLoadingText;
371            ProgressSpinner r = mLoadingSpinner;
372            r.draw(canvas, x - r.getWidth() / 2, y - r.getHeight() / 2);
373            m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
374            invalidate(); // we need to keep the spinner rotating
375        } else if (mLoadingState == LOADING_FAIL) {
376            StringTexture m = mNoThumbnailText;
377            m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
378        }
379
380        // Draw the video play icon (in the place where the spinner was)
381        if (mShowVideoPlayIcon
382                && mLoadingState != LOADING_INIT
383                && mLoadingState != LOADING_TIMEOUT) {
384            mVideoPlayIcon.draw(canvas, x - s / 2, y - s / 2, s, s);
385        }
386
387        if (mPositionController.advanceAnimation()) invalidate();
388    }
389
390    private void stopCurrentSwipingIfNeeded() {
391        // Enable fast sweeping
392        if (mTransitionMode == TRANS_SWITCH_NEXT) {
393            mTransitionMode = TRANS_NONE;
394            mPositionController.stopAnimation();
395            switchToNextImage();
396        } else if (mTransitionMode == TRANS_SWITCH_PREVIOUS) {
397            mTransitionMode = TRANS_NONE;
398            mPositionController.stopAnimation();
399            switchToPreviousImage();
400        }
401    }
402
403    private boolean swipeImages(float velocity) {
404        if (mTransitionMode != TRANS_NONE
405                && mTransitionMode != TRANS_SWITCH_NEXT
406                && mTransitionMode != TRANS_SWITCH_PREVIOUS) return false;
407
408        ScreenNailEntry next = mScreenNails[ENTRY_NEXT];
409        ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS];
410
411        int width = getWidth();
412
413        // If the edge of the current photo is visible and the sweeping velocity
414        // exceed the threshold, switch to next / previous image
415        PositionController controller = mPositionController;
416        if (controller.isAtMinimalScale()) {
417            if (velocity < -SWIPE_THRESHOLD) {
418                stopCurrentSwipingIfNeeded();
419                if (next.isEnabled()) {
420                    mTransitionMode = TRANS_SWITCH_NEXT;
421                    controller.startHorizontalSlide(next.mOffsetX - width / 2);
422                    return true;
423                }
424                return false;
425            }
426            if (velocity > SWIPE_THRESHOLD) {
427                stopCurrentSwipingIfNeeded();
428                if (prev.isEnabled()) {
429                    mTransitionMode = TRANS_SWITCH_PREVIOUS;
430                    controller.startHorizontalSlide(prev.mOffsetX - width / 2);
431                    return true;
432                }
433                return false;
434            }
435        }
436
437        if (mTransitionMode != TRANS_NONE) return false;
438
439        // Decide whether to swiping to the next/prev image in the zoom-in case
440        RectF bounds = controller.getImageBounds();
441        int left = Math.round(bounds.left);
442        int right = Math.round(bounds.right);
443        int threshold = SWITCH_THRESHOLD + gapToSide(right - left, width);
444
445        // If we have moved the picture a lot, switching.
446        if (next.isEnabled() && threshold < width - right) {
447            mTransitionMode = TRANS_SWITCH_NEXT;
448            controller.startHorizontalSlide(next.mOffsetX - width / 2);
449            return true;
450        }
451        if (prev.isEnabled() && threshold < left) {
452            mTransitionMode = TRANS_SWITCH_PREVIOUS;
453            controller.startHorizontalSlide(prev.mOffsetX - width / 2);
454            return true;
455        }
456
457        return false;
458    }
459
460    private boolean mIgnoreUpEvent = false;
461
462    private class MyGestureListener
463            extends GestureDetector.SimpleOnGestureListener {
464        @Override
465        public boolean onScroll(
466                MotionEvent e1, MotionEvent e2, float dx, float dy) {
467            if (mTransitionMode != TRANS_NONE) return true;
468            mPositionController.startScroll(dx, dy);
469            return true;
470        }
471
472        @Override
473        public boolean onSingleTapUp(MotionEvent e) {
474            if (mPhotoTapListener != null) {
475                mPhotoTapListener.onSingleTapUp((int) e.getX(), (int) e.getY());
476            }
477            return true;
478        }
479
480        @Override
481        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
482                float velocityY) {
483            if (swipeImages(velocityX)) {
484                mIgnoreUpEvent = true;
485            } else if (mTransitionMode != TRANS_NONE) {
486                // do nothing
487            } else if (mPositionController.fling(velocityX, velocityY)) {
488                mIgnoreUpEvent = true;
489            }
490            return true;
491        }
492
493        @Override
494        public boolean onDoubleTap(MotionEvent e) {
495            if (mTransitionMode != TRANS_NONE) return true;
496            PositionController controller = mPositionController;
497            float scale = controller.getCurrentScale();
498            // onDoubleTap happened on the second ACTION_DOWN.
499            // We need to ignore the next UP event.
500            mIgnoreUpEvent = true;
501            if (scale <= 1.0f || controller.isAtMinimalScale()) {
502                controller.zoomIn(
503                        e.getX(), e.getY(), Math.max(1.5f, scale * 1.5f));
504            } else {
505                controller.resetToFullView();
506            }
507            return true;
508        }
509    }
510
511    private class MyScaleListener
512            extends ScaleGestureDetector.SimpleOnScaleGestureListener {
513
514        @Override
515        public boolean onScale(ScaleGestureDetector detector) {
516            float scale = detector.getScaleFactor();
517            if (Float.isNaN(scale) || Float.isInfinite(scale)
518                    || mTransitionMode != TRANS_NONE) return true;
519            mPositionController.scaleBy(scale,
520                    detector.getFocusX(), detector.getFocusY());
521            return true;
522        }
523
524        @Override
525        public boolean onScaleBegin(ScaleGestureDetector detector) {
526            if (mTransitionMode != TRANS_NONE) return false;
527            mPositionController.beginScale(
528                detector.getFocusX(), detector.getFocusY());
529            return true;
530        }
531
532        @Override
533        public void onScaleEnd(ScaleGestureDetector detector) {
534            mPositionController.endScale();
535            swipeImages(0);
536        }
537    }
538
539    public boolean jumpTo(int index) {
540        if (mTransitionMode != TRANS_NONE) return false;
541        mModel.jumpTo(index);
542        return true;
543    }
544
545    public void notifyOnNewImage() {
546        mPositionController.setImageSize(0, 0);
547    }
548
549    public void startSlideInAnimation(int direction) {
550        PositionController a = mPositionController;
551        a.stopAnimation();
552        switch (direction) {
553            case TRANS_SLIDE_IN_LEFT:
554            case TRANS_SLIDE_IN_RIGHT: {
555                mTransitionMode = direction;
556                a.startSlideInAnimation(direction);
557                break;
558            }
559            default: throw new IllegalArgumentException(String.valueOf(direction));
560        }
561    }
562
563    private class MyDownUpListener implements DownUpDetector.DownUpListener {
564        public void onDown(MotionEvent e) {
565        }
566
567        public void onUp(MotionEvent e) {
568            if (mIgnoreUpEvent) {
569                mIgnoreUpEvent = false;
570                return;
571            }
572            if (!swipeImages(0) && mTransitionMode == TRANS_NONE) {
573                mPositionController.up();
574            }
575        }
576    }
577
578    private void switchToNextImage() {
579        // We update the texture here directly to prevent texture uploading.
580        ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
581        ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
582        mTileView.invalidateTiles();
583        if (prevNail.mTexture != null) prevNail.mTexture.recycle();
584        prevNail.mTexture = mTileView.mBackupImage;
585        mTileView.mBackupImage = nextNail.mTexture;
586        nextNail.mTexture = null;
587        mModel.next();
588    }
589
590    private void switchToPreviousImage() {
591        // We update the texture here directly to prevent texture uploading.
592        ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
593        ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
594        mTileView.invalidateTiles();
595        if (nextNail.mTexture != null) nextNail.mTexture.recycle();
596        nextNail.mTexture = mTileView.mBackupImage;
597        mTileView.mBackupImage = prevNail.mTexture;
598        nextNail.mTexture = null;
599        mModel.previous();
600    }
601
602    public void notifyTransitionComplete() {
603        mHandler.sendEmptyMessage(MSG_TRANSITION_COMPLETE);
604    }
605
606    private void onTransitionComplete() {
607        int mode = mTransitionMode;
608        mTransitionMode = TRANS_NONE;
609
610        if (mModel == null) return;
611        if (mode == TRANS_SWITCH_NEXT) {
612            switchToNextImage();
613        } else if (mode == TRANS_SWITCH_PREVIOUS) {
614            switchToPreviousImage();
615        }
616    }
617
618    public boolean isDown() {
619        return mDownUpDetector.isDown();
620    }
621
622    public static interface Model extends TileImageView.Model {
623        public void next();
624        public void previous();
625        public void jumpTo(int index);
626        public int getImageRotation();
627
628        // Return null if the specified image is unavailable.
629        public ImageData getNextImage();
630        public ImageData getPreviousImage();
631    }
632
633    public static class ImageData {
634        public int rotation;
635        public Bitmap bitmap;
636
637        public ImageData(Bitmap bitmap, int rotation) {
638            this.bitmap = bitmap;
639            this.rotation = rotation;
640        }
641    }
642
643    private static int getRotated(int degree, int original, int theother) {
644        return ((degree / 90) & 1) == 0 ? original : theother;
645    }
646
647    private class ScreenNailEntry {
648        private boolean mVisible;
649        private boolean mEnabled;
650
651        private int mRotation;
652        private int mDrawWidth;
653        private int mDrawHeight;
654        private int mOffsetX;
655
656        private BitmapTexture mTexture;
657
658        public void set(boolean enabled, Bitmap bitmap, int rotation) {
659            mEnabled = enabled;
660            mRotation = rotation;
661            if (bitmap == null) {
662                if (mTexture != null) mTexture.recycle();
663                mTexture = null;
664            } else {
665                if (mTexture != null) {
666                    if (mTexture.getBitmap() != bitmap) {
667                        mTexture.recycle();
668                        mTexture = new BitmapTexture(bitmap);
669                    }
670                } else {
671                    mTexture = new BitmapTexture(bitmap);
672                }
673                updateDrawingSize();
674            }
675        }
676
677        public void layoutRightEdgeAt(int x) {
678            mVisible = x > 0;
679            mOffsetX = x - getRotated(
680                    mRotation, mDrawWidth, mDrawHeight) / 2;
681        }
682
683        public void layoutLeftEdgeAt(int x) {
684            mVisible = x < getWidth();
685            mOffsetX = x + getRotated(
686                    mRotation, mDrawWidth, mDrawHeight) / 2;
687        }
688
689        public int gapToSide() {
690            return ((mRotation / 90) & 1) != 0
691                    ? PhotoView.gapToSide(mDrawHeight, getWidth())
692                    : PhotoView.gapToSide(mDrawWidth, getWidth());
693        }
694
695        public void updateDrawingSize() {
696            if (mTexture == null) return;
697
698            int width = mTexture.getWidth();
699            int height = mTexture.getHeight();
700
701            // Calculate the initial scale that will used by PositionController
702            // (usually fit-to-screen)
703            float s = ((mRotation / 90) & 0x01) == 0
704                    ? mPositionController.getMinimalScale(width, height)
705                    : mPositionController.getMinimalScale(height, width);
706
707            mDrawWidth = Math.round(width * s);
708            mDrawHeight = Math.round(height * s);
709        }
710
711        public boolean isEnabled() {
712            return mEnabled;
713        }
714
715        public void draw(GLCanvas canvas) {
716            int x = mOffsetX;
717            int y = getHeight() / 2;
718
719            if (mTexture != null) {
720                if (mRotation != 0) {
721                    canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
722                    canvas.translate(x, y, 0);
723                    canvas.rotate(mRotation, 0, 0, 1); //mRotation
724                    canvas.translate(-x, -y, 0);
725                }
726                mTexture.draw(canvas, x - mDrawWidth / 2, y - mDrawHeight / 2,
727                        mDrawWidth, mDrawHeight);
728                if (mRotation != 0) {
729                    canvas.restore();
730                }
731            }
732        }
733    }
734
735    public void pause() {
736        mPositionController.skipAnimation();
737        mTransitionMode = TRANS_NONE;
738        mTileView.freeTextures();
739        for (ScreenNailEntry entry : mScreenNails) {
740            entry.set(false, null, 0);
741        }
742    }
743
744    public void resume() {
745        mTileView.prepareTextures();
746    }
747
748    public void setOpenedItem(Path itemPath) {
749        mOpenedItemPath = itemPath;
750    }
751
752    public void showVideoPlayIcon(boolean show) {
753        mShowVideoPlayIcon = show;
754    }
755
756    // Returns the position saved by the previous page.
757    public Position retrieveSavedPosition() {
758        if (mOpenedItemPath != null) {
759            Position position = PositionRepository
760                    .getInstance(mActivity).get(Long.valueOf(
761                    System.identityHashCode(mOpenedItemPath)));
762            mOpenedItemPath = null;
763            return position;
764        }
765        return null;
766    }
767
768    public void openAnimationStarted() {
769        mTransitionMode = TRANS_OPEN_ANIMATION;
770    }
771
772    public boolean isInTransition() {
773        return mTransitionMode != TRANS_NONE;
774    }
775}
776