/*
* Copyright (c) 2012 Jason Polites 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.tweetlanes.android.core.widget.gestureimageview;
import android.content.Context;
import android.content.res.Configuration;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.ImageView;
import java.io.InputStream;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class GestureImageView extends ImageView {
private static final String GLOBAL_NS = "http://schemas.android.com/apk/res/android";
private static final String LOCAL_NS = "http://schemas.polites.com/android";
private final Semaphore mDrawLock = new Semaphore(0);
private Animator mAnimator;
private Drawable mDrawable;
private float mX = 0, mY = 0;
private boolean mLayout = false;
private float mScaleAdjust = 1.0f;
private float mStartingScale = -1.0f;
private float mMaxScale = 5.0f;
private float mMinScale = 0.75f;
private float mFitScaleHorizontal = 1.0f;
private float mFitScaleVertical = 1.0f;
private float mRotation = 0.0f;
private float mCenterX;
private float mCenterY;
private Float mStartX, mStartY;
private int mResId = -1;
private boolean mRecycle = false;
private boolean mStrict = false;
private int mDisplayHeight;
private int mDisplayWidth;
private int mAlpha = 255;
private ColorFilter mColorFilter;
private int mDeviceOrientation = -1;
private int mImageOrientation;
private GestureImageViewListener mGestureImageViewListener;
private GestureImageViewTouchListener mGestureImageViewTouchListener;
private OnTouchListener mCustomOnTouchListener;
private OnClickListener mOnClickListener;
public GestureImageView(Context context, AttributeSet attrs, int defStyle) {
this(context, attrs);
}
public GestureImageView(Context context, AttributeSet attrs) {
super(context, attrs);
String scaleType = attrs.getAttributeValue(GLOBAL_NS, "scaleType");
if (scaleType == null || scaleType.trim().length() == 0) {
setScaleType(ScaleType.CENTER_INSIDE);
}
String strStartX = attrs.getAttributeValue(LOCAL_NS, "start-x");
String strStartY = attrs.getAttributeValue(LOCAL_NS, "start-y");
if (strStartX != null && strStartX.trim().length() > 0) {
mStartX = Float.parseFloat(strStartX);
}
if (strStartY != null && strStartY.trim().length() > 0) {
mStartY = Float.parseFloat(strStartY);
}
setStartingScale(attrs.getAttributeFloatValue(LOCAL_NS, "start-scale",
mStartingScale));
setMinScale(attrs.getAttributeFloatValue(LOCAL_NS, "min-scale",
mMinScale));
setMaxScale(attrs.getAttributeFloatValue(LOCAL_NS, "max-scale",
mMaxScale));
setStrict(attrs.getAttributeBooleanValue(LOCAL_NS, "strict", mStrict));
setRecycle(attrs
.getAttributeBooleanValue(LOCAL_NS, "recycle", mRecycle));
initImage();
}
public GestureImageView(Context context) {
super(context);
setScaleType(ScaleType.CENTER_INSIDE);
initImage();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mDrawable != null) {
int orientation = getResources().getConfiguration().orientation;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
mDisplayHeight = MeasureSpec.getSize(heightMeasureSpec);
if (getLayoutParams().width == LayoutParams.WRAP_CONTENT) {
float ratio = (float) getImageWidth()
/ (float) getImageHeight();
mDisplayWidth = Math.round((float) mDisplayHeight * ratio);
} else {
mDisplayWidth = MeasureSpec.getSize(widthMeasureSpec);
}
} else {
mDisplayWidth = MeasureSpec.getSize(widthMeasureSpec);
if (getLayoutParams().height == LayoutParams.WRAP_CONTENT) {
float ratio = (float) getImageHeight()
/ (float) getImageWidth();
mDisplayHeight = Math.round((float) mDisplayWidth * ratio);
} else {
mDisplayHeight = MeasureSpec.getSize(heightMeasureSpec);
}
}
} else {
mDisplayHeight = MeasureSpec.getSize(heightMeasureSpec);
mDisplayWidth = MeasureSpec.getSize(widthMeasureSpec);
}
setMeasuredDimension(mDisplayWidth, mDisplayHeight);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (changed || !mLayout) {
setupCanvas(mDisplayWidth, mDisplayHeight, getResources()
.getConfiguration().orientation);
}
}
void setupCanvas(int measuredWidth, int measuredHeight,
int orientation) {
if (mDeviceOrientation != orientation) {
mLayout = false;
mDeviceOrientation = orientation;
}
if (mDrawable != null && !mLayout) {
int imageWidth = getImageWidth();
int imageHeight = getImageHeight();
int HWidth = Math.round(((float) imageWidth / 2.0f));
int HHeight = Math.round(((float) imageHeight / 2.0f));
measuredWidth -= (getPaddingLeft() + getPaddingRight());
measuredHeight -= (getPaddingTop() + getPaddingBottom());
computeCropScale(imageWidth, imageHeight, measuredWidth,
measuredHeight);
if (mStartingScale <= 0.0f) {
computeStartingScale(imageWidth, imageHeight, measuredWidth,
measuredHeight);
}
mScaleAdjust = mStartingScale;
this.mCenterX = (float) measuredWidth / 2.0f;
this.mCenterY = (float) measuredHeight / 2.0f;
if (mStartX == null) {
mX = mCenterX;
} else {
mX = mStartX;
}
if (mStartY == null) {
mY = mCenterY;
} else {
mY = mStartY;
}
mGestureImageViewTouchListener = new GestureImageViewTouchListener(
this, measuredWidth, measuredHeight);
if (isLandscape()) {
mGestureImageViewTouchListener.setMinScale(mMinScale
* mFitScaleHorizontal);
} else {
mGestureImageViewTouchListener.setMinScale(mMinScale
* mFitScaleVertical);
}
mGestureImageViewTouchListener.setMaxScale(mMaxScale
* mStartingScale);
mGestureImageViewTouchListener
.setFitScaleHorizontal(mFitScaleHorizontal);
mGestureImageViewTouchListener
.setFitScaleVertical(mFitScaleVertical);
mGestureImageViewTouchListener.setCanvasWidth(measuredWidth);
mGestureImageViewTouchListener.setCanvasHeight(measuredHeight);
mGestureImageViewTouchListener.setOnClickListener(mOnClickListener);
mDrawable.setBounds(-HWidth, -HHeight, HWidth, HHeight);
super.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (mCustomOnTouchListener != null) {
mCustomOnTouchListener.onTouch(v, event);
}
return mGestureImageViewTouchListener.onTouch(v, event);
}
});
mLayout = true;
}
}
void computeCropScale(int imageWidth, int imageHeight,
int measuredWidth, int measuredHeight) {
mFitScaleHorizontal = (float) measuredWidth / (float) imageWidth;
mFitScaleVertical = (float) measuredHeight / (float) imageHeight;
}
void computeStartingScale(int imageWidth, int imageHeight,
int measuredWidth, int measuredHeight) {
switch (getScaleType()) {
case CENTER:
// Center the image in the view, but perform no scaling.
mStartingScale = 1.0f;
break;
case CENTER_CROP:
mStartingScale = Math.max((float) measuredHeight
/ (float) imageHeight, (float) measuredWidth
/ (float) imageWidth);
break;
case CENTER_INSIDE:
if (isLandscape()) {
mStartingScale = mFitScaleHorizontal;
} else {
mStartingScale = mFitScaleVertical;
}
break;
}
}
boolean isNotRecycled() {
if (mDrawable != null && mDrawable instanceof BitmapDrawable) {
Bitmap bitmap = ((BitmapDrawable) mDrawable).getBitmap();
if (bitmap != null) {
return !bitmap.isRecycled();
}
}
return true;
}
void recycle() {
if (mRecycle && mDrawable != null
&& mDrawable instanceof BitmapDrawable) {
Bitmap bitmap = ((BitmapDrawable) mDrawable).getBitmap();
if (bitmap != null) {
bitmap.recycle();
}
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mLayout) {
if (mDrawable != null && isNotRecycled()) {
canvas.save();
float scale = 1.0f;
float adjustedScale = scale * mScaleAdjust;
canvas.translate(mX, mY);
if (mRotation != 0.0f) {
canvas.rotate(mRotation);
}
if (adjustedScale != 1.0f) {
canvas.scale(adjustedScale, adjustedScale);
}
mDrawable.draw(canvas);
canvas.restore();
}
if (mDrawLock.availablePermits() <= 0) {
mDrawLock.release();
}
}
}
/**
* Waits for a draw
*
* @param max time to wait for draw (ms)
* @throws InterruptedException
*/
public boolean waitForDraw(long timeout) throws InterruptedException {
return mDrawLock.tryAcquire(timeout, TimeUnit.MILLISECONDS);
}
@Override
protected void onAttachedToWindow() {
mAnimator = new Animator(this, "GestureImageViewAnimator");
mAnimator.start();
if (mResId >= 0 && mDrawable == null) {
setImageResource(mResId);
}
super.onAttachedToWindow();
}
public void animationStart(Animation animation) {
if (mAnimator != null) {
mAnimator.play(animation);
}
}
public void animationStop() {
if (mAnimator != null) {
mAnimator.cancel();
}
}
@Override
protected void onDetachedFromWindow() {
if (mAnimator != null) {
mAnimator.finish();
}
if (mRecycle && mDrawable != null && isNotRecycled()) {
recycle();
mDrawable = null;
}
super.onDetachedFromWindow();
}
void initImage() {
if (this.mDrawable != null) {
this.mDrawable.setAlpha(mAlpha);
this.mDrawable.setFilterBitmap(true);
if (mColorFilter != null) {
this.mDrawable.setColorFilter(mColorFilter);
}
}
if (!mLayout) {
requestLayout();
redraw();
}
}
public void setImageBitmap(Bitmap image) {
this.mDrawable = new BitmapDrawable(getResources(), image);
initImage();
}
@Override
public void setImageDrawable(Drawable drawable) {
this.mDrawable = drawable;
initImage();
}
public void setImageResource(int id) {
if (this.mDrawable != null) {
this.recycle();
}
if (id >= 0) {
this.mResId = id;
setImageDrawable(getContext().getResources().getDrawable(id));
}
}
public int getScaledWidth() {
return Math.round(getImageWidth() * getScale());
}
public int getScaledHeight() {
return Math.round(getImageHeight() * getScale());
}
public int getImageWidth() {
if (mDrawable != null) {
return mDrawable.getIntrinsicWidth();
}
return 0;
}
public int getImageHeight() {
if (mDrawable != null) {
return mDrawable.getIntrinsicHeight();
}
return 0;
}
public void moveBy(float x, float y) {
this.mX += x;
this.mY += y;
}
public void setPosition(float x, float y) {
this.mX = x;
this.mY = y;
}
public void redraw() {
postInvalidate();
}
void setMinScale(float min) {
this.mMinScale = min;
if (mGestureImageViewTouchListener != null) {
mGestureImageViewTouchListener.setMinScale(min
* mFitScaleHorizontal);
}
}
void setMaxScale(float max) {
this.mMaxScale = max;
if (mGestureImageViewTouchListener != null) {
mGestureImageViewTouchListener.setMaxScale(max * mStartingScale);
}
}
public void setScale(float scale) {
mScaleAdjust = scale;
}
public float getScale() {
return mScaleAdjust;
}
public float getImageX() {
return mX;
}
public float getImageY() {
return mY;
}
public boolean isStrict() {
return mStrict;
}
void setStrict(boolean strict) {
this.mStrict = strict;
}
public boolean isRecycle() {
return mRecycle;
}
void setRecycle(boolean recycle) {
this.mRecycle = recycle;
}
public void reset() {
mX = mCenterX;
mY = mCenterY;
mScaleAdjust = mStartingScale;
redraw();
}
public void setRotation(float rotation) {
this.mRotation = rotation;
}
public void setGestureImageViewListener(
GestureImageViewListener pinchImageViewListener) {
this.mGestureImageViewListener = pinchImageViewListener;
}
public GestureImageViewListener getGestureImageViewListener() {
return mGestureImageViewListener;
}
@Override
public Drawable getDrawable() {
return mDrawable;
}
@Override
public void setAlpha(int alpha) {
this.mAlpha = alpha;
if (mDrawable != null) {
mDrawable.setAlpha(alpha);
}
}
@Override
public void setColorFilter(ColorFilter cf) {
this.mColorFilter = cf;
if (mDrawable != null) {
mDrawable.setColorFilter(cf);
}
}
@Override
public void setImageURI(Uri mUri) {
if ("content".equals(mUri.getScheme())) {
try {
String[] orientationColumn = {MediaStore.Images.Media.ORIENTATION};
Cursor cur = getContext().getContentResolver().query(mUri,
orientationColumn, null, null, null);
if (cur != null && cur.moveToFirst()) {
mImageOrientation = cur.getInt(cur
.getColumnIndex(orientationColumn[0]));
}
InputStream in = null;
try {
in = getContext().getContentResolver()
.openInputStream(mUri);
Bitmap bmp = BitmapFactory.decodeStream(in);
if (mImageOrientation != 0) {
Matrix m = new Matrix();
m.postRotate(mImageOrientation);
Bitmap rotated = Bitmap.createBitmap(bmp, 0, 0,
bmp.getWidth(), bmp.getHeight(), m, true);
bmp.recycle();
setImageDrawable(new BitmapDrawable(getResources(),
rotated));
} else {
setImageDrawable(new BitmapDrawable(getResources(), bmp));
}
} finally {
if (in != null) {
in.close();
}
if (cur != null) {
cur.close();
}
}
} catch (Exception e) {
Log.w("GestureImageView", "Unable to open content: " + mUri, e);
}
} else {
setImageDrawable(Drawable.createFromPath(mUri.toString()));
}
if (mDrawable == null) {
Log.e("GestureImageView", "resolveUri failed on bad bitmap uri: "
+ mUri);
// Don't try again.
mUri = null;
}
}
@Override
public Matrix getImageMatrix() {
if (mStrict) {
throw new UnsupportedOperationException("Not supported");
}
return super.getImageMatrix();
}
@Override
public void setScaleType(ScaleType scaleType) {
if (scaleType == ScaleType.CENTER || scaleType == ScaleType.CENTER_CROP
|| scaleType == ScaleType.CENTER_INSIDE) {
super.setScaleType(scaleType);
} else if (mStrict) {
throw new UnsupportedOperationException("Not supported");
}
}
@Override
public void invalidateDrawable(Drawable dr) {
if (mStrict) {
throw new UnsupportedOperationException("Not supported");
}
super.invalidateDrawable(dr);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
if (mStrict) {
throw new UnsupportedOperationException("Not supported");
}
return super.onCreateDrawableState(extraSpace);
}
@Override
public void setAdjustViewBounds(boolean adjustViewBounds) {
if (mStrict) {
throw new UnsupportedOperationException("Not supported");
}
super.setAdjustViewBounds(adjustViewBounds);
}
@Override
public void setImageLevel(int level) {
if (mStrict) {
throw new UnsupportedOperationException("Not supported");
}
super.setImageLevel(level);
}
@Override
public void setImageMatrix(Matrix matrix) {
if (mStrict) {
throw new UnsupportedOperationException("Not supported");
}
}
@Override
public void setImageState(int[] state, boolean merge) {
if (mStrict) {
throw new UnsupportedOperationException("Not supported");
}
}
@Override
public void setSelected(boolean selected) {
if (mStrict) {
throw new UnsupportedOperationException("Not supported");
}
super.setSelected(selected);
}
@Override
public void setOnTouchListener(OnTouchListener l) {
this.mCustomOnTouchListener = l;
}
public float getCenterX() {
return mCenterX;
}
public float getCenterY() {
return mCenterY;
}
public boolean isLandscape() {
return getImageWidth() >= getImageHeight();
}
boolean isPortrait() {
return getImageWidth() <= getImageHeight();
}
void setStartingScale(float startingScale) {
this.mStartingScale = startingScale;
}
public void setStartingPosition(float x, float y) {
this.mStartX = x;
this.mStartY = y;
}
@Override
public void setOnClickListener(OnClickListener l) {
this.mOnClickListener = l;
if (mGestureImageViewTouchListener != null) {
mGestureImageViewTouchListener.setOnClickListener(l);
}
}
/**
* Returns true if the image dimensions are aligned with the orientation of
* the device.
*
* @return
*/
public boolean isOrientationAligned() {
if (mDeviceOrientation == Configuration.ORIENTATION_LANDSCAPE) {
return isLandscape();
} else if (mDeviceOrientation == Configuration.ORIENTATION_PORTRAIT) {
return isPortrait();
}
return true;
}
public int getDeviceOrientation() {
return mDeviceOrientation;
}
}