| package com.android.camera; |
| |
| import android.content.Context; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.Matrix; |
| import android.graphics.Paint; |
| import android.graphics.drawable.Drawable; |
| import android.os.Handler; |
| import android.os.SystemClock; |
| import android.util.AttributeSet; |
| import android.util.Config; |
| import android.util.Log; |
| import android.view.animation.Animation; |
| import android.view.animation.TranslateAnimation; |
| import android.view.KeyEvent; |
| import android.widget.ImageView; |
| |
| abstract public class ImageViewTouchBase extends ImageView { |
| private static final String TAG = "ImageViewTouchBase"; |
| |
| // if we're animating these images, it may be faster to cache the image |
| // at its target size first. to do this set this variable to true. |
| // currently we're not animating images, so we don't need to do this |
| // extra work. |
| private final boolean USE_PERFECT_FIT_OPTIMIZATION = false; |
| |
| // 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 chose 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 Matrix mDisplayMatrix = new Matrix(); |
| |
| // Temporary buffer used for getting the values out of a matrix. |
| private float[] mMatrixValues = new float[9]; |
| |
| // The current bitmap being displayed. |
| protected Bitmap mBitmapDisplayed; |
| |
| // The thumbnail bitmap. |
| protected Bitmap mThumbBitmap; |
| |
| // The full size bitmap which should be used once we start zooming. |
| private Bitmap mFullBitmap; |
| |
| // The bitmap which is exactly sized to what we need. The decoded bitmap is |
| // drawn into the mPerfectFitBitmap so that animation is faster. |
| protected Bitmap mPerfectFitBitmap; |
| |
| // True if the image is the thumbnail. |
| protected boolean mBitmapIsThumbnail; |
| |
| // True if the user is zooming -- use the full size image |
| protected boolean mIsZooming; |
| |
| // Paint to use to clear the "mPerfectFitBitmap" |
| protected Paint mPaint = new Paint(); |
| |
| static boolean sNewZoomControl = false; |
| |
| int mThisWidth = -1, mThisHeight = -1; |
| |
| float mMaxZoom; |
| |
| @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 != null) { |
| setBaseMatrix(mBitmapDisplayed, mBaseMatrix); |
| setImageMatrix(getImageViewMatrix()); |
| } |
| } |
| |
| @Override |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| if (keyCode == KeyEvent.KEYCODE_BACK && 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.onKeyDown(keyCode, event); |
| } |
| |
| protected Handler mHandler = new Handler(); |
| |
| protected int mLastXTouchPos; |
| protected int mLastYTouchPos; |
| |
| protected boolean doesScrolling() { |
| return true; |
| } |
| |
| // Translate a given point through a given matrix. |
| static private void translatePoint(Matrix matrix, float [] xy) { |
| matrix.mapPoints(xy); |
| } |
| |
| // Return the mapped x coordinate through the matrix. |
| static int mapXPoint(Matrix matrix, int point) { |
| // Matrix's mapPoints takes an array of x/y coordinates. |
| // That's why we have to allocte an array of length two |
| // even though we don't use the y coordinate. |
| float [] xy = new float[2]; |
| xy[0] = point; |
| xy[1] = 0F; |
| matrix.mapPoints(xy); |
| return (int) xy[0]; |
| } |
| |
| @Override |
| public void setImageBitmap(Bitmap bitmap) { |
| throw new NullPointerException(); |
| } |
| |
| public void setImageBitmap(Bitmap bitmap, boolean isThumbnail) { |
| super.setImageBitmap(bitmap); |
| Drawable d = getDrawable(); |
| if (d != null) |
| d.setDither(true); |
| mBitmapDisplayed = bitmap; |
| mBitmapIsThumbnail = isThumbnail; |
| } |
| |
| protected boolean usePerfectFitBitmap() { |
| return USE_PERFECT_FIT_OPTIMIZATION && !mIsZooming; |
| } |
| |
| public void recycleBitmaps() { |
| if (mFullBitmap != null) { |
| if (Config.LOGV) |
| Log.v(TAG, "recycling mFullBitmap " + mFullBitmap + "; this == " + this.hashCode()); |
| mFullBitmap.recycle(); |
| mFullBitmap = null; |
| } |
| if (mThumbBitmap != null) { |
| if (Config.LOGV) |
| Log.v(TAG, "recycling mThumbBitmap" + mThumbBitmap + "; this == " + this.hashCode()); |
| mThumbBitmap.recycle(); |
| mThumbBitmap = null; |
| } |
| |
| // mBitmapDisplayed is either mPerfectFitBitmap or mFullBitmap (in the case of zooming) |
| setImageBitmap(null, true); |
| } |
| |
| public void clear() { |
| mBitmapDisplayed = null; |
| recycleBitmaps(); |
| } |
| |
| private Runnable mOnLayoutRunnable = null; |
| |
| public void setImageBitmapResetBase(final Bitmap bitmap, final boolean resetSupp, final boolean isThumb) { |
| if ((bitmap != null) && (bitmap == mPerfectFitBitmap)) { |
| // TODO: this should be removed in production |
| throw new IllegalArgumentException("bitmap must not be mPerfectFitBitmap"); |
| } |
| |
| final int viewWidth = getWidth(); |
| final int viewHeight = getHeight(); |
| |
| if (viewWidth <= 0) { |
| mOnLayoutRunnable = new Runnable() { |
| public void run() { |
| setImageBitmapResetBase(bitmap, resetSupp, isThumb); |
| } |
| }; |
| return; |
| } |
| |
| if (isThumb && mThumbBitmap != bitmap) { |
| if (mThumbBitmap != null) { |
| mThumbBitmap.recycle(); |
| } |
| mThumbBitmap = bitmap; |
| } else if (!isThumb && mFullBitmap != bitmap) { |
| if (mFullBitmap != null) { |
| mFullBitmap.recycle(); |
| } |
| mFullBitmap = bitmap; |
| } |
| mBitmapIsThumbnail = isThumb; |
| |
| if (bitmap != null) { |
| if (!usePerfectFitBitmap()) { |
| setScaleType(ImageView.ScaleType.MATRIX); |
| setBaseMatrix(bitmap, mBaseMatrix); |
| setImageBitmap(bitmap, isThumb); |
| } else { |
| Matrix matrix = new Matrix(); |
| setBaseMatrix(bitmap, matrix); |
| if ((mPerfectFitBitmap == null) || |
| mPerfectFitBitmap.getWidth() != mThisWidth || |
| mPerfectFitBitmap.getHeight() != mThisHeight) { |
| if (mPerfectFitBitmap != null) { |
| if (Config.LOGV) |
| Log.v(TAG, "recycling mPerfectFitBitmap " + mPerfectFitBitmap.hashCode()); |
| mPerfectFitBitmap.recycle(); |
| } |
| mPerfectFitBitmap = Bitmap.createBitmap(mThisWidth, mThisHeight, Bitmap.Config.RGB_565); |
| } |
| Canvas canvas = new Canvas(mPerfectFitBitmap); |
| // clear the bitmap which may be bigger than the image and |
| // contain the the previous image. |
| canvas.drawColor(0xFF000000); |
| |
| final int bw = bitmap.getWidth(); |
| final int bh = bitmap.getHeight(); |
| final float widthScale = Math.min(viewWidth / (float)bw, 1.0f); |
| final float heightScale = Math.min(viewHeight/ (float)bh, 1.0f); |
| int translateX, translateY; |
| if (widthScale > heightScale) { |
| translateX = (int)((viewWidth -(float)bw*heightScale)*0.5f); |
| translateY = (int)((viewHeight-(float)bh*heightScale)*0.5f); |
| } else { |
| translateX = (int)((viewWidth -(float)bw*widthScale)*0.5f); |
| translateY = (int)((viewHeight-(float)bh*widthScale)*0.5f); |
| } |
| |
| android.graphics.Rect src = new android.graphics.Rect(0, 0, bw, bh); |
| android.graphics.Rect dst = new android.graphics.Rect( |
| translateX, translateY, |
| mThisWidth - translateX, mThisHeight - translateY); |
| canvas.drawBitmap(bitmap, src, dst, mPaint); |
| |
| setImageBitmap(mPerfectFitBitmap, isThumb); |
| setScaleType(ImageView.ScaleType.MATRIX); |
| setImageMatrix(null); |
| } |
| } else { |
| mBaseMatrix.reset(); |
| setImageBitmap(null, isThumb); |
| } |
| |
| 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 vertical, boolean horizontal, boolean animate) { |
| if (mBitmapDisplayed == null) |
| return; |
| |
| Matrix m = getImageViewMatrix(); |
| |
| float [] topLeft = new float[] { 0, 0 }; |
| float [] botRight = new float[] { mBitmapDisplayed.getWidth(), mBitmapDisplayed.getHeight() }; |
| |
| translatePoint(m, topLeft); |
| translatePoint(m, botRight); |
| |
| float height = botRight[1] - topLeft[1]; |
| float width = botRight[0] - topLeft[0]; |
| |
| float deltaX = 0, deltaY = 0; |
| |
| if (vertical) { |
| int viewHeight = getHeight(); |
| if (height < viewHeight) { |
| deltaY = (viewHeight - height)/2 - topLeft[1]; |
| } else if (topLeft[1] > 0) { |
| deltaY = -topLeft[1]; |
| } else if (botRight[1] < viewHeight) { |
| deltaY = getHeight() - botRight[1]; |
| } |
| } |
| |
| if (horizontal) { |
| int viewWidth = getWidth(); |
| if (width < viewWidth) { |
| deltaX = (viewWidth - width)/2 - topLeft[0]; |
| } else if (topLeft[0] > 0) { |
| deltaX = -topLeft[0]; |
| } else if (botRight[0] < viewWidth) { |
| deltaX = viewWidth - botRight[0]; |
| } |
| } |
| |
| postTranslate(deltaX, deltaY); |
| if (animate) { |
| Animation a = new TranslateAnimation(-deltaX, 0, -deltaY, 0); |
| a.setStartTime(SystemClock.elapsedRealtime()); |
| a.setDuration(250); |
| setAnimation(a); |
| } |
| setImageMatrix(getImageViewMatrix()); |
| } |
| |
| public void copyFrom(ImageViewTouchBase other) { |
| mSuppMatrix.set(other.mSuppMatrix); |
| mBaseMatrix.set(other.mBaseMatrix); |
| |
| if (mThumbBitmap != null) |
| mThumbBitmap.recycle(); |
| |
| if (mFullBitmap != null) |
| mFullBitmap.recycle(); |
| |
| // copy the data |
| mThumbBitmap = other.mThumbBitmap; |
| mFullBitmap = null; |
| |
| if (other.mFullBitmap != null) |
| other.mFullBitmap.recycle(); |
| |
| // transfer "ownership" |
| other.mThumbBitmap = null; |
| other.mFullBitmap = null; |
| other.mBitmapIsThumbnail = true; |
| |
| setImageMatrix(other.getImageMatrix()); |
| setScaleType(other.getScaleType()); |
| |
| setImageBitmapResetBase(mThumbBitmap, true, true); |
| } |
| |
| @Override |
| public void setImageDrawable(android.graphics.drawable.Drawable d) { |
| super.setImageDrawable(d); |
| } |
| |
| public ImageViewTouchBase(Context context) { |
| super(context); |
| init(); |
| } |
| |
| public ImageViewTouchBase(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| init(); |
| } |
| |
| private void init() { |
| setScaleType(ImageView.ScaleType.MATRIX); |
| mPaint.setDither(true); |
| mPaint.setFilterBitmap(true); |
| } |
| |
| 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); |
| } |
| |
| protected float getTranslateX() { |
| return getValue(mSuppMatrix, Matrix.MTRANS_X); |
| } |
| |
| protected float getTranslateY() { |
| return getValue(mSuppMatrix, Matrix.MTRANS_Y); |
| } |
| |
| // Setup the base matrix so that the image is centered and scaled properly. |
| private void setBaseMatrix(Bitmap bitmap, Matrix matrix) { |
| float viewWidth = getWidth(); |
| float viewHeight = getHeight(); |
| |
| matrix.reset(); |
| float widthScale = Math.min(viewWidth / (float)bitmap.getWidth(), 1.0f); |
| float heightScale = Math.min(viewHeight / (float)bitmap.getHeight(), 1.0f); |
| float scale; |
| if (widthScale > heightScale) { |
| scale = heightScale; |
| } else { |
| scale = widthScale; |
| } |
| matrix.setScale(scale, scale); |
| matrix.postTranslate( |
| (viewWidth - ((float)bitmap.getWidth() * scale))/2F, |
| (viewHeight - ((float)bitmap.getHeight() * scale))/2F); |
| } |
| |
| // Combine the base matrix and the supp matrix to make the final matrix. |
| protected Matrix getImageViewMatrix() { |
| mDisplayMatrix.set(mBaseMatrix); |
| mDisplayMatrix.postConcat(mSuppMatrix); |
| return mDisplayMatrix; |
| } |
| |
| private void onZoom() { |
| mIsZooming = true; |
| if (mFullBitmap != null && mFullBitmap != mBitmapDisplayed) { |
| setImageBitmapResetBase(mFullBitmap, false, mBitmapIsThumbnail); |
| } |
| } |
| |
| private String describe(Bitmap b) { |
| StringBuilder sb = new StringBuilder(); |
| if (b == null) { |
| sb.append("NULL"); |
| } else if (b.isRecycled()) { |
| sb.append(String.format("%08x: RECYCLED", b.hashCode())); |
| } else { |
| sb.append(String.format("%08x: LIVE", b.hashCode())); |
| sb.append(String.format("%d x %d (size == %d)", b.getWidth(), b.getHeight(), b.getWidth()*b.getHeight()*2)); |
| } |
| return sb.toString(); |
| } |
| |
| public void dump() { |
| if (Config.LOGV) { |
| Log.v(TAG, "dump ImageViewTouchBase " + this); |
| Log.v(TAG, "... mBitmapDisplayed = " + describe(mBitmapDisplayed)); |
| Log.v(TAG, "... mThumbBitmap = " + describe(mThumbBitmap)); |
| Log.v(TAG, "... mFullBitmap = " + describe(mFullBitmap)); |
| Log.v(TAG, "... mPerfectFitBitmap = " + describe(mPerfectFitBitmap)); |
| Log.v(TAG, "... mIsThumb = " + mBitmapIsThumbnail); |
| } |
| } |
| |
| static final float sPanRate = 7; |
| static final float sScaleRate = 1.05F; |
| |
| // 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 == null) |
| return 1F; |
| |
| float fw = (float) mBitmapDisplayed.getWidth() / (float)mThisWidth; |
| float fh = (float) mBitmapDisplayed.getHeight() / (float)mThisHeight; |
| float max = Math.max(fw, fh) * 4; |
| // Log.v(TAG, "Bitmap " + mBitmapDisplayed.getWidth() + "x" + mBitmapDisplayed.getHeight() + |
| // " view " + mThisWidth + "x" + mThisHeight + " max zoom " + max); |
| return max; |
| } |
| |
| protected void zoomTo(float scale, float centerX, float centerY) { |
| if (scale > mMaxZoom) { |
| scale = mMaxZoom; |
| } |
| onZoom(); |
| |
| float oldScale = getScale(); |
| float deltaScale = scale / oldScale; |
| |
| mSuppMatrix.postScale(deltaScale, deltaScale, centerX, centerY); |
| setImageMatrix(getImageViewMatrix()); |
| center(true, true, false); |
| } |
| |
| 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, (float)(now - startTime)); |
| float target = oldScale + (incrementPerMs * currentMs); |
| zoomTo(target, centerX, centerY); |
| |
| if (currentMs < durationMs) { |
| mHandler.post(this); |
| } |
| } |
| }); |
| } |
| |
| protected void zoomTo(float scale) { |
| float width = getWidth(); |
| float height = getHeight(); |
| |
| zoomTo(scale, width/2F, height/2F); |
| } |
| |
| protected void zoomIn() { |
| zoomIn(sScaleRate); |
| } |
| |
| protected void zoomOut() { |
| zoomOut(sScaleRate); |
| } |
| |
| protected void zoomIn(float rate) { |
| if (getScale() >= mMaxZoom) { |
| return; // Don't let the user zoom into the molecular level. |
| } |
| if (mBitmapDisplayed == null) { |
| return; |
| } |
| float width = getWidth(); |
| float height = getHeight(); |
| |
| mSuppMatrix.postScale(rate, rate, width/2F, height/2F); |
| setImageMatrix(getImageViewMatrix()); |
| |
| onZoom(); |
| } |
| |
| protected void zoomOut(float rate) { |
| if (mBitmapDisplayed == null) { |
| return; |
| } |
| |
| float width = getWidth(); |
| float height = getHeight(); |
| |
| Matrix tmp = new Matrix(mSuppMatrix); |
| tmp.postScale(1F/sScaleRate, 1F/sScaleRate, width/2F, height/2F); |
| if (getScale(tmp) < 1F) { |
| mSuppMatrix.setScale(1F, 1F, width/2F, height/2F); |
| } else { |
| mSuppMatrix.postScale(1F/rate, 1F/rate, width/2F, height/2F); |
| } |
| setImageMatrix(getImageViewMatrix()); |
| center(true, true, false); |
| |
| onZoom(); |
| } |
| |
| protected void postTranslate(float dx, float dy) { |
| mSuppMatrix.postTranslate(dx, dy); |
| } |
| |
| protected void panBy(float dx, float dy) { |
| postTranslate(dx, dy); |
| setImageMatrix(getImageViewMatrix()); |
| } |
| } |
| |