package com.frozendevs.periodictable.view; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Canvas; import android.graphics.Point; import android.os.Build; import android.support.v4.view.ViewCompat; import android.support.v4.widget.EdgeEffectCompat; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.widget.FrameLayout; import android.widget.OverScroller; import com.frozendevs.periodictable.R; import com.frozendevs.periodictable.widget.Zoomer; public class ZoomableScrollView extends FrameLayout implements GestureDetector.OnGestureListener, ScaleGestureDetector.OnScaleGestureListener, GestureDetector.OnDoubleTapListener { private static final float DEFAULT_MAX_ZOOM = 1f; private OverScroller mOverScroller; private Zoomer mZoomer; private ScaleGestureDetector mScaleDetector; private GestureDetector mGestureDetector; private EdgeEffectCompat mEdgeEffectTop; private EdgeEffectCompat mEdgeEffectBottom; private EdgeEffectCompat mEdgeEffectLeft; private EdgeEffectCompat mEdgeEffectRight; private boolean mIsScrolling = false; private float mMinZoom = 0f; private float mZoom = 0f; private float mMaxZoom = 1f; private Point mZoomFocalPoint = new Point(); private float mStartZoom; private boolean mEdgeEffectTopActive; private boolean mEdgeEffectBottomActive; private boolean mEdgeEffectLeftActive; private boolean mEdgeEffectRightActive; public ZoomableScrollView(Context context) { super(context); initZoomableScrollView(context); } public ZoomableScrollView(Context context, AttributeSet attrs) { super(context, attrs, R.attr.zoomableScrollViewStyle); initZoomableScrollView(context); } public ZoomableScrollView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initZoomableScrollView(context); } private void initZoomableScrollView(Context context) { setWillNotDraw(false); setHorizontalScrollBarEnabled(true); setVerticalScrollBarEnabled(true); setClickable(true); mOverScroller = new OverScroller(context); mZoomer = new Zoomer(context); mScaleDetector = new ScaleGestureDetector(context, this); mGestureDetector = new GestureDetector(context, this); mEdgeEffectTop = new EdgeEffectCompat(context); mEdgeEffectBottom = new EdgeEffectCompat(context); mEdgeEffectLeft = new EdgeEffectCompat(context); mEdgeEffectRight = new EdgeEffectCompat(context); } @Override public boolean onDown(MotionEvent e) { mEdgeEffectLeftActive = mEdgeEffectTopActive = mEdgeEffectRightActive = mEdgeEffectBottomActive = false; mEdgeEffectLeft.onRelease(); mEdgeEffectTop.onRelease(); mEdgeEffectRight.onRelease(); mEdgeEffectBottom.onRelease(); mOverScroller.forceFinished(true); return false; } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onSingleTapUp(MotionEvent e) { return false; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { boolean needsInvalidate = false; mIsScrolling = true; scrollTo(getScrollX() + (int) distanceX, getScrollY() + (int) distanceY); if (getScaledWidth() > getWidth()) { if (getScrollX() == getMinimalScrollX()) { needsInvalidate |= mEdgeEffectLeft.onPull(Math.abs(distanceX) / getWidth()); mEdgeEffectLeftActive = true; } else if (getScrollX() == getMaximalScrollX()) { needsInvalidate |= mEdgeEffectRight.onPull(Math.abs(distanceX) / getWidth()); mEdgeEffectRightActive = true; } } if (getScaledHeight() > getHeight()) { if (getScrollY() == getMinimalScrollY()) { needsInvalidate |= mEdgeEffectTop.onPull(Math.abs(distanceY) / getHeight()); mEdgeEffectTopActive = true; } else if (getScrollY() == getMaximalScrollY()) { needsInvalidate |= mEdgeEffectBottom.onPull(Math.abs(distanceY) / getHeight()); mEdgeEffectBottomActive = true; } } if (needsInvalidate) { ViewCompat.postInvalidateOnAnimation(this); } return true; } @Override public void onLongPress(MotionEvent e) { } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { mEdgeEffectLeft.onRelease(); mEdgeEffectTop.onRelease(); mEdgeEffectRight.onRelease(); mEdgeEffectBottom.onRelease(); mOverScroller.forceFinished(true); mOverScroller.fling(getScrollX(), getScrollY(), (int) -velocityX, (int) -velocityY, getMinimalScrollX(), getMaximalScrollX(), getMinimalScrollY(), getMaximalScrollY(), getWidth() / 2, getHeight() / 2); ViewCompat.postInvalidateOnAnimation(this); return true; } @Override public boolean onScale(ScaleGestureDetector detector) { zoomTo((int) detector.getFocusX(), (int) detector.getFocusY(), clamp(mMinZoom, mZoom * detector.getScaleFactor(), mMaxZoom)); return true; } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { return true; } public void zoomTo(int x, int y, float zoom) { if (mZoom != zoom) { float zoomRatio = zoom / mZoom; int oldX = getScrollX() - getMinimalScrollX() + x; int oldY = getScrollY() - getMinimalScrollY() + y; mZoom = clamp(mMinZoom, zoom, mMaxZoom); scrollTo(getMinimalScrollX() + Math.round(oldX * zoomRatio) - x, getMinimalScrollY() + Math.round(oldY * zoomRatio) - y); ViewCompat.postInvalidateOnAnimation(this); } } @Override public void onScaleEnd(ScaleGestureDetector detector) { } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); boolean isMinZoom = mMinZoom == mZoom; mMinZoom = getMinimalZoom(); mMaxZoom = mMinZoom > DEFAULT_MAX_ZOOM ? mMinZoom : DEFAULT_MAX_ZOOM; mZoom = isMinZoom ? mMinZoom : clamp(mMinZoom, mZoom, mMaxZoom); if (mZoom > 0f) { if (getScrollX() < getMinimalScrollX()) { scrollTo(getMinimalScrollX(), getScrollY()); } else if (getScrollX() > getMaximalScrollX()) { scrollTo(getMaximalScrollX(), getScrollY()); } if (getScrollY() < getMinimalScrollY()) { scrollTo(getScrollX(), getMinimalScrollY()); } else if (getScrollY() > getMaximalScrollY()) { scrollTo(getScrollX(), getMaximalScrollY()); } } super.onLayout(changed, l, t, r, b); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); final int overScrollMode = ViewCompat.getOverScrollMode(this); if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS) { if (!mEdgeEffectLeft.isFinished()) { final int restoreCount = canvas.save(); final int height = getHeight() - getPaddingTop() - getPaddingBottom(); final int width = getWidth(); canvas.rotate(270); canvas.translate(-height + getPaddingTop() - getScrollY(), getMinimalScrollX()); mEdgeEffectLeft.setSize(height, width); mEdgeEffectLeft.draw(canvas); canvas.restoreToCount(restoreCount); } if (!mEdgeEffectRight.isFinished()) { final int restoreCount = canvas.save(); final int width = getWidth(); final int height = getHeight() - getPaddingTop() - getPaddingBottom(); canvas.rotate(90); canvas.translate(-getPaddingTop() + getScrollY(), -getMaximalScrollX() - getWidth()); mEdgeEffectRight.setSize(height, width); mEdgeEffectRight.draw(canvas); canvas.restoreToCount(restoreCount); } if (!mEdgeEffectTop.isFinished()) { final int restoreCount = canvas.save(); final int height = getWidth() - getPaddingLeft() - getPaddingRight(); final int width = getHeight(); canvas.translate(getPaddingLeft() + getScrollX(), getMinimalScrollY()); mEdgeEffectTop.setSize(height, width); mEdgeEffectTop.draw(canvas); canvas.restoreToCount(restoreCount); } if (!mEdgeEffectBottom.isFinished()) { final int restoreCount = canvas.save(); final int height = getWidth() - getPaddingLeft() - getPaddingRight(); final int width = getHeight(); canvas.rotate(180); canvas.translate(-getWidth() + getPaddingLeft() - getScrollX(), -getMaximalScrollY() - getHeight()); mEdgeEffectBottom.setSize(height, width); mEdgeEffectBottom.draw(canvas); canvas.restoreToCount(restoreCount); } } else { mEdgeEffectLeft.finish(); mEdgeEffectRight.finish(); mEdgeEffectTop.finish(); mEdgeEffectBottom.finish(); } if (!mEdgeEffectLeft.isFinished() || !mEdgeEffectRight.isFinished() || !mEdgeEffectTop.isFinished() || !mEdgeEffectBottom.isFinished()) { ViewCompat.postInvalidateOnAnimation(this); } } private int getMinimalScrollX() { return Math.min((getWidth() - getScaledWidth()) / 2, 0); } private int getMinimalScrollY() { return Math.min((getHeight() - getScaledHeight()) / 2, 0); } private int getMaximalScrollX() { return getMinimalScrollX() + getScaledWidth() - getWidth(); } private int getMaximalScrollY() { return getMinimalScrollY() + getScaledHeight() - getHeight(); } @Override public void scrollTo(int x, int y) { super.scrollTo(clamp(getMinimalScrollX(), x, getMaximalScrollX()), clamp(getMinimalScrollY(), y, getMaximalScrollY())); } protected int getScaledWidth() { return Math.round(getMeasuredWidth() * mZoom); } protected int getScaledHeight() { return Math.round(getMeasuredHeight() * mZoom); } @Override protected int computeHorizontalScrollExtent() { return getWidth(); } @Override protected int computeHorizontalScrollOffset() { return getScrollX() - getMinimalScrollX(); } @Override protected int computeHorizontalScrollRange() { return getScaledWidth(); } @Override protected int computeVerticalScrollExtent() { return getHeight(); } @Override protected int computeVerticalScrollOffset() { return getScrollY() - getMinimalScrollY(); } @Override protected int computeVerticalScrollRange() { return getScaledHeight(); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @Override public void computeScroll() { boolean needsInvalidate = false; if (mOverScroller.computeScrollOffset()) { scrollTo(mOverScroller.getCurrX(), mOverScroller.getCurrY()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { if (getScaledWidth() > getWidth()) { if (getScrollX() == getMinimalScrollX() && mEdgeEffectLeft.isFinished() && !mEdgeEffectLeftActive) { needsInvalidate |= mEdgeEffectLeft.onAbsorb( (int) mOverScroller.getCurrVelocity()); mEdgeEffectLeftActive = true; } else if (getScrollX() == getMaximalScrollX() && mEdgeEffectRight.isFinished() && !mEdgeEffectRightActive) { needsInvalidate |= mEdgeEffectRight.onAbsorb( (int) mOverScroller.getCurrVelocity()); mEdgeEffectRightActive = true; } } if (getScaledHeight() > getHeight()) { if (getScrollY() == getMinimalScrollY() && mEdgeEffectTop.isFinished() && !mEdgeEffectTopActive) { needsInvalidate |= mEdgeEffectTop.onAbsorb( (int) mOverScroller.getCurrVelocity()); mEdgeEffectTopActive = true; } else if (getScrollY() == getMaximalScrollY() && mEdgeEffectBottom.isFinished() && !mEdgeEffectBottomActive) { needsInvalidate |= mEdgeEffectBottom.onAbsorb( (int) mOverScroller.getCurrVelocity()); mEdgeEffectBottomActive = true; } } } } if (mZoomer.computeZoom()) { zoomTo(mZoomFocalPoint.x, mZoomFocalPoint.y, mStartZoom + mZoomer.getCurrZoom()); needsInvalidate = true; } if (needsInvalidate) { ViewCompat.postInvalidateOnAnimation(this); } } public float getMinimalZoom() { return Math.min(getWidth() / getMeasuredWidth(), getHeight() / getMeasuredHeight()); } public float getZoom() { return mZoom; } public float getMaximalZoom() { return mMaxZoom; } @Override public boolean dispatchTouchEvent(MotionEvent event) { if (isEnabled() && mScaleDetector.onTouchEvent(event) && !mScaleDetector.isInProgress()) { mGestureDetector.onTouchEvent(event); } switch (event.getActionMasked()) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mIsScrolling = false; break; } return isEnabled() && super.dispatchTouchEvent(event); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { return isEnabled() && (mScaleDetector.isInProgress() || mIsScrolling); } private float clamp(float min, float val, float max) { if (Float.isNaN(min)) min = 0f; if (Float.isNaN(val)) val = 0f; if (Float.isNaN(max)) max = 0f; return Math.max(min, Math.min(val, max)); } private int clamp(int min, int val, int max) { return Math.max(min, Math.min(val, max)); } @Override public boolean onSingleTapConfirmed(MotionEvent e) { return false; } @Override public boolean onDoubleTap(MotionEvent e) { mZoomer.forceFinished(true); mZoomFocalPoint.x = (int) e.getX(); mZoomFocalPoint.y = (int) e.getY(); mStartZoom = mZoom; mZoomer.startZoom(mZoom > mMinZoom + 0.001f ? -(mZoom - mMinZoom) : mMaxZoom - mZoom); ViewCompat.postInvalidateOnAnimation(this); return true; } @Override public boolean onDoubleTapEvent(MotionEvent e) { return false; } public boolean isScrolling() { return mIsScrolling; } }