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