/******************************************************************************* * Copyright Alessandro Crugnola * * 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.fanfou.app.opensource.ui.imagezoom; 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.util.Log; import android.widget.ImageView; public class ImageViewTouchBase extends ImageView { protected enum Command { Center, Move, Zoom, Layout, Reset, }; public interface OnBitmapChangedListener { void onBitmapChanged(Bitmap bitmap); }; public static final String LOG_TAG = "image"; public static float easeOut(float time, final float start, final float end, final float duration) { return (end * (((time = (time / duration) - 1) * time * time) + 1)) + start; } protected Matrix mBaseMatrix = new Matrix(); protected Matrix mSuppMatrix = new Matrix(); protected Handler mHandler = new Handler(); protected Runnable mOnLayoutRunnable = null; protected float mMaxZoom; protected final Matrix mDisplayMatrix = new Matrix(); protected final float[] mMatrixValues = new float[9]; protected int mThisWidth = -1, mThisHeight = -1; final protected RotateBitmap mBitmapDisplayed = new RotateBitmap(null, 0); final protected float MAX_ZOOM = 2.0f; private OnBitmapChangedListener mListener; public ImageViewTouchBase(final Context context) { super(context); init(); } public ImageViewTouchBase(final Context context, final AttributeSet attrs) { super(context, attrs); init(); } protected void center(final boolean horizontal, final boolean vertical) { if (this.mBitmapDisplayed.getBitmap() == null) { return; } final RectF rect = getCenter(horizontal, vertical); if ((rect.left != 0) || (rect.top != 0)) { postTranslate(rect.left, rect.top); } } public void clear() { setImageBitmapReset(null, true); } public void dispose() { if (this.mBitmapDisplayed.getBitmap() != null) { if (!this.mBitmapDisplayed.getBitmap().isRecycled()) { this.mBitmapDisplayed.getBitmap().recycle(); } } clear(); } protected RectF getBitmapRect() { if (this.mBitmapDisplayed.getBitmap() == null) { return null; } final Matrix m = getImageViewMatrix(); final RectF rect = new RectF(0, 0, this.mBitmapDisplayed.getBitmap() .getWidth(), this.mBitmapDisplayed.getBitmap().getHeight()); m.mapRect(rect); return rect; } protected RectF getCenter(final boolean horizontal, final boolean vertical) { if (this.mBitmapDisplayed.getBitmap() == null) { return new RectF(0, 0, 0, 0); } final RectF rect = getBitmapRect(); final float height = rect.height(); final float width = rect.width(); float deltaX = 0, deltaY = 0; if (vertical) { final 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) { final 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; } } return new RectF(deltaX, deltaY, 0, 0); } public RotateBitmap getDisplayBitmap() { return this.mBitmapDisplayed; } protected Matrix getImageViewMatrix() { this.mDisplayMatrix.set(this.mBaseMatrix); this.mDisplayMatrix.postConcat(this.mSuppMatrix); return this.mDisplayMatrix; } public float getMaxZoom() { return this.mMaxZoom; } /** * Setup the base matrix so that the image is centered and scaled properly. * * @param bitmap * @param matrix */ protected void getProperBaseMatrix(final RotateBitmap bitmap, final Matrix matrix) { final float viewWidth = getWidth(); final float viewHeight = getHeight(); final float w = bitmap.getWidth(); final float h = bitmap.getHeight(); matrix.reset(); final float widthScale = Math.min(viewWidth / w, this.MAX_ZOOM); final float heightScale = Math.min(viewHeight / h, this.MAX_ZOOM); final float scale = Math.min(widthScale, heightScale); matrix.postConcat(bitmap.getRotateMatrix()); matrix.postScale(scale, scale); matrix.postTranslate((viewWidth - (w * scale)) / this.MAX_ZOOM, (viewHeight - (h * scale)) / this.MAX_ZOOM); } public float getScale() { return getScale(this.mSuppMatrix); } protected float getScale(final Matrix matrix) { return getValue(matrix, Matrix.MSCALE_X); } protected float getValue(final Matrix matrix, final int whichValue) { matrix.getValues(this.mMatrixValues); return this.mMatrixValues[whichValue]; } protected void init() { setScaleType(ImageView.ScaleType.MATRIX); } protected float maxZoom() { if (this.mBitmapDisplayed.getBitmap() == null) { return 1F; } final float fw = (float) this.mBitmapDisplayed.getWidth() / (float) this.mThisWidth; final float fh = (float) this.mBitmapDisplayed.getHeight() / (float) this.mThisHeight; final float max = Math.max(fw, fh) * 4; return max; } @Override protected void onLayout(final boolean changed, final int left, final int top, final int right, final int bottom) { super.onLayout(changed, left, top, right, bottom); this.mThisWidth = right - left; this.mThisHeight = bottom - top; final Runnable r = this.mOnLayoutRunnable; if (r != null) { this.mOnLayoutRunnable = null; r.run(); } if (this.mBitmapDisplayed.getBitmap() != null) { getProperBaseMatrix(this.mBitmapDisplayed, this.mBaseMatrix); setImageMatrix(Command.Layout, getImageViewMatrix()); } } protected void onZoom(final float scale) { } protected void panBy(final float dx, final float dy) { final RectF rect = getBitmapRect(); final RectF srect = new RectF(dx, dy, 0, 0); updateRect(rect, srect); postTranslate(srect.left, srect.top); center(true, true); } protected void postScale(final float scale, final float centerX, final float centerY) { this.mSuppMatrix.postScale(scale, scale, centerX, centerY); setImageMatrix(Command.Zoom, getImageViewMatrix()); } protected void postTranslate(final float deltaX, final float deltaY) { this.mSuppMatrix.postTranslate(deltaX, deltaY); setImageMatrix(Command.Move, getImageViewMatrix()); } public void scrollBy(final float x, final float y) { panBy(x, y); } protected void scrollBy(final float distanceX, final float distanceY, final float durationMs) { final float dx = distanceX; final float dy = distanceY; final long startTime = System.currentTimeMillis(); this.mHandler.post(new Runnable() { float old_x = 0; float old_y = 0; @Override public void run() { final long now = System.currentTimeMillis(); final float currentMs = Math.min(durationMs, now - startTime); final float x = ImageViewTouchBase.easeOut(currentMs, 0, dx, durationMs); final float y = ImageViewTouchBase.easeOut(currentMs, 0, dy, durationMs); panBy((x - this.old_x), (y - this.old_y)); this.old_x = x; this.old_y = y; if (currentMs < durationMs) { ImageViewTouchBase.this.mHandler.post(this); } else { final RectF centerRect = getCenter(true, true); if ((centerRect.left != 0) || (centerRect.top != 0)) { scrollBy(centerRect.left, centerRect.top); } } } }); } @Override public void setImageBitmap(final Bitmap bitmap) { setImageBitmap(bitmap, 0); } /** * This is the ultimate method called when a new bitmap is set * * @param bitmap * @param rotation */ protected void setImageBitmap(final Bitmap bitmap, final int rotation) { super.setImageBitmap(bitmap); final Drawable d = getDrawable(); if (d != null) { d.setDither(true); } this.mBitmapDisplayed.setBitmap(bitmap); this.mBitmapDisplayed.setRotation(rotation); } public void setImageBitmapReset(final Bitmap bitmap, final boolean reset) { setImageRotateBitmapReset(new RotateBitmap(bitmap, 0), reset); } public void setImageBitmapReset(final Bitmap bitmap, final int rotation, final boolean reset) { setImageRotateBitmapReset(new RotateBitmap(bitmap, rotation), reset); } protected void setImageMatrix(final Command command, final Matrix matrix) { setImageMatrix(matrix); } public void setImageRotateBitmapReset(final RotateBitmap bitmap, final boolean reset) { Log.d(ImageViewTouchBase.LOG_TAG, "setImageRotateBitmapReset"); final int viewWidth = getWidth(); if (viewWidth <= 0) { this.mOnLayoutRunnable = new Runnable() { @Override public void run() { setImageBitmapReset(bitmap.getBitmap(), bitmap.getRotation(), reset); } }; return; } if (bitmap.getBitmap() != null) { getProperBaseMatrix(bitmap, this.mBaseMatrix); setImageBitmap(bitmap.getBitmap(), bitmap.getRotation()); } else { this.mBaseMatrix.reset(); setImageBitmap(null); } if (reset) { this.mSuppMatrix.reset(); } setImageMatrix(Command.Reset, getImageViewMatrix()); this.mMaxZoom = maxZoom(); if (this.mListener != null) { this.mListener.onBitmapChanged(bitmap.getBitmap()); } } public void setOnBitmapChangedListener( final OnBitmapChangedListener listener) { this.mListener = listener; } protected void updateRect(final RectF bitmapRect, final RectF scrollRect) { final float width = getWidth(); final float height = getHeight(); if ((bitmapRect.top >= 0) && (bitmapRect.bottom <= height)) { scrollRect.top = 0; } if ((bitmapRect.left >= 0) && (bitmapRect.right <= width)) { scrollRect.left = 0; } if (((bitmapRect.top + scrollRect.top) >= 0) && (bitmapRect.bottom > height)) { scrollRect.top = (int) (0 - bitmapRect.top); } if (((bitmapRect.bottom + scrollRect.top) <= (height - 0)) && (bitmapRect.top < 0)) { scrollRect.top = (int) ((height - 0) - bitmapRect.bottom); } if ((bitmapRect.left + scrollRect.left) >= 0) { scrollRect.left = (int) (0 - bitmapRect.left); } if ((bitmapRect.right + scrollRect.left) <= (width - 0)) { scrollRect.left = (int) ((width - 0) - bitmapRect.right); // Log.d( LOG_TAG, "scrollRect(2): " + scrollRect.toString() ); } } protected void zoomTo(final float scale) { final float cx = getWidth() / 2F; final float cy = getHeight() / 2F; zoomTo(scale, cx, cy); } public void zoomTo(final float scale, final float durationMs) { final float cx = getWidth() / 2F; final float cy = getHeight() / 2F; zoomTo(scale, cx, cy, durationMs); } protected void zoomTo(float scale, final float centerX, final float centerY) { if (scale > this.mMaxZoom) { scale = this.mMaxZoom; } final float oldScale = getScale(); final float deltaScale = scale / oldScale; postScale(deltaScale, centerX, centerY); onZoom(getScale()); center(true, true); } protected void zoomTo(final float scale, final float centerX, final float centerY, final float durationMs) { // Log.d( LOG_TAG, "zoomTo: " + scale + ", " + centerX + ": " + centerY // ); final long startTime = System.currentTimeMillis(); final float incrementPerMs = (scale - getScale()) / durationMs; final float oldScale = getScale(); this.mHandler.post(new Runnable() { @Override public void run() { final long now = System.currentTimeMillis(); final float currentMs = Math.min(durationMs, now - startTime); final float target = oldScale + (incrementPerMs * currentMs); zoomTo(target, centerX, centerY); if (currentMs < durationMs) { ImageViewTouchBase.this.mHandler.post(this); } else { // if ( getScale() < 1f ) {} } } }); } }