PositionController.java revision 532d93caddc91a7aa33ca113adbc0b8255d498eb
1/*
2 * Copyright (C) 2011 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;
34import android.widget.Scroller;
35
36class PositionController {
37    private static final String TAG = "PositionController";
38    private long mAnimationStartTime = NO_ANIMATION;
39    private static final long NO_ANIMATION = -1;
40    private static final long LAST_ANIMATION = -2;
41
42    private int mAnimationKind;
43    private float mAnimationDuration;
44    private final static int ANIM_KIND_SCROLL = 0;
45    private final static int ANIM_KIND_SCALE = 1;
46    private final static int ANIM_KIND_SNAPBACK = 2;
47    private final static int ANIM_KIND_SLIDE = 3;
48    private final static int ANIM_KIND_ZOOM = 4;
49    private final static int ANIM_KIND_FLING = 5;
50
51    // Animation time in milliseconds. The order must match ANIM_KIND_* above.
52    private final static int ANIM_TIME[] = {
53        0,    // ANIM_KIND_SCROLL
54        50,   // ANIM_KIND_SCALE
55        600,  // ANIM_KIND_SNAPBACK
56        400,  // ANIM_KIND_SLIDE
57        300,  // ANIM_KIND_ZOOM
58        0,    // ANIM_KIND_FLING (the duration is calculated dynamically)
59    };
60
61    // We try to scale up the image to fill the screen. But in order not to
62    // scale too much for small icons, we limit the max up-scaling factor here.
63    private static final float SCALE_LIMIT = 4;
64
65    private PhotoView mViewer;
66    private EdgeView mEdgeView;
67    private int mImageW, mImageH;
68    private int mViewW, mViewH;
69
70    // The X, Y are the coordinate on bitmap which shows on the center of
71    // the view. We always keep the mCurrent{X,Y,Scale} sync with the actual
72    // values used currently.
73    private int mCurrentX, mFromX, mToX;
74    private int mCurrentY, mFromY, mToY;
75    private float mCurrentScale, mFromScale, mToScale;
76
77    // The focus point of the scaling gesture (in bitmap coordinates).
78    private int mFocusBitmapX;
79    private int mFocusBitmapY;
80    private boolean mInScale;
81
82    // The minimum and maximum scale we allow.
83    private float mScaleMin, mScaleMax = SCALE_LIMIT;
84
85    // This is used by the fling animation
86    private FlingScroller mScroller;
87
88    // The bound of the stable region, see the comments above
89    // calculateStableBound() for details.
90    private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;
91
92    // Assume the image size is the same as view size before we know the actual
93    // size of image.
94    private boolean mUseViewSize = true;
95
96    private RectF mTempRect = new RectF();
97    private float[] mTempPoints = new float[8];
98
99    public PositionController(PhotoView viewer, Context context,
100            EdgeView edgeView) {
101        mViewer = viewer;
102        mEdgeView = edgeView;
103        mScroller = new FlingScroller();
104    }
105
106    public void setImageSize(int width, int height) {
107
108        // If no image available, use view size.
109        if (width == 0 || height == 0) {
110            mUseViewSize = true;
111            mImageW = mViewW;
112            mImageH = mViewH;
113            mCurrentX = mImageW / 2;
114            mCurrentY = mImageH / 2;
115            mCurrentScale = 1;
116            mScaleMin = 1;
117            mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
118            return;
119        }
120
121        mUseViewSize = false;
122
123        float ratio = Math.min(
124                (float) mImageW / width, (float) mImageH / height);
125
126        // See the comment above translate() for details.
127        mCurrentX = translate(mCurrentX, mImageW, width, ratio);
128        mCurrentY = translate(mCurrentY, mImageH, height, ratio);
129        mCurrentScale = mCurrentScale * ratio;
130
131        mFromX = translate(mFromX, mImageW, width, ratio);
132        mFromY = translate(mFromY, mImageH, height, ratio);
133        mFromScale = mFromScale * ratio;
134
135        mToX = translate(mToX, mImageW, width, ratio);
136        mToY = translate(mToY, mImageH, height, ratio);
137        mToScale = mToScale * ratio;
138
139        mFocusBitmapX = translate(mFocusBitmapX, mImageW, width, ratio);
140        mFocusBitmapY = translate(mFocusBitmapY, mImageH, height, ratio);
141
142        mImageW = width;
143        mImageH = height;
144
145        mScaleMin = getMinimalScale(mImageW, mImageH);
146
147        // Start animation from the saved position if we have one.
148        Position position = mViewer.retrieveSavedPosition();
149        if (position != null) {
150            // The animation starts from 240 pixels and centers at the image
151            // at the saved position.
152            float scale = 240f / Math.min(width, height);
153            mCurrentX = Math.round((mViewW / 2f - position.x) / scale) + mImageW / 2;
154            mCurrentY = Math.round((mViewH / 2f - position.y) / scale) + mImageH / 2;
155            mCurrentScale = scale;
156            mViewer.openAnimationStarted();
157            startSnapback();
158        } else if (mAnimationStartTime == NO_ANIMATION) {
159            mCurrentScale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
160        }
161        mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
162    }
163
164    public void zoomIn(float tapX, float tapY, float targetScale) {
165        if (targetScale > mScaleMax) targetScale = mScaleMax;
166
167        // Convert the tap position to image coordinate
168        int tempX = Math.round((tapX - mViewW / 2) / mCurrentScale + mCurrentX);
169        int tempY = Math.round((tapY - mViewH / 2) / mCurrentScale + mCurrentY);
170
171        calculateStableBound(targetScale);
172        int targetX = Utils.clamp(tempX, mBoundLeft, mBoundRight);
173        int targetY = Utils.clamp(tempY, mBoundTop, mBoundBottom);
174
175        startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
176    }
177
178    public void resetToFullView() {
179        startAnimation(mImageW / 2, mImageH / 2, mScaleMin, ANIM_KIND_ZOOM);
180    }
181
182    public float getMinimalScale(int w, int h) {
183        return Math.min(SCALE_LIMIT,
184                Math.min((float) mViewW / w, (float) mViewH / h));
185    }
186
187    // Translate a coordinate on bitmap if the bitmap size changes.
188    // If the aspect ratio doesn't change, it's easy:
189    //
190    //         r  = w / w' (= h / h')
191    //         x' = x / r
192    //         y' = y / r
193    //
194    // However the aspect ratio may change. That happens when the user slides
195    // a image before it's loaded, we don't know the actual aspect ratio, so
196    // we will assume one. When we receive the actual bitmap size, we need to
197    // translate the coordinate from the old bitmap into the new bitmap.
198    //
199    // What we want to do is center the bitmap at the original position.
200    //
201    //         ...+--+...
202    //         .  |  |  .
203    //         .  |  |  .
204    //         ...+--+...
205    //
206    // First we scale down the new bitmap by a factor r = min(w/w', h/h').
207    // Overlay it onto the original bitmap. Now (0, 0) of the old bitmap maps
208    // to (-(w-w'*r)/2 / r, -(h-h'*r)/2 / r) in the new bitmap. So (x, y) of
209    // the old bitmap maps to (x', y') in the new bitmap, where
210    //         x' = (x-(w-w'*r)/2) / r = w'/2 + (x-w/2)/r
211    //         y' = (y-(h-h'*r)/2) / r = h'/2 + (y-h/2)/r
212    private static int translate(int value, int size, int newSize, float ratio) {
213        return Math.round(newSize / 2f + (value - size / 2f) / ratio);
214    }
215
216    public void setViewSize(int viewW, int viewH) {
217        boolean needLayout = mViewW == 0 || mViewH == 0;
218
219        mViewW = viewW;
220        mViewH = viewH;
221
222        if (mUseViewSize) {
223            mImageW = viewW;
224            mImageH = viewH;
225            mCurrentX = mImageW / 2;
226            mCurrentY = mImageH / 2;
227            mCurrentScale = 1;
228            mScaleMin = 1;
229            mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
230            return;
231        }
232
233        // In most cases we want to keep the scaling factor intact when the
234        // view size changes. The cases we want to reset the scaling factor
235        // (to fit the view if possible) are (1) the scaling factor is too
236        // small for the new view size (2) the scaling factor has not been
237        // changed by the user.
238        boolean wasMinScale = (mCurrentScale == mScaleMin);
239        mScaleMin = getMinimalScale(mImageW, mImageH);
240
241        if (needLayout || mCurrentScale < mScaleMin || wasMinScale) {
242            mCurrentX = mImageW / 2;
243            mCurrentY = mImageH / 2;
244            mCurrentScale = mScaleMin;
245            mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
246        }
247    }
248
249    public void stopAnimation() {
250        mAnimationStartTime = NO_ANIMATION;
251    }
252
253    public void skipAnimation() {
254        if (mAnimationStartTime == NO_ANIMATION) return;
255        mAnimationStartTime = NO_ANIMATION;
256        mCurrentX = mToX;
257        mCurrentY = mToY;
258        mCurrentScale = mToScale;
259    }
260
261    public void beginScale(float focusX, float focusY) {
262        mInScale = true;
263        mFocusBitmapX = Math.round(mCurrentX +
264                (focusX - mViewW / 2f) / mCurrentScale);
265        mFocusBitmapY = Math.round(mCurrentY +
266                (focusY - mViewH / 2f) / mCurrentScale);
267    }
268
269    public void scaleBy(float s, float focusX, float focusY) {
270
271        // We want to keep the focus point (on the bitmap) the same as when
272        // we begin the scale guesture, that is,
273        //
274        // mCurrentX' + (focusX - mViewW / 2f) / scale = mFocusBitmapX
275        //
276        s *= getTargetScale();
277        int x = Math.round(mFocusBitmapX - (focusX - mViewW / 2f) / s);
278        int y = Math.round(mFocusBitmapY - (focusY - mViewH / 2f) / s);
279
280        startAnimation(x, y, s, ANIM_KIND_SCALE);
281    }
282
283    public void endScale() {
284        mInScale = false;
285        startSnapbackIfNeeded();
286    }
287
288    public float getCurrentScale() {
289        return mCurrentScale;
290    }
291
292    public boolean isAtMinimalScale() {
293        return isAlmostEquals(mCurrentScale, mScaleMin);
294    }
295
296    private static boolean isAlmostEquals(float a, float b) {
297        float diff = a - b;
298        return (diff < 0 ? -diff : diff) < 0.02f;
299    }
300
301    public void up() {
302        startSnapback();
303    }
304
305    //             |<--| (1/2) * mImageW
306    // +-------+-------+-------+
307    // |       |       |       |
308    // |       |   o   |       |
309    // |       |       |       |
310    // +-------+-------+-------+
311    // |<----------| (3/2) * mImageW
312    // Slide in the image from left or right.
313    // Precondition: mCurrentScale = 1 (mView{W|H} == mImage{W|H}).
314    // Sliding from left:  mCurrentX = (1/2) * mImageW
315    //              right: mCurrentX = (3/2) * mImageW
316    public void startSlideInAnimation(int direction) {
317        int fromX = (direction == PhotoView.TRANS_SLIDE_IN_LEFT) ?
318                mImageW / 2 : 3 * mImageW / 2;
319        mFromX = Math.round(fromX);
320        mFromY = Math.round(mImageH / 2f);
321        mCurrentX = mFromX;
322        mCurrentY = mFromY;
323        startAnimation(
324                mImageW / 2, mImageH / 2, mCurrentScale, ANIM_KIND_SLIDE);
325    }
326
327    public void startHorizontalSlide(int distance) {
328        scrollBy(distance, 0, ANIM_KIND_SLIDE);
329    }
330
331    private void scrollBy(float dx, float dy, int type) {
332        startAnimation(getTargetX() + Math.round(dx / mCurrentScale),
333                getTargetY() + Math.round(dy / mCurrentScale),
334                mCurrentScale, type);
335    }
336
337    public void startScroll(float dx, float dy, boolean hasNext,
338            boolean hasPrev) {
339        int x = getTargetX() + Math.round(dx / mCurrentScale);
340        int y = getTargetY() + Math.round(dy / mCurrentScale);
341
342        calculateStableBound(mCurrentScale);
343
344        // Vertical direction: If we have space to move in the vertical
345        // direction, we show the edge effect when scrolling reaches the edge.
346        if (mBoundTop != mBoundBottom) {
347            if (y < mBoundTop) {
348                mEdgeView.onPull(mBoundTop - y, EdgeView.TOP);
349            } else if (y > mBoundBottom) {
350                mEdgeView.onPull(y - mBoundBottom, EdgeView.BOTTOM);
351            }
352        }
353
354        y = Utils.clamp(y, mBoundTop, mBoundBottom);
355
356        // Horizontal direction: we show the edge effect when the scrolling
357        // tries to go left of the first image or go right of the last image.
358        if (!hasPrev && x < mBoundLeft) {
359            int pixels = Math.round((mBoundLeft - x) * mCurrentScale);
360            mEdgeView.onPull(pixels, EdgeView.LEFT);
361            x = mBoundLeft;
362        } else if (!hasNext && x > mBoundRight) {
363            int pixels = Math.round((x - mBoundRight) * mCurrentScale);
364            mEdgeView.onPull(pixels, EdgeView.RIGHT);
365            x = mBoundRight;
366        }
367
368        startAnimation(x, y, mCurrentScale, ANIM_KIND_SCROLL);
369    }
370
371    public boolean fling(float velocityX, float velocityY) {
372        // We only want to do fling when the picture is zoomed-in.
373        if (mImageW * mCurrentScale <= mViewW &&
374            mImageH * mCurrentScale <= mViewH) {
375            return false;
376        }
377
378        calculateStableBound(mCurrentScale);
379        mScroller.fling(mCurrentX, mCurrentY,
380                Math.round(-velocityX / mCurrentScale),
381                Math.round(-velocityY / mCurrentScale),
382                mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
383        int targetX = mScroller.getFinalX();
384        int targetY = mScroller.getFinalY();
385        mAnimationDuration = mScroller.getDuration();
386        startAnimation(targetX, targetY, mCurrentScale, ANIM_KIND_FLING);
387        return true;
388    }
389
390    private void startAnimation(
391            int targetX, int targetY, float scale, int kind) {
392        if (targetX == mCurrentX && targetY == mCurrentY
393                && scale == mCurrentScale) return;
394
395        mFromX = mCurrentX;
396        mFromY = mCurrentY;
397        mFromScale = mCurrentScale;
398
399        mToX = targetX;
400        mToY = targetY;
401        mToScale = Utils.clamp(scale, 0.6f * mScaleMin, 1.4f * mScaleMax);
402
403        // If the scaled height is smaller than the view height,
404        // force it to be in the center.
405        // (We do for height only, not width, because the user may
406        // want to scroll to the previous/next image.)
407        if (Math.floor(mImageH * mToScale) <= mViewH) {
408            mToY = mImageH / 2;
409        }
410
411        mAnimationStartTime = SystemClock.uptimeMillis();
412        mAnimationKind = kind;
413        if (mAnimationKind != ANIM_KIND_FLING) {
414            mAnimationDuration = ANIM_TIME[mAnimationKind];
415        }
416        if (advanceAnimation()) mViewer.invalidate();
417    }
418
419    // Returns true if redraw is needed.
420    public boolean advanceAnimation() {
421        if (mAnimationStartTime == NO_ANIMATION) {
422            return false;
423        } else if (mAnimationStartTime == LAST_ANIMATION) {
424            mAnimationStartTime = NO_ANIMATION;
425            if (mViewer.isInTransition()) {
426                mViewer.notifyTransitionComplete();
427                return false;
428            } else {
429                return startSnapbackIfNeeded();
430            }
431        }
432
433        long now = SystemClock.uptimeMillis();
434        float progress;
435        if (mAnimationDuration == 0) {
436            progress = 1;
437        } else {
438            progress = (now - mAnimationStartTime) / mAnimationDuration;
439        }
440
441        if (progress >= 1) {
442            progress = 1;
443            mCurrentX = mToX;
444            mCurrentY = mToY;
445            mCurrentScale = mToScale;
446            mAnimationStartTime = LAST_ANIMATION;
447        } else {
448            float f = 1 - progress;
449            switch (mAnimationKind) {
450                case ANIM_KIND_SCROLL:
451                case ANIM_KIND_FLING:
452                    progress = 1 - f;  // linear
453                    break;
454                case ANIM_KIND_SCALE:
455                    progress = 1 - f * f;  // quadratic
456                    break;
457                case ANIM_KIND_SNAPBACK:
458                case ANIM_KIND_ZOOM:
459                case ANIM_KIND_SLIDE:
460                    progress = 1 - f * f * f * f * f; // x^5
461                    break;
462            }
463            if (mAnimationKind == ANIM_KIND_FLING) {
464                flingInterpolate(progress);
465            } else {
466                linearInterpolate(progress);
467            }
468        }
469        mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
470        return true;
471    }
472
473    private void flingInterpolate(float progress) {
474        mScroller.computeScrollOffset(progress);
475        int oldX = mCurrentX;
476        int oldY = mCurrentY;
477        mCurrentX = mScroller.getCurrX();
478        mCurrentY = mScroller.getCurrY();
479
480        // Check if we hit the edges; show edge effects if we do.
481        if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
482            int v = Math.round(-mScroller.getCurrVelocityX() * mCurrentScale);
483            mEdgeView.onAbsorb(v, EdgeView.LEFT);
484        } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
485            int v = Math.round(mScroller.getCurrVelocityX() * mCurrentScale);
486            mEdgeView.onAbsorb(v, EdgeView.RIGHT);
487        }
488
489        if (oldY > mBoundTop && mCurrentY == mBoundTop) {
490            int v = Math.round(-mScroller.getCurrVelocityY() * mCurrentScale);
491            mEdgeView.onAbsorb(v, EdgeView.TOP);
492        } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
493            int v = Math.round(mScroller.getCurrVelocityY() * mCurrentScale);
494            mEdgeView.onAbsorb(v, EdgeView.BOTTOM);
495        }
496    }
497
498    // Interpolates mCurrent{X,Y,Scale} given the progress in [0, 1].
499    private void linearInterpolate(float progress) {
500        // To linearly interpolate the position on view coordinates, we do the
501        // following steps:
502        // (1) convert a bitmap position (x, y) to view coordinates:
503        //     from: (x - mFromX) * mFromScale + mViewW / 2
504        //     to: (x - mToX) * mToScale + mViewW / 2
505        // (2) interpolate between the "from" and "to" coordinates:
506        //     (x - mFromX) * mFromScale * (1 - p) + (x - mToX) * mToScale * p
507        //     + mViewW / 2
508        //     should be equal to
509        //     (x - mCurrentX) * mCurrentScale + mViewW / 2
510        // (3) The x-related terms in the above equation can be removed because
511        //     mFromScale * (1 - p) + ToScale * p = mCurrentScale
512        // (4) Solve for mCurrentX, we have mCurrentX =
513        // (mFromX * mFromScale * (1 - p) + mToX * mToScale * p) / mCurrentScale
514        float fromX = mFromX * mFromScale;
515        float toX = mToX * mToScale;
516        float currentX = fromX + progress * (toX - fromX);
517
518        float fromY = mFromY * mFromScale;
519        float toY = mToY * mToScale;
520        float currentY = fromY + progress * (toY - fromY);
521
522        mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
523        mCurrentX = Math.round(currentX / mCurrentScale);
524        mCurrentY = Math.round(currentY / mCurrentScale);
525    }
526
527    // Returns true if redraw is needed.
528    private boolean startSnapbackIfNeeded() {
529        if (mAnimationStartTime != NO_ANIMATION) return false;
530        if (mInScale) return false;
531        if (mAnimationKind == ANIM_KIND_SCROLL && mViewer.isDown()) {
532            return false;
533        }
534        return startSnapback();
535    }
536
537    public boolean startSnapback() {
538        boolean needAnimation = false;
539        float scale = mCurrentScale;
540
541        if (mCurrentScale < mScaleMin || mCurrentScale > mScaleMax) {
542            needAnimation = true;
543            scale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
544        }
545
546        calculateStableBound(scale);
547        int x = Utils.clamp(mCurrentX, mBoundLeft, mBoundRight);
548        int y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom);
549
550        if (mCurrentX != x || mCurrentY != y || mCurrentScale != scale) {
551            needAnimation = true;
552        }
553
554        if (needAnimation) {
555            startAnimation(x, y, scale, ANIM_KIND_SNAPBACK);
556        }
557
558        return needAnimation;
559    }
560
561    // Calculates the stable region of mCurrent{X/Y}, where "stable" means
562    //
563    // (1) If the dimension of scaled image >= view dimension, we will not
564    // see black region outside the image (at that dimension).
565    // (2) If the dimension of scaled image < view dimension, we will center
566    // the scaled image.
567    //
568    // We might temporarily go out of this stable during user interaction,
569    // but will "snap back" after user stops interaction.
570    //
571    // The results are stored in mBound{Left/Right/Top/Bottom}.
572    //
573    private void calculateStableBound(float scale) {
574        // The number of pixels between the center of the view
575        // and the edge when the edge is aligned.
576        mBoundLeft = (int) Math.ceil(mViewW / (2 * scale));
577        mBoundRight = mImageW - mBoundLeft;
578        mBoundTop = (int) Math.ceil(mViewH / (2 * scale));
579        mBoundBottom = mImageH - mBoundTop;
580
581        // If the scaled height is smaller than the view height,
582        // force it to be in the center.
583        if (Math.floor(mImageH * scale) <= mViewH) {
584            mBoundTop = mBoundBottom = mImageH / 2;
585        }
586
587        // Same for width
588        if (Math.floor(mImageW * scale) <= mViewW) {
589            mBoundLeft = mBoundRight = mImageW / 2;
590        }
591    }
592
593    private boolean useCurrentValueAsTarget() {
594        return mAnimationStartTime == NO_ANIMATION ||
595                mAnimationKind == ANIM_KIND_SNAPBACK ||
596                mAnimationKind == ANIM_KIND_FLING;
597    }
598
599    private float getTargetScale() {
600        return useCurrentValueAsTarget() ? mCurrentScale : mToScale;
601    }
602
603    private int getTargetX() {
604        return useCurrentValueAsTarget() ? mCurrentX : mToX;
605    }
606
607    private int getTargetY() {
608        return useCurrentValueAsTarget() ? mCurrentY : mToY;
609    }
610
611    public RectF getImageBounds() {
612        float points[] = mTempPoints;
613
614        /*
615         * (p0,p1)----------(p2,p3)
616         *   |                  |
617         *   |                  |
618         * (p4,p5)----------(p6,p7)
619         */
620        points[0] = points[4] = -mCurrentX;
621        points[1] = points[3] = -mCurrentY;
622        points[2] = points[6] = mImageW - mCurrentX;
623        points[5] = points[7] = mImageH - mCurrentY;
624
625        RectF rect = mTempRect;
626        rect.set(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
627                Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);
628
629        float scale = mCurrentScale;
630        float offsetX = mViewW / 2;
631        float offsetY = mViewH / 2;
632        for (int i = 0; i < 4; ++i) {
633            float x = points[i + i] * scale + offsetX;
634            float y = points[i + i + 1] * scale + offsetY;
635            if (x < rect.left) rect.left = x;
636            if (x > rect.right) rect.right = x;
637            if (y < rect.top) rect.top = y;
638            if (y > rect.bottom) rect.bottom = y;
639        }
640        return rect;
641    }
642
643    public int getImageWidth() {
644        return mImageW;
645    }
646
647    public int getImageHeight() {
648        return mImageH;
649    }
650
651    public boolean isAtLeftEdge() {
652        calculateStableBound(mCurrentScale);
653        return mCurrentX <= mBoundLeft;
654    }
655
656    public boolean isAtRightEdge() {
657        calculateStableBound(mCurrentScale);
658        return mCurrentX >= mBoundRight;
659    }
660}
661