/*
* Tweetings - Twitter client for Android
*
* Copyright (C) 2012-2013 RBD Solutions Limited <apps@tweetings.net>
* Copyright (C) 2012 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.dwdesign.tweetings.view;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.FloatMath;
import android.view.MotionEvent;
import android.view.View;
public class ImageViewer extends View {
private Bitmap mBitmap;
private final Paint mPaint = new Paint(), mBackgroundPaint = new Paint();
private volatile int mDx = 0, mDy = 0;
private volatile float mZoomFactor = 3.0f, mMinZoomFactor;
private boolean mMotionControl;
private int mSavedX, mSavedY;
private float mStartPinchDistance2 = -1, mStartZoomFactor;
private boolean isWaitingForDoubleTap = false;
private static final int ZOOM_IN_1 = 1, ZOOM_OUT_1 = 2, DOUBLE_TAP_TIMEOUT = 3, ZOOM_IN_2 = 4, ZOOM_OUT_2 = 5;
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(final Message msg) {
switch (msg.what) {
case ZOOM_IN_1: {
mHandler.removeMessages(ZOOM_IN_1);
if (mZoomFactor < mMinZoomFactor) {
final long delay = (long) (mMinZoomFactor / mZoomFactor) / 4;
mZoomFactor *= 1.1f;
if (mZoomFactor > mMinZoomFactor) {
mZoomFactor = mMinZoomFactor;
}
mHandler.sendEmptyMessageDelayed(ZOOM_IN_1, delay > 0 ? delay : 1);
} else {
mZoomFactor = mMinZoomFactor;
}
invalidate();
break;
}
case ZOOM_OUT_1: {
mHandler.removeMessages(ZOOM_OUT_1);
if (mZoomFactor > 1.0f) {
final long delay = (long) mZoomFactor / 4;
mZoomFactor /= 1.1f;
if (mZoomFactor < 1.0f) {
mZoomFactor = 1.0f;
}
mHandler.sendEmptyMessageDelayed(ZOOM_OUT_1, delay > 0 ? delay : 1);
} else {
mZoomFactor = 1.0f;
}
invalidate();
break;
}
case DOUBLE_TAP_TIMEOUT: {
mHandler.removeMessages(DOUBLE_TAP_TIMEOUT);
isWaitingForDoubleTap = false;
break;
}
case ZOOM_IN_2: {
mHandler.removeMessages(ZOOM_IN_2);
if (mZoomFactor < 1.0f) {
final long delay = (long) mZoomFactor / 4;
mZoomFactor *= 1.2f;
if (mZoomFactor > 1.0f) {
mZoomFactor = 1.0f;
}
mHandler.sendEmptyMessageDelayed(ZOOM_IN_2, delay > 0 ? delay : 1);
} else {
mZoomFactor = 1.0f;
}
invalidate();
break;
}
case ZOOM_OUT_2: {
mHandler.removeMessages(ZOOM_OUT_2);
if (mZoomFactor > mMinZoomFactor) {
final long delay = (long) (mMinZoomFactor / mZoomFactor) / 4;
mZoomFactor /= 1.2f;
if (mZoomFactor < mMinZoomFactor) {
mZoomFactor = mMinZoomFactor;
}
mHandler.sendEmptyMessageDelayed(ZOOM_OUT_2, delay > 0 ? delay : 1);
} else {
mZoomFactor = mMinZoomFactor;
}
invalidate();
break;
}
}
}
};
public ImageViewer(final Context context) {
this(context, null);
}
public ImageViewer(final Context context, final AttributeSet attrs) {
this(context, attrs, 0);
}
public ImageViewer(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
mPaint.setColor(Color.BLACK);
mBackgroundPaint.setColor(Color.TRANSPARENT);
}
@Override
public boolean onTouchEvent(final MotionEvent event) {
final int pointer_count = Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR ? GetPointerCountAccessor
.getPointerCount(event) : 1;
if (pointer_count > 1)
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR ? OnMultiTouchEventCompat.onMultiTouchEvent(
this, event) : true;
else if (pointer_count == 1) return onSingleTouchEvent(event);
return false;
}
public void recycle() {
if (mBitmap != null && !mBitmap.isRecycled()) {
mBitmap.recycle();
mBitmap = null;
}
}
public void setBitmap(final Bitmap bitmap) {
recycle();
mBitmap = bitmap;
mMinZoomFactor = getMinZoom(bitmap);
mZoomFactor = mMinZoomFactor;
invalidate();
}
@SuppressLint("DrawAllocation")
@Override
protected void onDraw(final Canvas canvas) {
final int w = getWidth();
final int h = getHeight();
canvas.drawRect(0, 0, w, h, mBackgroundPaint);
if (mBitmap == null || mBitmap.isRecycled()) return;
final int bw = (int) (mBitmap.getWidth() * mZoomFactor);
final int bh = (int) (mBitmap.getHeight() * mZoomFactor);
final Rect src = new Rect(0, 0, (int) (w / mZoomFactor), (int) (h / mZoomFactor));
final Rect dst = new Rect(0, 0, w, h);
if (bw <= w) {
src.left = 0;
src.right = mBitmap.getWidth();
dst.left = (w - bw) / 2;
dst.right = dst.left + bw;
} else {
final int bWidth = mBitmap.getWidth();
final int pWidth = (int) (w / mZoomFactor);
src.left = Math.min(bWidth - pWidth, Math.max((bWidth - pWidth) / 2 - mDx, 0));
src.right += src.left;
}
if (bh <= h) {
src.top = 0;
src.bottom = mBitmap.getHeight();
dst.top = (h - bh) / 2;
dst.bottom = dst.top + bh;
} else {
final int bHeight = mBitmap.getHeight();
final int pHeight = (int) (h / mZoomFactor);
src.top = Math.min(bHeight - pHeight, Math.max((bHeight - pHeight) / 2 - mDy, 0));
src.bottom += src.top;
}
canvas.drawBitmap(mBitmap, src, dst, mPaint);
}
private float getMinZoom(final Bitmap bitmap) {
float zoom = 1.0f;
if (bitmap != null) {
zoom = Math.min((float) getWidth() / bitmap.getWidth(), (float) getHeight() / bitmap.getHeight());
if (zoom >= 1) {
zoom = 1.0f;
}
}
return zoom;
}
private boolean onSingleTouchEvent(final MotionEvent event) {
final int x = (int) event.getX();
final int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
mMotionControl = false;
break;
case MotionEvent.ACTION_DOWN:
mMotionControl = true;
mSavedX = x;
mSavedY = y;
if (!isWaitingForDoubleTap) {
isWaitingForDoubleTap = true;
mHandler.sendEmptyMessageDelayed(DOUBLE_TAP_TIMEOUT, 300L);
} else {
if (mZoomFactor <= mMinZoomFactor) {
mHandler.sendEmptyMessage(ZOOM_IN_2);
} else if (mZoomFactor <= 1.0f) {
mHandler.sendEmptyMessage(ZOOM_OUT_2);
}
isWaitingForDoubleTap = false;
mHandler.removeMessages(DOUBLE_TAP_TIMEOUT);
}
break;
case MotionEvent.ACTION_MOVE:
if (mMotionControl) {
shift((int) ((x - mSavedX) / mZoomFactor), (int) ((y - mSavedY) / mZoomFactor));
}
mMotionControl = true;
mSavedX = x;
mSavedY = y;
break;
}
return true;
}
private void shift(final int dx, final int dy) {
if (mBitmap == null || mBitmap.isRecycled()) return;
final int w = (int) (getWidth() / mZoomFactor);
final int h = (int) (getHeight() / mZoomFactor);
final int bw = mBitmap.getWidth();
final int bh = mBitmap.getHeight();
final int newDx, newDy;
if (w < bw) {
final int delta = (bw - w) / 2;
newDx = Math.max(-delta, Math.min(delta, mDx + dx));
} else {
newDx = mDx;
}
if (h < bh) {
final int delta = (bh - h) / 2;
newDy = Math.max(-delta, Math.min(delta, mDy + dy));
} else {
newDy = mDy;
}
if (newDx != mDx || newDy != mDy) {
mDx = newDx;
mDy = newDy;
postInvalidate();
}
}
static class GetPointerCountAccessor {
@TargetApi(Build.VERSION_CODES.ECLAIR)
public static int getPointerCount(final MotionEvent event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR) return event.getPointerCount();
return 1;
}
}
@TargetApi(Build.VERSION_CODES.ECLAIR)
static class OnMultiTouchEventCompat {
private static boolean onMultiTouchEvent(final ImageViewer viewer, final MotionEvent event) {
viewer.isWaitingForDoubleTap = false;
final int pointer_count = event.getPointerCount();
final float diffX = event.getX(0) - event.getX(1), diffY = event.getY(0) - event.getY(1);
int x = (int) ((event.getX(0) + event.getX(1)) / 2);
int y = (int) ((event.getY(0) + event.getY(1)) / 2);
for (int i = 0; i < pointer_count; i++) {
x += event.getX(i);
}
x /= pointer_count;
for (int i = 0; i < pointer_count; i++) {
y += event.getY(i);
}
y /= pointer_count;
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_POINTER_UP: {
viewer.mStartPinchDistance2 = -1;
viewer.mMotionControl = false;
if (viewer.mZoomFactor < viewer.mMinZoomFactor) {
viewer.mHandler.sendEmptyMessage(ZOOM_IN_1);
}/* else if (viewer.mZoomFactor > 1.0f) {
viewer.mHandler.sendEmptyMessage(ZOOM_OUT_1);
}*/
break;
}
case MotionEvent.ACTION_POINTER_DOWN: {
viewer.mHandler.removeCallbacksAndMessages(null);
viewer.mMotionControl = true;
viewer.mSavedX = x;
viewer.mSavedY = y;
viewer.mStartPinchDistance2 = Math.max(diffX * diffX + diffY * diffY, 10f);
viewer.mStartZoomFactor = viewer.mZoomFactor;
break;
}
case MotionEvent.ACTION_MOVE: {
if (viewer.mMotionControl) {
viewer.shift((int) ((x - viewer.mSavedX) / viewer.mZoomFactor),
(int) ((y - viewer.mSavedY) / viewer.mZoomFactor));
}
viewer.mMotionControl = true;
viewer.mSavedX = x;
viewer.mSavedY = y;
final float distance2 = Math.max(diffX * diffX + diffY * diffY, 10f);
if (viewer.mStartPinchDistance2 < 0) {
viewer.mStartPinchDistance2 = distance2;
viewer.mStartZoomFactor = viewer.mZoomFactor;
} else {
viewer.mZoomFactor = viewer.mStartZoomFactor
* FloatMath.sqrt(distance2 / viewer.mStartPinchDistance2);
if (viewer.mZoomFactor < viewer.mMinZoomFactor * 0.75f) {
viewer.mZoomFactor = viewer.mMinZoomFactor * 0.75f;
} else if (viewer.mZoomFactor > 3.25f) {
viewer.mZoomFactor = 3.25f;
}
viewer.postInvalidate();
}
break;
}
}
return true;
}
}
}