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    @Override
120    public void setImageBitmap(Bitmap bitmap) {
121        setImageBitmap(bitmap, 0);
122    }
123
124    private void setImageBitmap(Bitmap bitmap, int rotation) {
125        super.setImageBitmap(bitmap);
126        Drawable d = getDrawable();
127        if (d != null) {
128            d.setDither(true);
129        }
130
131        Bitmap old = mBitmapDisplayed.getBitmap();
132        mBitmapDisplayed.setBitmap(bitmap);
133        mBitmapDisplayed.setRotation(rotation);
134
135        if (old != null && old != bitmap && mRecycler != null) {
136            mRecycler.recycle(old);
137        }
138    }
139
140    public void clear() {
141        setImageBitmapResetBase(null, true);
142    }
143
144    private Runnable mOnLayoutRunnable = null;
145
146    // This function changes bitmap, reset base matrix according to the size
147    // of the bitmap, and optionally reset the supplementary matrix.
148    public void setImageBitmapResetBase(final Bitmap bitmap,
149            final boolean resetSupp) {
150        setImageRotateBitmapResetBase(new RotateBitmap(bitmap), resetSupp);
151    }
152
153    public void setImageRotateBitmapResetBase(final RotateBitmap bitmap,
154            final boolean resetSupp) {
155        final int viewWidth = getWidth();
156
157        if (viewWidth <= 0)  {
158            mOnLayoutRunnable = new Runnable() {
159                public void run() {
160                    setImageRotateBitmapResetBase(bitmap, resetSupp);
161                }
162            };
163            return;
164        }
165
166        if (bitmap.getBitmap() != null) {
167            getProperBaseMatrix(bitmap, mBaseMatrix);
168            setImageBitmap(bitmap.getBitmap(), bitmap.getRotation());
169        } else {
170            mBaseMatrix.reset();
171            setImageBitmap(null);
172        }
173
174        if (resetSupp) {
175            mSuppMatrix.reset();
176        }
177        setImageMatrix(getImageViewMatrix());
178        mMaxZoom = maxZoom();
179    }
180
181    // Center as much as possible in one or both axis.  Centering is
182    // defined as follows:  if the image is scaled down below the
183    // view's dimensions then center it (literally).  If the image
184    // is scaled larger than the view and is translated out of view
185    // then translate it back into view (i.e. eliminate black bars).
186    protected void center(boolean horizontal, boolean vertical) {
187        if (mBitmapDisplayed.getBitmap() == null) {
188            return;
189        }
190
191        Matrix m = getImageViewMatrix();
192
193        RectF rect = new RectF(0, 0,
194                mBitmapDisplayed.getBitmap().getWidth(),
195                mBitmapDisplayed.getBitmap().getHeight());
196
197        m.mapRect(rect);
198
199        float height = rect.height();
200        float width  = rect.width();
201
202        float deltaX = 0, deltaY = 0;
203
204        if (vertical) {
205            int viewHeight = getHeight();
206            if (height < viewHeight) {
207                deltaY = (viewHeight - height) / 2 - rect.top;
208            } else if (rect.top > 0) {
209                deltaY = -rect.top;
210            } else if (rect.bottom < viewHeight) {
211                deltaY = getHeight() - rect.bottom;
212            }
213        }
214
215        if (horizontal) {
216            int viewWidth = getWidth();
217            if (width < viewWidth) {
218                deltaX = (viewWidth - width) / 2 - rect.left;
219            } else if (rect.left > 0) {
220                deltaX = -rect.left;
221            } else if (rect.right < viewWidth) {
222                deltaX = viewWidth - rect.right;
223            }
224        }
225
226        postTranslate(deltaX, deltaY);
227        setImageMatrix(getImageViewMatrix());
228    }
229
230    public ImageViewTouchBase(Context context) {
231        super(context);
232        init();
233    }
234
235    public ImageViewTouchBase(Context context, AttributeSet attrs) {
236        super(context, attrs);
237        init();
238    }
239
240    private void init() {
241        setScaleType(ImageView.ScaleType.MATRIX);
242    }
243
244    protected float getValue(Matrix matrix, int whichValue) {
245        matrix.getValues(mMatrixValues);
246        return mMatrixValues[whichValue];
247    }
248
249    // Get the scale factor out of the matrix.
250    protected float getScale(Matrix matrix) {
251        return getValue(matrix, Matrix.MSCALE_X);
252    }
253
254    protected float getScale() {
255        return getScale(mSuppMatrix);
256    }
257
258    // Setup the base matrix so that the image is centered and scaled properly.
259    private void getProperBaseMatrix(RotateBitmap bitmap, Matrix matrix) {
260        float viewWidth = getWidth();
261        float viewHeight = getHeight();
262
263        float w = bitmap.getWidth();
264        float h = bitmap.getHeight();
265        matrix.reset();
266
267        // We limit up-scaling to 3x otherwise the result may look bad if it's
268        // a small icon.
269        float widthScale = Math.min(viewWidth / w, 3.0f);
270        float heightScale = Math.min(viewHeight / h, 3.0f);
271        float scale = Math.min(widthScale, heightScale);
272
273        matrix.postConcat(bitmap.getRotateMatrix());
274        matrix.postScale(scale, scale);
275
276        matrix.postTranslate(
277                (viewWidth  - w * scale) / 2F,
278                (viewHeight - h * scale) / 2F);
279    }
280
281    // Combine the base matrix and the supp matrix to make the final matrix.
282    protected Matrix getImageViewMatrix() {
283        // The final matrix is computed as the concatentation of the base matrix
284        // and the supplementary matrix.
285        mDisplayMatrix.set(mBaseMatrix);
286        mDisplayMatrix.postConcat(mSuppMatrix);
287        return mDisplayMatrix;
288    }
289
290    static final float SCALE_RATE = 1.25F;
291
292    // Sets the maximum zoom, which is a scale relative to the base matrix. It
293    // is calculated to show the image at 400% zoom regardless of screen or
294    // image orientation. If in the future we decode the full 3 megapixel image,
295    // rather than the current 1024x768, this should be changed down to 200%.
296    protected float maxZoom() {
297        if (mBitmapDisplayed.getBitmap() == null) {
298            return 1F;
299        }
300
301        float fw = (float) mBitmapDisplayed.getWidth()  / (float) mThisWidth;
302        float fh = (float) mBitmapDisplayed.getHeight() / (float) mThisHeight;
303        float max = Math.max(fw, fh) * 4;
304        return max;
305    }
306
307    protected void zoomTo(float scale, float centerX, float centerY) {
308        if (scale > mMaxZoom) {
309            scale = mMaxZoom;
310        }
311
312        float oldScale = getScale();
313        float deltaScale = scale / oldScale;
314
315        mSuppMatrix.postScale(deltaScale, deltaScale, centerX, centerY);
316        setImageMatrix(getImageViewMatrix());
317        center(true, true);
318    }
319
320    protected void zoomTo(final float scale, final float centerX,
321                          final float centerY, final float durationMs) {
322        final float incrementPerMs = (scale - getScale()) / durationMs;
323        final float oldScale = getScale();
324        final long startTime = System.currentTimeMillis();
325
326        mHandler.post(new Runnable() {
327            public void run() {
328                long now = System.currentTimeMillis();
329                float currentMs = Math.min(durationMs, now - startTime);
330                float target = oldScale + (incrementPerMs * currentMs);
331                zoomTo(target, centerX, centerY);
332
333                if (currentMs < durationMs) {
334                    mHandler.post(this);
335                }
336            }
337        });
338    }
339
340    protected void zoomTo(float scale) {
341        float cx = getWidth() / 2F;
342        float cy = getHeight() / 2F;
343
344        zoomTo(scale, cx, cy);
345    }
346
347    protected void zoomToPoint(float scale, float pointX, float pointY) {
348        float cx = getWidth() / 2F;
349        float cy = getHeight() / 2F;
350
351        panBy(cx - pointX, cy - pointY);
352        zoomTo(scale, cx, cy);
353    }
354
355    protected void zoomIn() {
356        zoomIn(SCALE_RATE);
357    }
358
359    protected void zoomOut() {
360        zoomOut(SCALE_RATE);
361    }
362
363    protected void zoomIn(float rate) {
364        if (getScale() >= mMaxZoom) {
365            return;     // Don't let the user zoom into the molecular level.
366        }
367        if (mBitmapDisplayed.getBitmap() == null) {
368            return;
369        }
370
371        float cx = getWidth() / 2F;
372        float cy = getHeight() / 2F;
373
374        mSuppMatrix.postScale(rate, rate, cx, cy);
375        setImageMatrix(getImageViewMatrix());
376    }
377
378    protected void zoomOut(float rate) {
379        if (mBitmapDisplayed.getBitmap() == null) {
380            return;
381        }
382
383        float cx = getWidth() / 2F;
384        float cy = getHeight() / 2F;
385
386        // Zoom out to at most 1x.
387        Matrix tmp = new Matrix(mSuppMatrix);
388        tmp.postScale(1F / rate, 1F / rate, cx, cy);
389
390        if (getScale(tmp) < 1F) {
391            mSuppMatrix.setScale(1F, 1F, cx, cy);
392        } else {
393            mSuppMatrix.postScale(1F / rate, 1F / rate, cx, cy);
394        }
395        setImageMatrix(getImageViewMatrix());
396        center(true, true);
397    }
398
399    protected void postTranslate(float dx, float dy) {
400        mSuppMatrix.postTranslate(dx, dy);
401    }
402
403    protected void panBy(float dx, float dy) {
404        postTranslate(dx, dy);
405        setImageMatrix(getImageViewMatrix());
406    }
407}
408