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 
17 package com.android.camera;
18 
19 import android.content.Context;
20 import android.graphics.Bitmap;
21 import android.graphics.Matrix;
22 import android.graphics.RectF;
23 import android.graphics.drawable.Drawable;
24 import android.os.Handler;
25 import android.util.AttributeSet;
26 import android.view.KeyEvent;
27 import android.widget.ImageView;
28 
29 abstract 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 {
recycle(Bitmap b)67         public void recycle(Bitmap b);
68     }
69 
setRecycler(Recycler r)70     public void setRecycler(Recycler r) {
71         mRecycler = r;
72     }
73 
74     private Recycler mRecycler;
75 
76     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)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
onKeyDown(int keyCode, KeyEvent event)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
onKeyUp(int keyCode, KeyEvent event)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
setImageBitmap(Bitmap bitmap)120     public void setImageBitmap(Bitmap bitmap) {
121         setImageBitmap(bitmap, 0);
122     }
123 
setImageBitmap(Bitmap bitmap, int rotation)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 
clear()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.
setImageBitmapResetBase(final Bitmap bitmap, final boolean resetSupp)148     public void setImageBitmapResetBase(final Bitmap bitmap,
149             final boolean resetSupp) {
150         setImageRotateBitmapResetBase(new RotateBitmap(bitmap), resetSupp);
151     }
152 
setImageRotateBitmapResetBase(final RotateBitmap bitmap, final boolean resetSupp)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).
center(boolean horizontal, boolean vertical)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 
ImageViewTouchBase(Context context)230     public ImageViewTouchBase(Context context) {
231         super(context);
232         init();
233     }
234 
ImageViewTouchBase(Context context, AttributeSet attrs)235     public ImageViewTouchBase(Context context, AttributeSet attrs) {
236         super(context, attrs);
237         init();
238     }
239 
init()240     private void init() {
241         setScaleType(ImageView.ScaleType.MATRIX);
242     }
243 
getValue(Matrix matrix, int whichValue)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.
getScale(Matrix matrix)250     protected float getScale(Matrix matrix) {
251         return getValue(matrix, Matrix.MSCALE_X);
252     }
253 
getScale()254     protected float getScale() {
255         return getScale(mSuppMatrix);
256     }
257 
258     // Setup the base matrix so that the image is centered and scaled properly.
getProperBaseMatrix(RotateBitmap bitmap, Matrix matrix)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.
getImageViewMatrix()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%.
maxZoom()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 
zoomTo(float scale, float centerX, float centerY)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 
zoomTo(final float scale, final float centerX, final float centerY, final float durationMs)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 
zoomTo(float scale)340     protected void zoomTo(float scale) {
341         float cx = getWidth() / 2F;
342         float cy = getHeight() / 2F;
343 
344         zoomTo(scale, cx, cy);
345     }
346 
zoomToPoint(float scale, float pointX, float pointY)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 
zoomIn()355     protected void zoomIn() {
356         zoomIn(SCALE_RATE);
357     }
358 
zoomOut()359     protected void zoomOut() {
360         zoomOut(SCALE_RATE);
361     }
362 
zoomIn(float rate)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 
zoomOut(float rate)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 
postTranslate(float dx, float dy)399     protected void postTranslate(float dx, float dy) {
400         mSuppMatrix.postTranslate(dx, dy);
401     }
402 
panBy(float dx, float dy)403     protected void panBy(float dx, float dy) {
404         postTranslate(dx, dy);
405         setImageMatrix(getImageViewMatrix());
406     }
407 }
408