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