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.videoeditor.widgets;
18
19
20import android.content.Context;
21import android.graphics.Bitmap;
22import android.graphics.Matrix;
23import android.graphics.RectF;
24import android.graphics.drawable.Drawable;
25import android.util.AttributeSet;
26import android.view.MotionEvent;
27import android.widget.ImageView;
28
29/**
30 * An image view which can be panned and zoomed.
31 */
32public class ImageViewTouchBase extends ImageView {
33    private static final float SCALE_RATE = 1.25F;
34    // Zoom scale is applied after the transform that fits the image screen,
35    // so 1.0 is a perfect fit and it doesn't make sense to allow smaller
36    // values.
37    private static final float MIN_ZOOM_SCALE = 1.0f;
38
39    // This is the base transformation which is used to show the image
40    // initially.  The current computation for this shows the image in
41    // it's entirety, letterboxing as needed.  One could choose to
42    // show the image as cropped instead.
43    //
44    // This matrix is recomputed when we go from the thumbnail image to
45    // the full size image.
46    private Matrix mBaseMatrix = new Matrix();
47
48    // This is the supplementary transformation which reflects what
49    // the user has done in terms of zooming and panning.
50    //
51    // This matrix remains the same when we go from the thumbnail image
52    // to the full size image.
53    private Matrix mSuppMatrix = new Matrix();
54
55    // This is the final matrix which is computed as the concatenation
56    // of the base matrix and the supplementary matrix.
57    private final Matrix mDisplayMatrix = new Matrix();
58
59    // Temporary buffer used for getting the values out of a matrix.
60    private final float[] mMatrixValues = new float[9];
61
62    // The current bitmap being displayed.
63    private Bitmap mBitmapDisplayed;
64
65    // The width and height of the view
66    private int mThisWidth = -1, mThisHeight = -1;
67
68    private boolean mStretch = true;
69    // The zoom scale
70    private float mMaxZoom;
71    private Runnable mOnLayoutRunnable = null;
72    private ImageTouchEventListener mEventListener;
73
74    /**
75     * Touch interface
76     */
77    public interface ImageTouchEventListener {
78        public boolean onImageTouchEvent(MotionEvent ev);
79    }
80    /**
81     * Constructor
82     *
83     * @param context The context
84     */
85    public ImageViewTouchBase(Context context) {
86        super(context);
87        setScaleType(ImageView.ScaleType.MATRIX);
88    }
89
90    /**
91     * Constructor
92     *
93     * @param context The context
94     * @param attrs The attributes
95     */
96    public ImageViewTouchBase(Context context, AttributeSet attrs) {
97        super(context, attrs);
98        setScaleType(ImageView.ScaleType.MATRIX);
99    }
100
101    /**
102     * Constructor
103     *
104     * @param context The context
105     * @param attrs The attributes
106     * @param defStyle The default style
107     */
108    public ImageViewTouchBase(Context context, AttributeSet attrs, int defStyle) {
109        super(context, attrs, defStyle);
110        setScaleType(ImageView.ScaleType.MATRIX);
111    }
112
113    /*
114     * {@inheritDoc}
115     */
116    @Override
117    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
118        super.onLayout(changed, left, top, right, bottom);
119
120        mThisWidth = right - left;
121        mThisHeight = bottom - top;
122        final Runnable r = mOnLayoutRunnable;
123        if (r != null) {
124            mOnLayoutRunnable = null;
125            r.run();
126        } else {
127            if (mBitmapDisplayed != null) {
128                getProperBaseMatrix(mBitmapDisplayed, mBaseMatrix);
129                setImageMatrix(getImageViewMatrix());
130            }
131        }
132    }
133
134    /*
135     * {@inheritDoc}
136     */
137    @Override
138    public boolean dispatchTouchEvent(MotionEvent ev) {
139        if (mEventListener != null) {
140            return mEventListener.onImageTouchEvent(ev);
141        } else {
142            return false;
143        }
144    }
145
146    /*
147     * {@inheritDoc}
148     */
149    @Override
150    public void setImageBitmap(Bitmap bitmap) {
151        super.setImageBitmap(bitmap);
152
153        final Drawable d = getDrawable();
154        if (d != null) {
155            d.setDither(true);
156        }
157
158        mBitmapDisplayed = bitmap;
159    }
160
161    /**
162     * @param listener The listener
163     */
164    public void setEventListener(ImageTouchEventListener listener) {
165        mEventListener = listener;
166    }
167
168    /**
169     * @return The image bitmap
170     */
171    public Bitmap getImageBitmap() {
172        return mBitmapDisplayed;
173    }
174
175    /**
176     * If the view has not yet been measured delay the method
177     *
178     * @param bitmap The bitmap
179     * @param resetSupp true to reset the transform matrix
180     */
181    public void setImageBitmapResetBase(final Bitmap bitmap, final boolean resetSupp) {
182        mStretch = true;
183        final int viewWidth = getWidth();
184        if (viewWidth <= 0) {
185            mOnLayoutRunnable = new Runnable() {
186                @Override
187                public void run() {
188                    setImageBitmapResetBase(bitmap, resetSupp);
189                }
190            };
191            return;
192        }
193
194        if (bitmap != null) {
195            getProperBaseMatrix(bitmap, mBaseMatrix);
196            setImageBitmap(bitmap);
197        } else {
198            mBaseMatrix.reset();
199            setImageBitmap(null);
200        }
201
202        if (resetSupp) {
203            mSuppMatrix.reset();
204        }
205
206        setImageMatrix(getImageViewMatrix());
207        mMaxZoom = maxZoom();
208    }
209
210    /**
211     * Reset the transform of the current image
212     */
213    public void reset() {
214        if (mBitmapDisplayed != null) {
215            setImageBitmapResetBase(mBitmapDisplayed, true);
216        }
217    }
218
219    /**
220     * Pan
221     *
222     * @param dx The horizontal offset
223     * @param dy The vertical offset
224     */
225    public void postTranslateCenter(float dx, float dy) {
226        mSuppMatrix.postTranslate(dx, dy);
227
228        center(true, true);
229    }
230
231    /**
232     * Pan by the specified horizontal and vertical amount
233     *
234     * @param dx Pan by this horizontal amount
235     * @param dy Pan by this vertical amount
236     */
237    private void panBy(float dx, float dy) {
238        mSuppMatrix.postTranslate(dx, dy);
239
240        setImageMatrix(getImageViewMatrix());
241    }
242
243    /**
244     * @return The scale
245     */
246    public float getScale() {
247        return getValue(mSuppMatrix, Matrix.MSCALE_X);
248    }
249
250    /**
251     * @param rect The input/output rectangle
252     */
253    public void mapRect(RectF rect) {
254        mSuppMatrix.mapRect(rect);
255    }
256
257    /**
258     * Setup the base matrix so that the image is centered and scaled properly.
259     *
260     * @param bitmap The bitmap
261     * @param matrix The matrix
262     */
263    private void getProperBaseMatrix(Bitmap bitmap, Matrix matrix) {
264        final float viewWidth = getWidth();
265        final float viewHeight = getHeight();
266
267        final float w = bitmap.getWidth();
268        final float h = bitmap.getHeight();
269        matrix.reset();
270
271        if (mStretch) {
272            // We limit up-scaling to 10x otherwise the result may look bad if
273            // it's a small icon.
274            float widthScale = Math.min(viewWidth / w, 10.0f);
275            float heightScale = Math.min(viewHeight / h, 10.0f);
276            float scale = Math.min(widthScale, heightScale);
277            matrix.postScale(scale, scale);
278            matrix.postTranslate((viewWidth - w * scale) / 2F, (viewHeight - h * scale) / 2F);
279        } else {
280            matrix.postTranslate((viewWidth - w) / 2F, (viewHeight - h) / 2F);
281        }
282    }
283
284    /**
285     * Combine the base matrix and the supp matrix to make the final matrix.
286     */
287    private Matrix getImageViewMatrix() {
288        // The final matrix is computed as the concatenation of the base matrix
289        // and the supplementary matrix.
290        mDisplayMatrix.set(mBaseMatrix);
291        mDisplayMatrix.postConcat(mSuppMatrix);
292        return mDisplayMatrix;
293    }
294
295    /**
296     * @return The maximum zoom
297     */
298    public float getMaxZoom() {
299        return mMaxZoom;
300    }
301
302    /**
303     * Sets the maximum zoom, which is a scale relative to the base matrix. It
304     * is calculated to show the image at 400% zoom regardless of screen or
305     * image orientation. If in the future we decode the full 3 megapixel
306     * image, rather than the current 1024x768, this should be changed down
307     * to 200%.
308     */
309    private float maxZoom() {
310        if (mBitmapDisplayed == null) {
311            return 1F;
312        }
313
314        final float fw = (float)mBitmapDisplayed.getWidth() / mThisWidth;
315        final float fh = (float)mBitmapDisplayed.getHeight() / mThisHeight;
316
317        return Math.max(fw, fh) * 4;
318    }
319
320    /**
321     * Sets the maximum zoom, which is a scale relative to the base matrix. It
322     * is calculated to show the image at 400% zoom regardless of screen or
323     * image orientation. If in the future we decode the full 3 megapixel
324     * image, rather than the current 1024x768, this should be changed down
325     * to 200%.
326     */
327    public static float maxZoom(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight) {
328        final float fw = (float)bitmapWidth / viewWidth;
329        final float fh = (float)bitmapHeight / viewHeight;
330
331        return Math.max(fw, fh) * 4;
332    }
333
334    /**
335     * Ensure the scale factor is within limits
336     *
337     * @param scale The scale factor
338     *
339     * @return The corrected scaled factor
340     */
341    private float correctedZoomScale(float scale) {
342        float result = scale;
343        if (result > mMaxZoom) {
344            result = mMaxZoom;
345        } else if (result < MIN_ZOOM_SCALE) {
346            result = MIN_ZOOM_SCALE;
347        }
348
349        return result;
350    }
351
352    /**
353     * Zoom to the specified scale factor
354     *
355     * @param scale The scale factor
356     * @param centerX The horizontal center
357     * @param centerY The vertical center
358     */
359    public void zoomTo(float scale, float centerX, float centerY) {
360        float correctedScale = correctedZoomScale(scale);
361
362        float oldScale = getScale();
363        float deltaScale = correctedScale / oldScale;
364
365        mSuppMatrix.postScale(deltaScale, deltaScale, centerX, centerY);
366        setImageMatrix(getImageViewMatrix());
367        center(true, true);
368    }
369
370    /**
371     * Zoom to the specified scale factor
372     *
373     * @param scale The scale factor
374     */
375    public void zoomTo(float scale) {
376        final float cx = getWidth() / 2F;
377        final float cy = getHeight() / 2F;
378
379        zoomTo(scale, cx, cy);
380    }
381
382    /**
383     * Zoom to the specified scale factor and center point
384     *
385     * @param scale The scale factor
386     * @param pointX The horizontal position
387     * @param pointY The vertical position
388     */
389    public void zoomToPoint(float scale, float pointX, float pointY) {
390        final float cx = getWidth() / 2F;
391        final float cy = getHeight() / 2F;
392
393        panBy(cx - pointX, cy - pointY);
394        zoomTo(scale, cx, cy);
395    }
396
397    /**
398     * Zoom to the specified scale factor and point
399     *
400     * @param scale The scale factor
401     * @param pointX The horizontal position
402     * @param pointY The vertical position
403     */
404    public void zoomToOffset(float scale, float pointX, float pointY) {
405
406        float correctedScale = correctedZoomScale(scale);
407
408        float oldScale = getScale();
409        float deltaScale = correctedScale / oldScale;
410
411        mSuppMatrix.postScale(deltaScale, deltaScale);
412        setImageMatrix(getImageViewMatrix());
413
414        panBy(-pointX, -pointY);
415    }
416
417    /**
418     * Zoom in by a preset scale rate
419     */
420    public void zoomIn() {
421        zoomIn(SCALE_RATE);
422    }
423
424    /**
425     * Zoom in by the specified scale rate
426     *
427     * @param rate The scale rate
428     */
429    public void zoomIn(float rate) {
430        if (getScale() < mMaxZoom && mBitmapDisplayed != null) {
431            float cx = getWidth() / 2F;
432            float cy = getHeight() / 2F;
433
434            mSuppMatrix.postScale(rate, rate, cx, cy);
435            setImageMatrix(getImageViewMatrix());
436        }
437    }
438
439    /**
440     * Zoom out by a preset scale rate
441     */
442    public void zoomOut() {
443        zoomOut(SCALE_RATE);
444    }
445
446    /**
447     * Zoom out by the specified scale rate
448     *
449     * @param rate The scale rate
450     */
451    public void zoomOut(float rate) {
452        if (getScale() > MIN_ZOOM_SCALE && mBitmapDisplayed != null) {
453            float cx = getWidth() / 2F;
454            float cy = getHeight() / 2F;
455
456            // Zoom out to at most 1x.
457            Matrix tmp = new Matrix(mSuppMatrix);
458            tmp.postScale(1F / rate, 1F / rate, cx, cy);
459
460            if (getValue(tmp, Matrix.MSCALE_X) < 1F) {
461                mSuppMatrix.setScale(1F, 1F, cx, cy);
462            } else {
463                mSuppMatrix.postScale(1F / rate, 1F / rate, cx, cy);
464            }
465            setImageMatrix(getImageViewMatrix());
466            center(true, true);
467        }
468    }
469
470    /**
471     * Center as much as possible in one or both axis. Centering is
472     * defined as follows: if the image is scaled down below the
473     * view's dimensions then center it (literally). If the image
474     * is scaled larger than the view and is translated out of view
475     * then translate it back into view (i.e. eliminate black bars).
476     */
477    private void center(boolean horizontal, boolean vertical) {
478        if (mBitmapDisplayed == null) {
479            return;
480        }
481
482        final Matrix m = getImageViewMatrix();
483        final RectF rect = new RectF(0, 0, mBitmapDisplayed.getWidth(),
484                mBitmapDisplayed.getHeight());
485
486        m.mapRect(rect);
487
488        final float height = rect.height();
489        final float width = rect.width();
490        float deltaX = 0, deltaY = 0;
491
492        if (vertical) {
493            int viewHeight = getHeight();
494            if (height < viewHeight) {
495                deltaY = (viewHeight - height) / 2 - rect.top;
496            } else if (rect.top > 0) {
497                deltaY = -rect.top;
498            } else if (rect.bottom < viewHeight) {
499                deltaY = getHeight() - rect.bottom;
500            }
501        }
502
503        if (horizontal) {
504            int viewWidth = getWidth();
505            if (width < viewWidth) {
506                deltaX = (viewWidth - width) / 2 - rect.left;
507            } else if (rect.left > 0) {
508                deltaX = -rect.left;
509            } else if (rect.right < viewWidth) {
510                deltaX = viewWidth - rect.right;
511            }
512        }
513
514        mSuppMatrix.postTranslate(deltaX, deltaY);
515
516        setImageMatrix(getImageViewMatrix());
517    }
518
519    /**
520     * Get a matrix transform value
521     *
522     * @param matrix The matrix
523     * @param whichValue Which value
524     * @return The value
525     */
526    private float getValue(Matrix matrix, int whichValue) {
527        matrix.getValues(mMatrixValues);
528        return mMatrixValues[whichValue];
529    }
530}
531