diff options
Diffstat (limited to 'src/com/android/camera/ImageViewTouchBase.java')
-rw-r--r-- | src/com/android/camera/ImageViewTouchBase.java | 559 |
1 files changed, 559 insertions, 0 deletions
diff --git a/src/com/android/camera/ImageViewTouchBase.java b/src/com/android/camera/ImageViewTouchBase.java new file mode 100644 index 0000000..1774e46 --- /dev/null +++ b/src/com/android/camera/ImageViewTouchBase.java @@ -0,0 +1,559 @@ +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.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 == 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()); + } +} + |