Skip to content

Instantly share code, notes, and snippets.

@funorpain
Last active August 29, 2015 14:22
Show Gist options
  • Save funorpain/398d479b6f0fed78d626 to your computer and use it in GitHub Desktop.
Save funorpain/398d479b6f0fed78d626 to your computer and use it in GitHub Desktop.
PhotoView using fresco
package com.example.frescotest;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Matrix.ScaleToFit;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.OverScroller;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.controller.BaseControllerListener;
import com.facebook.drawee.controller.ControllerListener;
import com.facebook.drawee.generic.GenericDraweeHierarchy;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.view.DraweeHolder;
import com.facebook.imagepipeline.image.ImageInfo;
public class MyPhotoView extends View {
DraweeHolder<GenericDraweeHierarchy> mDraweeHolder;
int mImageWidth;
int mImageHeight;
RectF mBoundsRect = new RectF();
float mScale;
boolean mImageLoaded;
RectF mTmpRectF = new RectF();
Rect mTmpRect = new Rect();
Matrix mMatrix = new Matrix();
int mActivePointerId = -1;
float mLastX;
float mLastY;
GestureDetector mGestureDetector;
ScaleGestureDetector mScaleGestureDetector;
static final Interpolator sInterpolator = new AccelerateDecelerateInterpolator();
ZoomAnimation mZoomAnimation = new ZoomAnimation();
OverScroller mScroller;
FlingAnimation mFlingAnimation = new FlingAnimation();
ControllerListener<ImageInfo> mControllerListener = new BaseControllerListener<ImageInfo>() {
@Override
public void onFinalImageSet(String id, @Nullable ImageInfo imageInfo,
@Nullable Animatable anim) {
mImageWidth = imageInfo.getWidth();
mImageHeight = imageInfo.getHeight();
Drawable drawable = mDraweeHolder.getHierarchy()
.getTopLevelDrawable();
drawable.setBounds(0, 0, mImageWidth, mImageHeight);
final float wScale = (float) getWidth() / mImageWidth;
final float hScale = (float) getHeight() / mImageHeight;
final float scale = Math.min(wScale, hScale);
mBoundsRect.left = (getWidth() - mImageWidth * scale) / 2f;
mBoundsRect.top = (getHeight() - mImageHeight * scale) / 2f;
mBoundsRect.right = mBoundsRect.left + mImageWidth * scale;
mBoundsRect.bottom = mBoundsRect.top + mImageHeight * scale;
mScale = 1f;
mImageLoaded = true;
}
@Override
public void onIntermediateImageSet(String id,
@Nullable ImageInfo imageInfo) {
}
@Override
public void onFailure(String id, Throwable throwable) {
}
};
public MyPhotoView(Context context) {
super(context);
init();
}
public MyPhotoView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MyPhotoView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(
getResources()).build();
mDraweeHolder = DraweeHolder.create(hierarchy, getContext());
mDraweeHolder.getTopLevelDrawable().setCallback(this);
mGestureDetector = new GestureDetector(getContext(),
new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
mScroller.forceFinished(true);
ViewCompat.postInvalidateOnAnimation(MyPhotoView.this);
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return performClick();
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
fling((int) velocityX, (int) velocityY);
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
float targetScale = mScale < 2f ? 2f : 1f;
scale(targetScale / mScale, e.getX(), e.getY(), true);
return true;
}
});
mScaleGestureDetector = new ScaleGestureDetector(getContext(),
new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
scale(detector.getScaleFactor(), detector.getFocusX(),
detector.getFocusY(), false);
return true;
}
});
mScroller = new OverScroller(getContext());
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
mDraweeHolder.onDetach();
}
@Override
public void onStartTemporaryDetach() {
super.onStartTemporaryDetach();
mDraweeHolder.onDetach();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
mDraweeHolder.onAttach();
}
@Override
public void onFinishTemporaryDetach() {
super.onFinishTemporaryDetach();
mDraweeHolder.onAttach();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
mScaleGestureDetector.onTouchEvent(event);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
final float x = event.getX();
final float y = event.getY();
mActivePointerId = event.getPointerId(event.getActionIndex());
mLastX = x;
mLastY = y;
break;
}
case MotionEvent.ACTION_MOVE: {
final int pointerIndex = event.findPointerIndex(mActivePointerId);
final float x = event.getX(pointerIndex);
final float y = event.getY(pointerIndex);
float dx = x - mLastX;
float dy = y - mLastY;
mLastX = x;
mLastY = y;
move(dx, dy);
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
mActivePointerId = -1;
if (mScale < 1f) {
scale(1f / mScale, 0, 0, true);
}
break;
}
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = event.getActionIndex();
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastX = event.getX(newPointerIndex);
mLastY = event.getY(newPointerIndex);
mActivePointerId = event.getPointerId(newPointerIndex);
}
if (event.getPointerCount() == 2 && mScale < 1f) {
scale(1f / mScale, 0, 0, true);
}
break;
}
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
final int sc = canvas.save();
mTmpRectF.set(0, 0, mImageWidth, mImageHeight);
mMatrix.setRectToRect(mTmpRectF, mBoundsRect, ScaleToFit.FILL);
canvas.concat(mMatrix);
Drawable drawable = mDraweeHolder.getHierarchy().getTopLevelDrawable();
drawable.draw(canvas);
canvas.restoreToCount(sc);
}
@Override
protected boolean verifyDrawable(Drawable who) {
if (who == mDraweeHolder.getHierarchy().getTopLevelDrawable()) {
return true;
}
// other logic for other Drawables in your view, if any
return super.verifyDrawable(who);
}
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
if (drawable == mDraweeHolder.getHierarchy().getTopLevelDrawable()) {
invalidate();
} else {
super.invalidateDrawable(drawable);
}
}
public void setImageURI(Uri uri) {
DraweeController controller = Fresco.newDraweeControllerBuilder()
.setUri(uri).setAutoPlayAnimations(true)
.setOldController(mDraweeHolder.getController())
.setControllerListener(mControllerListener).build();
mDraweeHolder.setController(controller);
}
private void scale(final float scale, final float focusX,
final float focusY, boolean animation) {
if (mZoomAnimation.isRunning()) {
return;
}
final float targetScale = mScale * scale;
mTmpRectF.set(mBoundsRect);
mMatrix.reset();
mMatrix.postScale(scale, scale, focusX, focusY);
mMatrix.mapRect(mBoundsRect);
final int w = getWidth();
final int h = getHeight();
final float ww = mBoundsRect.right - mBoundsRect.left;
final float hh = mBoundsRect.bottom - mBoundsRect.top;
if (ww > w) {
if (mBoundsRect.left > 0) {
mBoundsRect.right -= mBoundsRect.left;
mBoundsRect.left = 0;
} else if (mBoundsRect.right < w) {
mBoundsRect.left += w - mBoundsRect.right;
mBoundsRect.right = w;
}
} else {
mBoundsRect.left = (w - ww) / 2;
mBoundsRect.right = mBoundsRect.left + ww;
}
if (hh > h) {
if (mBoundsRect.top > 0) {
mBoundsRect.bottom -= mBoundsRect.top;
mBoundsRect.top = 0;
} else if (mBoundsRect.bottom < h) {
mBoundsRect.top += h - mBoundsRect.bottom;
mBoundsRect.bottom = h;
}
} else {
mBoundsRect.top = (h - hh) / 2;
mBoundsRect.bottom = mBoundsRect.top + hh;
}
mScale = targetScale;
if (animation) {
mZoomAnimation.start(mTmpRectF);
} else {
invalidate();
}
}
private void move(float dx, float dy) {
if (mZoomAnimation.isRunning()) {
return;
}
final int w = getWidth();
final int h = getHeight();
if (dx > 0) {
if (mBoundsRect.left >= 0) {
dx = 0;
} else if (mBoundsRect.left + dx > 0) {
dx = -mBoundsRect.left;
}
} else if (dx < 0) {
if (mBoundsRect.right <= w) {
dx = 0;
} else if (mBoundsRect.right + dx < w) {
dx = w - mBoundsRect.right;
}
}
if (dy > 0) {
if (mBoundsRect.top >= 0) {
dy = 0;
} else if (mBoundsRect.top + dy > 0) {
dy = -mBoundsRect.top;
}
} else if (dy < 0) {
if (mBoundsRect.bottom <= h) {
dy = 0;
} else if (mBoundsRect.bottom + dy < h) {
dy = h - mBoundsRect.bottom;
}
}
mBoundsRect.left += dx;
mBoundsRect.right += dx;
mBoundsRect.top += dy;
mBoundsRect.bottom += dy;
invalidate();
}
private void fling(int vx, int vy) {
if (mZoomAnimation.isRunning()) {
return;
}
mFlingAnimation.start(vx, vy);
}
class ZoomAnimation implements Runnable {
private RectF mSource = new RectF();
private RectF mOffset = new RectF();
private long mDuration = 300;
private long mStartTime;
private long mEndTime;
private boolean mRunning;
public void start(final RectF source) {
mSource.set(source);
mOffset.left = mBoundsRect.left - mSource.left;
mOffset.top = mBoundsRect.top - mSource.top;
mOffset.right = mBoundsRect.right - mSource.right;
mOffset.bottom = mBoundsRect.bottom - mSource.bottom;
mBoundsRect.set(mSource);
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mEndTime = mStartTime + mDuration;
mRunning = true;
ViewCompat.postOnAnimation(MyPhotoView.this, this);
}
public boolean isRunning() {
return mRunning;
}
@Override
public void run() {
if (!mRunning) {
return;
}
final long time = AnimationUtils.currentAnimationTimeMillis();
final float t;
if (time < mEndTime) {
t = (float) (time - mStartTime) / mDuration;
} else {
t = 1f;
mRunning = false;
}
final float val = sInterpolator.getInterpolation(t);
mBoundsRect.left = mSource.left + mOffset.left * val;
mBoundsRect.top = mSource.top + mOffset.top * val;
mBoundsRect.right = mSource.right + mOffset.right * val;
mBoundsRect.bottom = mSource.bottom + mOffset.bottom * val;
invalidate();
if (mRunning) {
ViewCompat.postOnAnimation(MyPhotoView.this, this);
}
}
}
class FlingAnimation implements Runnable {
private RectF mSource = new RectF();
public void start(int vx, int vy) {
mSource.set(mBoundsRect);
final int w = getWidth();
final int h = getHeight();
mBoundsRect.roundOut(mTmpRect);
int minX = Math.min(w - mTmpRect.right, 0);
int maxX = Math.max(-mTmpRect.left, 0);
int minY = Math.min(h - mTmpRect.bottom, 0);
int maxY = Math.max(-mTmpRect.top, 0);
mScroller.forceFinished(true);
mScroller.fling(0, 0, vx, vy, minX, maxX, minY, maxY);
ViewCompat.postOnAnimation(MyPhotoView.this, this);
}
@Override
public void run() {
if (!mScroller.computeScrollOffset()) {
return;
}
final int x = mScroller.getCurrX();
final int y = mScroller.getCurrY();
mBoundsRect.set(mSource);
mBoundsRect.offset(x, y);
invalidate();
ViewCompat.postOnAnimation(MyPhotoView.this, this);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment