| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.camera; |
| |
| import android.content.Context; |
| import android.graphics.Bitmap; |
| import android.graphics.Matrix; |
| import android.graphics.RectF; |
| import android.graphics.drawable.Drawable; |
| import android.os.Handler; |
| import android.util.AttributeSet; |
| import android.view.KeyEvent; |
| import android.widget.ImageView; |
| |
| abstract class ImageViewTouchBase extends ImageView { |
| |
| @SuppressWarnings("unused") |
| private static final String TAG = "ImageViewTouchBase"; |
| |
| // This is the base transformation which is used to show the image |
| // initially. The current computation for this shows the image in |
| // it's entirety, letterboxing as needed. One could choose to |
| // show the image as cropped instead. |
| // |
| // This matrix is recomputed when we go from the thumbnail image to |
| // the full size image. |
| protected Matrix mBaseMatrix = new Matrix(); |
| |
| // This is the supplementary transformation which reflects what |
| // the user has done in terms of zooming and panning. |
| // |
| // This matrix remains the same when we go from the thumbnail image |
| // to the full size image. |
| protected Matrix mSuppMatrix = new Matrix(); |
| |
| // This is the final matrix which is computed as the concatentation |
| // of the base matrix and the supplementary matrix. |
| private final Matrix mDisplayMatrix = new Matrix(); |
| |
| // Temporary buffer used for getting the values out of a matrix. |
| private final float[] mMatrixValues = new float[9]; |
| |
| // The current bitmap being displayed. |
| protected final RotateBitmap mBitmapDisplayed = new RotateBitmap(null); |
| |
| int mThisWidth = -1, mThisHeight = -1; |
| |
| float mMaxZoom; |
| |
| // ImageViewTouchBase will pass a Bitmap to the Recycler if it has finished |
| // its use of that Bitmap. |
| public interface Recycler { |
| public void recycle(Bitmap b); |
| } |
| |
| public void setRecycler(Recycler r) { |
| mRecycler = r; |
| } |
| |
| private Recycler mRecycler; |
| |
| @Override |
| protected void onLayout(boolean changed, int left, int top, |
| int right, int bottom) { |
| super.onLayout(changed, left, top, right, bottom); |
| mThisWidth = right - left; |
| mThisHeight = bottom - top; |
| Runnable r = mOnLayoutRunnable; |
| if (r != null) { |
| mOnLayoutRunnable = null; |
| r.run(); |
| } |
| if (mBitmapDisplayed.getBitmap() != null) { |
| getProperBaseMatrix(mBitmapDisplayed, mBaseMatrix); |
| setImageMatrix(getImageViewMatrix()); |
| } |
| } |
| |
| @Override |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| if (keyCode == KeyEvent.KEYCODE_BACK |
| && event.getRepeatCount() == 0) { |
| event.startTracking(); |
| return true; |
| } |
| return super.onKeyDown(keyCode, event); |
| } |
| |
| @Override |
| public boolean onKeyUp(int keyCode, KeyEvent event) { |
| if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() |
| && !event.isCanceled()) { |
| if (getScale() > 1.0f) { |
| // If we're zoomed in, pressing Back jumps out to show the |
| // entire image, otherwise Back returns the user to the gallery. |
| zoomTo(1.0f); |
| return true; |
| } |
| } |
| return super.onKeyUp(keyCode, event); |
| } |
| |
| protected Handler mHandler = new Handler(); |
| |
| @Override |
| public void setImageBitmap(Bitmap bitmap) { |
| setImageBitmap(bitmap, 0); |
| } |
| |
| private void setImageBitmap(Bitmap bitmap, int rotation) { |
| super.setImageBitmap(bitmap); |
| Drawable d = getDrawable(); |
| if (d != null) { |
| d.setDither(true); |
| } |
| |
| Bitmap old = mBitmapDisplayed.getBitmap(); |
| mBitmapDisplayed.setBitmap(bitmap); |
| mBitmapDisplayed.setRotation(rotation); |
| |
| if (old != null && old != bitmap && mRecycler != null) { |
| mRecycler.recycle(old); |
| } |
| } |
| |
| public void clear() { |
| setImageBitmapResetBase(null, true); |
| } |
| |
| private Runnable mOnLayoutRunnable = null; |
| |
| // This function changes bitmap, reset base matrix according to the size |
| // of the bitmap, and optionally reset the supplementary matrix. |
| public void setImageBitmapResetBase(final Bitmap bitmap, |
| final boolean resetSupp) { |
| setImageRotateBitmapResetBase(new RotateBitmap(bitmap), resetSupp); |
| } |
| |
| public void setImageRotateBitmapResetBase(final RotateBitmap bitmap, |
| final boolean resetSupp) { |
| final int viewWidth = getWidth(); |
| |
| if (viewWidth <= 0) { |
| mOnLayoutRunnable = new Runnable() { |
| public void run() { |
| setImageRotateBitmapResetBase(bitmap, resetSupp); |
| } |
| }; |
| return; |
| } |
| |
| if (bitmap.getBitmap() != null) { |
| getProperBaseMatrix(bitmap, mBaseMatrix); |
| setImageBitmap(bitmap.getBitmap(), bitmap.getRotation()); |
| } else { |
| mBaseMatrix.reset(); |
| setImageBitmap(null); |
| } |
| |
| if (resetSupp) { |
| mSuppMatrix.reset(); |
| } |
| setImageMatrix(getImageViewMatrix()); |
| mMaxZoom = maxZoom(); |
| } |
| |
| // Center as much as possible in one or both axis. Centering is |
| // defined as follows: if the image is scaled down below the |
| // view's dimensions then center it (literally). If the image |
| // is scaled larger than the view and is translated out of view |
| // then translate it back into view (i.e. eliminate black bars). |
| protected void center(boolean horizontal, boolean vertical) { |
| if (mBitmapDisplayed.getBitmap() == null) { |
| return; |
| } |
| |
| Matrix m = getImageViewMatrix(); |
| |
| RectF rect = new RectF(0, 0, |
| mBitmapDisplayed.getBitmap().getWidth(), |
| mBitmapDisplayed.getBitmap().getHeight()); |
| |
| m.mapRect(rect); |
| |
| float height = rect.height(); |
| float width = rect.width(); |
| |
| float deltaX = 0, deltaY = 0; |
| |
| if (vertical) { |
| int viewHeight = getHeight(); |
| if (height < viewHeight) { |
| deltaY = (viewHeight - height) / 2 - rect.top; |
| } else if (rect.top > 0) { |
| deltaY = -rect.top; |
| } else if (rect.bottom < viewHeight) { |
| deltaY = getHeight() - rect.bottom; |
| } |
| } |
| |
| if (horizontal) { |
| int viewWidth = getWidth(); |
| if (width < viewWidth) { |
| deltaX = (viewWidth - width) / 2 - rect.left; |
| } else if (rect.left > 0) { |
| deltaX = -rect.left; |
| } else if (rect.right < viewWidth) { |
| deltaX = viewWidth - rect.right; |
| } |
| } |
| |
| postTranslate(deltaX, deltaY); |
| setImageMatrix(getImageViewMatrix()); |
| } |
| |
| public ImageViewTouchBase(Context context) { |
| super(context); |
| init(); |
| } |
| |
| public ImageViewTouchBase(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| init(); |
| } |
| |
| private void init() { |
| setScaleType(ImageView.ScaleType.MATRIX); |
| } |
| |
| protected float getValue(Matrix matrix, int whichValue) { |
| matrix.getValues(mMatrixValues); |
| return mMatrixValues[whichValue]; |
| } |
| |
| // Get the scale factor out of the matrix. |
| protected float getScale(Matrix matrix) { |
| return getValue(matrix, Matrix.MSCALE_X); |
| } |
| |
| protected float getScale() { |
| return getScale(mSuppMatrix); |
| } |
| |
| // Setup the base matrix so that the image is centered and scaled properly. |
| private void getProperBaseMatrix(RotateBitmap bitmap, Matrix matrix) { |
| float viewWidth = getWidth(); |
| float viewHeight = getHeight(); |
| |
| float w = bitmap.getWidth(); |
| float h = bitmap.getHeight(); |
| matrix.reset(); |
| |
| // We limit up-scaling to 3x otherwise the result may look bad if it's |
| // a small icon. |
| float widthScale = Math.min(viewWidth / w, 3.0f); |
| float heightScale = Math.min(viewHeight / h, 3.0f); |
| float scale = Math.min(widthScale, heightScale); |
| |
| matrix.postConcat(bitmap.getRotateMatrix()); |
| matrix.postScale(scale, scale); |
| |
| matrix.postTranslate( |
| (viewWidth - w * scale) / 2F, |
| (viewHeight - h * scale) / 2F); |
| } |
| |
| // Combine the base matrix and the supp matrix to make the final matrix. |
| protected Matrix getImageViewMatrix() { |
| // The final matrix is computed as the concatentation of the base matrix |
| // and the supplementary matrix. |
| mDisplayMatrix.set(mBaseMatrix); |
| mDisplayMatrix.postConcat(mSuppMatrix); |
| return mDisplayMatrix; |
| } |
| |
| static final float SCALE_RATE = 1.25F; |
| |
| // Sets the maximum zoom, which is a scale relative to the base matrix. It |
| // is calculated to show the image at 400% zoom regardless of screen or |
| // image orientation. If in the future we decode the full 3 megapixel image, |
| // rather than the current 1024x768, this should be changed down to 200%. |
| protected float maxZoom() { |
| if (mBitmapDisplayed.getBitmap() == null) { |
| return 1F; |
| } |
| |
| float fw = (float) mBitmapDisplayed.getWidth() / (float) mThisWidth; |
| float fh = (float) mBitmapDisplayed.getHeight() / (float) mThisHeight; |
| float max = Math.max(fw, fh) * 4; |
| return max; |
| } |
| |
| protected void zoomTo(float scale, float centerX, float centerY) { |
| if (scale > mMaxZoom) { |
| scale = mMaxZoom; |
| } |
| |
| float oldScale = getScale(); |
| float deltaScale = scale / oldScale; |
| |
| mSuppMatrix.postScale(deltaScale, deltaScale, centerX, centerY); |
| setImageMatrix(getImageViewMatrix()); |
| center(true, true); |
| } |
| |
| protected void zoomTo(final float scale, final float centerX, |
| final float centerY, final float durationMs) { |
| final float incrementPerMs = (scale - getScale()) / durationMs; |
| final float oldScale = getScale(); |
| final long startTime = System.currentTimeMillis(); |
| |
| mHandler.post(new Runnable() { |
| public void run() { |
| long now = System.currentTimeMillis(); |
| float currentMs = Math.min(durationMs, now - startTime); |
| float target = oldScale + (incrementPerMs * currentMs); |
| zoomTo(target, centerX, centerY); |
| |
| if (currentMs < durationMs) { |
| mHandler.post(this); |
| } |
| } |
| }); |
| } |
| |
| protected void zoomTo(float scale) { |
| float cx = getWidth() / 2F; |
| float cy = getHeight() / 2F; |
| |
| zoomTo(scale, cx, cy); |
| } |
| |
| protected void zoomToPoint(float scale, float pointX, float pointY) { |
| float cx = getWidth() / 2F; |
| float cy = getHeight() / 2F; |
| |
| panBy(cx - pointX, cy - pointY); |
| zoomTo(scale, cx, cy); |
| } |
| |
| protected void zoomIn() { |
| zoomIn(SCALE_RATE); |
| } |
| |
| protected void zoomOut() { |
| zoomOut(SCALE_RATE); |
| } |
| |
| protected void zoomIn(float rate) { |
| if (getScale() >= mMaxZoom) { |
| return; // Don't let the user zoom into the molecular level. |
| } |
| if (mBitmapDisplayed.getBitmap() == null) { |
| return; |
| } |
| |
| float cx = getWidth() / 2F; |
| float cy = getHeight() / 2F; |
| |
| mSuppMatrix.postScale(rate, rate, cx, cy); |
| setImageMatrix(getImageViewMatrix()); |
| } |
| |
| protected void zoomOut(float rate) { |
| if (mBitmapDisplayed.getBitmap() == null) { |
| return; |
| } |
| |
| float cx = getWidth() / 2F; |
| float cy = getHeight() / 2F; |
| |
| // Zoom out to at most 1x. |
| Matrix tmp = new Matrix(mSuppMatrix); |
| tmp.postScale(1F / rate, 1F / rate, cx, cy); |
| |
| if (getScale(tmp) < 1F) { |
| mSuppMatrix.setScale(1F, 1F, cx, cy); |
| } else { |
| mSuppMatrix.postScale(1F / rate, 1F / rate, cx, cy); |
| } |
| setImageMatrix(getImageViewMatrix()); |
| center(true, true); |
| } |
| |
| protected void postTranslate(float dx, float dy) { |
| mSuppMatrix.postTranslate(dx, dy); |
| } |
| |
| protected void panBy(float dx, float dy) { |
| postTranslate(dx, dy); |
| setImageMatrix(getImageViewMatrix()); |
| } |
| } |