/* * 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.smartandroid.sa.zmImageview; import java.io.InputStream; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; 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; public class ZoomImageView extends ImageView { public static final String GLOBAL_NS = "http://schemas.android.com/apk/res/android"; public static final String LOCAL_NS = "http://schemas.polites.com/android"; private final Semaphore drawLock = new Semaphore(0); private Animator animator; private Drawable drawable; private float x = 0, y = 0; private boolean layout = false; private float scaleAdjust = 1.0f; private float startingScale = -1.0f; private float scale = 1.0f; private float maxScale = 5.0f; private float minScale = 0.75f; private float fitScaleHorizontal = 1.0f; private float fitScaleVertical = 1.0f; private float rotation = 0.0f; private float centerX; private float centerY; private Float startX, startY; private int hWidth; private int hHeight; private int resId = -1; private boolean recycle = false; private boolean strict = false; private int displayHeight; private int displayWidth; private int alpha = 255; private ColorFilter colorFilter; private int deviceOrientation = -1; private int imageOrientation; private GestureImageViewListener gestureImageViewListener; private GestureImageViewTouchListener gestureImageViewTouchListener; private OnTouchListener customOnTouchListener; private OnClickListener onClickListener; public ZoomImageView(Context context, AttributeSet attrs, int defStyle) { this(context, attrs); } public ZoomImageView(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) { startX = Float.parseFloat(strStartX); } if(strStartY != null && strStartY.trim().length() > 0) { startY = Float.parseFloat(strStartY); } setStartingScale(attrs.getAttributeFloatValue(LOCAL_NS, "start-scale", startingScale)); setMinScale(attrs.getAttributeFloatValue(LOCAL_NS, "min-scale", minScale)); setMaxScale(attrs.getAttributeFloatValue(LOCAL_NS, "max-scale", maxScale)); setStrict(attrs.getAttributeBooleanValue(LOCAL_NS, "strict", strict)); setRecycle(attrs.getAttributeBooleanValue(LOCAL_NS, "recycle", recycle)); initImage(); } public ZoomImageView(Context context) { super(context); setScaleType(ScaleType.CENTER_INSIDE); initImage(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if(drawable != null) { int orientation = getResources().getConfiguration().orientation; if(orientation == Configuration.ORIENTATION_LANDSCAPE) { displayHeight = MeasureSpec.getSize(heightMeasureSpec); if(getLayoutParams().width == LayoutParams.WRAP_CONTENT) { float ratio = (float) getImageWidth() / (float) getImageHeight(); displayWidth = Math.round( (float) displayHeight * ratio) ; } else { displayWidth = MeasureSpec.getSize(widthMeasureSpec); } } else { displayWidth = MeasureSpec.getSize(widthMeasureSpec); if(getLayoutParams().height == LayoutParams.WRAP_CONTENT) { float ratio = (float) getImageHeight() / (float) getImageWidth(); displayHeight = Math.round( (float) displayWidth * ratio) ; } else { displayHeight = MeasureSpec.getSize(heightMeasureSpec); } } } else { displayHeight = MeasureSpec.getSize(heightMeasureSpec); displayWidth = MeasureSpec.getSize(widthMeasureSpec); } setMeasuredDimension(displayWidth, displayHeight); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if(changed || !layout) { setupCanvas(displayWidth, displayHeight, getResources().getConfiguration().orientation); } } protected void setupCanvas(int measuredWidth, int measuredHeight, int orientation) { if(deviceOrientation != orientation) { layout = false; deviceOrientation = orientation; } if(drawable != null && !layout) { int imageWidth = getImageWidth(); int imageHeight = getImageHeight(); hWidth = Math.round(((float)imageWidth / 2.0f)); hHeight = Math.round(((float)imageHeight / 2.0f)); measuredWidth -= (getPaddingLeft() + getPaddingRight()); measuredHeight -= (getPaddingTop() + getPaddingBottom()); computeCropScale(imageWidth, imageHeight, measuredWidth, measuredHeight); if(startingScale <= 0.0f) { computeStartingScale(imageWidth, imageHeight, measuredWidth, measuredHeight); } scaleAdjust = startingScale; this.centerX = (float) measuredWidth / 2.0f; this.centerY = (float) measuredHeight / 2.0f; if(startX == null) { x = centerX; } else { x = startX; } if(startY == null) { y = centerY; } else { y = startY; } gestureImageViewTouchListener = new GestureImageViewTouchListener(this, measuredWidth, measuredHeight); if(isLandscape()) { gestureImageViewTouchListener.setMinScale(minScale * fitScaleHorizontal); } else { gestureImageViewTouchListener.setMinScale(minScale * fitScaleVertical); } gestureImageViewTouchListener.setMaxScale(maxScale * startingScale); gestureImageViewTouchListener.setFitScaleHorizontal(fitScaleHorizontal); gestureImageViewTouchListener.setFitScaleVertical(fitScaleVertical); gestureImageViewTouchListener.setCanvasWidth(measuredWidth); gestureImageViewTouchListener.setCanvasHeight(measuredHeight); gestureImageViewTouchListener.setOnClickListener(onClickListener); drawable.setBounds(-hWidth,-hHeight,hWidth,hHeight); super.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if(customOnTouchListener != null) { customOnTouchListener.onTouch(v, event); } return gestureImageViewTouchListener.onTouch(v, event); } }); layout = true; } } protected void computeCropScale(int imageWidth, int imageHeight, int measuredWidth, int measuredHeight) { fitScaleHorizontal = (float) measuredWidth / (float) imageWidth; fitScaleVertical = (float) measuredHeight / (float) imageHeight; } protected void computeStartingScale(int imageWidth, int imageHeight, int measuredWidth, int measuredHeight) { switch(getScaleType()) { case CENTER: // Center the image in the view, but perform no scaling. startingScale = 1.0f; break; case CENTER_CROP: // Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions // (width and height) of the image will be equal to or larger than the corresponding dimension of the view (minus padding). startingScale = Math.max((float) measuredHeight / (float) imageHeight, (float) measuredWidth/ (float) imageWidth); break; case CENTER_INSIDE: // Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions // (width and height) of the image will be equal to or less than the corresponding dimension of the view (minus padding). float wRatio = (float) imageWidth / (float) measuredWidth; float hRatio = (float) imageHeight / (float) measuredHeight; if(wRatio > hRatio) { startingScale = fitScaleHorizontal; } else { startingScale = fitScaleVertical; } break; } } protected boolean isRecycled() { if(drawable != null && drawable instanceof BitmapDrawable) { Bitmap bitmap = ((BitmapDrawable)drawable).getBitmap(); if(bitmap != null) { return bitmap.isRecycled(); } } return false; } protected void recycle() { if(recycle && drawable != null && drawable instanceof BitmapDrawable) { Bitmap bitmap = ((BitmapDrawable)drawable).getBitmap(); if(bitmap != null) { bitmap.recycle(); } } } @Override protected void onDraw(Canvas canvas) { if(layout) { if(drawable != null && !isRecycled()) { canvas.save(); float adjustedScale = scale * scaleAdjust; canvas.translate(x, y); if(rotation != 0.0f) { canvas.rotate(rotation); } if(adjustedScale != 1.0f) { canvas.scale(adjustedScale, adjustedScale); } drawable.draw(canvas); canvas.restore(); } if(drawLock.availablePermits() <= 0) { drawLock.release(); } } } /** * Waits for a draw * @param max time to wait for draw (ms) * @throws InterruptedException */ public boolean waitForDraw(long timeout) throws InterruptedException { return drawLock.tryAcquire(timeout, TimeUnit.MILLISECONDS); } @Override protected void onAttachedToWindow() { animator = new Animator(this, "GestureImageViewAnimator"); animator.start(); if(resId >= 0 && drawable == null) { setImageResource(resId); } super.onAttachedToWindow(); } public void animationStart(Animation animation) { if(animator != null) { animator.play(animation); } } public void animationStop() { if(animator != null) { animator.cancel(); } } @Override protected void onDetachedFromWindow() { if(animator != null) { animator.finish(); } if(recycle && drawable != null && !isRecycled()) { recycle(); drawable = null; } super.onDetachedFromWindow(); } protected void initImage() { if(this.drawable != null) { this.drawable.setAlpha(alpha); this.drawable.setFilterBitmap(true); if(colorFilter != null) { this.drawable.setColorFilter(colorFilter); } } if(!layout) { requestLayout(); redraw(); } } public void setImageBitmap(Bitmap image) { this.drawable = new BitmapDrawable(getResources(), image); initImage(); } @Override public void setImageDrawable(Drawable drawable) { this.drawable = drawable; initImage(); } public void setImageResource(int id) { if(this.drawable != null) { this.recycle(); } if(id >= 0) { this.resId = 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(drawable != null) { return drawable.getIntrinsicWidth(); } return 0; } public int getImageHeight() { if(drawable != null) { return drawable.getIntrinsicHeight(); } return 0; } public void moveBy(float x, float y) { this.x += x; this.y += y; } public void setPosition(float x, float y) { this.x = x; this.y = y; } public void redraw() { postInvalidate(); } public void setMinScale(float min) { this.minScale = min; if(gestureImageViewTouchListener != null) { gestureImageViewTouchListener.setMinScale(min * fitScaleHorizontal); } } public void setMaxScale(float max) { this.maxScale = max; if(gestureImageViewTouchListener != null) { gestureImageViewTouchListener.setMaxScale(max * startingScale); } } public void setScale(float scale) { scaleAdjust = scale; } public float getScale() { return scaleAdjust; } public float getImageX() { return x; } public float getImageY() { return y; } public boolean isStrict() { return strict; } public void setStrict(boolean strict) { this.strict = strict; } public boolean isRecycle() { return recycle; } public void setRecycle(boolean recycle) { this.recycle = recycle; } public void reset() { x = centerX; y = centerY; scaleAdjust = startingScale; if (gestureImageViewTouchListener != null) { gestureImageViewTouchListener.reset(); } redraw(); } public void setRotation(float rotation) { this.rotation = rotation; } public void setGestureImageViewListener(GestureImageViewListener pinchImageViewListener) { this.gestureImageViewListener = pinchImageViewListener; } public GestureImageViewListener getGestureImageViewListener() { return gestureImageViewListener; } @Override public Drawable getDrawable() { return drawable; } @Override public void setAlpha(int alpha) { this.alpha = alpha; if(drawable != null) { drawable.setAlpha(alpha); } } @Override public void setColorFilter(ColorFilter cf) { this.colorFilter = cf; if(drawable != null) { drawable.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()) { imageOrientation = cur.getInt(cur.getColumnIndex(orientationColumn[0])); } InputStream in = null; try { in = getContext().getContentResolver().openInputStream(mUri); Bitmap bmp = BitmapFactory.decodeStream(in); if(imageOrientation != 0) { Matrix m = new Matrix(); m.postRotate(imageOrientation); 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 (drawable == null) { Log.e("GestureImageView", "resolveUri failed on bad bitmap uri: " + mUri); // Don't try again. mUri = null; } } @Override public Matrix getImageMatrix() { if(strict) { 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(strict) { throw new UnsupportedOperationException("Not supported"); } } @Override public void invalidateDrawable(Drawable dr) { if(strict) { throw new UnsupportedOperationException("Not supported"); } super.invalidateDrawable(dr); } @Override public int[] onCreateDrawableState(int extraSpace) { if(strict) { throw new UnsupportedOperationException("Not supported"); } return super.onCreateDrawableState(extraSpace); } @Override public void setAdjustViewBounds(boolean adjustViewBounds) { if(strict) { throw new UnsupportedOperationException("Not supported"); } super.setAdjustViewBounds(adjustViewBounds); } @Override public void setImageLevel(int level) { if(strict) { throw new UnsupportedOperationException("Not supported"); } super.setImageLevel(level); } @Override public void setImageMatrix(Matrix matrix) { if(strict) { throw new UnsupportedOperationException("Not supported"); } } @Override public void setImageState(int[] state, boolean merge) { if(strict) { throw new UnsupportedOperationException("Not supported"); } } @Override public void setSelected(boolean selected) { if(strict) { throw new UnsupportedOperationException("Not supported"); } super.setSelected(selected); } @Override public void setOnTouchListener(OnTouchListener l) { this.customOnTouchListener = l; } public float getCenterX() { return centerX; } public float getCenterY() { return centerY; } public boolean isLandscape() { return getImageWidth() >= getImageHeight(); } public boolean isPortrait() { return getImageWidth() <= getImageHeight(); } public void setStartingScale(float startingScale) { this.startingScale = startingScale; } public void setStartingPosition(float x, float y) { this.startX = x; this.startY = y; } @Override public void setOnClickListener(OnClickListener l) { this.onClickListener = l; if(gestureImageViewTouchListener != null) { gestureImageViewTouchListener.setOnClickListener(l); } } /** * Returns true if the image dimensions are aligned with the orientation of the device. * @return */ public boolean isOrientationAligned() { if(deviceOrientation == Configuration.ORIENTATION_LANDSCAPE) { return isLandscape(); } else if(deviceOrientation == Configuration.ORIENTATION_PORTRAIT) { return isPortrait(); } return true; } public int getDeviceOrientation() { return deviceOrientation; } }