Last active
August 29, 2015 14:22
-
-
Save funorpain/398d479b6f0fed78d626 to your computer and use it in GitHub Desktop.
PhotoView using fresco
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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