ImageViewTouchBase.java revision be2f475fe49538e38753ad391fb3176f9b0d1c69
1/*
2 * Copyright (C) 2009 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.camera;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.Matrix;
22import android.graphics.RectF;
23import android.graphics.drawable.Drawable;
24import android.os.Handler;
25import android.util.AttributeSet;
26import android.view.KeyEvent;
27import android.widget.ImageView;
28
29abstract class ImageViewTouchBase extends ImageView {
30
31    @SuppressWarnings("unused")
32    private static final String TAG = "ImageViewTouchBase";
33
34    // This is the base transformation which is used to show the image
35    // initially.  The current computation for this shows the image in
36    // it's entirety, letterboxing as needed.  One could choose to
37    // show the image as cropped instead.
38    //
39    // This matrix is recomputed when we go from the thumbnail image to
40    // the full size image.
41    protected Matrix mBaseMatrix = new Matrix();
42
43    // This is the supplementary transformation which reflects what
44    // the user has done in terms of zooming and panning.
45    //
46    // This matrix remains the same when we go from the thumbnail image
47    // to the full size image.
48    protected Matrix mSuppMatrix = new Matrix();
49
50    // This is the final matrix which is computed as the concatentation
51    // of the base matrix and the supplementary matrix.
52    private final Matrix mDisplayMatrix = new Matrix();
53
54    // Temporary buffer used for getting the values out of a matrix.
55    private final float[] mMatrixValues = new float[9];
56
57    // The current bitmap being displayed.
58    protected final RotateBitmap mBitmapDisplayed = new RotateBitmap(null);
59
60    int mThisWidth = -1, mThisHeight = -1;
61
62    float mMaxZoom;
63
64    // ImageViewTouchBase will pass a Bitmap to the Recycler if it has finished
65    // its use of that Bitmap.
66    public interface Recycler {
67        public void recycle(Bitmap b);
68    }
69
70    public void setRecycler(Recycler r) {
71        mRecycler = r;
72    }
73
74    private Recycler mRecycler;
75
76    @Override
77    protected void onLayout(boolean changed, int left, int top,
78                            int right, int bottom) {
79        super.onLayout(changed, left, top, right, bottom);
80        mThisWidth = right - left;
81        mThisHeight = bottom - top;
82        Runnable r = mOnLayoutRunnable;
83        if (r != null) {
84            mOnLayoutRunnable = null;
85            r.run();
86        }
87        if (mBitmapDisplayed.getBitmap() != null) {
88            getProperBaseMatrix(mBitmapDisplayed, mBaseMatrix);
89            setImageMatrix(getImageViewMatrix());
90        }
91    }
92
93    @Override
94    public boolean onKeyDown(int keyCode, KeyEvent event) {
95        if (keyCode == KeyEvent.KEYCODE_BACK
96                && event.getRepeatCount() == 0) {
97            event.startTracking();
98            return true;
99        }
100        return super.onKeyDown(keyCode, event);
101    }
102
103    @Override
104    public boolean onKeyUp(int keyCode, KeyEvent event) {
105        if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
106                && !event.isCanceled()) {
107            if (getScale() > 1.0f) {
108                // If we're zoomed in, pressing Back jumps out to show the
109                // entire image, otherwise Back returns the user to the gallery.
110                zoomTo(1.0f);
111                return true;
112            }
113        }
114        return super.onKeyUp(keyCode, event);
115    }
116
117    protected Handler mHandler = new Handler();
118
119    protected int mLastXTouchPos;
120    protected int mLastYTouchPos;
121
122    @Override
123    public void setImageBitmap(Bitmap bitmap) {
124        setImageBitmap(bitmap, 0);
125    }
126
127    private void setImageBitmap(Bitmap bitmap, int rotation) {
128        super.setImageBitmap(bitmap);
129        Drawable d = getDrawable();
130        if (d != null) {
131            d.setDither(true);
132        }
133
134        Bitmap old = mBitmapDisplayed.getBitmap();
135        mBitmapDisplayed.setBitmap(bitmap);
136        mBitmapDisplayed.setRotation(rotation);
137
138        if (old != null && old != bitmap && mRecycler != null) {
139            mRecycler.recycle(old);
140        }
141    }
142
143    public void clear() {
144        setImageBitmapResetBase(null, true);
145    }
146
147    private Runnable mOnLayoutRunnable = null;
148
149    // This function changes bitmap, reset base matrix according to the size
150    // of the bitmap, and optionally reset the supplementary matrix.
151    public void setImageBitmapResetBase(final Bitmap bitmap,
152            final boolean resetSupp) {
153        setImageRotateBitmapResetBase(new RotateBitmap(bitmap), resetSupp);
154    }
155
156    public void setImageRotateBitmapResetBase(final RotateBitmap bitmap,
157            final boolean resetSupp) {
158        final int viewWidth = getWidth();
159
160        if (viewWidth <= 0)  {
161            mOnLayoutRunnable = new Runnable() {
162                public void run() {
163                    setImageRotateBitmapResetBase(bitmap, resetSupp);
164                }
165            };
166            return;
167        }
168
169        if (bitmap.getBitmap() != null) {
170            getProperBaseMatrix(bitmap, mBaseMatrix);
171            setImageBitmap(bitmap.getBitmap(), bitmap.getRotation());
172        } else {
173            mBaseMatrix.reset();
174            setImageBitmap(null);
175        }
176
177        if (resetSupp) {
178            mSuppMatrix.reset();
179        }
180        setImageMatrix(getImageViewMatrix());
181        mMaxZoom = maxZoom();
182    }
183
184    // Center as much as possible in one or both axis.  Centering is
185    // defined as follows:  if the image is scaled down below the
186    // view's dimensions then center it (literally).  If the image
187    // is scaled larger than the view and is translated out of view
188    // then translate it back into view (i.e. eliminate black bars).
189    protected void center(boolean horizontal, boolean vertical) {
190        if (mBitmapDisplayed.getBitmap() == null) {
191            return;
192        }
193
194        Matrix m = getImageViewMatrix();
195
196        RectF rect = new RectF(0, 0,
197                mBitmapDisplayed.getBitmap().getWidth(),
198                mBitmapDisplayed.getBitmap().getHeight());
199
200        m.mapRect(rect);
201
202        float height = rect.height();
203        float width  = rect.width();
204
205        float deltaX = 0, deltaY = 0;
206
207        if (vertical) {
208            int viewHeight = getHeight();
209            if (height < viewHeight) {
210                deltaY = (viewHeight - height) / 2 - rect.top;
211            } else if (rect.top > 0) {
212                deltaY = -rect.top;
213            } else if (rect.bottom < viewHeight) {
214                deltaY = getHeight() - rect.bottom;
215            }
216        }
217
218        if (horizontal) {
219            int viewWidth = getWidth();
220            if (width < viewWidth) {
221                deltaX = (viewWidth - width) / 2 - rect.left;
222            } else if (rect.left > 0) {
223                deltaX = -rect.left;
224            } else if (rect.right < viewWidth) {
225                deltaX = viewWidth - rect.right;
226            }
227        }
228
229        postTranslate(deltaX, deltaY);
230        setImageMatrix(getImageViewMatrix());
231    }
232
233    public ImageViewTouchBase(Context context) {
234        super(context);
235        init();
236    }
237
238    public ImageViewTouchBase(Context context, AttributeSet attrs) {
239        super(context, attrs);
240        init();
241    }
242
243    private void init() {
244        setScaleType(ImageView.ScaleType.MATRIX);
245    }
246
247    protected float getValue(Matrix matrix, int whichValue) {
248        matrix.getValues(mMatrixValues);
249        return mMatrixValues[whichValue];
250    }
251
252    // Get the scale factor out of the matrix.
253    protected float getScale(Matrix matrix) {
254        return getValue(matrix, Matrix.MSCALE_X);
255    }
256
257    protected float getScale() {
258        return getScale(mSuppMatrix);
259    }
260
261    // Setup the base matrix so that the image is centered and scaled properly.
262    private void getProperBaseMatrix(RotateBitmap bitmap, Matrix matrix) {
263        float viewWidth = getWidth();
264        float viewHeight = getHeight();
265
266        float w = bitmap.getWidth();
267        float h = bitmap.getHeight();
268        matrix.reset();
269
270        // We limit up-scaling to 3x otherwise the result may look bad if it's
271        // a small icon.
272        float widthScale = Math.min(viewWidth / w, 3.0f);
273        float heightScale = Math.min(viewHeight / h, 3.0f);
274        float scale = Math.min(widthScale, heightScale);
275
276        matrix.postConcat(bitmap.getRotateMatrix());
277        matrix.postScale(scale, scale);
278
279        matrix.postTranslate(
280                (viewWidth  - w * scale) / 2F,
281                (viewHeight - h * scale) / 2F);
282    }
283
284    // Combine the base matrix and the supp matrix to make the final matrix.
285    protected Matrix getImageViewMatrix() {
286        // The final matrix is computed as the concatentation of the base matrix
287        // and the supplementary matrix.
288        mDisplayMatrix.set(mBaseMatrix);
289        mDisplayMatrix.postConcat(mSuppMatrix);
290        return mDisplayMatrix;
291    }
292
293    static final float SCALE_RATE = 1.25F;
294
295    // Sets the maximum zoom, which is a scale relative to the base matrix. It
296    // is calculated to show the image at 400% zoom regardless of screen or
297    // image orientation. If in the future we decode the full 3 megapixel image,
298    // rather than the current 1024x768, this should be changed down to 200%.
299    protected float maxZoom() {
300        if (mBitmapDisplayed.getBitmap() == null) {
301            return 1F;
302        }
303
304        float fw = (float) mBitmapDisplayed.getWidth()  / (float) mThisWidth;
305        float fh = (float) mBitmapDisplayed.getHeight() / (float) mThisHeight;
306        float max = Math.max(fw, fh) * 4;
307        return max;
308    }
309
310    protected void zoomTo(float scale, float centerX, float centerY) {
311        if (scale > mMaxZoom) {
312            scale = mMaxZoom;
313        }
314
315        float oldScale = getScale();
316        float deltaScale = scale / oldScale;
317
318        mSuppMatrix.postScale(deltaScale, deltaScale, centerX, centerY);
319        setImageMatrix(getImageViewMatrix());
320        center(true, true);
321    }
322
323    protected void zoomTo(final float scale, final float centerX,
324                          final float centerY, final float durationMs) {
325        final float incrementPerMs = (scale - getScale()) / durationMs;
326        final float oldScale = getScale();
327        final long startTime = System.currentTimeMillis();
328
329        mHandler.post(new Runnable() {
330            public void run() {
331                long now = System.currentTimeMillis();
332                float currentMs = Math.min(durationMs, now - startTime);
333                float target = oldScale + (incrementPerMs * currentMs);
334                zoomTo(target, centerX, centerY);
335
336                if (currentMs < durationMs) {
337                    mHandler.post(this);
338                }
339            }
340        });
341    }
342
343    protected void zoomTo(float scale) {
344        float cx = getWidth() / 2F;
345        float cy = getHeight() / 2F;
346
347        zoomTo(scale, cx, cy);
348    }
349
350    protected void zoomToPoint(float scale, float pointX, float pointY) {
351        float cx = getWidth() / 2F;
352        float cy = getHeight() / 2F;
353
354        panBy(cx - pointX, cy - pointY);
355        zoomTo(scale, cx, cy);
356    }
357
358    protected void zoomIn() {
359        zoomIn(SCALE_RATE);
360    }
361
362    protected void zoomOut() {
363        zoomOut(SCALE_RATE);
364    }
365
366    protected void zoomIn(float rate) {
367        if (getScale() >= mMaxZoom) {
368            return;     // Don't let the user zoom into the molecular level.
369        }
370        if (mBitmapDisplayed.getBitmap() == null) {
371            return;
372        }
373
374        float cx = getWidth() / 2F;
375        float cy = getHeight() / 2F;
376
377        mSuppMatrix.postScale(rate, rate, cx, cy);
378        setImageMatrix(getImageViewMatrix());
379    }
380
381    protected void zoomOut(float rate) {
382        if (mBitmapDisplayed.getBitmap() == null) {
383            return;
384        }
385
386        float cx = getWidth() / 2F;
387        float cy = getHeight() / 2F;
388
389        // Zoom out to at most 1x.
390        Matrix tmp = new Matrix(mSuppMatrix);
391        tmp.postScale(1F / rate, 1F / rate, cx, cy);
392
393        if (getScale(tmp) < 1F) {
394            mSuppMatrix.setScale(1F, 1F, cx, cy);
395        } else {
396            mSuppMatrix.postScale(1F / rate, 1F / rate, cx, cy);
397        }
398        setImageMatrix(getImageViewMatrix());
399        center(true, true);
400    }
401
402    protected void postTranslate(float dx, float dy) {
403        mSuppMatrix.postTranslate(dx, dy);
404    }
405
406    protected void panBy(float dx, float dy) {
407        postTranslate(dx, dy);
408        setImageMatrix(getImageViewMatrix());
409    }
410}
411