package com.marshalchen.common.uimodule.panningview; import android.content.res.Configuration; import android.graphics.Matrix; import android.graphics.RectF; import android.util.Log; import android.view.ViewTreeObserver; import android.view.animation.LinearInterpolator; import android.widget.ImageView; import com.marshalchen.common.uimodule.nineoldandroids.animation.Animator; import com.marshalchen.common.uimodule.nineoldandroids.animation.AnimatorListenerAdapter; import com.marshalchen.common.uimodule.nineoldandroids.animation.ValueAnimator; import java.lang.ref.WeakReference; /** * Created by f.laurent on 25/07/13. */ public class PanningViewAttacher implements ViewTreeObserver.OnGlobalLayoutListener { public static final int DEFAULT_PANNING_DURATION_IN_MS = 5000; private static final String TAG = "PanningViewAttacher"; private enum Way {R2L, L2R, T2B, B2T}; private WeakReference<ImageView> mImageView; private int mIvTop, mIvRight, mIvBottom, mIvLeft; private ViewTreeObserver mViewTreeObserver; private Matrix mMatrix; private RectF mDisplayRect = new RectF(); private ValueAnimator mCurrentAnimator; private LinearInterpolator mLinearInterpolator; private boolean mIsPortrait; private long mDuration; private long mCurrentPlayTime; private long mTotalTime; private Way mWay; private boolean mIsPanning; public PanningViewAttacher(ImageView imageView, long duration) { if(imageView == null) { throw new IllegalArgumentException("imageView must not be null"); } if(!hasDrawable(imageView)) { throw new IllegalArgumentException("drawable must not be null"); } mLinearInterpolator = new LinearInterpolator(); mDuration = duration; mImageView = new WeakReference<ImageView>(imageView); mViewTreeObserver = imageView.getViewTreeObserver(); mViewTreeObserver.addOnGlobalLayoutListener(this); setImageViewScaleTypeMatrix(imageView); mMatrix = imageView.getImageMatrix(); if(mMatrix == null) { mMatrix = new Matrix(); } mIsPortrait = imageView.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; update(); } /** * */ public void update() { mWay = null; mTotalTime = 0; mCurrentPlayTime = 0; getImageView().post(new Runnable() { @Override public void run() { scale(); refreshDisplayRect(); } }); } public boolean isPanning() { return mIsPanning; } /** * scale and start to pan the image background */ public void startPanning() { if(mIsPanning) { return; } mIsPanning = true; final Runnable panningRunnable = new Runnable() { @Override public void run() { animate_(); } }; getImageView().post(panningRunnable); } /** * stop current panning */ public void stopPanning() { if(!mIsPanning) { return; } mIsPanning = false; Log.d(TAG, "panning animation stopped by user"); if (mCurrentAnimator != null) { mCurrentAnimator.removeAllListeners(); mCurrentAnimator.cancel(); mCurrentAnimator = null; } mTotalTime += mCurrentPlayTime; Log.d(TAG, "mTotalTime : " + mTotalTime); } /** * Clean-up the resources attached to this object. This needs to be called * when the ImageView is no longer used. A good example is from * {@link android.view.View#onDetachedFromWindow()} or from {@link android.app.Activity#onDestroy()}. * This is automatically called if you are using {@link com.marshalchen.common.uimodule.panningview.PanningView}. */ public final void cleanup() { if (null != mImageView) { getImageView().getViewTreeObserver().removeGlobalOnLayoutListener(this); } mViewTreeObserver = null; stopPanning(); // Finally, clear ImageView mImageView = null; } public final ImageView getImageView() { ImageView imageView = null; if (null != mImageView) { imageView = mImageView.get(); } // If we don't have an ImageView, call cleanup() if (null == imageView) { cleanup(); throw new IllegalStateException("ImageView no longer exists. You should not use this PanningViewAttacher any more."); } return imageView; } private int getDrawableIntrinsicHeight() { return getImageView().getDrawable().getIntrinsicHeight(); } private int getDrawableIntrinsicWidth() { return getImageView().getDrawable().getIntrinsicWidth(); } private int getImageViewWidth() { return getImageView().getWidth(); } private int getImageViewHeight() { return getImageView().getHeight(); } /** * Set's the ImageView's ScaleType to Matrix. */ private static void setImageViewScaleTypeMatrix(ImageView imageView) { if (null != imageView && !(imageView instanceof PanningView)) { imageView.setScaleType(ImageView.ScaleType.MATRIX); } } /** * @return true if the ImageView exists, and it's Drawable exists */ private static boolean hasDrawable(ImageView imageView) { return null != imageView && null != imageView.getDrawable(); } @Override public void onGlobalLayout() { ImageView imageView = getImageView(); if (null != imageView) { final int top = imageView.getTop(); final int right = imageView.getRight(); final int bottom = imageView.getBottom(); final int left = imageView.getLeft(); /** * We need to check whether the ImageView's bounds have changed. * This would be easier if we targeted API 11+ as we could just use * View.OnLayoutChangeListener. Instead we have to replicate the * work, keeping track of the ImageView's bounds and then checking * if the values change. */ if (top != mIvTop || bottom != mIvBottom || left != mIvLeft || right != mIvRight) { update(); // Update values as something has changed mIvTop = top; mIvRight = right; mIvBottom = bottom; mIvLeft = left; } } } private void animate_() { refreshDisplayRect(); if(mWay == null) { mWay = mIsPortrait ? Way.R2L : Way.B2T; } Log.d(TAG, "mWay : " + mWay); Log.d(TAG, "mDisplayRect : " + mDisplayRect); long remainingDuration = mDuration - mTotalTime; if(mIsPortrait) { if(mWay == Way.R2L) { animate(mDisplayRect.left, mDisplayRect.left - (mDisplayRect.right - getImageViewWidth()), remainingDuration); } else { animate(mDisplayRect.left, 0.0f, remainingDuration); } } else { if(mWay == Way.B2T) { animate(mDisplayRect.top, mDisplayRect.top - (mDisplayRect.bottom - getImageViewHeight()), remainingDuration); } else { animate(mDisplayRect.top, 0.0f, remainingDuration); } } } private void changeWay() { if(mWay == Way.R2L) { mWay = Way.L2R; } else if(mWay == Way.L2R) { mWay = Way.R2L; } else if(mWay == Way.T2B) { mWay = Way.B2T; } else if(mWay == Way.B2T) { mWay = Way.T2B; } mCurrentPlayTime = 0; mTotalTime = 0; } private void animate(float start, float end, long duration) { Log.d(TAG, "startPanning : " + start + " to " + end + ", in " + duration + "ms"); mCurrentAnimator = ValueAnimator.ofFloat(start, end); mCurrentAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float value = (Float) animation.getAnimatedValue(); mMatrix.reset(); applyScaleOnMatrix(); if(mIsPortrait) { mMatrix.postTranslate(value, 0); } else { mMatrix.postTranslate(0, value); } refreshDisplayRect(); mCurrentPlayTime = animation.getCurrentPlayTime(); setCurrentImageMatrix(); } }); mCurrentAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { Log.d(TAG, "animation has finished, startPanning in the other way"); changeWay(); animate_(); } @Override public void onAnimationCancel(Animator animation) { Log.d(TAG, "panning animation canceled"); } }); mCurrentAnimator.setDuration(duration); mCurrentAnimator.setInterpolator(mLinearInterpolator); mCurrentAnimator.start(); } private void setCurrentImageMatrix() { getImageView().setImageMatrix(mMatrix); getImageView().invalidate(); getImageView().requestLayout(); } private void refreshDisplayRect() { mDisplayRect.set(0, 0, getDrawableIntrinsicWidth(), getDrawableIntrinsicHeight()); mMatrix.mapRect(mDisplayRect); } private void scale() { mMatrix.reset(); applyScaleOnMatrix(); setCurrentImageMatrix(); } private void applyScaleOnMatrix() { int drawableSize = mIsPortrait ? getDrawableIntrinsicHeight() : getDrawableIntrinsicWidth(); int imageViewSize = mIsPortrait ? getImageViewHeight() : getImageViewWidth(); float scaleFactor = (float)imageViewSize / (float)drawableSize; mMatrix.postScale(scaleFactor, scaleFactor); } }