/******************************************************************************* * Copyright 2009 Robot Media SL * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package net.robotmedia.acv.ui.widget; import net.robotmedia.acv.Constants; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.preference.PreferenceManager; import android.util.AttributeSet; import android.view.Gravity; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.ViewConfiguration; import android.view.ViewGroup.LayoutParams; import android.view.animation.Animation; import android.view.animation.AnimationSet; import android.view.animation.DecelerateInterpolator; import android.view.animation.ScaleAnimation; import android.view.animation.TranslateAnimation; import android.view.animation.Animation.AnimationListener; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.Scroller; /** * @author hermespique * */ public class SuperImageView extends ImageView { private Scroller mScroller; private VelocityTracker mVelocityTracker; private Rect mCurrentFrame; private float zoomFactor = -1; private SuperImageViewListener mCSVListener; private boolean mScaled = false; public void setCSVListener(SuperImageViewListener listener) { mCSVListener = listener; } public SuperImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initView(); } public SuperImageView(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private void initView() { mScroller = new Scroller(getContext()); } public void abortScrollerAnimation() { if (!mScroller.isFinished()) { mScroller.abortAnimation(); } } private FrameLayout.LayoutParams createLayoutParams(int width, int height) { int gravity = Gravity.NO_GRAVITY; if (width < getRootViewWidth()) { gravity = gravity | Gravity.CENTER_HORIZONTAL; } if (height < getRootViewHeight()) { gravity = gravity | Gravity.CENTER_VERTICAL; } final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(width, height, gravity); return params; } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { // This is called at drawing time by ViewGroup. We don't want to // re-show the scrollbars at this point, which scrollTo will do, // so we replicate most of scrollTo here. // // It's a little odd to call onScrollChanged from inside the // drawing. // // It is, except when you remember that computeScroll() is used to // animate scrolling. So unless we want to defer the // onScrollChanged() // until the end of the animated scrolling, we don't really have a // choice here. // // I agree. The alternative, which I think would be worse, is to // post // something and tell the subclasses later. This is bad because // there // will be a window where mScrollX/Y is different from what the app // thinks it is. // int oldX = getScrollX(); int oldY = getScrollY(); int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); // Log.d("computeScroll", "scrollX =" + String.valueOf(x) + "; scrollY = " + String.valueOf(y)); super.scrollTo(x, y); if (oldX != x || oldY != y) { onScrollChanged(x, y, oldX, oldY); postInvalidate(); } } } private int fitHeight(int height, boolean layout) { int newHeight = Math.round((float) getRootViewHeight() * ((float) getOriginalHeight() / (float) height)); int newWidth = Math.round((float) getOriginalWidth() * (float) newHeight / (float) getOriginalHeight()); if (layout) { setScaleType(ScaleType.FIT_CENTER); setLayoutParams(createLayoutParams(newWidth, newHeight)); } return newWidth; } private int fitHeight() { int height = getOriginalHeight(); return fitHeight(height, true); } private int fitWidth(int width, boolean layout) { int newWidth = Math.round((float) getRootViewWidth() * ((float) getOriginalWidth() / (float) width)); if (layout) { int newHeight = Math.round((float) getOriginalHeight() * (float) newWidth / (float) getOriginalWidth()); setScaleType(ScaleType.FIT_CENTER); setLayoutParams(createLayoutParams(newWidth, newHeight)); } return newWidth; } private int fitWidth() { int width = getOriginalWidth(); return fitWidth(width, true); } private int fitFrame() { setScaleType(ScaleType.CENTER_CROP); int newWidth = mCurrentFrame.width(); int newHeight = mCurrentFrame.height(); setLayoutParams(createLayoutParams(newWidth, newHeight)); return newWidth; } public boolean flingXY(int initialVelocityX, int initialVelocityY) { mScroller.fling(getScrollX(), getScrollY(), initialVelocityX, initialVelocityY, 0, getWidth() - getRootViewWidth(), 0, getHeight() - getRootViewHeight()); invalidate(); return true; } private int getInitialScrollX(int width) { SharedPreferences preferences = PreferenceManager .getDefaultSharedPreferences(getContext()); String direction = preferences.getString(Constants.DIRECTION_KEY, Constants.DIRECTION_LEFT_TO_RIGHT_VALUE); if (Constants.DIRECTION_LEFT_TO_RIGHT_VALUE.equals(direction)) { return 0; } else { return Math.max(0, width - getRootViewWidth()); } } private int getRootViewHeight() { return getRootView().getHeight(); } private int getRootViewWidth() { return getRootView().getWidth(); } public int getOriginalWidth() { Drawable image = getDrawable(); BitmapDrawable bitmapDrawable; if (image instanceof BitmapDrawable && (bitmapDrawable = (BitmapDrawable) image).getBitmap() != null) { return bitmapDrawable.getBitmap().getWidth(); } else if (image != null) { // Should ComicScreenView work with anything else than bitmap drawables? return image.getIntrinsicWidth(); } else { return 0; } } public Rect getOriginalSize() { final Rect frame = new Rect(0, 0, getOriginalWidth(), getOriginalHeight()); return frame; } public int getOriginalHeight() { Drawable image = getDrawable(); BitmapDrawable bitmapDrawable; if (image instanceof BitmapDrawable && (bitmapDrawable = (BitmapDrawable) image).getBitmap() != null) { return bitmapDrawable.getBitmap().getHeight(); } else if (image != null) { // Should ComicScreenView work with anything else than bitmap drawables? return image.getIntrinsicHeight(); } else { return 0; } } private float getMaxWidth() { return getOriginalWidth() * Constants.MAX_ZOOM_FACTOR; } private float getMaxHeight() { return getOriginalHeight() * Constants.MAX_ZOOM_FACTOR; } public float getZoomFactor() { return zoomFactor; } private boolean isBiggerThanAllowed(int width, int height) { return width > getMaxWidth() || height > getMaxHeight(); } private boolean isSmallerThanAllowed(int width, int height) { int originalWidth = getOriginalWidth(); int originalHeight = getOriginalHeight(); int rootViewWidth = getRootViewWidth(); int rootViewHeight = getRootViewHeight(); if (originalWidth < rootViewWidth && originalHeight < rootViewHeight) { return width < originalWidth || height < originalHeight; } else { return width < rootViewWidth && height < rootViewHeight; } } public boolean isLeftMost() { return getScrollX() <= 0; } public boolean isRightMost() { return getScrollX() >= getWidth() - getRootViewWidth(); } public boolean isTopMost() { return getScrollY() <= 0; } public boolean isBottomMost() { return getScrollY() >= getHeight() - getRootViewHeight(); } public boolean isSmallerThanRootView() { return isSmallerThanRootView(getWidth(), getHeight()); } private boolean isSmallerThanRootView(int width, int height) { return width <= getRootViewWidth() && height <= getRootViewHeight(); } private int scaleNone() { int width = getOriginalWidth(); int height = getOriginalHeight(); setScaleType(ScaleType.FIT_CENTER); setLayoutParams(createLayoutParams(width, height)); return width; } private void recalculateScroll(float ratio, int newWidth, int newHeight) { int scrollX = Math.round((float) getScrollX() * ratio); int scrollY = Math.round((float) getScrollY() * ratio); // TODO: Explain this formula scrollX += (newWidth - getWidth()) * getRootViewWidth() / (2 * getWidth()); scrollY += (newHeight - getHeight()) * getRootViewHeight() / (2 * getHeight()); this.safeScrollTo(scrollX, scrollY, newWidth, newHeight); } private Point calculateSafeScroll(int scrollX, int scrollY, int width, int height) { scrollX = Math.min(width - getRootViewWidth(), scrollX); scrollY = Math.min(height - getRootViewHeight(), scrollY); scrollX = Math.max(0, scrollX); scrollY = Math.max(0, scrollY); return new Point(scrollX, scrollY); } private void safeScrollTo(int scrollX, int scrollY, int width, int height) { Point scroll = calculateSafeScroll(scrollX, scrollY, width, height); scrollTo(scroll.x, scroll.y); } private void safeScrollBy(int distanceX, int distanceY, int width, int height) { int scrollX = getScrollX() + distanceX; int scrollY = getScrollY() + distanceY; safeScrollTo(scrollX, scrollY, width, height); } public void safeScrollBy(int distanceX, int distanceY) { safeScrollBy(distanceX, distanceY, getWidth(), getHeight()); } public void scale(String scaleMode, boolean recalculateScroll) { abortScrollerAnimation(); int newWidth; if (Constants.SCALE_MODE_BEST_VALUE.equals(scaleMode)) { newWidth = scaleFit(); } else if (Constants.SCALE_MODE_WIDTH_VALUE.equals(scaleMode)) { newWidth = fitWidth(); } else if (Constants.SCALE_MODE_HEIGHT_VALUE.equals(scaleMode)) { newWidth = fitHeight(); } else if (Constants.SCALE_MODE_FRAME_VALUE.equals(scaleMode)) { newWidth = fitFrame(); } else { newWidth = scaleNone(); } int oldWidth = getWidth(); int oldHeight = getHeight(); if (recalculateScroll && oldWidth > 0 && oldHeight > 0) { float ratio = (float) newWidth / (float) oldWidth; int newHeight = Math.round(ratio * oldHeight); recalculateScroll(ratio, newWidth, newHeight); } else { // First scroll scrollTo(getInitialScrollX(newWidth), 0); } zoomFactor = (float) newWidth / getOriginalWidth(); mScaled = true; } /** * Scrolls to the given frame of the image. * @param frame Frame of the image to scroll to. * @param keepInside * @return Frame Frame of the view to where it was scrolled. */ public Rect scrollTo(Rect frame, boolean keepInside) { abortScrollerAnimation(); int newWidth = scaleFit(frame.width(), frame.height(), true); Rect newFrame = resize(frame, newWidth, keepInside); scrollTo(newFrame.left, newFrame.top); zoomFactor = (float) newWidth / getOriginalWidth(); mScaled = false; return newFrame; } public class LayoutMeasures { public int width; public int height; public int scrollX; public int scrollY; public int top; public int left; } public LayoutMeasures calculateLayout(Rect frame, boolean keepInside) { final LayoutMeasures result = new LayoutMeasures(); result.width = scaleFit(frame.width(), frame.height(), false); result.height = Math.round((float) getOriginalHeight() * (float) result.width / (float) getOriginalWidth()); Rect newFrame = resize(frame, result.width, keepInside); result.scrollX = newFrame.left; result.scrollY = newFrame.top; final int rootWidth = getRootViewWidth(); if (result.width < rootWidth) { result.left = (rootWidth - result.width) / 2; } final int rootHeight = getRootViewHeight(); if (result.height < rootHeight) { result.top = (rootHeight - result.height) / 2; } return result; } /** * Recalculates the given frame of the original image considering the given width as the width of the image. * Also centers the new frame in the parent view * @param frame Frame of the original image. * @param newWidth Width of the image. * @param keepInside * @return */ private Rect resize(Rect frame, int newWidth, boolean keepInside) { // Recalculate frame based on new width final Rect newFrame = new Rect(); float scale = (float) newWidth / (float) getOriginalWidth(); newFrame.left = Math.round(frame.left * scale); newFrame.top = Math.round(frame.top * scale); newFrame.right = Math.round(frame.right * scale); newFrame.bottom = Math.round(frame.bottom * scale); int dx = - (Math.min(newWidth, getRootViewWidth()) - newFrame.width()) / 2; final int newHeight = Math.round(getOriginalHeight() * scale); int dy = - (Math.min(newHeight, getRootViewHeight()) - newFrame.height()) / 2; newFrame.offset(dx, dy); if (keepInside) { // No letterbox Point scroll = calculateSafeScroll(newFrame.left, newFrame.top, newWidth, newHeight); newFrame.offsetTo(scroll.x, scroll.y); } return newFrame; } private boolean mAnimating = false; public boolean isAnimating() { return mAnimating; } public boolean isScaled() { return mScaled; } public Rect animateTo(final Rect frame, final boolean keepInside, long duration) { mAnimating = true; abortScrollerAnimation(); final int newWidth = scaleFit(frame.width(), frame.height(), false); final Rect newFrame = resize(frame, newWidth, keepInside); final AnimationSet animation = createAnimation(newWidth, newFrame.left, newFrame.top, duration); animation.setAnimationListener(new AnimationListener() { @Override public void onAnimationEnd(Animation arg0) { SuperImageView.this.postDelayed(new Runnable() { @Override public void run() { SuperImageView.this.scrollTo(frame, keepInside); SuperImageView.this.clearAnimation(); mAnimating = false; if (mCSVListener != null) { mCSVListener.onAnimationEnd(SuperImageView.this); } }}, 50); } @Override public void onAnimationRepeat(Animation arg0) {} @Override public void onAnimationStart(Animation arg0) {} }); this.startAnimation(animation); return newFrame; } private AnimationSet createAnimation(final int newWidth, final int newScrollX, final int newScrollY, long duration) { final AnimationSet animation = new AnimationSet(true); animation.setFillAfter(true); animation.setInterpolator(new DecelerateInterpolator()); final float scale = (float) newWidth / (float) getWidth(); final int fromXDelta = getScrollX(); final int fromYDelta = getScrollY(); this.scrollTo(0, 0); int toXDelta = Math.round((float) newScrollX / scale); int toYDelta = Math.round((float) newScrollY / scale); // Because the imageSwitcher centers the view if smaller final int fromXCenteringDelta = Math.max(getRootViewWidth() - getWidth(), 0) / 2; final int toXCenteringDelta = Math.max(getRootViewWidth() - newWidth, 0) / 2; final int xCenteringDelta = Math.round((toXCenteringDelta - fromXCenteringDelta) / scale); toXDelta -= xCenteringDelta; final int newHeight = Math.round((float) getOriginalHeight() * (float) newWidth / (float) getOriginalWidth()); final int fromYCenteringDelta = Math.max(getRootViewHeight() - getHeight(), 0) / 2; final int toYCenteringDelta = Math.max(getRootViewHeight() - newHeight, 0) / 2; final int yCenteringDelta = Math.round((toYCenteringDelta - fromYCenteringDelta) / scale); toYDelta -= yCenteringDelta; final TranslateAnimation translateAnimation = new TranslateAnimation(-fromXDelta, -toXDelta, -fromYDelta, -toYDelta); translateAnimation.setDuration(duration); animation.addAnimation(translateAnimation); final ScaleAnimation scaleAnimation = new ScaleAnimation(1, scale, 1, scale); scaleAnimation.setDuration(duration); animation.addAnimation(scaleAnimation); return animation; } private int scaleFit(int width, int height, boolean layout) { if (width < height) { // Portrait float ratio = (float) width / (float) height; float containerRatio = (float) getRootViewWidth() / (float) getRootViewHeight(); if (ratio < containerRatio) { return fitHeight(height, layout); } else { return fitWidth(width, layout); } } else { // Landscape float ratio = (float) height / (float) width; float containerRatio = (float) getRootViewHeight() / (float) getRootViewWidth(); if (ratio < containerRatio) { return fitWidth(width, layout); } else { return fitHeight(height, layout); } } } private int scaleFit() { int width = getOriginalWidth(); int height = getOriginalHeight(); return scaleFit(width, height, true); } public void smoothScroll(MotionEvent motionEvent) { // Log.d("smoothScroll", "enter"); if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(motionEvent); final int action = motionEvent.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: abortScrollerAnimation(); break; case MotionEvent.ACTION_UP: final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000); int initialVelocityX = (int) velocityTracker.getXVelocity(); int initialVelocityY = (int) velocityTracker.getYVelocity(); int initialFilteredVelocityX = 0; int initialFilteredVelocityY = 0; if (Math.abs(initialVelocityX) > ViewConfiguration.getMinimumFlingVelocity()) { initialFilteredVelocityX = -initialVelocityX; } if (Math.abs(initialVelocityY) > ViewConfiguration.getMinimumFlingVelocity()) { initialFilteredVelocityY = -initialVelocityY; } if (initialFilteredVelocityX != 0 || initialFilteredVelocityY != 0) { flingXY(initialFilteredVelocityX, initialFilteredVelocityY); } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } break; } } /** * Scales the view by the given factor without assuming anything of the size of the view. * Scaling is performed based on the original size of the drawable. * Like all other scale methods, scroll is positioned in the initial position for reading. * @param factor */ public void scaleByFactor(float factor) { int newWidth = Math.round(getOriginalWidth() * factor); int newHeight = Math.round(getOriginalHeight() * factor); if (isBiggerThanAllowed(newWidth, newHeight)) { newWidth = Math.round(getMaxWidth()); newHeight = Math.round(getMaxHeight()); factor = (float) newWidth / (float) getWidth(); setScaleType(ScaleType.FIT_CENTER); setLayoutParams(createLayoutParams(newWidth, newHeight)); zoomFactor = Constants.MAX_ZOOM_FACTOR; } else if (isSmallerThanAllowed(newWidth, newHeight)) { if (isSmallerThanRootView(getOriginalWidth(), getOriginalHeight())) { scaleNone(); zoomFactor = 1; } else { newWidth = scaleFit(); zoomFactor = (float) newWidth / getOriginalWidth(); } } else { setScaleType(ScaleType.FIT_CENTER); setLayoutParams(createLayoutParams(newWidth, newHeight)); zoomFactor = (float) newWidth / getOriginalWidth(); } scrollTo(getInitialScrollX(newWidth), 0); mScaled = false; } public void zoom(int increment, Point viewPoint) { float factor = (float)Math.pow(Constants.ZOOM_STEP, increment); Point imagePoint = viewPoint != null ? toImagePoint(viewPoint) : null; zoom(factor, imagePoint); } public void zoom(float factor, Point imagePoint) { abortScrollerAnimation(); if (imagePoint != null) { // Scroll to point before zooming final float scale = (float) getWidth() / (float) getOriginalWidth(); int x = Math.round(imagePoint.x * scale); int y = Math.round(imagePoint.y * scale); x -= Math.round((float) Math.min(getWidth(), getRootViewWidth()) / 2f); y -= Math.round((float) Math.min(getHeight(), getRootViewHeight()) / 2f); this.scrollTo(x, y); } int newWidth = Math.round(getWidth() * factor); int newHeight = Math.round(getHeight() * factor); if (isBiggerThanAllowed(newWidth, newHeight)) { newWidth = Math.round(getMaxWidth()); newHeight = Math.round(getMaxHeight()); factor = (float) newWidth / (float) getWidth(); setScaleType(ScaleType.FIT_CENTER); setLayoutParams(createLayoutParams(newWidth, newHeight)); zoomFactor = Constants.MAX_ZOOM_FACTOR; recalculateScroll(factor, newWidth, newHeight); } else if (isSmallerThanAllowed(newWidth, newHeight)) { if (isSmallerThanRootView(getOriginalWidth(), getOriginalHeight())) { scaleNone(); zoomFactor = 1; } else { newWidth = scaleFit(); zoomFactor = (float) newWidth / getOriginalWidth(); } scrollTo(0, 0); } else { setScaleType(ScaleType.FIT_CENTER); setLayoutParams(createLayoutParams(newWidth, newHeight)); zoomFactor = (float) newWidth / getOriginalWidth(); recalculateScroll(factor, newWidth, newHeight); } mScaled = false; } public void recycleBitmap() { Drawable drawable = getDrawable(); if (drawable != null && drawable instanceof BitmapDrawable) { Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); bitmap.recycle(); // FIXME: Is this the best way to say the view should show nothing? setImageDrawable(new ColorDrawable()); setLayoutParams(new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); } } public Point toImagePoint(Point viewPoint) { Point imagePoint = new Point(); int marginX = Math.max(0, getRootViewWidth() - getWidth()) / 2; int marginY = Math.max(0, getRootViewHeight() - getHeight()) / 2; imagePoint.x = getScrollX() + viewPoint.x - marginX; imagePoint.y = getScrollY() + viewPoint.y - marginY; imagePoint.x = Math.min(getWidth(), imagePoint.x); imagePoint.x = Math.max(0, imagePoint.x); imagePoint.y = Math.min(getHeight(), imagePoint.y); imagePoint.y = Math.max(0, imagePoint.y); float scale = (float) getOriginalWidth() / (float)getWidth(); imagePoint.x = Math.round(imagePoint.x * scale); imagePoint.y = Math.round(imagePoint.y * scale); return imagePoint; } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { this.abortScrollerAnimation(); } return super.onTouchEvent(event); } }