/* * Copyright (C) 2008 The Android Open Source Project * * 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 android.widget; import android.app.AlertDialog; import android.app.Dialog; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.graphics.PixelFormat; import android.graphics.Rect; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.provider.Settings; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.Window; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.animation.DecelerateInterpolator; // TODO: make sure no px values exist, only dip (scale if necessary from Viewconfiguration) /** * TODO: Docs * * If you are using this with a custom View, please call * {@link #setVisible(boolean) setVisible(false)} from the * {@link View#onDetachedFromWindow}. * * @hide */ public class ZoomRingController implements ZoomRing.OnZoomRingCallback, View.OnTouchListener, View.OnKeyListener { private static final int ZOOM_RING_RADIUS_INSET = 24; private static final int ZOOM_RING_RECENTERING_DURATION = 500; private static final String TAG = "ZoomRing"; public static final boolean USE_OLD_ZOOM = false; public static boolean useOldZoom(Context context) { return Settings.System.getInt(context.getContentResolver(), "zoom", 1) == 0; } private static final int ZOOM_CONTROLS_TIMEOUT = (int) ViewConfiguration.getZoomControlsTimeout(); // TODO: move these to ViewConfiguration or re-use existing ones // TODO: scale px values based on latest from ViewConfiguration private static final int SECOND_TAP_TIMEOUT = 500; private static final int ZOOM_RING_DISMISS_DELAY = SECOND_TAP_TIMEOUT / 2; // TODO: view config? at least scaled private static final int MAX_PAN_GAP = 20; private static final int MAX_INITIATE_PAN_GAP = 10; // TODO view config private static final int INITIATE_PAN_DELAY = 300; private static final String SETTING_NAME_SHOWN_TOAST = "shown_zoom_ring_toast"; private Context mContext; private WindowManager mWindowManager; /** * The view that is being zoomed by this zoom ring. */ private View mOwnerView; /** * The bounds of the owner view in global coordinates. This is recalculated * each time the zoom ring is shown. */ private Rect mOwnerViewBounds = new Rect(); /** * The container that is added as a window. */ private FrameLayout mContainer; private LayoutParams mContainerLayoutParams; /** * The view (or null) that should receive touch events. This will get set if * the touch down hits the container. It will be reset on the touch up. */ private View mTouchTargetView; /** * The {@link #mTouchTargetView}'s location in window, set on touch down. */ private int[] mTouchTargetLocationInWindow = new int[2]; /** * If the zoom ring is dismissed but the user is still in a touch * interaction, we set this to true. This will ignore all touch events until * up/cancel, and then set the owner's touch listener to null. */ private boolean mReleaseTouchListenerOnUp; /* * Tap-drag is an interaction where the user first taps and then (quickly) * does the clockwise or counter-clockwise drag. In reality, this is: (down, * up, down, move in circles, up). This differs from the usual events of: * (down, up, down, up, down, move in circles, up). While the only * difference is the omission of an (up, down), for power-users this is a * pretty big improvement as it now only requires them to focus on the * screen once (for the first tap down) instead of twice (for the first tap * down and then to grab the thumb). */ private int mTapDragStartX; private int mTapDragStartY; private static final int TOUCH_MODE_IDLE = 0; private static final int TOUCH_MODE_WAITING_FOR_SECOND_TAP = 1; private static final int TOUCH_MODE_WAITING_FOR_TAP_DRAG_MOVEMENT = 2; private static final int TOUCH_MODE_FORWARDING_FOR_TAP_DRAG = 3; private int mTouchMode; private boolean mIsZoomRingVisible; private ZoomRing mZoomRing; private int mZoomRingWidth; private int mZoomRingHeight; /** Invokes panning of owner view if the zoom ring is touching an edge. */ private Panner mPanner; private long mTouchingEdgeStartTime; private boolean mPanningEnabledForThisInteraction; private ImageView mPanningArrows; private Animation mPanningArrowsEnterAnimation; private Animation mPanningArrowsExitAnimation; private Rect mTempRect = new Rect(); private OnZoomListener mCallback; private ViewConfiguration mViewConfig; /** * When the zoom ring is centered on screen, this will be the x value used * for the container's layout params. */ private int mCenteredContainerX = Integer.MIN_VALUE; /** * When the zoom ring is centered on screen, this will be the y value used * for the container's layout params. */ private int mCenteredContainerY = Integer.MIN_VALUE; /** * Scroller used to re-center the zoom ring if the user had dragged it to a * corner and then double-taps any point on the owner view (the owner view * will center the double-tapped point, but we should re-center the zoom * ring). *
* The (x,y) of the scroller is the (x,y) of the container's layout params. */ private Scroller mScroller; /** * When showing the zoom ring, we add the view as a new window. However, * there is logic that needs to know the size of the zoom ring which is * determined after it's laid out. Therefore, we must post this logic onto * the UI thread so it will be exceuted AFTER the layout. This is the logic. */ private Runnable mPostedVisibleInitializer; /** * Only touch from the main thread. */ private static Dialog sTutorialDialog; private static long sTutorialShowTime; private static final int TUTORIAL_MIN_DISPLAY_TIME = 2000; private IntentFilter mConfigurationChangedFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); private BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (!mIsZoomRingVisible) return; mHandler.removeMessages(MSG_POST_CONFIGURATION_CHANGED); mHandler.sendEmptyMessage(MSG_POST_CONFIGURATION_CHANGED); } }; /** Keeps the scroller going (or starts it). */ private static final int MSG_SCROLLER_TICK = 1; /** When configuration changes, this is called after the UI thread is idle. */ private static final int MSG_POST_CONFIGURATION_CHANGED = 2; /** Used to delay the zoom ring dismissal. */ private static final int MSG_DISMISS_ZOOM_RING = 3; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_SCROLLER_TICK: onScrollerTick(); break; case MSG_POST_CONFIGURATION_CHANGED: onPostConfigurationChanged(); break; case MSG_DISMISS_ZOOM_RING: setVisible(false); break; } } }; public ZoomRingController(Context context, View ownerView) { mContext = context; mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mPanner = new Panner(); mOwnerView = ownerView; mZoomRing = new ZoomRing(context); mZoomRing.setId(com.android.internal.R.id.zoomControls); mZoomRing.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); mZoomRing.setCallback(this); createPanningArrows(); mContainerLayoutParams = new LayoutParams(); mContainerLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; mContainerLayoutParams.flags = LayoutParams.FLAG_NOT_TOUCHABLE | LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_LAYOUT_NO_LIMITS; mContainerLayoutParams.height = LayoutParams.WRAP_CONTENT; mContainerLayoutParams.width = LayoutParams.WRAP_CONTENT; mContainerLayoutParams.type = LayoutParams.TYPE_APPLICATION_PANEL; mContainerLayoutParams.format = PixelFormat.TRANSPARENT; // TODO: make a new animation for this mContainerLayoutParams.windowAnimations = com.android.internal.R.style.Animation_Dialog; mContainer = new FrameLayout(context); mContainer.setLayoutParams(mContainerLayoutParams); mContainer.setMeasureAllChildren(true); mContainer.addView(mZoomRing); mContainer.addView(mPanningArrows); mScroller = new Scroller(context, new DecelerateInterpolator()); mViewConfig = ViewConfiguration.get(context); } private void createPanningArrows() { // TODO: style mPanningArrows = new ImageView(mContext); mPanningArrows.setImageResource(com.android.internal.R.drawable.zoom_ring_arrows); mPanningArrows.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); mPanningArrows.setVisibility(View.INVISIBLE); mPanningArrowsEnterAnimation = AnimationUtils.loadAnimation(mContext, com.android.internal.R.anim.fade_in); mPanningArrowsExitAnimation = AnimationUtils.loadAnimation(mContext, com.android.internal.R.anim.fade_out); } /** * Sets the angle (in radians) a user must travel in order for the client to * get a callback. Once there is a callback, the accumulator resets. For * example, if you set this to PI/6, it will give a callback every time the * user moves PI/6 amount on the ring. * * @param callbackThreshold The angle for the callback threshold, in radians */ public void setZoomCallbackThreshold(float callbackThreshold) { mZoomRing.setCallbackThreshold((int) (callbackThreshold * ZoomRing.RADIAN_INT_MULTIPLIER)); } /** * Sets a drawable for the zoom ring track. * * @param drawable The drawable to use for the track. * @hide Need a better way of doing this, but this one-off for browser so it * can have its final look for the usability study */ public void setZoomRingTrack(int drawable) { mZoomRing.setBackgroundResource(drawable); } public void setCallback(OnZoomListener callback) { mCallback = callback; } public void setThumbAngle(float angle) { mZoomRing.setThumbAngle((int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER)); } public void setThumbAngleAnimated(float angle) { mZoomRing.setThumbAngleAnimated((int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER), 0); } public void setResetThumbAutomatically(boolean resetThumbAutomatically) { mZoomRing.setResetThumbAutomatically(resetThumbAutomatically); } public void setThumbClockwiseBound(float angle) { mZoomRing.setThumbClockwiseBound(angle >= 0 ? (int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER) : Integer.MIN_VALUE); } public void setThumbCounterclockwiseBound(float angle) { mZoomRing.setThumbCounterclockwiseBound(angle >= 0 ? (int) (angle * ZoomRing.RADIAN_INT_MULTIPLIER) : Integer.MIN_VALUE); } public boolean isVisible() { return mIsZoomRingVisible; } public void setVisible(boolean visible) { if (useOldZoom(mContext)) return; if (visible) { dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); } else { mPanner.stop(); } if (mIsZoomRingVisible == visible) { return; } mIsZoomRingVisible = visible; if (visible) { if (mContainerLayoutParams.token == null) { mContainerLayoutParams.token = mOwnerView.getWindowToken(); } mWindowManager.addView(mContainer, mContainerLayoutParams); if (mPostedVisibleInitializer == null) { mPostedVisibleInitializer = new Runnable() { public void run() { refreshPositioningVariables(); resetZoomRing(); // TODO: remove this 'update' and just center zoom ring before the // 'add', but need to make sure we have the width and height (which // probably can only be retrieved after it's measured, which happens // after it's added). mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); if (mCallback != null) { mCallback.onVisibilityChanged(true); } } }; } mPanningArrows.setAnimation(null); mHandler.post(mPostedVisibleInitializer); // Handle configuration changes when visible mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter); // Steal key/touches events from the owner mOwnerView.setOnKeyListener(this); mOwnerView.setOnTouchListener(this); mReleaseTouchListenerOnUp = false; } else { // Don't want to steal any more keys/touches mOwnerView.setOnKeyListener(null); if (mTouchTargetView != null) { // We are still stealing the touch events for this touch // sequence, so release the touch listener later mReleaseTouchListenerOnUp = true; } else { mOwnerView.setOnTouchListener(null); } // No longer care about configuration changes mContext.unregisterReceiver(mConfigurationChangedReceiver); mWindowManager.removeView(mContainer); mHandler.removeCallbacks(mPostedVisibleInitializer); if (mCallback != null) { mCallback.onVisibilityChanged(false); } } } /** * TODO: docs * * Notes: * - Touch dispatching is different. Only direct children who are clickable are eligble for touch events. * - Please ensure you set your View to INVISIBLE not GONE when hiding it. * * @return */ public FrameLayout getContainer() { return mContainer; } public int getZoomRingId() { return mZoomRing.getId(); } private void dismissZoomRingDelayed(int delay) { mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_RING, delay); } private void resetZoomRing() { mScroller.abortAnimation(); mContainerLayoutParams.x = mCenteredContainerX; mContainerLayoutParams.y = mCenteredContainerY; // Reset the thumb mZoomRing.resetThumbAngle(); } /** * Should be called by the client for each event belonging to the second tap * (the down, move, up, and cancel events). * * @param event The event belonging to the second tap. * @return Whether the event was consumed. */ public boolean handleDoubleTapEvent(MotionEvent event) { int action = event.getAction(); // TODO: make sure this works well with the // ownerView.setOnTouchListener(this) instead of window receiving // touches if (action == MotionEvent.ACTION_DOWN) { mTouchMode = TOUCH_MODE_WAITING_FOR_TAP_DRAG_MOVEMENT; int x = (int) event.getX(); int y = (int) event.getY(); refreshPositioningVariables(); setVisible(true); centerPoint(x, y); ensureZoomRingIsCentered(); // Tap drag mode stuff mTapDragStartX = x; mTapDragStartY = y; } else if (action == MotionEvent.ACTION_CANCEL) { mTouchMode = TOUCH_MODE_IDLE; } else { // action is move or up switch (mTouchMode) { case TOUCH_MODE_WAITING_FOR_TAP_DRAG_MOVEMENT: { switch (action) { case MotionEvent.ACTION_MOVE: int x = (int) event.getX(); int y = (int) event.getY(); if (Math.abs(x - mTapDragStartX) > mViewConfig.getScaledTouchSlop() || Math.abs(y - mTapDragStartY) > mViewConfig.getScaledTouchSlop()) { mZoomRing.setTapDragMode(true, x, y); mTouchMode = TOUCH_MODE_FORWARDING_FOR_TAP_DRAG; setTouchTargetView(mZoomRing); } return true; case MotionEvent.ACTION_UP: mTouchMode = TOUCH_MODE_IDLE; break; } break; } case TOUCH_MODE_FORWARDING_FOR_TAP_DRAG: { switch (action) { case MotionEvent.ACTION_MOVE: giveTouchToZoomRing(event); return true; case MotionEvent.ACTION_UP: mTouchMode = TOUCH_MODE_IDLE; /* * This is a power-user feature that only shows the * zoom while the user is performing the tap-drag. * That means once it is released, the zoom ring * should disappear. */ mZoomRing.setTapDragMode(false, (int) event.getX(), (int) event.getY()); dismissZoomRingDelayed(0); break; } break; } } } return true; } private void ensureZoomRingIsCentered() { LayoutParams lp = mContainerLayoutParams; if (lp.x != mCenteredContainerX || lp.y != mCenteredContainerY) { int width = mContainer.getWidth(); int height = mContainer.getHeight(); mScroller.startScroll(lp.x, lp.y, mCenteredContainerX - lp.x, mCenteredContainerY - lp.y, ZOOM_RING_RECENTERING_DURATION); mHandler.sendEmptyMessage(MSG_SCROLLER_TICK); } } private void refreshPositioningVariables() { mZoomRingWidth = mZoomRing.getWidth(); mZoomRingHeight = mZoomRing.getHeight(); // Calculate the owner view's bounds mOwnerView.getGlobalVisibleRect(mOwnerViewBounds); // Get the center Gravity.apply(Gravity.CENTER, mContainer.getWidth(), mContainer.getHeight(), mOwnerViewBounds, mTempRect); mCenteredContainerX = mTempRect.left; mCenteredContainerY = mTempRect.top; } /** * Centers the point (in owner view's coordinates). */ private void centerPoint(int x, int y) { if (mCallback != null) { mCallback.onCenter(x, y); } } private void giveTouchToZoomRing(MotionEvent event) { int rawX = (int) event.getRawX(); int rawY = (int) event.getRawY(); int x = rawX - mContainerLayoutParams.x - mZoomRing.getLeft(); int y = rawY - mContainerLayoutParams.y - mZoomRing.getTop(); mZoomRing.handleTouch(event.getAction(), event.getEventTime(), x, y, rawX, rawY); } public void onZoomRingSetMovableHintVisible(boolean visible) { setPanningArrowsVisible(visible); } public void onUserInteractionStarted() { mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); } public void onUserInteractionStopped() { dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); } public void onZoomRingMovingStarted() { mScroller.abortAnimation(); mTouchingEdgeStartTime = 0; if (mCallback != null) { mCallback.onBeginPan(); } } private void setPanningArrowsVisible(boolean visible) { mPanningArrows.startAnimation(visible ? mPanningArrowsEnterAnimation : mPanningArrowsExitAnimation); mPanningArrows.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); } public boolean onZoomRingMoved(int deltaX, int deltaY) { WindowManager.LayoutParams lp = mContainerLayoutParams; Rect ownerBounds = mOwnerViewBounds; int zoomRingLeft = mZoomRing.getLeft(); int zoomRingTop = mZoomRing.getTop(); int newX = lp.x + deltaX; int newZoomRingX = newX + zoomRingLeft; newZoomRingX = (newZoomRingX <= ownerBounds.left) ? ownerBounds.left : (newZoomRingX + mZoomRingWidth > ownerBounds.right) ? ownerBounds.right - mZoomRingWidth : newZoomRingX; lp.x = newZoomRingX - zoomRingLeft; int newY = lp.y + deltaY; int newZoomRingY = newY + zoomRingTop; newZoomRingY = (newZoomRingY <= ownerBounds.top) ? ownerBounds.top : (newZoomRingY + mZoomRingHeight > ownerBounds.bottom) ? ownerBounds.bottom - mZoomRingHeight : newZoomRingY; lp.y = newZoomRingY - zoomRingTop; mWindowManager.updateViewLayout(mContainer, lp); // Check for pan boolean horizontalPanning = true; int leftGap = newZoomRingX - ownerBounds.left; if (leftGap < MAX_PAN_GAP) { if (shouldPan(leftGap)) { mPanner.setHorizontalStrength(-getStrengthFromGap(leftGap)); } } else { int rightGap = ownerBounds.right - (lp.x + mZoomRingWidth + zoomRingLeft); if (rightGap < MAX_PAN_GAP) { if (shouldPan(rightGap)) { mPanner.setHorizontalStrength(getStrengthFromGap(rightGap)); } } else { mPanner.setHorizontalStrength(0); horizontalPanning = false; } } int topGap = newZoomRingY - ownerBounds.top; if (topGap < MAX_PAN_GAP) { if (shouldPan(topGap)) { mPanner.setVerticalStrength(-getStrengthFromGap(topGap)); } } else { int bottomGap = ownerBounds.bottom - (lp.y + mZoomRingHeight + zoomRingTop); if (bottomGap < MAX_PAN_GAP) { if (shouldPan(bottomGap)) { mPanner.setVerticalStrength(getStrengthFromGap(bottomGap)); } } else { mPanner.setVerticalStrength(0); if (!horizontalPanning) { // Neither are panning, reset any timer to start pan mode mTouchingEdgeStartTime = 0; mPanningEnabledForThisInteraction = false; mPanner.stop(); } } } return true; } private boolean shouldPan(int gap) { if (mPanningEnabledForThisInteraction) return true; if (gap < MAX_INITIATE_PAN_GAP) { long time = SystemClock.elapsedRealtime(); if (mTouchingEdgeStartTime != 0 && mTouchingEdgeStartTime + INITIATE_PAN_DELAY < time) { mPanningEnabledForThisInteraction = true; return true; } else if (mTouchingEdgeStartTime == 0) { mTouchingEdgeStartTime = time; } else { } } else { // Moved away from the initiate pan gap, so reset the timer mTouchingEdgeStartTime = 0; } return false; } public void onZoomRingMovingStopped() { mPanner.stop(); setPanningArrowsVisible(false); if (mCallback != null) { mCallback.onEndPan(); } } private int getStrengthFromGap(int gap) { return gap > MAX_PAN_GAP ? 0 : (MAX_PAN_GAP - gap) * 100 / MAX_PAN_GAP; } public void onZoomRingThumbDraggingStarted() { if (mCallback != null) { mCallback.onBeginDrag(); } } public boolean onZoomRingThumbDragged(int numLevels, int startAngle, int curAngle) { if (mCallback != null) { int deltaZoomLevel = -numLevels; int globalZoomCenterX = mContainerLayoutParams.x + mZoomRing.getLeft() + mZoomRingWidth / 2; int globalZoomCenterY = mContainerLayoutParams.y + mZoomRing.getTop() + mZoomRingHeight / 2; return mCallback.onDragZoom(deltaZoomLevel, globalZoomCenterX - mOwnerViewBounds.left, globalZoomCenterY - mOwnerViewBounds.top, (float) startAngle / ZoomRing.RADIAN_INT_MULTIPLIER, (float) curAngle / ZoomRing.RADIAN_INT_MULTIPLIER); } return false; } public void onZoomRingThumbDraggingStopped() { if (mCallback != null) { mCallback.onEndDrag(); } } public void onZoomRingDismissed(boolean dismissImmediately) { if (dismissImmediately) { mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); setVisible(false); } else { dismissZoomRingDelayed(ZOOM_RING_DISMISS_DELAY); } } public void onRingDown(int tickAngle, int touchAngle) { } public boolean onTouch(View v, MotionEvent event) { if (sTutorialDialog != null && sTutorialDialog.isShowing() && SystemClock.elapsedRealtime() - sTutorialShowTime >= TUTORIAL_MIN_DISPLAY_TIME) { finishZoomTutorial(); } int action = event.getAction(); if (mReleaseTouchListenerOnUp) { // The ring was dismissed but we need to throw away all events until the up if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { mOwnerView.setOnTouchListener(null); setTouchTargetView(null); mReleaseTouchListenerOnUp = false; } // Eat this event return true; } View targetView = mTouchTargetView; switch (action) { case MotionEvent.ACTION_DOWN: targetView = getViewForTouch((int) event.getRawX(), (int) event.getRawY()); setTouchTargetView(targetView); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: setTouchTargetView(null); break; } if (targetView != null) { // The upperleft corner of the target view in raw coordinates int targetViewRawX = mContainerLayoutParams.x + mTouchTargetLocationInWindow[0]; int targetViewRawY = mContainerLayoutParams.y + mTouchTargetLocationInWindow[1]; MotionEvent containerEvent = MotionEvent.obtain(event); // Convert the motion event into the target view's coordinates (from // owner view's coordinates) containerEvent.offsetLocation(mOwnerViewBounds.left - targetViewRawX, mOwnerViewBounds.top - targetViewRawY); boolean retValue = targetView.dispatchTouchEvent(containerEvent); containerEvent.recycle(); return retValue; } else { if (action == MotionEvent.ACTION_DOWN) { dismissZoomRingDelayed(ZOOM_RING_DISMISS_DELAY); } return false; } } private void setTouchTargetView(View view) { mTouchTargetView = view; if (view != null) { view.getLocationInWindow(mTouchTargetLocationInWindow); } } /** * Returns the View that should receive a touch at the given coordinates. * * @param rawX The raw X. * @param rawY The raw Y. * @return The view that should receive the touches, or null if there is not one. */ private View getViewForTouch(int rawX, int rawY) { // Check to see if it is touching the ring int containerCenterX = mContainerLayoutParams.x + mContainer.getWidth() / 2; int containerCenterY = mContainerLayoutParams.y + mContainer.getHeight() / 2; int distanceFromCenterX = rawX - containerCenterX; int distanceFromCenterY = rawY - containerCenterY; int zoomRingRadius = mZoomRingWidth / 2 - ZOOM_RING_RADIUS_INSET; if (distanceFromCenterX * distanceFromCenterX + distanceFromCenterY * distanceFromCenterY <= zoomRingRadius * zoomRingRadius) { return mZoomRing; } // Check to see if it is touching any other clickable View. // Reverse order so the child drawn on top gets first dibs. int containerCoordsX = rawX - mContainerLayoutParams.x; int containerCoordsY = rawY - mContainerLayoutParams.y; Rect frame = mTempRect; for (int i = mContainer.getChildCount() - 1; i >= 0; i--) { View child = mContainer.getChildAt(i); if (child == mZoomRing || child.getVisibility() != View.VISIBLE || !child.isClickable()) { continue; } child.getHitRect(frame); if (frame.contains(containerCoordsX, containerCoordsY)) { return child; } } return null; } /** Steals key events from the owner view. */ public boolean onKey(View v, int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_RIGHT: // Eat these return true; case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_DOWN: // Keep the zoom alive a little longer dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); // They started zooming, hide the thumb arrows mZoomRing.setThumbArrowsVisible(false); if (mCallback != null && event.getAction() == KeyEvent.ACTION_DOWN) { mCallback.onSimpleZoom(keyCode == KeyEvent.KEYCODE_DPAD_UP); } return true; } return false; } private void onScrollerTick() { if (!mScroller.computeScrollOffset() || !mIsZoomRingVisible) return; mContainerLayoutParams.x = mScroller.getCurrX(); mContainerLayoutParams.y = mScroller.getCurrY(); mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); mHandler.sendEmptyMessage(MSG_SCROLLER_TICK); } private void onPostConfigurationChanged() { dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); refreshPositioningVariables(); ensureZoomRingIsCentered(); } /* * This is static so Activities can call this instead of the Views * (Activities usually do not have a reference to the ZoomRingController * instance.) */ /** * Shows a "tutorial" (some text) to the user teaching her the new zoom * invocation method. Must call from the main thread. *
* It checks the global system setting to ensure this has not been seen * before. Furthermore, if the application does not have privilege to write * to the system settings, it will store this bit locally in a shared * preference. * * @hide This should only be used by our main apps--browser, maps, and * gallery */ public static void showZoomTutorialOnce(Context context) { ContentResolver cr = context.getContentResolver(); if (Settings.System.getInt(cr, SETTING_NAME_SHOWN_TOAST, 0) == 1) { return; } SharedPreferences sp = context.getSharedPreferences("_zoom", Context.MODE_PRIVATE); if (sp.getInt(SETTING_NAME_SHOWN_TOAST, 0) == 1) { return; } if (sTutorialDialog != null && sTutorialDialog.isShowing()) { sTutorialDialog.dismiss(); } sTutorialDialog = new AlertDialog.Builder(context) .setMessage( com.android.internal.R.string.tutorial_double_tap_to_zoom_message_short) .setIcon(0) .create(); Window window = sTutorialDialog.getWindow(); window.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND | WindowManager.LayoutParams.FLAG_BLUR_BEHIND); window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); sTutorialDialog.show(); sTutorialShowTime = SystemClock.elapsedRealtime(); } public void finishZoomTutorial() { if (sTutorialDialog == null) return; sTutorialDialog.dismiss(); sTutorialDialog = null; // Record that they have seen the tutorial try { Settings.System.putInt(mContext.getContentResolver(), SETTING_NAME_SHOWN_TOAST, 1); } catch (SecurityException e) { /* * The app does not have permission to clear this global flag, make * sure the user does not see the message when he comes back to this * same app at least. */ SharedPreferences sp = mContext.getSharedPreferences("_zoom", Context.MODE_PRIVATE); sp.edit().putInt(SETTING_NAME_SHOWN_TOAST, 1).commit(); } } public void setPannerStartVelocity(float startVelocity) { mPanner.mStartVelocity = startVelocity; } public void setPannerAcceleration(float acceleration) { mPanner.mAcceleration = acceleration; } public void setPannerMaxVelocity(float maxVelocity) { mPanner.mMaxVelocity = maxVelocity; } public void setPannerStartAcceleratingDuration(int duration) { mPanner.mStartAcceleratingDuration = duration; } private class Panner implements Runnable { private static final int RUN_DELAY = 15; private static final float STOP_SLOWDOWN = 0.8f; private final Handler mUiHandler = new Handler(); private int mVerticalStrength; private int mHorizontalStrength; private boolean mStopping; /** The time this current pan started. */ private long mStartTime; /** The time of the last callback to pan the map/browser/etc. */ private long mPreviousCallbackTime; // TODO Adjust to be DPI safe private float mStartVelocity = 135; private float mAcceleration = 160; private float mMaxVelocity = 1000; private int mStartAcceleratingDuration = 700; private float mVelocity; /** -100 (full left) to 0 (none) to 100 (full right) */ public void setHorizontalStrength(int horizontalStrength) { if (mHorizontalStrength == 0 && mVerticalStrength == 0 && horizontalStrength != 0) { start(); } else if (mVerticalStrength == 0 && horizontalStrength == 0) { stop(); } mHorizontalStrength = horizontalStrength; mStopping = false; } /** -100 (full up) to 0 (none) to 100 (full down) */ public void setVerticalStrength(int verticalStrength) { if (mHorizontalStrength == 0 && mVerticalStrength == 0 && verticalStrength != 0) { start(); } else if (mHorizontalStrength == 0 && verticalStrength == 0) { stop(); } mVerticalStrength = verticalStrength; mStopping = false; } private void start() { mUiHandler.post(this); mPreviousCallbackTime = 0; mStartTime = 0; } public void stop() { mStopping = true; } public void run() { if (mStopping) { mHorizontalStrength *= STOP_SLOWDOWN; mVerticalStrength *= STOP_SLOWDOWN; } if (mHorizontalStrength == 0 && mVerticalStrength == 0) { return; } boolean firstRun = mPreviousCallbackTime == 0; long curTime = SystemClock.elapsedRealtime(); int panAmount = getPanAmount(mPreviousCallbackTime, curTime); mPreviousCallbackTime = curTime; if (firstRun) { mStartTime = curTime; mVelocity = mStartVelocity; } else { int panX = panAmount * mHorizontalStrength / 100; int panY = panAmount * mVerticalStrength / 100; if (mCallback != null) { mCallback.onPan(panX, panY); } } mUiHandler.postDelayed(this, RUN_DELAY); } private int getPanAmount(long previousTime, long currentTime) { if (mVelocity > mMaxVelocity) { mVelocity = mMaxVelocity; } else if (mVelocity < mMaxVelocity) { // See if it's time to add in some acceleration if (currentTime - mStartTime > mStartAcceleratingDuration) { mVelocity += (currentTime - previousTime) * mAcceleration / 1000; } } return (int) ((currentTime - previousTime) * mVelocity) / 1000; } } public interface OnZoomListener { void onBeginDrag(); boolean onDragZoom(int deltaZoomLevel, int centerX, int centerY, float startAngle, float curAngle); void onEndDrag(); void onSimpleZoom(boolean deltaZoomLevel); void onBeginPan(); boolean onPan(int deltaX, int deltaY); void onEndPan(); void onCenter(int x, int y); void onVisibilityChanged(boolean visible); } }