/* * Copyright 2010 Sony Ericsson Mobile Communications AB * * 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 roman10.zoomablegallery; import android.os.Handler; import android.os.SystemClock; import android.util.Log; import java.util.Observable; import java.util.Observer; import roman10.zoomablegallery.dynamics.SpringDynamics; /** * The DynamicZoomControl is responsible for controlling a ZoomState. It makes * sure that pan movement follows the finger, that limits are satisfied and that * we can zoom into specific positions. * * In order to implement these control mechanisms access to certain content and * view state data is required which is made possible through the * ZoomContentViewState. */ public class DynamicZoomControl implements Observer { /** Minimum zoom level limit */ private static final float MIN_ZOOM = 1; /** Maximum zoom level limit */ private static final float MAX_ZOOM = 16; /** Velocity tolerance for calculating if dynamic state is resting */ private static final float REST_VELOCITY_TOLERANCE = 0.004f; /** Position tolerance for calculating if dynamic state is resting */ private static final float REST_POSITION_TOLERANCE = 0.01f; /** Target FPS when animating behavior such as fling and snap to */ private static final int FPS = 50; /** Factor applied to pan motion outside of pan snap limits. */ private static final float PAN_OUTSIDE_SNAP_FACTOR = .4f; /** Zoom state under control */ private final ZoomState mState = new ZoomState(); /** Object holding aspect quotient of view and content */ private AspectQuotient mAspectQuotient; /** * Dynamics object for creating dynamic fling and snap to behavior for pan * in x-dimension. */ private final SpringDynamics mPanDynamicsX = new SpringDynamics(); /** * Dynamics object for creating dynamic fling and snap to behavior for pan * in y-dimension. */ private final SpringDynamics mPanDynamicsY = new SpringDynamics(); /** Minimum snap to position for pan in x-dimension */ private float mPanMinX; /** Maximum snap to position for pan in x-dimension */ private float mPanMaxX; /** Minimum snap to position for pan in y-dimension */ private float mPanMinY; /** Maximum snap to position for pan in y-dimension */ private float mPanMaxY; /** Handler for posting runnables */ private final Handler mHandler = new Handler(); /** Creates new zoom control */ public DynamicZoomControl() { mPanDynamicsX.setFriction(2f); mPanDynamicsY.setFriction(2f); mPanDynamicsX.setSpring(50f, 1f); mPanDynamicsY.setSpring(50f, 1f); } /** * Set reference object holding aspect quotient * * @param aspectQuotient Object holding aspect quotient */ public void setAspectQuotient(AspectQuotient aspectQuotient) { if (mAspectQuotient != null) { mAspectQuotient.deleteObserver(this); } mAspectQuotient = aspectQuotient; mAspectQuotient.addObserver(this); } /** * Get zoom state being controlled * * @return The zoom state */ public ZoomState getZoomState() { return mState; } /** * Zoom * * @param f Factor of zoom to apply * @param x X-coordinate of invariant position * @param y Y-coordinate of invariant position */ public void zoom(float f, float x, float y) { final float aspectQuotient = mAspectQuotient.get(); final float prevZoomX = mState.getZoomX(aspectQuotient); final float prevZoomY = mState.getZoomY(aspectQuotient); mState.setZoom(mState.getZoom() * f); limitZoom(); final float newZoomX = mState.getZoomX(aspectQuotient); final float newZoomY = mState.getZoomY(aspectQuotient); // Pan to keep x and y coordinate invariant mState.setPanX(mState.getPanX() + (x - .5f) * (1f / prevZoomX - 1f / newZoomX)); mState.setPanY(mState.getPanY() + (y - .5f) * (1f / prevZoomY - 1f / newZoomY)); updatePanLimits(); //Log.e("ZOOM", mState.getZoom() + ":" + mState.getPanX() + ":" + mState.getPanY()+ "-" + aspectQuotient); mState.notifyObservers(); } /** * Pan * * @param dx Amount to pan in x-dimension * @param dy Amount to pan in y-dimension */ public void pan(float dx, float dy) { final float aspectQuotient = mAspectQuotient.get(); float temp = mState.getZoomX(aspectQuotient); dx /= mState.getZoomX(aspectQuotient); dy /= mState.getZoomY(aspectQuotient); if (mState.getPanX() > mPanMaxX && dx > 0 || mState.getPanX() < mPanMinX && dx < 0) { dx *= PAN_OUTSIDE_SNAP_FACTOR; } if (mState.getPanY() > mPanMaxY && dy > 0 || mState.getPanY() < mPanMinY && dy < 0) { dy *= PAN_OUTSIDE_SNAP_FACTOR; } final float newPanX = mState.getPanX() + dx; final float newPanY = mState.getPanY() + dy; mState.setPanX(newPanX); mState.setPanY(newPanY); //Log.e("PAN", mState.getZoom() + ":" + mState.getPanX() + ":" + mState.getPanY() + "-" + aspectQuotient); mState.notifyObservers(); } /** * Runnable that updates dynamics state */ private final Runnable mUpdateRunnable = new Runnable() { public void run() { final long startTime = SystemClock.uptimeMillis(); mPanDynamicsX.update(startTime); mPanDynamicsY.update(startTime); final boolean isAtRest = mPanDynamicsX.isAtRest(REST_VELOCITY_TOLERANCE, REST_POSITION_TOLERANCE) && mPanDynamicsY.isAtRest(REST_VELOCITY_TOLERANCE, REST_POSITION_TOLERANCE); mState.setPanX(mPanDynamicsX.getPosition()); mState.setPanY(mPanDynamicsY.getPosition()); if (!isAtRest) { final long stopTime = SystemClock.uptimeMillis(); mHandler.postDelayed(mUpdateRunnable, 1000 / FPS - (stopTime - startTime)); } mState.notifyObservers(); } }; /** * Release control and start pan fling animation * * @param vx Velocity in x-dimension * @param vy Velocity in y-dimension */ public void startFling(float vx, float vy) { final float aspectQuotient = mAspectQuotient.get(); final long now = SystemClock.uptimeMillis(); mPanDynamicsX.setState(mState.getPanX(), vx / mState.getZoomX(aspectQuotient), now); mPanDynamicsY.setState(mState.getPanY(), vy / mState.getZoomY(aspectQuotient), now); mPanDynamicsX.setMinPosition(mPanMinX); mPanDynamicsX.setMaxPosition(mPanMaxX); mPanDynamicsY.setMinPosition(mPanMinY); mPanDynamicsY.setMaxPosition(mPanMaxY); mHandler.post(mUpdateRunnable); } /** * Stop fling animation */ public void stopFling() { mHandler.removeCallbacks(mUpdateRunnable); } /** * Help function to figure out max delta of pan from center position. * * @param zoom Zoom value * @return Max delta of pan */ private float getMaxPanDelta(float zoom) { return Math.max(0f, .5f * ((zoom - 1) / zoom)); } /** * Force zoom to stay within limits */ private void limitZoom() { if (mState.getZoom() < MIN_ZOOM) { mState.setZoom(MIN_ZOOM); } else if (mState.getZoom() > MAX_ZOOM) { mState.setZoom(MAX_ZOOM); } } public void setZoom(float zoom) { mState.setZoom(zoom); limitZoom(); } /** * Update limit values for pan */ private void updatePanLimits() { final float aspectQuotient = mAspectQuotient.get(); final float zoomX = mState.getZoomX(aspectQuotient); final float zoomY = mState.getZoomY(aspectQuotient); mPanMinX = .5f - getMaxPanDelta(zoomX); mPanMaxX = .5f + getMaxPanDelta(zoomX); mPanMinY = .5f - getMaxPanDelta(zoomY); mPanMaxY = .5f + getMaxPanDelta(zoomY); } // Observable interface implementation public void update(Observable observable, Object data) { limitZoom(); updatePanLimits(); } }