diff options
Diffstat (limited to 'core/java')
32 files changed, 967 insertions, 3146 deletions
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index e1c1f64..824fd9b 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -1042,6 +1042,14 @@ public class Intent implements Parcelable { */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_SCREEN_ON = "android.intent.action.SCREEN_ON"; + + /** + * Broadcast Action: Sent when the user is present after device wakes up (e.g when the + * keyguard is gone). + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_USER_PRESENT= "android.intent.action.USER_PRESENT"; + /** * Broadcast Action: The current time has changed. Sent every * minute. You can <em>not</em> receive this through components declared diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 4ae8b08..2dcb483 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -260,8 +260,9 @@ public class PackageParser { boolean assetError = true; try { assmgr = new AssetManager(); - if(assmgr.addAssetPath(mArchiveSourcePath) != 0) { - parser = assmgr.openXmlResourceParser("AndroidManifest.xml"); + int cookie = assmgr.addAssetPath(mArchiveSourcePath); + if(cookie != 0) { + parser = assmgr.openXmlResourceParser(cookie, "AndroidManifest.xml"); assetError = false; } else { Log.w(TAG, "Failed adding asset path:"+mArchiveSourcePath); diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index fadcb35..1c91736 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -567,8 +567,8 @@ public final class AssetManager { /** * Add an additional set of assets to the asset manager. This can be - * either a directory or ZIP file. Not for use by applications. A - * zero return value indicates failure. + * either a directory or ZIP file. Not for use by applications. Returns + * the cookie of the added asset, or 0 on failure. * {@hide} */ public native final int addAssetPath(String path); diff --git a/core/java/android/emoji/EmojiFactory.java b/core/java/android/emoji/EmojiFactory.java new file mode 100644 index 0000000..389bd07 --- /dev/null +++ b/core/java/android/emoji/EmojiFactory.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2009 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.emoji; + +import android.graphics.Bitmap; + +import java.lang.ref.WeakReference; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A class for the factories which produce Emoji (pictgram) images. + * This is intended to be used by IME, Email app, etc. + * There's no plan to make this public for now. + * @hide + */ +public final class EmojiFactory { + // private static final String LOG_TAG = "EmojiFactory"; + + private int sCacheSize = 100; + + // HashMap for caching Bitmap object. In order not to make an cache object + // blow up, we use LinkedHashMap with size limit. + private class CustomLinkedHashMap<K, V> extends LinkedHashMap<K, V> { + public CustomLinkedHashMap() { + // These magic numbers are gotten from the source code of + // LinkedHashMap.java and HashMap.java. + super(16, 0.75f, true); + } + + /* + * If size() becomes more than sCacheSize, least recently used cache + * is erased. + * @see java.util.LinkedHashMap#removeEldestEntry(java.util.Map.Entry) + */ + @Override + protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { + return size() > sCacheSize; + } + } + + // A pointer to native EmojiFactory object. + private int mNativeEmojiFactory; + private String mName; + // Cache. + private Map<Integer, WeakReference<Bitmap>> mCache; + + /** + * @noinspection UnusedDeclaration + */ + /* + * Private constructor that must received an already allocated native + * EmojiFactory int (pointer). + * + * This can be called from JNI code. + */ + private EmojiFactory(int nativeEmojiFactory, String name) { + mNativeEmojiFactory = nativeEmojiFactory; + mName = name; + mCache = new CustomLinkedHashMap<Integer, WeakReference<Bitmap>>(); + } + + @Override + protected void finalize() throws Throwable { + try { + nativeDestructor(mNativeEmojiFactory); + } finally { + super.finalize(); + } + } + + public String name() { + return mName; + } + + /** + * Returns Bitmap object corresponding to the AndroidPua. + * + * Note that each Bitmap is cached by this class, which means that, if you modify a + * Bitmap object (using setPos() method), all same emoji Bitmap will be modified. + * If it is unacceptable, please copy the object before modifying it. + * + * @param pua A unicode codepoint. + * @return Bitmap object when this factory knows the Bitmap relevant to the codepoint. + * Otherwise null is returned. + */ + public synchronized Bitmap getBitmapFromAndroidPua(int pua) { + WeakReference<Bitmap> cache = mCache.get(pua); + if (cache == null) { + Bitmap ret = nativeGetBitmapFromAndroidPua(mNativeEmojiFactory, pua); + // There is no need to cache returned null, since in most cases it means there + // is no map from the AndroidPua to a specific image. In other words, it usually does + // not include the cost of creating Bitmap object. + if (ret != null) { + mCache.put(pua, new WeakReference<Bitmap>(ret)); + } + return ret; + } else { + Bitmap tmp = cache.get(); + if (tmp == null) { + Bitmap ret = nativeGetBitmapFromAndroidPua(mNativeEmojiFactory, pua); + mCache.put(pua, new WeakReference<Bitmap>(ret)); + return ret; + } else { + return tmp; + } + } + } + + /** + * Returns Bitmap object corresponding to the vendor specified sjis. + * + * See comments in getBitmapFromAndroidPua(). + * + * @param sjis sjis code specific to each career(vendor) + * @return Bitmap object when this factory knows the Bitmap relevant to the code. Otherwise + * null is returned. + */ + public synchronized Bitmap getBitmapFromVendorSpecificSjis(char sjis) { + return getBitmapFromAndroidPua(getAndroidPuaFromVendorSpecificSjis(sjis)); + } + + /** + * Returns Bitmap object corresponding to the vendor specific Unicode. + * + * See comments in getBitmapFromAndroidPua(). + * + * @param vsp vendor specific PUA. + * @return Bitmap object when this factory knows the Bitmap relevant to the code. Otherwise + * null is returned. + */ + public synchronized Bitmap getBitmapFromVendorSpecificPua(int vsp) { + return getBitmapFromAndroidPua(getAndroidPuaFromVendorSpecificPua(vsp)); + } + + /** + * Returns Unicode PUA for Android corresponding to the vendor specific sjis. + * + * @param sjis vendor specific sjis + * @return Unicode PUA for Android, or -1 if there's no map for the sjis. + */ + public int getAndroidPuaFromVendorSpecificSjis(char sjis) { + return nativeGetAndroidPuaFromVendorSpecificSjis(mNativeEmojiFactory, sjis); + } + + /** + * Returns vendor specific sjis corresponding to the Unicode AndroidPua. + * + * @param pua Unicode PUA for Android, + * @return vendor specific sjis, or -1 if there's no map for the AndroidPua. + */ + public int getVendorSpecificSjisFromAndroidPua(int pua) { + return nativeGetVendorSpecificSjisFromAndroidPua(mNativeEmojiFactory, pua); + } + + /** + * Returns Unicode PUA for Android corresponding to the vendor specific Unicode. + * + * @param vsp vendor specific PUA. + * @return Unicode PUA for Android, or -1 if there's no map for the + * Unicode. + */ + public int getAndroidPuaFromVendorSpecificPua(int vsp) { + return nativeGetAndroidPuaFromVendorSpecificPua(mNativeEmojiFactory, vsp); + } + + public String getAndroidPuaFromVendorSpecificPua(String vspString) { + if (vspString == null) { + return null; + } + int minVsp = nativeGetMinimumVendorSpecificPua(mNativeEmojiFactory); + int maxVsp = nativeGetMaximumVendorSpecificPua(mNativeEmojiFactory); + int len = vspString.length(); + int[] codePoints = new int[vspString.codePointCount(0, len)]; + + int new_len = 0; + for (int i = 0; i < len; i = vspString.offsetByCodePoints(i, 1), new_len++) { + int codePoint = vspString.codePointAt(i); + if (minVsp <= codePoint && codePoint <= maxVsp) { + int newCodePoint = getAndroidPuaFromVendorSpecificPua(codePoint); + if (newCodePoint > 0) { + codePoints[new_len] = newCodePoint; + continue; + } + } + codePoints[new_len] = codePoint; + } + return new String(codePoints, 0, new_len); + } + + /** + * Returns vendor specific Unicode corresponding to the Unicode AndroidPua. + * + * @param pua Unicode PUA for Android, + * @return vendor specific sjis, or -1 if there's no map for the AndroidPua. + */ + public int getVendorSpecificPuaFromAndroidPua(int pua) { + return nativeGetVendorSpecificPuaFromAndroidPua(mNativeEmojiFactory, pua); + } + + public String getVendorSpecificPuaFromAndroidPua(String puaString) { + if (puaString == null) { + return null; + } + int minVsp = nativeGetMinimumAndroidPua(mNativeEmojiFactory); + int maxVsp = nativeGetMaximumAndroidPua(mNativeEmojiFactory); + int len = puaString.length(); + int[] codePoints = new int[puaString.codePointCount(0, len)]; + + int new_len = 0; + for (int i = 0; i < len; i = puaString.offsetByCodePoints(i, 1), new_len++) { + int codePoint = puaString.codePointAt(i); + if (minVsp <= codePoint && codePoint <= maxVsp) { + int newCodePoint = getVendorSpecificPuaFromAndroidPua(codePoint); + if (newCodePoint > 0) { + codePoints[new_len] = newCodePoint; + continue; + } + } + codePoints[new_len] = codePoint; + } + return new String(codePoints, 0, new_len); + } + + /** + * Constructs an instance of EmojiFactory corresponding to the name. + * + * @param class_name Name of the factory. This must include complete package name. + * @return A concrete EmojiFactory instance corresponding to factory_name. + * If factory_name is invalid, null is returned. + */ + public static native EmojiFactory newInstance(String class_name); + + /** + * Constructs an instance of available EmojiFactory. + * + * @return A concrete EmojiFactory instance. If there are several available + * EmojiFactory class, preferred one is chosen by the system. If there isn't, null + * is returned. + */ + public static native EmojiFactory newAvailableInstance(); + + // native methods + + private native void nativeDestructor(int factory); + private native Bitmap nativeGetBitmapFromAndroidPua(int nativeEmojiFactory, int AndroidPua); + private native int nativeGetAndroidPuaFromVendorSpecificSjis(int nativeEmojiFactory, + char sjis); + private native int nativeGetVendorSpecificSjisFromAndroidPua(int nativeEmojiFactory, + int pua); + private native int nativeGetAndroidPuaFromVendorSpecificPua(int nativeEmojiFactory, + int vsp); + private native int nativeGetVendorSpecificPuaFromAndroidPua(int nativeEmojiFactory, + int pua); + private native int nativeGetMaximumVendorSpecificPua(int nativeEmojiFactory); + private native int nativeGetMinimumVendorSpecificPua(int nativeEmojiFactory); + private native int nativeGetMaximumAndroidPua(int nativeEmojiFactory); + private native int nativeGetMinimumAndroidPua(int nativeEmojiFactory); +} diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index f1e613e..34d5695 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -1377,9 +1377,11 @@ public class InputMethodService extends AbstractInputMethodService { if (mExtractedToken != token) { return; } - if (mExtractEditText != null && text != null) { - mExtractedText = text; - mExtractEditText.setExtractedText(text); + if (text != null) { + if (mExtractEditText != null) { + mExtractedText = text; + mExtractEditText.setExtractedText(text); + } } } diff --git a/core/java/android/provider/Checkin.java b/core/java/android/provider/Checkin.java index 1454089..d11a9c5 100644 --- a/core/java/android/provider/Checkin.java +++ b/core/java/android/provider/Checkin.java @@ -132,8 +132,6 @@ public final class Checkin { BROWSER_SNAP_CENTER, BROWSER_TEXT_SIZE_CHANGE, BROWSER_ZOOM_OVERVIEW, - BROWSER_ZOOM_RING, - BROWSER_ZOOM_RING_DRAG, CRASHES_REPORTED, CRASHES_TRUNCATED, ELAPSED_REALTIME_SEC, diff --git a/core/java/android/server/BluetoothDeviceService.java b/core/java/android/server/BluetoothDeviceService.java index 87153cf..6f5513a 100644 --- a/core/java/android/server/BluetoothDeviceService.java +++ b/core/java/android/server/BluetoothDeviceService.java @@ -237,7 +237,28 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { public void run() { boolean res = (enableNative() == 0); if (res) { - mEventLoop.start(); + int retryCount = 2; + boolean running = false; + while ((retryCount-- > 0) && !running) { + mEventLoop.start(); + // it may take a momement for the other thread to do its + // thing. Check periodically for a while. + int pollCount = 5; + while ((pollCount-- > 0) && !running) { + if (mEventLoop.isEventLoopRunning()) { + running = true; + break; + } + try { + Thread.sleep(100); + } catch (InterruptedException e) {} + } + } + if (!running) { + log("bt EnableThread giving up"); + res = false; + disableNative(); + } } if (mEnableCallback != null) { @@ -254,14 +275,20 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { persistBluetoothOnSetting(true); } mIsDiscovering = false; - Intent intent = new Intent(BluetoothIntent.ENABLED_ACTION); mBondState.loadBondState(); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); mHandler.sendMessageDelayed(mHandler.obtainMessage(REGISTER_SDP_RECORDS), 3000); // Update mode mEventLoop.onModeChanged(getModeNative()); } + Intent intent = null; + if (res) { + intent = new Intent(BluetoothIntent.ENABLED_ACTION); + } else { + intent = new Intent(BluetoothIntent.DISABLED_ACTION); + } + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + mEnableThread = null; } } diff --git a/core/java/android/server/BluetoothEventLoop.java b/core/java/android/server/BluetoothEventLoop.java index 8b09583..e4ebcca 100644 --- a/core/java/android/server/BluetoothEventLoop.java +++ b/core/java/android/server/BluetoothEventLoop.java @@ -44,6 +44,7 @@ class BluetoothEventLoop { private int mNativeData; private Thread mThread; + private boolean mStarted; private boolean mInterrupted; private HashMap<String, Integer> mPasskeyAgentRequestData; private HashMap<String, IBluetoothDeviceCallback> mGetRemoteServiceChannelCallbacks; @@ -122,14 +123,18 @@ class BluetoothEventLoop { public void run() { try { if (setUpEventLoopNative()) { + mStarted = true; while (!mInterrupted) { waitForAndDispatchEvent(0); sleep(500); } - tearDownEventLoopNative(); } + // tear down even in the error case to clean + // up anything we started to setup + tearDownEventLoopNative(); } catch (InterruptedException e) { } if (DBG) log("Event Loop thread finished"); + mThread = null; } }; if (DBG) log("Starting Event Loop thread"); @@ -152,7 +157,7 @@ class BluetoothEventLoop { } public synchronized boolean isEventLoopRunning() { - return mThread != null; + return mThread != null && mStarted; } /*package*/ void onModeChanged(String bluezMode) { diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java index 8aa49af..17c7a6c 100644 --- a/core/java/android/text/method/ArrowKeyMovementMethod.java +++ b/core/java/android/text/method/ArrowKeyMovementMethod.java @@ -204,7 +204,7 @@ implements MovementMethod MotionEvent event) { boolean handled = Touch.onTouchEvent(widget, buffer, event); - if (widget.isFocused() && !widget.didTouchFocusSelectAll()) { + if (widget.isFocused() && !widget.didTouchFocusSelect()) { if (event.getAction() == MotionEvent.ACTION_UP) { int x = (int) event.getX(); int y = (int) event.getY(); diff --git a/core/java/android/text/method/MetaKeyKeyListener.java b/core/java/android/text/method/MetaKeyKeyListener.java index 39ad976..61ec67f 100644 --- a/core/java/android/text/method/MetaKeyKeyListener.java +++ b/core/java/android/text/method/MetaKeyKeyListener.java @@ -159,13 +159,21 @@ public abstract class MetaKeyKeyListener { /** * Returns true if this object is one that this class would use to - * keep track of meta state in the specified text. + * keep track of any meta state in the specified text. */ public static boolean isMetaTracker(CharSequence text, Object what) { return what == CAP || what == ALT || what == SYM || what == SELECTING; } + /** + * Returns true if this object is one that this class would use to + * keep track of the selecting meta state in the specified text. + */ + public static boolean isSelectingMetaTracker(CharSequence text, Object what) { + return what == SELECTING; + } + private static void adjust(Spannable content, Object what) { int current = content.getSpanFlags(what); diff --git a/core/java/android/view/GestureDetector.java b/core/java/android/view/GestureDetector.java index 88ff3c5..f7ac522 100644 --- a/core/java/android/view/GestureDetector.java +++ b/core/java/android/view/GestureDetector.java @@ -498,7 +498,6 @@ public class GestureDetector { if (mIsDoubleTapping) { // Finally, give the up event of the double-tap handled |= mDoubleTapListener.onDoubleTapEvent(ev); - mIsDoubleTapping = false; } else if (mInLongPress) { mHandler.removeMessages(TAP); mInLongPress = false; @@ -520,6 +519,7 @@ public class GestureDetector { mPreviousUpEvent = MotionEvent.obtain(ev); mVelocityTracker.recycle(); mVelocityTracker = null; + mIsDoubleTapping = false; mHandler.removeMessages(SHOW_PRESS); mHandler.removeMessages(LONG_PRESS); break; @@ -529,6 +529,7 @@ public class GestureDetector { mHandler.removeMessages(TAP); mVelocityTracker.recycle(); mVelocityTracker = null; + mIsDoubleTapping = false; mStillDown = false; if (mInLongPress) { mInLongPress = false; diff --git a/core/java/android/view/HapticFeedbackConstants.java b/core/java/android/view/HapticFeedbackConstants.java index cc3563c..841066c 100644 --- a/core/java/android/view/HapticFeedbackConstants.java +++ b/core/java/android/view/HapticFeedbackConstants.java @@ -26,9 +26,6 @@ public class HapticFeedbackConstants { public static final int LONG_PRESS = 0; - /** @hide pending API council */ - public static final int ZOOM_RING_TICK = 1; - /** * Flag for {@link View#performHapticFeedback(int, int) * View.performHapticFeedback(int, int)}: Ignore the setting in the diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 3e762f5..d78320a 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -3663,8 +3663,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * would make sense to automatically display a soft input window for * it. Subclasses should override this if they implement * {@link #onCreateInputConnection(EditorInfo)} to return true if - * a call on that method would return a non-null InputConnection. The - * default implementation always returns false. + * a call on that method would return a non-null InputConnection, and + * they are really a first-class editor that the user would normally + * start typing on when the go into a window containing your view. + * + * <p>The default implementation always returns false. This does + * <em>not</em> mean that its {@link #onCreateInputConnection(EditorInfo)} + * will not be called or the user can not otherwise perform edits on your + * view; it is just a hint to the system that this is not the primary + * purpose of this view. * * @return Returns true if this view is a text editor, else false. */ @@ -3689,6 +3696,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } /** + * Called by the {@link android.view.inputmethod.InputMethodManager} + * when a view who is not the current + * input connection target is trying to make a call on the manager. The + * default implementation returns false; you can override this to return + * true for certain views if you are performing InputConnection proxying + * to them. + * @param view The View that is making the InputMethodManager call. + * @return Return true to allow the call, false to reject. + */ + public boolean checkInputConnectionProxy(View view) { + return false; + } + + /** * Show the context menu for this view. It is not safe to hold on to the * menu after returning from this method. * @@ -5016,7 +5037,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { (viewFlags & SCROLLBARS_VERTICAL) == SCROLLBARS_VERTICAL ? getVerticalScrollbarWidth() : 0; - scrollBar.setBounds(scrollX + (mPaddingLeft & inside) + getScrollBarPaddingLeft(), top, + scrollBar.setBounds(scrollX + (mPaddingLeft & inside), top, scrollX + width - (mUserPaddingRight & inside) - verticalScrollBarGap, top + size); scrollBar.setParameters( computeHorizontalScrollRange(), @@ -6503,32 +6524,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback { return mBGDrawable; } - private int getScrollBarPaddingLeft() { - // TODO: Deal with RTL languages - return 0; - } - - /* - * Returns the pixels occupied by the vertical scrollbar, if not overlaid - */ - private int getScrollBarPaddingRight() { - // TODO: Deal with RTL languages - if ((mViewFlags & SCROLLBARS_VERTICAL) == 0) { - return 0; - } - return (mViewFlags & SCROLLBARS_INSET_MASK) == 0 ? 0 : getVerticalScrollbarWidth(); - } - - /* - * Returns the pixels occupied by the horizontal scrollbar, if not overlaid - */ - private int getScrollBarPaddingBottom() { - if ((mViewFlags & SCROLLBARS_HORIZONTAL) == 0) { - return 0; - } - return (mViewFlags & SCROLLBARS_INSET_MASK) == 0 ? 0 : getHorizontalScrollbarHeight(); - } - /** * Sets the padding. The view may add on the space required to display * the scrollbars, depending on the style and visibility of the scrollbars. @@ -6552,7 +6547,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback { mUserPaddingRight = right; mUserPaddingBottom = bottom; - if (mPaddingLeft != left + getScrollBarPaddingLeft()) { + final int viewFlags = mViewFlags; + + // Common case is there are no scroll bars. + if ((viewFlags & (SCROLLBARS_VERTICAL|SCROLLBARS_HORIZONTAL)) != 0) { + // TODO: Deal with RTL languages to adjust left padding instead of right. + if ((viewFlags & SCROLLBARS_VERTICAL) != 0) { + right += (viewFlags & SCROLLBARS_INSET_MASK) == 0 + ? 0 : getVerticalScrollbarWidth(); + } + if ((viewFlags & SCROLLBARS_HORIZONTAL) == 0) { + bottom += (viewFlags & SCROLLBARS_INSET_MASK) == 0 + ? 0 : getHorizontalScrollbarHeight(); + } + } + + if (mPaddingLeft != left) { changed = true; mPaddingLeft = left; } @@ -6560,13 +6570,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback { changed = true; mPaddingTop = top; } - if (mPaddingRight != right + getScrollBarPaddingRight()) { + if (mPaddingRight != right) { changed = true; - mPaddingRight = right + getScrollBarPaddingRight(); + mPaddingRight = right; } - if (mPaddingBottom != bottom + getScrollBarPaddingBottom()) { + if (mPaddingBottom != bottom) { changed = true; - mPaddingBottom = bottom + getScrollBarPaddingBottom(); + mPaddingBottom = bottom; } if (changed) { diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index 52b4107..deca910 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -386,7 +386,14 @@ public class BaseInputConnection implements InputConnection { // anyway. return true; } - Selection.setSelection(content, start, end); + if (start == end && MetaKeyKeyListener.getMetaState(content, + MetaKeyKeyListener.META_SELECTING) != 0) { + // If we are in selection mode, then we want to extend the + // selection instead of replacing it. + Selection.extendSelection(content, start); + } else { + Selection.setSelection(content, start, end); + } return true; } diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java index 1c0d42a..b00e565 100644 --- a/core/java/android/view/inputmethod/EditorInfo.java +++ b/core/java/android/view/inputmethod/EditorInfo.java @@ -33,43 +33,49 @@ public class EditorInfo implements InputType, Parcelable { public static final int IME_MASK_ACTION = 0x000000ff; /** - * Bits of {@link #IME_MASK_ACTION}: there is no special action - * associated with this editor. + * Bits of {@link #IME_MASK_ACTION}: no specific action has been + * associated with this editor, let the editor come up with its own if + * it can. */ - public static final int IME_ACTION_NONE = 0x00000000; + public static final int IME_ACTION_UNSPECIFIED = 0x00000000; + + /** + * Bits of {@link #IME_MASK_ACTION}: there is no available action. + */ + public static final int IME_ACTION_NONE = 0x00000001; /** * Bits of {@link #IME_MASK_ACTION}: the action key performs a "go" * operation to take the user to the target of the text they typed. * Typically used, for example, when entering a URL. */ - public static final int IME_ACTION_GO = 0x00000001; + public static final int IME_ACTION_GO = 0x00000002; /** * Bits of {@link #IME_MASK_ACTION}: the action key performs a "search" * operation, taking the user to the results of searching for the text * the have typed (in whatever context is appropriate). */ - public static final int IME_ACTION_SEARCH = 0x00000002; + public static final int IME_ACTION_SEARCH = 0x00000003; /** * Bits of {@link #IME_MASK_ACTION}: the action key performs a "send" * operation, delivering the text to its target. This is typically used * when composing a message. */ - public static final int IME_ACTION_SEND = 0x00000003; + public static final int IME_ACTION_SEND = 0x00000004; /** * Bits of {@link #IME_MASK_ACTION}: the action key performs a "next" * operation, taking the user to the next field that will accept text. */ - public static final int IME_ACTION_NEXT = 0x00000004; + public static final int IME_ACTION_NEXT = 0x00000005; /** * Bits of {@link #IME_MASK_ACTION}: the action key performs a "done" * operation, typically meaning the IME will be closed. */ - public static final int IME_ACTION_DONE = 0x00000005; + public static final int IME_ACTION_DONE = 0x00000006; /** * Flag of {@link #imeOptions}: used in conjunction with @@ -82,21 +88,15 @@ public class EditorInfo implements InputType, Parcelable { public static final int IME_FLAG_NO_ENTER_ACTION = 0x40000000; /** - * Generic non-special type for {@link #imeOptions}. - */ - public static final int IME_NORMAL = 0x00000000; - - /** - * Special code for when the ime option has been undefined. This is not - * used with the EditorInfo structure, but can be used elsewhere. + * Generic unspecified type for {@link #imeOptions}. */ - public static final int IME_UNDEFINED = 0x80000000; + public static final int IME_NULL = 0x00000000; /** * Extended type information for the editor, to help the IME better * integrate with it. */ - public int imeOptions = IME_NORMAL; + public int imeOptions = IME_NULL; /** * A string supplying additional information options that are diff --git a/core/java/android/view/inputmethod/ExtractedText.java b/core/java/android/view/inputmethod/ExtractedText.java index e5d3cae..c2851d6 100644 --- a/core/java/android/view/inputmethod/ExtractedText.java +++ b/core/java/android/view/inputmethod/ExtractedText.java @@ -55,6 +55,11 @@ public class ExtractedText implements Parcelable { public static final int FLAG_SINGLE_LINE = 0x0001; /** + * Bit for {@link #flags}: set if the editor is currently in selection mode. + */ + public static final int FLAG_SELECTING = 0x0002; + + /** * Additional bit flags of information about the edited text. */ public int flags; @@ -72,7 +77,7 @@ public class ExtractedText implements Parcelable { dest.writeInt(partialEndOffset); dest.writeInt(selectionStart); dest.writeInt(selectionEnd); - dest.writeInt(flags); + dest.writeInt(this.flags); } /** diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java index 32cce35..8b6831e 100644 --- a/core/java/android/view/inputmethod/InputConnection.java +++ b/core/java/android/view/inputmethod/InputConnection.java @@ -17,7 +17,6 @@ package android.view.inputmethod; import android.os.Bundle; -import android.text.Spanned; import android.view.KeyCharacterMap; import android.view.KeyEvent; @@ -135,7 +134,7 @@ public interface InputConnection { * @return Returns true on success, false if the input connection is no longer * valid. */ - boolean deleteSurroundingText(int leftLength, int rightLength); + public boolean deleteSurroundingText(int leftLength, int rightLength); /** * Set composing text around the current cursor position with the given text, diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java new file mode 100644 index 0000000..e3d5e62 --- /dev/null +++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2007-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.view.inputmethod; + +import android.os.Bundle; +import android.view.KeyEvent; + +/** + * <p>Wrapper class for proxying calls to another InputConnection. Subclass + * and have fun! + */ +public class InputConnectionWrapper implements InputConnection { + private final InputConnection mTarget; + + public InputConnectionWrapper(InputConnection target) { + mTarget = target; + } + + public CharSequence getTextBeforeCursor(int n, int flags) { + return mTarget.getTextBeforeCursor(n, flags); + } + + public CharSequence getTextAfterCursor(int n, int flags) { + return mTarget.getTextAfterCursor(n, flags); + } + + public int getCursorCapsMode(int reqModes) { + return mTarget.getCursorCapsMode(reqModes); + } + + public ExtractedText getExtractedText(ExtractedTextRequest request, + int flags) { + return mTarget.getExtractedText(request, flags); + } + + public boolean deleteSurroundingText(int leftLength, int rightLength) { + return mTarget.deleteSurroundingText(leftLength, rightLength); + } + + public boolean setComposingText(CharSequence text, int newCursorPosition) { + return mTarget.setComposingText(text, newCursorPosition); + } + + public boolean finishComposingText() { + return mTarget.finishComposingText(); + } + + public boolean commitText(CharSequence text, int newCursorPosition) { + return mTarget.commitText(text, newCursorPosition); + } + + public boolean commitCompletion(CompletionInfo text) { + return mTarget.commitCompletion(text); + } + + public boolean setSelection(int start, int end) { + return mTarget.setSelection(start, end); + } + + public boolean performEditorAction(int editorAction) { + return mTarget.performEditorAction(editorAction); + } + + public boolean performContextMenuAction(int id) { + return mTarget.performContextMenuAction(id); + } + + public boolean beginBatchEdit() { + return mTarget.beginBatchEdit(); + } + + public boolean endBatchEdit() { + return mTarget.endBatchEdit(); + } + + public boolean sendKeyEvent(KeyEvent event) { + return mTarget.sendKeyEvent(event); + } + + public boolean clearMetaKeyStates(int states) { + return mTarget.clearMetaKeyStates(states); + } + + public boolean reportFullscreenMode(boolean enabled) { + return mTarget.reportFullscreenMode(enabled); + } + + public boolean performPrivateCommand(String action, Bundle data) { + return mTarget.performPrivateCommand(action, data); + } +} diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 916ffea..7f2142e 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -529,7 +529,10 @@ public final class InputMethodManager { public boolean isActive(View view) { checkFocus(); synchronized (mH) { - return mServedView == view && mCurrentTextBoxAttribute != null; + return (mServedView == view + || (mServedView != null + && mServedView.checkInputConnectionProxy(view))) + && mCurrentTextBoxAttribute != null; } } @@ -620,7 +623,8 @@ public final class InputMethodManager { public void displayCompletions(View view, CompletionInfo[] completions) { checkFocus(); synchronized (mH) { - if (mServedView != view) { + if (mServedView != view && (mServedView == null + || !mServedView.checkInputConnectionProxy(view))) { return; } @@ -637,7 +641,8 @@ public final class InputMethodManager { public void updateExtractedText(View view, int token, ExtractedText text) { checkFocus(); synchronized (mH) { - if (mServedView != view) { + if (mServedView != view && (mServedView == null + || !mServedView.checkInputConnectionProxy(view))) { return; } @@ -730,7 +735,8 @@ public final class InputMethodManager { ResultReceiver resultReceiver) { checkFocus(); synchronized (mH) { - if (mServedView != view) { + if (mServedView != view && (mServedView == null + || !mServedView.checkInputConnectionProxy(view))) { return false; } @@ -871,7 +877,8 @@ public final class InputMethodManager { public void restartInput(View view) { checkFocus(); synchronized (mH) { - if (mServedView != view) { + if (mServedView != view && (mServedView == null + || !mServedView.checkInputConnectionProxy(view))) { return; } @@ -1032,7 +1039,7 @@ public final class InputMethodManager { if (DEBUG) Log.v(TAG, "focusOut: " + view + " mServedView=" + mServedView + " winFocus=" + view.hasWindowFocus()); - if (mServedView == view) { + if (mServedView != view) { // The following code would auto-hide the IME if we end up // with no more views with focus. This can happen, however, // whenever we go into touch mode, so it ends up hiding @@ -1129,8 +1136,9 @@ public final class InputMethodManager { try { final boolean isTextEditor = focusedView != null && focusedView.onCheckIsTextEditor(); - mService.windowGainedFocus(mClient, focusedView != null, - isTextEditor, softInputMode, first, windowFlags); + mService.windowGainedFocus(mClient, rootView.getWindowToken(), + focusedView != null, isTextEditor, softInputMode, first, + windowFlags); } catch (RemoteException e) { } } @@ -1150,8 +1158,9 @@ public final class InputMethodManager { int candidatesStart, int candidatesEnd) { checkFocus(); synchronized (mH) { - if (mServedView != view || mCurrentTextBoxAttribute == null - || mCurMethod == null) { + if ((mServedView != view && (mServedView == null + || !mServedView.checkInputConnectionProxy(view))) + || mCurrentTextBoxAttribute == null || mCurMethod == null) { return; } @@ -1189,8 +1198,9 @@ public final class InputMethodManager { public void updateCursor(View view, int left, int top, int right, int bottom) { checkFocus(); synchronized (mH) { - if (mServedView != view || mCurrentTextBoxAttribute == null - || mCurMethod == null) { + if ((mServedView != view && (mServedView == null + || !mServedView.checkInputConnectionProxy(view))) + || mCurrentTextBoxAttribute == null || mCurMethod == null) { return; } @@ -1223,7 +1233,8 @@ public final class InputMethodManager { public void sendAppPrivateCommand(View view, String action, Bundle data) { checkFocus(); synchronized (mH) { - if ((view != null && mServedView != view) + if ((mServedView != view && (mServedView == null + || !mServedView.checkInputConnectionProxy(view))) || mCurrentTextBoxAttribute == null || mCurMethod == null) { return; } diff --git a/core/java/android/webkit/TextDialog.java b/core/java/android/webkit/TextDialog.java index 39806dc..efc131f 100644 --- a/core/java/android/webkit/TextDialog.java +++ b/core/java/android/webkit/TextDialog.java @@ -32,8 +32,6 @@ import android.text.Spannable; import android.text.TextPaint; import android.text.TextUtils; import android.text.method.MovementMethod; -import android.text.method.PasswordTransformationMethod; -import android.text.method.TextKeyListener; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.view.KeyCharacterMap; @@ -475,15 +473,10 @@ import java.util.ArrayList; * @param inPassword True if the textfield is a password field. */ /* package */ void setInPassword(boolean inPassword) { - PasswordTransformationMethod method; if (inPassword) { - method = PasswordTransformationMethod.getInstance(); - setInputType(EditorInfo.TYPE_CLASS_TEXT|EditorInfo. + setInputType(EditorInfo.TYPE_CLASS_TEXT | EditorInfo. TYPE_TEXT_VARIATION_PASSWORD); - } else { - method = null; } - setTransformationMethod(method); } /* package */ void setMaxLength(int maxLength) { @@ -545,20 +538,13 @@ import java.util.ArrayList; * removing the password input type. */ public void setSingleLine(boolean single) { - if (mSingle != single) { - TextKeyListener.Capitalize cap; - int inputType = EditorInfo.TYPE_CLASS_TEXT; - if (single) { - cap = TextKeyListener.Capitalize.NONE; - } else { - cap = TextKeyListener.Capitalize.SENTENCES; - inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; - } - setKeyListener(TextKeyListener.getInstance(!single, cap)); - mSingle = single; - setHorizontallyScrolling(single); - setInputType(inputType); + int inputType = EditorInfo.TYPE_CLASS_TEXT; + if (!single) { + inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; } + mSingle = single; + setHorizontallyScrolling(single); + setInputType(inputType); } /** diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 91795a3..dc39b90 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -73,7 +73,6 @@ import android.widget.Scroller; import android.widget.Toast; import android.widget.ZoomButtonsController; import android.widget.ZoomControls; -import android.widget.ZoomRingController; import android.widget.FrameLayout; import android.widget.AdapterView.OnItemClickListener; @@ -239,8 +238,8 @@ public class WebView extends AbsoluteLayout */ VelocityTracker mVelocityTracker; - private static boolean mShowZoomRingTutorial = true; - private static final int ZOOM_RING_TUTORIAL_DURATION = 3000; + private static boolean mShowZoomTutorial = true; + private static final int ZOOM_TUTORIAL_DURATION = 3000; /** * Touch mode @@ -265,11 +264,6 @@ public class WebView extends AbsoluteLayout // Whether to forward the touch events to WebCore private boolean mForwardTouchEvents = false; - // Whether we are in the drag tap mode, which exists starting at the second - // tap's down, through its move, and includes its up. These events should be - // given to the method on the zoom controller. - private boolean mInZoomTapDragMode = false; - // Whether to prevent drag during touch. The initial value depends on // mForwardTouchEvents. If WebCore wants touch events, we assume it will // take control of touch events unless it says no for touch down event. @@ -361,7 +355,7 @@ public class WebView extends AbsoluteLayout private static final int UPDATE_TEXT_ENTRY_ADAPTER = 6; private static final int SWITCH_TO_ENTER = 7; private static final int RESUME_WEBCORE_UPDATE = 8; - private static final int DISMISS_ZOOM_RING_TUTORIAL = 9; + private static final int DISMISS_ZOOM_TUTORIAL = 9; //! arg1=x, arg2=y static final int SCROLL_TO_MSG_ID = 10; @@ -417,11 +411,6 @@ public class WebView extends AbsoluteLayout // width which view is considered to be fully zoomed out static final int ZOOM_OUT_WIDTH = 1024; - private static final float MAX_ZOOM_RING_ANGLE = (float) (Math.PI * 2 / 3); - private static final int ZOOM_RING_STEPS = 4; - private static final float ZOOM_RING_ANGLE_UNIT = MAX_ZOOM_RING_ANGLE - / ZOOM_RING_STEPS; - private static final float DEFAULT_MAX_ZOOM_SCALE = 2; private static final float DEFAULT_MIN_ZOOM_SCALE = (float) 1/3; // scale limit, which can be set through viewport meta tag in the web page @@ -560,146 +549,52 @@ public class WebView extends AbsoluteLayout } private ZoomButtonsController mZoomButtonsController; + private ImageView mZoomOverviewButton; - private ZoomRingController mZoomRingController; - private ImageView mZoomRingOverview; - private Animation mZoomRingOverviewExitAnimation; - - // These keep track of the center point of the zoom ring. They are used to + // These keep track of the center point of the zoom. They are used to // determine the point around which we should zoom. private float mZoomCenterX; private float mZoomCenterY; - private ZoomRingController.OnZoomListener mZoomListener = - new ZoomRingController.OnZoomListener() { - - private float mClockwiseBound; - private float mCounterClockwiseBound; - private float mStartScale; + private ZoomButtonsController.OnZoomListener mZoomListener = + new ZoomButtonsController.OnZoomListener() { public void onCenter(int x, int y) { // Don't translate when the control is invoked, hence we do nothing // in this callback } - public void onBeginPan() { - setZoomOverviewVisible(false); + public void onOverview() { + mZoomButtonsController.setVisible(false); + zoomScrollOut(); if (mLogEvent) { Checkin.updateStats(mContext.getContentResolver(), - Checkin.Stats.Tag.BROWSER_ZOOM_RING_DRAG, 1, 0.0); + Checkin.Stats.Tag.BROWSER_ZOOM_OVERVIEW, 1, 0.0); } } - public boolean onPan(int deltaX, int deltaY) { - return pinScrollBy(deltaX, deltaY, false, 0); - } - - public void onEndPan() { - } - public void onVisibilityChanged(boolean visible) { if (visible) { switchOutDrawHistory(); - if (mMaxZoomScale - 1 > ZOOM_RING_STEPS * 0.01f) { - mClockwiseBound = (float) (2 * Math.PI - MAX_ZOOM_RING_ANGLE); - } else { - mClockwiseBound = (float) (2 * Math.PI); - } - mZoomRingController.setThumbClockwiseBound(mClockwiseBound); - if (1 - mMinZoomScale > ZOOM_RING_STEPS * 0.01f) { - mCounterClockwiseBound = MAX_ZOOM_RING_ANGLE; - } else { - mCounterClockwiseBound = 0; - } - mZoomRingController - .setThumbCounterclockwiseBound(mCounterClockwiseBound); - float angle = 0f; - if (mActualScale > 1 && mClockwiseBound < (float) (2 * Math.PI)) { - angle = -(float) Math.round(ZOOM_RING_STEPS - * (mActualScale - 1) / (mMaxZoomScale - 1)) - / ZOOM_RING_STEPS; - } else if (mActualScale < 1 && mCounterClockwiseBound > 0) { - angle = (float) Math.round(ZOOM_RING_STEPS - * (1 - mActualScale) / (1 - mMinZoomScale)) - / ZOOM_RING_STEPS; - } - mZoomRingController.setThumbAngle(angle * MAX_ZOOM_RING_ANGLE); - - // Don't show a thumb if the user cannot zoom - mZoomRingController.setThumbVisible(mMinZoomScale != mMaxZoomScale); - - // Show the zoom overview tab on the ring - setZoomOverviewVisible(true); - if (mLogEvent) { - Checkin.updateStats(mContext.getContentResolver(), - Checkin.Stats.Tag.BROWSER_ZOOM_RING, 1, 0.0); - } + mZoomButtonsController.setOverviewVisible(true); + updateButtonsEnabled(); } } - public void onBeginDrag() { - mPreviewZoomOnly = true; - mStartScale = mActualScale; - setZoomOverviewVisible(false); - } - - public void onEndDrag() { - mPreviewZoomOnly = false; - if (mLogEvent) { - EventLog.writeEvent(EVENT_LOG_ZOOM_LEVEL_CHANGE, - (int) mStartScale * 100, (int) mActualScale * 100, - System.currentTimeMillis()); - } - setNewZoomScale(mActualScale, true); + private void updateButtonsEnabled() { + mZoomButtonsController.setZoomInEnabled(mActualScale < mMaxZoomScale); + mZoomButtonsController.setZoomOutEnabled(mActualScale > mMinZoomScale); } - public boolean onDragZoom(int deltaZoomLevel, int centerX, - int centerY, float startAngle, float curAngle) { - if (deltaZoomLevel < 0 - && Math.abs(mActualScale - mMinZoomScale) < 0.01f - || deltaZoomLevel > 0 - && Math.abs(mActualScale - mMaxZoomScale) < 0.01f - || deltaZoomLevel == 0) { - return false; - } - mZoomCenterX = (float) centerX; - mZoomCenterY = (float) centerY; - - float scale = 1.0f; - // curAngle is [0, 2 * Math.PI) - if (curAngle < (float) Math.PI) { - if (curAngle >= mCounterClockwiseBound) { - scale = mMinZoomScale; - } else { - scale = 1 - (float) Math.round(curAngle - / ZOOM_RING_ANGLE_UNIT) / ZOOM_RING_STEPS - * (1 - mMinZoomScale); - } - } else { - if (curAngle <= mClockwiseBound) { - scale = mMaxZoomScale; - } else { - scale = 1 + (float) Math.round( - ((float) 2 * Math.PI - curAngle) - / ZOOM_RING_ANGLE_UNIT) / ZOOM_RING_STEPS - * (mMaxZoomScale - 1); - } - } - zoomWithPreview(scale); - return true; - } - - public void onSimpleZoom(boolean zoomIn, int centerX, int centerY) { - mZoomCenterX = (float) centerX; - mZoomCenterY = (float) centerY; - + public void onZoom(boolean zoomIn) { if (zoomIn) { zoomIn(); } else { zoomOut(); } + + updateButtonsEnabled(); } - }; /** @@ -738,34 +633,8 @@ public class WebView extends AbsoluteLayout mFocusData.mX = 0; mFocusData.mY = 0; mScroller = new Scroller(context); - mZoomRingController = new ZoomRingController(context, this); - mZoomRingController.setCallback(mZoomListener); - mZoomRingController.setTrackDrawable( - com.android.internal.R.drawable.zoom_ring_track_absolute); - float density = context.getResources().getDisplayMetrics().density; - mZoomRingController.setPannerAcceleration((int) (160 * density)); - mZoomRingController.setPannerStartAcceleratingDuration((int) (700 * density)); - createZoomRingOverviewTab(); mZoomButtonsController = new ZoomButtonsController(context, this); - mZoomButtonsController.setOverviewVisible(true); - mZoomButtonsController.setCallback(new ZoomButtonsController.OnZoomListener() { - public void onCenter(int x, int y) { - mZoomListener.onCenter(x, y); - } - - public void onOverview() { - mZoomButtonsController.setVisible(false); - zoomScrollOut(); - } - - public void onVisibilityChanged(boolean visible) { - mZoomListener.onVisibilityChanged(visible); - } - - public void onZoom(boolean zoomIn) { - mZoomListener.onSimpleZoom(zoomIn, getWidth() / 2, getHeight() / 2); - } - }); + mZoomButtonsController.setCallback(mZoomListener); } private void init() { @@ -783,67 +652,6 @@ public class WebView extends AbsoluteLayout mDoubleTapSlopSquare = doubleTapslop * doubleTapslop; } - private void createZoomRingOverviewTab() { - Context context = getContext(); - - mZoomRingOverviewExitAnimation = AnimationUtils.loadAnimation(context, - com.android.internal.R.anim.fade_out); - - mZoomRingOverview = new ImageView(context); - mZoomRingOverview.setBackgroundResource( - com.android.internal.R.drawable.zoom_ring_overview_tab); - mZoomRingOverview.setImageResource(com.android.internal.R.drawable.btn_zoom_page); - - FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( - FrameLayout.LayoutParams.WRAP_CONTENT, - FrameLayout.LayoutParams.WRAP_CONTENT, - Gravity.CENTER); - // TODO: magic constant that's based on the zoom ring radius + some offset - lp.topMargin = 200; - mZoomRingOverview.setLayoutParams(lp); - mZoomRingOverview.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - // Hide the zoom ring - mZoomRingController.setVisible(false); - if (mLogEvent) { - Checkin.updateStats(mContext.getContentResolver(), - Checkin.Stats.Tag.BROWSER_ZOOM_OVERVIEW, 1, 0.0); - } - zoomScrollOut(); - }}); - - // Measure the overview View to figure out its height - mZoomRingOverview.forceLayout(); - mZoomRingOverview.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - - ViewGroup container = mZoomRingController.getContainer(); - // Find the index of the zoom ring in the container - View zoomRing = container.findViewById(mZoomRingController.getZoomRingId()); - int zoomRingIndex; - for (zoomRingIndex = container.getChildCount() - 1; zoomRingIndex >= 0; zoomRingIndex--) { - if (container.getChildAt(zoomRingIndex) == zoomRing) break; - } - // Add the overview tab below the zoom ring (so we don't steal its events) - container.addView(mZoomRingOverview, zoomRingIndex); - // Since we use margins to adjust the vertical placement of the tab, the widget - // ends up getting clipped off. Ensure the container is big enough for - // us. - int myHeight = mZoomRingOverview.getMeasuredHeight() + lp.topMargin / 2; - // Multiplied by 2 b/c the zoom ring needs to be centered on the screen - container.setMinimumHeight(myHeight * 2); - } - - private void setZoomOverviewVisible(boolean visible) { - int newVisibility = visible ? View.VISIBLE : View.INVISIBLE; - if (mZoomRingOverview.getVisibility() == newVisibility) return; - - if (!visible) { - mZoomRingOverview.startAnimation(mZoomRingOverviewExitAnimation); - } - mZoomRingOverview.setVisibility(newVisibility); - } - /* package */ boolean onSavePassword(String schemePlusHost, String username, String password, final Message resumeMsg) { boolean rVal = false; @@ -2988,6 +2796,7 @@ public class WebView extends AbsoluteLayout * @param end End of selection to delete. */ /* package */ void deleteSelection(int start, int end) { + mTextGeneration++; mWebViewCore.sendMessage(EventHub.DELETE_SELECTION, start, end, new WebViewCore.FocusData(mFocusData)); } @@ -3452,8 +3261,7 @@ public class WebView extends AbsoluteLayout p.setOnHierarchyChangeListener(null); } - // Clean up the zoom ring - mZoomRingController.setVisible(false); + // Clean up the zoom controller mZoomButtonsController.setVisible(false); } @@ -3565,9 +3373,7 @@ public class WebView extends AbsoluteLayout @Override protected void onSizeChanged(int w, int h, int ow, int oh) { super.onSizeChanged(w, h, ow, oh); - // Center zooming to the center of the screen. This is appropriate for - // this case of zooming, and it also sets us up properly if we remove - // the new zoom ring controller + // Center zooming to the center of the screen. mZoomCenterX = getViewWidth() * .5f; mZoomCenterY = getViewHeight() * .5f; @@ -3641,13 +3447,13 @@ public class WebView extends AbsoluteLayout return false; } - if (mShowZoomRingTutorial && getSettings().supportZoom() - && (mMaxZoomScale - mMinZoomScale) > ZOOM_RING_STEPS * 0.01f) { - ZoomRingController.showZoomTutorialOnce(mContext); - mShowZoomRingTutorial = false; + if (mShowZoomTutorial && getSettings().supportZoom() + && (mMaxZoomScale != mMinZoomScale)) { + ZoomButtonsController.showZoomTutorialOnce(mContext); + mShowZoomTutorial = false; mPrivateHandler.sendMessageDelayed(mPrivateHandler - .obtainMessage(DISMISS_ZOOM_RING_TUTORIAL), - ZOOM_RING_TUTORIAL_DURATION); + .obtainMessage(DISMISS_ZOOM_TUTORIAL), + ZOOM_TUTORIAL_DURATION); } if (LOGV_ENABLED) { @@ -3655,15 +3461,6 @@ public class WebView extends AbsoluteLayout + mTouchMode); } - if ((mZoomRingController.isVisible() || mZoomButtonsController.isVisible()) - && mInZoomTapDragMode) { - if (ev.getAction() == MotionEvent.ACTION_UP) { - // Just released the second tap, no longer in tap-drag mode - mInZoomTapDragMode = false; - } - return mZoomRingController.handleDoubleTapEvent(ev); - } - int action = ev.getAction(); float x = ev.getX(); float y = ev.getY(); @@ -3689,7 +3486,7 @@ public class WebView extends AbsoluteLayout WebViewCore.TouchEventData ted = new WebViewCore.TouchEventData(); ted.mAction = action; ted.mX = viewToContent((int) x + mScrollX); - ted.mY = viewToContent((int) y + mScrollY);; + ted.mY = viewToContent((int) y + mScrollY); mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); mLastSentTouchTime = eventTime; } @@ -3721,7 +3518,7 @@ public class WebView extends AbsoluteLayout nativeMoveSelection(viewToContent(mSelectX) , viewToContent(mSelectY), false); mTouchSelection = mExtendSelection = true; - } else if (!ZoomRingController.useOldZoom(mContext) && + } else if (!ZoomButtonsController.useOldZoom(mContext) && mPrivateHandler.hasMessages(RELEASE_SINGLE_TAP) && (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare)) { // Found doubletap, invoke the zoom controller @@ -3732,13 +3529,11 @@ public class WebView extends AbsoluteLayout mTextEntry.updateCachedTextfield(); } nativeClearFocus(contentX, contentY); - mInZoomTapDragMode = true; if (mLogEvent) { EventLog.writeEvent(EVENT_LOG_DOUBLE_TAP_DURATION, (eventTime - mLastTouchUpTime), eventTime); } - return mZoomRingController.handleDoubleTapEvent(ev) || - mZoomButtonsController.handleDoubleTapEvent(ev); + return mZoomButtonsController.handleDoubleTapEvent(ev); } else { mTouchMode = TOUCH_INIT_MODE; mPreventDrag = mForwardTouchEvents; @@ -3747,9 +3542,8 @@ public class WebView extends AbsoluteLayout (eventTime - mLastTouchUpTime), eventTime); } } - // don't trigger the link if zoom ring is visible - if (mTouchMode == TOUCH_INIT_MODE - && !mZoomRingController.isVisible()) { + // Trigger the link + if (mTouchMode == TOUCH_INIT_MODE) { mPrivateHandler.sendMessageDelayed(mPrivateHandler .obtainMessage(SWITCH_TO_SHORTPRESS), TAP_TIMEOUT); } @@ -3885,7 +3679,7 @@ public class WebView extends AbsoluteLayout mUserScroll = true; } - if (ZoomRingController.useOldZoom(mContext)) { + if (ZoomButtonsController.useOldZoom(mContext)) { boolean showPlusMinus = mMinZoomScale < mMaxZoomScale; boolean showMagnify = canZoomScrollOut(); if (mZoomControls != null && (showPlusMinus || showMagnify)) { @@ -3909,15 +3703,6 @@ public class WebView extends AbsoluteLayout mLastTouchUpTime = eventTime; switch (mTouchMode) { case TOUCH_INIT_MODE: // tap - if (mZoomRingController.isVisible()) { - // don't trigger the link if zoom ring is visible, - // but still allow the double tap - mPrivateHandler.sendMessageDelayed(mPrivateHandler - .obtainMessage(RELEASE_SINGLE_TAP, - new Boolean(false)), - DOUBLE_TAP_TIMEOUT); - break; - } mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); if (getSettings().supportZoom()) { mPrivateHandler.sendMessageDelayed(mPrivateHandler @@ -4432,14 +4217,6 @@ public class WebView extends AbsoluteLayout } /** - * @hide pending API council? Assuming we make ZoomRingController itself - * public, which I think we will. - */ - public ZoomRingController getZoomRingController() { - return mZoomRingController; - } - - /** * Perform zoom in in the webview * @return TRUE if zoom in succeeds. FALSE if no zoom changes. */ @@ -4482,9 +4259,6 @@ public class WebView extends AbsoluteLayout }); zoomControls.setOnZoomMagnifyClickListener(new OnClickListener() { public void onClick(View v) { - // Hide the zoom ring - mZoomRingController.setVisible(false); - mPrivateHandler.removeCallbacks(mZoomControlRunnable); mPrivateHandler.postDelayed(mZoomControlRunnable, ZOOM_CONTROLS_TIMEOUT); @@ -4693,6 +4467,7 @@ public class WebView extends AbsoluteLayout arg.put("replace", replace); arg.put("start", new Integer(newStart)); arg.put("end", new Integer(newEnd)); + mTextGeneration++; mWebViewCore.sendMessage(EventHub.REPLACE_TEXT, oldStart, oldEnd, arg); } @@ -5006,8 +4781,8 @@ public class WebView extends AbsoluteLayout } break; - case DISMISS_ZOOM_RING_TUTORIAL: - mZoomRingController.finishZoomTutorial(); + case DISMISS_ZOOM_TUTORIAL: + mZoomButtonsController.finishZoomTutorial(); break; default: diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index d72570a..f517dc9 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -41,6 +41,10 @@ import android.view.ViewConfiguration; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputConnectionWrapper; import android.view.inputmethod.InputMethodManager; import android.view.ContextMenu.ContextMenuInfo; @@ -899,6 +903,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } private boolean acceptFilter() { + if (!mTextFilterEnabled || !(getAdapter() instanceof Filterable) || + ((Filterable) getAdapter()).getFilter() == null) { + return false; + } final Context context = mContext; final InputMethodManager inputManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); @@ -1107,7 +1115,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te // filter popup needs to follow the widget. if (mFiltered && changed && getWindowVisibility() == View.VISIBLE && mPopup != null && mPopup.isShowing()) { - positionPopup(true); + positionPopup(); } return changed; @@ -2767,20 +2775,20 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te // Make sure we have a window before showing the popup if (getWindowVisibility() == View.VISIBLE) { createTextFilter(true); - positionPopup(false); + positionPopup(); // Make sure we get focus if we are showing the popup checkFocus(); } } - private void positionPopup(boolean update) { + private void positionPopup() { int screenHeight = getResources().getDisplayMetrics().heightPixels; final int[] xy = new int[2]; getLocationOnScreen(xy); // TODO: The 20 below should come from the theme and be expressed in dip // TODO: And the gravity should be defined in the theme as well final int bottomGap = screenHeight - xy[1] - getHeight() + (int) (mDensityScale * 20); - if (!update) { + if (!mPopup.isShowing()) { mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, xy[0], bottomGap); } else { @@ -2849,8 +2857,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * @return True if the text filter handled the event, false otherwise. */ boolean sendToTextFilter(int keyCode, int count, KeyEvent event) { - if (!mTextFilterEnabled || !(getAdapter() instanceof Filterable) || - ((Filterable) getAdapter()).getFilter() == null) { + if (!acceptFilter()) { return false; } @@ -2879,7 +2886,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te break; } - if (okToSend && acceptFilter()) { + if (okToSend) { createTextFilter(true); KeyEvent forwardEvent = event; @@ -2906,6 +2913,27 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } /** + * Return an InputConnection for editing of the filter text. + */ + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + // XXX we need to have the text filter created, so we can get an + // InputConnection to proxy to. Unfortunately this means we pretty + // much need to make it as soon as a list view gets focus. + createTextFilter(false); + return mTextFilter.onCreateInputConnection(outAttrs); + } + + /** + * For filtering we proxy an input connection to an internal text editor, + * and this allows the proxying to happen. + */ + @Override + public boolean checkInputConnectionProxy(View view) { + return view == mTextFilter; + } + + /** * Creates the window for the text filter and populates it with an EditText field; * * @param animateEntrance true if the window should appear with an animation @@ -2918,6 +2946,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te c.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mTextFilter = (EditText) layoutInflater.inflate( com.android.internal.R.layout.typing_filter, null); + // For some reason setting this as the "real" input type changes + // the text view in some way that it doesn't work, and I don't + // want to figure out why this is. + mTextFilter.setRawInputType(EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_VARIATION_FILTER); mTextFilter.addTextChangedListener(this); p.setFocusable(false); p.setTouchable(false); diff --git a/core/java/android/widget/FastScroller.java b/core/java/android/widget/FastScroller.java index 0a552e8..3368477 100644 --- a/core/java/android/widget/FastScroller.java +++ b/core/java/android/widget/FastScroller.java @@ -26,7 +26,6 @@ import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.SystemClock; -import android.util.TypedValue; import android.view.MotionEvent; /** @@ -313,12 +312,17 @@ class FastScroller { // Non-existent letter while (section > 0) { section--; - prevIndex = mSectionIndexer.getPositionForSection(section); - if (prevIndex != index) { - prevSection = section; - sectionIndex = section; - break; - } + prevIndex = mSectionIndexer.getPositionForSection(section); + if (prevIndex != index) { + prevSection = section; + sectionIndex = section; + break; + } else if (section == 0) { + // When section reaches 0 here, sectionIndex must follow it. + // Assuming mSectionIndexer.getPositionForSection(0) == 0. + sectionIndex = 0; + break; + } } } // Find the next index, in case the assumed next index is not diff --git a/core/java/android/widget/Filter.java b/core/java/android/widget/Filter.java index 1d0fd5e..7e55c78 100644 --- a/core/java/android/widget/Filter.java +++ b/core/java/android/widget/Filter.java @@ -42,10 +42,12 @@ public abstract class Filter { private static final String THREAD_NAME = "Filter"; private static final int FILTER_TOKEN = 0xD0D0F00D; private static final int FINISH_TOKEN = 0xDEADBEEF; - + private Handler mThreadHandler; private Handler mResultHandler; + private final Object mLock = new Object(); + /** * <p>Creates a new asynchronous filter.</p> */ @@ -81,8 +83,7 @@ public abstract class Filter { * @see #publishResults(CharSequence, android.widget.Filter.FilterResults) */ public final void filter(CharSequence constraint, FilterListener listener) { - synchronized (this) { - + synchronized (mLock) { if (mThreadHandler == null) { HandlerThread thread = new HandlerThread(THREAD_NAME); thread.start(); @@ -221,7 +222,7 @@ public abstract class Filter { message.sendToTarget(); } - synchronized (this) { + synchronized (mLock) { if (mThreadHandler != null) { Message finishMessage = mThreadHandler.obtainMessage(FINISH_TOKEN); mThreadHandler.sendMessageDelayed(finishMessage, 3000); @@ -229,7 +230,7 @@ public abstract class Filter { } break; case FINISH_TOKEN: - synchronized (this) { + synchronized (mLock) { if (mThreadHandler != null) { mThreadHandler.getLooper().quit(); mThreadHandler = null; diff --git a/core/java/android/widget/MediaController.java b/core/java/android/widget/MediaController.java index f2cec92..227fb95 100644 --- a/core/java/android/widget/MediaController.java +++ b/core/java/android/widget/MediaController.java @@ -451,6 +451,7 @@ public class MediaController extends FrameLayout { public void onProgressChanged(SeekBar bar, int progress, boolean fromtouch) { if (fromtouch) { mDragging = true; + duration = mPlayer.getDuration(); long newposition = (duration * progress) / 1000L; mPlayer.seekTo( (int) newposition); if (mCurrentTime != null) diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 3f4912f..8271a9a 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -43,6 +43,7 @@ import android.text.GetChars; import android.text.GraphicsOperations; import android.text.ClipboardManager; import android.text.InputFilter; +import android.text.InputType; import android.text.Layout; import android.text.ParcelableSpan; import android.text.Selection; @@ -235,7 +236,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private CharWrapper mCharWrapper = null; private boolean mSelectionMoved = false; - private boolean mTouchFocusSelectedAll = false; + private boolean mTouchFocusSelected = false; private Marquee mMarquee; private boolean mRestartMarquee; @@ -243,7 +244,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private int mMarqueeRepeatLimit = 3; class InputContentType { - int imeOptions = EditorInfo.IME_UNDEFINED; + int imeOptions = EditorInfo.IME_NULL; String privateImeOptions; CharSequence imeActionLabel; int imeActionId; @@ -261,6 +262,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final ExtractedText mTmpExtracted = new ExtractedText(); int mBatchEditNesting; boolean mCursorChanged; + boolean mSelectionModeChanged; boolean mContentChanged; int mChangedStart, mChangedEnd, mChangedDelta; } @@ -287,8 +289,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @param v The view that was clicked. * @param actionId Identifier of the action. This will be either the - * identifier you supplied, or {@link EditorInfo#IME_UNDEFINED - * EditorInfo.IME_UNDEFINED} if being called due to the enter key + * identifier you supplied, or {@link EditorInfo#IME_NULL + * EditorInfo.IME_NULL} if being called due to the enter key * being pressed. * @param event If triggered by an enter key, this is the event; * otherwise, this is null. @@ -1492,10 +1494,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public void setPadding(int left, int top, int right, int bottom) { - if (left != getPaddingLeft() || - right != getPaddingRight() || - top != getPaddingTop() || - bottom != getPaddingBottom()) { + if (left != mPaddingLeft || + right != mPaddingRight || + top != mPaddingTop || + bottom != mPaddingBottom) { nullLayouts(); } @@ -2951,7 +2953,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public int getImeOptions() { return mInputContentType != null - ? mInputContentType.imeOptions : EditorInfo.IME_UNDEFINED; + ? mInputContentType.imeOptions : EditorInfo.IME_NULL; } /** @@ -3013,9 +3015,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * Called when an attached input method calls * {@link InputConnection#performEditorAction(int) * InputConnection.performEditorAction()} - * for this text view. The default implementation will call your click - * listener supplied to {@link #setOnEditorActionListener}, - * or generate an enter key down/up pair to invoke the action if not. + * for this text view. The default implementation will call your action + * listener supplied to {@link #setOnEditorActionListener}, or perform + * a standard operation for {@link EditorInfo#IME_ACTION_NEXT + * EditorInfo.IME_ACTION_NEXT} or {@link EditorInfo#IME_ACTION_DONE + * EditorInfo.IME_ACTION_DONE}. + * + * <p>For backwards compatibility, if no IME options have been set and the + * text view would not normally advance focus on enter, then + * the NEXT and DONE actions received here will be turned into an enter + * key down/up pair to go through the normal key handling. * * @param actionCode The code of the action being performed. * @@ -3052,6 +3061,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (imm != null) { imm.hideSoftInputFromWindow(getWindowToken(), 0); } + return; } } @@ -3844,7 +3854,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (imm != null) { if (imm.isActive(this)) { boolean reported = false; - if (ims.mContentChanged) { + if (ims.mContentChanged || ims.mSelectionModeChanged) { // We are in extract mode and the content has changed // in some way... just report complete new text to the // input method. @@ -4197,7 +4207,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener && mInputContentType.enterDown) { mInputContentType.enterDown = false; if (mInputContentType.onEditorActionListener.onEditorAction( - this, EditorInfo.IME_UNDEFINED, event)) { + this, EditorInfo.IME_NULL, event)) { return true; } } @@ -4272,17 +4282,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener outAttrs.actionId = mInputContentType.imeActionId; outAttrs.extras = mInputContentType.extras; } else { - outAttrs.imeOptions = EditorInfo.IME_UNDEFINED; + outAttrs.imeOptions = EditorInfo.IME_NULL; } - if (outAttrs.imeOptions == EditorInfo.IME_UNDEFINED) { + if ((outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION) + == EditorInfo.IME_ACTION_UNSPECIFIED) { if (focusSearch(FOCUS_DOWN) != null) { // An action has not been set, but the enter key will move to // the next focus, so set the action to that. - outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT; + outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT; } else { // An action has not been set, and there is no focus to move // to, so let's just supply a "done" action. - outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE; + outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE; } if (!shouldAdvanceFocusOnEnter()) { outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION; @@ -4307,51 +4318,64 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public boolean extractText(ExtractedTextRequest request, ExtractedText outText) { - return extractTextInternal(request, -1, -1, -1, outText); + return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN, + EXTRACT_UNKNOWN, outText); } + static final int EXTRACT_NOTHING = -2; + static final int EXTRACT_UNKNOWN = -1; + boolean extractTextInternal(ExtractedTextRequest request, int partialStartOffset, int partialEndOffset, int delta, ExtractedText outText) { final CharSequence content = mText; if (content != null) { - final int N = content.length(); - if (partialStartOffset < 0) { - outText.partialStartOffset = outText.partialEndOffset = -1; - partialStartOffset = 0; - partialEndOffset = N; - } else { - // Adjust offsets to ensure we contain full spans. - if (content instanceof Spanned) { - Spanned spanned = (Spanned)content; - Object[] spans = spanned.getSpans(partialStartOffset, - partialEndOffset, ParcelableSpan.class); - int i = spans.length; - while (i > 0) { - i--; - int j = spanned.getSpanStart(spans[i]); - if (j < partialStartOffset) partialStartOffset = j; - j = spanned.getSpanEnd(spans[i]); - if (j > partialEndOffset) partialEndOffset = j; + if (partialStartOffset != EXTRACT_NOTHING) { + final int N = content.length(); + if (partialStartOffset < 0) { + outText.partialStartOffset = outText.partialEndOffset = -1; + partialStartOffset = 0; + partialEndOffset = N; + } else { + // Adjust offsets to ensure we contain full spans. + if (content instanceof Spanned) { + Spanned spanned = (Spanned)content; + Object[] spans = spanned.getSpans(partialStartOffset, + partialEndOffset, ParcelableSpan.class); + int i = spans.length; + while (i > 0) { + i--; + int j = spanned.getSpanStart(spans[i]); + if (j < partialStartOffset) partialStartOffset = j; + j = spanned.getSpanEnd(spans[i]); + if (j > partialEndOffset) partialEndOffset = j; + } + } + outText.partialStartOffset = partialStartOffset; + outText.partialEndOffset = partialEndOffset; + // Now use the delta to determine the actual amount of text + // we need. + partialEndOffset += delta; + if (partialEndOffset > N) { + partialEndOffset = N; + } else if (partialEndOffset < 0) { + partialEndOffset = 0; } } - outText.partialStartOffset = partialStartOffset; - outText.partialEndOffset = partialEndOffset; - // Now use the delta to determine the actual amount of text - // we need. - partialEndOffset += delta; - if (partialEndOffset > N) { - partialEndOffset = N; - } else if (partialEndOffset < 0) { - partialEndOffset = 0; + if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) { + outText.text = content.subSequence(partialStartOffset, + partialEndOffset); + } else { + outText.text = TextUtils.substring(content, partialStartOffset, + partialEndOffset); } } - if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) { - outText.text = content.subSequence(partialStartOffset, - partialEndOffset); - } else { - outText.text = TextUtils.substring(content, partialStartOffset, - partialEndOffset); + outText.flags = 0; + if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) { + outText.flags |= ExtractedText.FLAG_SELECTING; + } + if (mSingleLine) { + outText.flags |= ExtractedText.FLAG_SINGLE_LINE; } outText.startOffset = 0; outText.selectionStart = Selection.getSelectionStart(content); @@ -4363,8 +4387,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean reportExtractedText() { final InputMethodState ims = mInputMethodState; - if (ims != null && ims.mContentChanged) { + final boolean contentChanged = ims.mContentChanged; + if (ims != null && (contentChanged || ims.mSelectionModeChanged)) { ims.mContentChanged = false; + ims.mSelectionModeChanged = false; final ExtractedTextRequest req = mInputMethodState.mExtracting; if (req != null) { InputMethodManager imm = InputMethodManager.peekInstance(); @@ -4372,6 +4398,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (DEBUG_EXTRACT) Log.v(TAG, "Retrieving extracted start=" + ims.mChangedStart + " end=" + ims.mChangedEnd + " delta=" + ims.mChangedDelta); + if (ims.mChangedStart < 0 && !contentChanged) { + ims.mChangedStart = EXTRACT_NOTHING; + } if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd, ims.mChangedDelta, ims.mTmpExtracted)) { if (DEBUG_EXTRACT) Log.v(TAG, "Reporting extracted start=" @@ -4408,19 +4437,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public void setExtractedText(ExtractedText text) { Editable content = getEditableText(); - if (content == null) { - setText(text.text, TextView.BufferType.EDITABLE); - } else if (text.partialStartOffset < 0) { - removeParcelableSpans(content, 0, content.length()); - content.replace(0, content.length(), text.text); - } else { - final int N = content.length(); - int start = text.partialStartOffset; - if (start > N) start = N; - int end = text.partialEndOffset; - if (end > N) end = N; - removeParcelableSpans(content, start, end); - content.replace(start, end, text.text); + if (text.text != null) { + if (content == null) { + setText(text.text, TextView.BufferType.EDITABLE); + } else if (text.partialStartOffset < 0) { + removeParcelableSpans(content, 0, content.length()); + content.replace(0, content.length(), text.text); + } else { + final int N = content.length(); + int start = text.partialStartOffset; + if (start > N) start = N; + int end = text.partialEndOffset; + if (end > N) end = N; + removeParcelableSpans(content, start, end); + content.replace(start, end, text.text); + } } // Now set the selection position... make sure it is in range, to @@ -4436,6 +4467,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (end < 0) end = 0; else if (end > N) end = N; Selection.setSelection(sp, start, end); + + // Finally, update the selection mode. + if ((text.flags&ExtractedText.FLAG_SELECTING) != 0) { + MetaKeyKeyListener.startSelecting(this, sp); + } else { + MetaKeyKeyListener.stopSelecting(this, sp); + } } /** @@ -4473,8 +4511,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener ims.mChangedStart = 0; ims.mChangedEnd = mText.length(); } else { - ims.mChangedStart = -1; - ims.mChangedEnd = -1; + ims.mChangedStart = EXTRACT_UNKNOWN; + ims.mChangedEnd = EXTRACT_UNKNOWN; ims.mContentChanged = false; } onBeginBatchEdit(); @@ -4503,7 +4541,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener void finishBatchEdit(final InputMethodState ims) { onEndBatchEdit(); - if (ims.mContentChanged) { + if (ims.mContentChanged || ims.mSelectionModeChanged) { updateAfterEdit(); reportExtractedText(); } else if (ims.mCursorChanged) { @@ -5888,6 +5926,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (MetaKeyKeyListener.isMetaTracker(buf, what)) { mHighlightPathBogus = true; + if (ims != null && MetaKeyKeyListener.isSelectingMetaTracker(buf, what)) { + ims.mSelectionModeChanged = true; + } if (Selection.getSelectionStart(buf) >= 0) { if (ims == null || ims.mBatchEditNesting == 0) { @@ -6026,7 +6067,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mSelectAllOnFocus) { Selection.setSelection((Spannable) mText, 0, mText.length()); - mTouchFocusSelectedAll = true; } if (selMoved && selStart >= 0 && selEnd >= 0) { @@ -6042,6 +6082,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Selection.setSelection((Spannable) mText, selStart, selEnd); } + mTouchFocusSelected = true; } mFrozenWithFocus = false; @@ -6149,7 +6190,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (action == MotionEvent.ACTION_DOWN) { // Reset this state; it will be re-set if super.onTouchEvent // causes focus to move to the view. - mTouchFocusSelectedAll = false; + mTouchFocusSelected = false; } final boolean superResult = super.onTouchEvent(event); @@ -6218,10 +6259,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * Returns true, only while processing a touch gesture, if the initial * touch down event caused focus to move to the text view and as a result - * it selected all of its text. + * its selection changed. Only valid while processing the touch gesture + * of interest. */ - public boolean didTouchFocusSelectAll() { - return mTouchFocusSelectedAll; + public boolean didTouchFocusSelect() { + return mTouchFocusSelected; } @Override @@ -6351,6 +6393,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return super.computeVerticalScrollRange(); } + @Override + protected int computeVerticalScrollExtent() { + return getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom(); + } + public enum BufferType { NORMAL, SPANNABLE, EDITABLE, } @@ -6497,6 +6544,26 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * or null if there is no cursor or no word at the cursor. */ private String getWordForDictionary() { + /* + * Quick return if the input type is one where adding words + * to the dictionary doesn't make any sense. + */ + int klass = mInputType & InputType.TYPE_MASK_CLASS; + if (klass == InputType.TYPE_CLASS_NUMBER || + klass == InputType.TYPE_CLASS_PHONE || + klass == InputType.TYPE_CLASS_DATETIME) { + return null; + } + + int variation = mInputType & InputType.TYPE_MASK_VARIATION; + if (variation == InputType.TYPE_TEXT_VARIATION_URI || + variation == InputType.TYPE_TEXT_VARIATION_PASSWORD || + variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD || + variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || + variation == InputType.TYPE_TEXT_VARIATION_FILTER) { + return null; + } + int end = getSelectionEnd(); if (end < 0) { diff --git a/core/java/android/widget/VideoView.java b/core/java/android/widget/VideoView.java index 1227afd..ec6f88b 100644 --- a/core/java/android/widget/VideoView.java +++ b/core/java/android/widget/VideoView.java @@ -46,8 +46,10 @@ import java.io.IOException; * such as scaling and tinting. */ public class VideoView extends SurfaceView implements MediaPlayerControl { + private String TAG = "VideoView"; // settable by the client private Uri mUri; + private int mDuration; // All the stuff we need for playing and showing a video private SurfaceHolder mSurfaceHolder = null; @@ -184,6 +186,8 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { mMediaPlayer.setOnPreparedListener(mPreparedListener); mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener); mIsPrepared = false; + Log.v(TAG, "reset duration to -1 in openVideo"); + mDuration = -1; mMediaPlayer.setOnCompletionListener(mCompletionListener); mMediaPlayer.setOnErrorListener(mErrorListener); mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener); @@ -195,10 +199,10 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { mMediaPlayer.prepareAsync(); attachMediaController(); } catch (IOException ex) { - Log.w("VideoView", "Unable to open content: " + mUri, ex); + Log.w(TAG, "Unable to open content: " + mUri, ex); return; } catch (IllegalArgumentException ex) { - Log.w("VideoView", "Unable to open content: " + mUri, ex); + Log.w(TAG, "Unable to open content: " + mUri, ex); return; } } @@ -299,7 +303,7 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { private MediaPlayer.OnErrorListener mErrorListener = new MediaPlayer.OnErrorListener() { public boolean onError(MediaPlayer mp, int a, int b) { - Log.d("VideoView", "Error: " + a + "," + b); + Log.d(TAG, "Error: " + a + "," + b); if (mMediaController != null) { mMediaController.hide(); } @@ -497,9 +501,14 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { public int getDuration() { if (mMediaPlayer != null && mIsPrepared) { - return mMediaPlayer.getDuration(); + if (mDuration > 0) { + return mDuration; + } + mDuration = mMediaPlayer.getDuration(); + return mDuration; } - return -1; + mDuration = -1; + return mDuration; } public int getCurrentPosition() { diff --git a/core/java/android/widget/ZoomButtonsController.java b/core/java/android/widget/ZoomButtonsController.java index ec45e23..1ba2dce 100644 --- a/core/java/android/widget/ZoomButtonsController.java +++ b/core/java/android/widget/ZoomButtonsController.java @@ -16,14 +16,19 @@ 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.view.Gravity; import android.view.KeyEvent; @@ -31,6 +36,7 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; +import android.view.Window; import android.view.WindowManager; import android.view.View.OnClickListener; import android.view.WindowManager.LayoutParams; @@ -46,7 +52,7 @@ import android.view.WindowManager.LayoutParams; * * @hide */ -public class ZoomButtonsController implements View.OnTouchListener { +public class ZoomButtonsController implements View.OnTouchListener, View.OnKeyListener { private static final String TAG = "ZoomButtonsController"; @@ -60,13 +66,13 @@ public class ZoomButtonsController implements View.OnTouchListener { private WindowManager mWindowManager; /** - * The view that is being zoomed by this zoom ring. + * The view that is being zoomed by this zoom controller. */ private View mOwnerView; /** * The bounds of the owner view in global coordinates. This is recalculated - * each time the zoom ring is shown. + * each time the zoom controller is shown. */ private Rect mOwnerViewBounds = new Rect(); @@ -89,11 +95,13 @@ public class ZoomButtonsController implements View.OnTouchListener { */ private int[] mTouchTargetLocationInWindow = new int[2]; /** - * If the zoom ring is dismissed but the user is still in a touch + * If the zoom controller 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; + + private boolean mIsSecondTapDown; private boolean mIsVisible; @@ -122,10 +130,16 @@ public class ZoomButtonsController implements View.OnTouchListener { } }; + /** + * The setting name that tracks whether we've shown the zoom tutorial. + */ + private static final String SETTING_NAME_SHOWN_TUTORIAL = "shown_zoom_tutorial"; + private static Dialog sTutorialDialog; + /** 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; + /** Used to delay the zoom controller dismissal. */ + private static final int MSG_DISMISS_ZOOM_CONTROLS = 3; /** * If setVisible(true) is called and the owner view's window token is null, * we delay the setVisible(true) call until it is not null. @@ -140,7 +154,7 @@ public class ZoomButtonsController implements View.OnTouchListener { onPostConfigurationChanged(); break; - case MSG_DISMISS_ZOOM_RING: + case MSG_DISMISS_ZOOM_CONTROLS: setVisible(false); break; @@ -148,7 +162,7 @@ public class ZoomButtonsController implements View.OnTouchListener { if (mOwnerView.getWindowToken() == null) { // Doh, it is still null, throw an exception throw new IllegalArgumentException( - "Cannot make the zoom ring visible if the owner view is " + + "Cannot make the zoom controller visible if the owner view is " + "not attached to a window."); } setVisible(true); @@ -165,11 +179,24 @@ public class ZoomButtonsController implements View.OnTouchListener { mContainer = createContainer(); } - + + public void setZoomInEnabled(boolean enabled) { + mControls.setIsZoomInEnabled(enabled); + } + + public void setZoomOutEnabled(boolean enabled) { + mControls.setIsZoomOutEnabled(enabled); + } + + public void setZoomSpeed(long speed) { + mControls.setZoomSpeed(speed); + } + private FrameLayout createContainer() { LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); lp.gravity = Gravity.BOTTOM | Gravity.CENTER; lp.flags = LayoutParams.FLAG_NOT_TOUCHABLE | + LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_LAYOUT_NO_LIMITS; lp.height = LayoutParams.WRAP_CONTENT; lp.width = LayoutParams.FILL_PARENT; @@ -182,6 +209,7 @@ public class ZoomButtonsController implements View.OnTouchListener { FrameLayout container = new FrameLayout(mContext); container.setLayoutParams(lp); container.setMeasureAllChildren(true); + container.setOnKeyListener(this); LayoutInflater inflater = (LayoutInflater) mContext .getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -326,13 +354,13 @@ public class ZoomButtonsController implements View.OnTouchListener { return mContainer; } - public int getZoomRingId() { + public int getZoomControlsId() { return mControls.getId(); } private void dismissControlsDelayed(int delay) { - mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); - mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_RING, delay); + mHandler.removeMessages(MSG_DISMISS_ZOOM_CONTROLS); + mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_CONTROLS, delay); } /** @@ -351,8 +379,21 @@ public class ZoomButtonsController implements View.OnTouchListener { int x = (int) event.getX(); int y = (int) event.getY(); + /* + * This class will consume all events in the second tap (down, + * move(s), up). But, the owner already got the second tap's down, + * so cancel that. Do this before setVisible, since that call + * will set us as a touch listener. + */ + MotionEvent cancelEvent = MotionEvent.obtain(event.getDownTime(), + SystemClock.elapsedRealtime(), + MotionEvent.ACTION_CANCEL, 0, 0, 0); + mOwnerView.dispatchTouchEvent(cancelEvent); + cancelEvent.recycle(); + setVisible(true); centerPoint(x, y); + mIsSecondTapDown = true; } return true; @@ -373,11 +414,23 @@ public class ZoomButtonsController implements View.OnTouchListener { } } + public boolean onKey(View v, int keyCode, KeyEvent event) { + dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT); + return false; + } + public boolean onTouch(View v, MotionEvent event) { int action = event.getAction(); + // Consume all events during the second-tap interaction (down, move, up/cancel) + boolean consumeEvent = mIsSecondTapDown; + if ((action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL)) { + // The second tap can no longer be down + mIsSecondTapDown = false; + } + if (mReleaseTouchListenerOnUp) { - // The ring was dismissed but we need to throw away all events until the up + // The controls were 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); @@ -417,10 +470,10 @@ public class ZoomButtonsController implements View.OnTouchListener { mOwnerViewBounds.top - targetViewRawY); boolean retValue = targetView.dispatchTouchEvent(containerEvent); containerEvent.recycle(); - return retValue; + return retValue || consumeEvent; } else { - return false; + return consumeEvent; } } @@ -465,10 +518,102 @@ public class ZoomButtonsController implements View.OnTouchListener { refreshPositioningVariables(); } + /* + * This is static so Activities can call this instead of the Views + * (Activities usually do not have a reference to the ZoomButtonsController + * instance.) + */ + /** + * Shows a "tutorial" (some text) to the user teaching her the new zoom + * invocation method. Must call from the main thread. + * <p> + * 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_TUTORIAL, 0) == 1) { + return; + } + + SharedPreferences sp = context.getSharedPreferences("_zoom", Context.MODE_PRIVATE); + if (sp.getInt(SETTING_NAME_SHOWN_TUTORIAL, 0) == 1) { + return; + } + + if (sTutorialDialog != null && sTutorialDialog.isShowing()) { + sTutorialDialog.dismiss(); + } + + LayoutInflater layoutInflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + TextView textView = (TextView) layoutInflater.inflate( + com.android.internal.R.layout.alert_dialog_simple_text, null) + .findViewById(android.R.id.text1); + textView.setText(com.android.internal.R.string.tutorial_double_tap_to_zoom_message_short); + + sTutorialDialog = new AlertDialog.Builder(context) + .setView(textView) + .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(); + } + + /** @hide Should only be used by Android platform apps */ + public static void finishZoomTutorial(Context context, boolean userNotified) { + if (sTutorialDialog == null) return; + + sTutorialDialog.dismiss(); + sTutorialDialog = null; + + // Record that they have seen the tutorial + if (userNotified) { + try { + Settings.System.putInt(context.getContentResolver(), SETTING_NAME_SHOWN_TUTORIAL, + 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 = context.getSharedPreferences("_zoom", Context.MODE_PRIVATE); + sp.edit().putInt(SETTING_NAME_SHOWN_TUTORIAL, 1).commit(); + } + } + } + + /** @hide Should only be used by Android platform apps */ + public void finishZoomTutorial() { + finishZoomTutorial(mContext, true); + } + + // Temporary methods for different zoom types + static int getZoomType(Context context) { + return Settings.System.getInt(context.getContentResolver(), "zoom", 1); + } + + public static boolean useOldZoom(Context context) { + return getZoomType(context) == 0; + } + public static boolean useThisZoom(Context context) { - return ZoomRingController.getZoomType(context) == 2; + return getZoomType(context) == 2; } - + public interface OnZoomListener { void onCenter(int x, int y); void onVisibilityChanged(boolean visible); diff --git a/core/java/android/widget/ZoomControls.java b/core/java/android/widget/ZoomControls.java index fdc05b6..e978db8 100644 --- a/core/java/android/widget/ZoomControls.java +++ b/core/java/android/widget/ZoomControls.java @@ -81,7 +81,7 @@ public class ZoomControls extends LinearLayout { } public void show() { - if (ZoomRingController.useOldZoom(mContext)) { + if (ZoomButtonsController.useOldZoom(mContext)) { fade(View.VISIBLE, 0.0f, 1.0f); } } diff --git a/core/java/android/widget/ZoomRing.java b/core/java/android/widget/ZoomRing.java deleted file mode 100644 index 83a1225..0000000 --- a/core/java/android/widget/ZoomRing.java +++ /dev/null @@ -1,1274 +0,0 @@ -/* - * Copyright (C) 2009 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 com.android.internal.R; - -import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.RotateDrawable; -import android.os.Handler; -import android.os.Message; -import android.os.SystemClock; -import android.util.AttributeSet; -import android.util.Log; -import android.view.HapticFeedbackConstants; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; - -/** - * A view that has a draggable thumb on a circle. - * - * @hide - */ -public class ZoomRing extends View { - private static final String TAG = "ZoomRing"; - - // TODO: Temporary until the trail is done - private static final boolean DRAW_TRAIL = false; - - /** - * To avoid floating point calculations and int round-offs, we multiply - * radians by this value. - */ - public static final int RADIAN_INT_MULTIPLIER = 10000; - /** The allowable margin of error when comparing two angles. */ - public static final int RADIAN_INT_ERROR = 100; - public static final int PI_INT_MULTIPLIED = (int) (Math.PI * RADIAN_INT_MULTIPLIER); - public static final int TWO_PI_INT_MULTIPLIED = PI_INT_MULTIPLIED * 2; - private static final int HALF_PI_INT_MULTIPLIED = PI_INT_MULTIPLIED / 2; - - private static final int DOUBLE_TAP_DISMISS_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); - private final int mTouchSlop; - - /** The slop when the user is grabbing the thumb. */ - private static final int THUMB_GRAB_SLOP = PI_INT_MULTIPLIED / 8; - /** The slop until a user starts dragging the thumb. */ - private static final int THUMB_DRAG_SLOP = PI_INT_MULTIPLIED / 12; - - /** The distance (in px) from the center of the ring to the center of the thumb. */ - private int mThumbDistance; - - /** The angle on a unit circle that is considered to be the zoom ring's 0 degree. */ - private int mZeroAngle = HALF_PI_INT_MULTIPLIED * 3; - - /** - * The maximum delta angle that the thumb can move. The primary use is to - * ensure that when a user taps on the ring, the movement to reach that - * target angle is not ambiguous (for example, if the thumb is at 0 and he - * taps 180, should the thumb go clockwise or counterclockwise? - * <p> - * Includes error because we compare this to the result of - * getDelta(getClosestTickeAngle(..), oldAngle) which ends up having some - * rounding error. - */ - private static final int MAX_ABS_JUMP_DELTA_ANGLE = (2 * PI_INT_MULTIPLIED / 3) + - RADIAN_INT_ERROR; - - /** The cached X of the zoom ring's center (in zoom ring coordinates). */ - private int mCenterX; - /** The cached Y of the zoom ring's center (in zoom ring coordinates). */ - private int mCenterY; - - /** The angle of the thumb (in int radians) */ - private int mThumbAngle; - /** The cached width/2 of the zoom ring. */ - private int mThumbHalfWidth; - /** The cached height/2 of the zoom ring. */ - private int mThumbHalfHeight; - - /** - * The bound for the thumb's movement when it is being dragged clockwise. - * Can be Integer.MIN_VALUE if there is no bound in this direction. - */ - private int mThumbCwBound = Integer.MIN_VALUE; - /** - * The bound for the thumb's movement when it is being dragged - * counterclockwise. Can be Integer.MIN_VALUE if there is no bound in this - * direction. - */ - private int mThumbCcwBound = Integer.MIN_VALUE; - - /** - * Whether to enforce the maximum absolute jump delta. See - * {@link #MAX_ABS_JUMP_DELTA_ANGLE}. - */ - private boolean mEnforceMaxAbsJump = true; - - /** The inner radius of the track. */ - private int mTrackInnerRadius; - /** Cached square of the inner radius of the track. */ - private int mTrackInnerRadiusSquared; - /** The outer radius of the track. */ - private int mTrackOuterRadius; - /** Cached square of the outer radius of the track. */ - private int mTrackOuterRadiusSquared; - - /** The raw X of where the widget previously was located. */ - private int mPreviousWidgetDragX; - /** The raw Y of where the widget previously was located. */ - private int mPreviousWidgetDragY; - - /** Whether the thumb should be visible. */ - private boolean mThumbVisible = true; - - /** The drawable for the thumb. */ - private Drawable mThumbDrawable; - - /** Shown beneath the thumb if we can still zoom in. */ - private Drawable mZoomInArrowDrawable; - /** Shown beneath the thumb if we can still zoom out. */ - private Drawable mZoomOutArrowDrawable; - - /** @see #mThumbArrowsToDraw */ - private static final int THUMB_ARROW_PLUS = 1 << 0; - /** @see #mThumbArrowsToDraw */ - private static final int THUMB_ARROW_MINUS = 1 << 1; - /** Bitwise-OR of {@link #THUMB_ARROW_MINUS} and {@link #THUMB_ARROW_PLUS} */ - private int mThumbArrowsToDraw; - - /** The duration for the thumb arrows fading out */ - private static final int THUMB_ARROWS_FADE_DURATION = 300; - /** The time when the fade out started. */ - private long mThumbArrowsFadeStartTime; - /** The current alpha for the thumb arrows. */ - private int mThumbArrowsAlpha = 255; - - /** The distance from the center to the zoom arrow hints (usually plus and minus). */ - private int mZoomArrowHintDistance; - /** The offset angle from the thumb angle to draw the zoom arrow hints. */ - private int mZoomArrowHintOffsetAngle = TWO_PI_INT_MULTIPLIED / 11; - /** Drawn (without rotation) on top of the arrow. */ - private Drawable mZoomInArrowHintDrawable; - /** Drawn (without rotation) on top of the arrow. */ - private Drawable mZoomOutArrowHintDrawable; - - /** Zoom ring is just chillin' */ - private static final int MODE_IDLE = 0; - /** - * User has his finger down somewhere on the ring (besides the thumb) and we - * are waiting for him to move the slop amount before considering him in the - * drag thumb state. - */ - private static final int MODE_WAITING_FOR_DRAG_THUMB_AFTER_JUMP = 5; - /** User is dragging the thumb. */ - private static final int MODE_DRAG_THUMB = 1; - /** - * User has his finger down, but we are waiting for him to pass the touch - * slop before going into the #MODE_MOVE_ZOOM_RING. This is a good time to - * show the movable hint. - */ - private static final int MODE_WAITING_FOR_MOVE_ZOOM_RING = 4; - /** User is moving the zoom ring. */ - private static final int MODE_MOVE_ZOOM_RING = 2; - /** User is dragging the thumb via tap-drag. */ - private static final int MODE_TAP_DRAG = 3; - /** Ignore the touch interaction until the user touches the thumb again. */ - private static final int MODE_IGNORE_UNTIL_TOUCHES_THUMB = 6; - /** The current mode of interaction. */ - private int mMode; - /** Records the last mode the user was in. */ - private int mPreviousMode; - - /** The previous time of the up-touch on the center. */ - private long mPreviousCenterUpTime; - /** The previous X of down-touch. */ - private int mPreviousDownX; - /** The previous Y of down-touch. */ - private int mPreviousDownY; - - /** The angle where the user first grabbed the thumb. */ - private int mInitialGrabThumbAngle; - - /** The callback. */ - private OnZoomRingCallback mCallback; - /** The tick angle that we previously called back with. */ - private int mPreviousCallbackTickAngle; - /** The delta angle between ticks. A tick is a callback point. */ - private int mTickDelta = Integer.MAX_VALUE; - /** If the user drags to within __% of a tick, snap to that tick. */ - private int mFuzzyTickDelta = Integer.MAX_VALUE; - - /** The angle where the thumb is officially starting to be dragged. */ - private int mThumbDragStartAngle; - - /** The drawable for the zoom trail. */ - private Drawable mTrail; - /** The accumulated angle for the trail. */ - private double mAcculumalatedTrailAngle; - - /** The animation-step tracker for scrolling the thumb to a particular position. */ - private Scroller mThumbScroller; - - /** Whether to ever vibrate when passing a tick. */ - private boolean mVibration = true; - - /** The drawable used to hint that this can pan its owner. */ - private Drawable mPanningArrowsDrawable; - - private static final int MSG_THUMB_SCROLLER_STEP = 1; - private static final int MSG_THUMB_ARROWS_FADE_STEP = 2; - private Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_THUMB_SCROLLER_STEP: - onThumbScrollerStep(); - break; - - case MSG_THUMB_ARROWS_FADE_STEP: - onThumbArrowsFadeStep(); - break; - } - } - }; - - public ZoomRing(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - ViewConfiguration viewConfiguration = ViewConfiguration.get(context); - mTouchSlop = viewConfiguration.getScaledTouchSlop(); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ZoomRing, defStyle, 0); - mThumbDistance = (int) a.getDimension(R.styleable.ZoomRing_thumbDistance, 0); - setTrackRadii( - (int) a.getDimension(R.styleable.ZoomRing_trackInnerRadius, 0), - (int) a.getDimension(R.styleable.ZoomRing_trackOuterRadius, Integer.MAX_VALUE)); - mThumbDrawable = a.getDrawable(R.styleable.ZoomRing_thumbDrawable); - mZoomInArrowDrawable = a.getDrawable(R.styleable.ZoomRing_zoomInArrowDrawable); - mZoomOutArrowDrawable = a.getDrawable(R.styleable.ZoomRing_zoomOutArrowDrawable); - mZoomInArrowHintDrawable = a.getDrawable(R.styleable.ZoomRing_zoomInArrowHintDrawable); - mZoomOutArrowHintDrawable = a.getDrawable(R.styleable.ZoomRing_zoomOutArrowHintDrawable); - mZoomArrowHintDistance = - (int) a.getDimension(R.styleable.ZoomRing_zoomArrowHintDistance, 0); - mZoomArrowHintOffsetAngle = - (int) (a.getInteger(R.styleable.ZoomRing_zoomArrowHintOffsetAngle, 0) - * TWO_PI_INT_MULTIPLIED / 360); - mPanningArrowsDrawable = a.getDrawable(R.styleable.ZoomRing_panningArrowsDrawable); - a.recycle(); - - Resources res = context.getResources(); - if (DRAW_TRAIL) { - // TODO get drawables from style instead - mTrail = res.getDrawable(R.drawable.zoom_ring_trail).mutate(); - } - - mThumbHalfHeight = mThumbDrawable.getIntrinsicHeight() / 2; - mThumbHalfWidth = mThumbDrawable.getIntrinsicWidth() / 2; - - setTickDelta(PI_INT_MULTIPLIED / 6); - } - - public ZoomRing(Context context, AttributeSet attrs) { - this(context, attrs, com.android.internal.R.attr.zoomRingStyle); - } - - public ZoomRing(Context context) { - this(context, null); - } - - public void setTrackDrawable(Drawable drawable) { - setBackgroundDrawable(drawable); - } - - public void setCallback(OnZoomRingCallback callback) { - mCallback = callback; - } - - /** - * Sets the distance between ticks. This will be used as a callback threshold. - * - * @param angle The angle between ticks. - */ - public void setTickDelta(int angle) { - mTickDelta = angle; - mFuzzyTickDelta = (int) (angle * 0.65f); - } - - public void setVibration(boolean vibration) { - mVibration = vibration; - } - - public void setThumbVisible(boolean thumbVisible) { - if (mThumbVisible != thumbVisible) { - mThumbVisible = thumbVisible; - invalidate(); - } - } - - public Drawable getPanningArrowsDrawable() { - return mPanningArrowsDrawable; - } - - public void setTrackRadii(int innerRadius, int outerRadius) { - mTrackInnerRadius = innerRadius; - mTrackOuterRadius = outerRadius; - - mTrackInnerRadiusSquared = innerRadius * innerRadius; - if (mTrackInnerRadiusSquared < innerRadius) { - // Prevent overflow - mTrackInnerRadiusSquared = Integer.MAX_VALUE; - } - - mTrackOuterRadiusSquared = outerRadius * outerRadius; - if (mTrackOuterRadiusSquared < outerRadius) { - // Prevent overflow - mTrackOuterRadiusSquared = Integer.MAX_VALUE; - } - } - - public int getTrackInnerRadius() { - return mTrackInnerRadius; - } - - public int getTrackOuterRadius() { - return mTrackOuterRadius; - } - - public void setThumbClockwiseBound(int angle) { - if (angle < 0) { - mThumbCwBound = Integer.MIN_VALUE; - } else { - mThumbCwBound = getClosestTickAngle(angle); - } - updateEnforceMaxAbsJump(); - } - - public void setThumbCounterclockwiseBound(int angle) { - if (angle < 0) { - mThumbCcwBound = Integer.MIN_VALUE; - } else { - mThumbCcwBound = getClosestTickAngle(angle); - } - updateEnforceMaxAbsJump(); - } - - private void updateEnforceMaxAbsJump() { - // If there are bounds in both direction, there is no reason to restrict - // the amount that a user can absolute jump to - mEnforceMaxAbsJump = - mThumbCcwBound == Integer.MIN_VALUE || mThumbCwBound == Integer.MIN_VALUE; - } - - public int getThumbAngle() { - return mThumbAngle; - } - - public void setThumbAngle(int angle) { - angle = getValidAngle(angle); - mPreviousCallbackTickAngle = getClosestTickAngle(angle); - setThumbAngleAuto(angle, false, false); - } - - /** - * Sets the thumb angle. If already animating, will continue the animation, - * otherwise it will do a direct jump. - * - * @param angle - * @param useDirection Whether to use the ccw parameter - * @param ccw Whether going counterclockwise (only used if useDirection is true) - */ - private void setThumbAngleAuto(int angle, boolean useDirection, boolean ccw) { - if (mThumbScroller == null - || mThumbScroller.isFinished() - || Math.abs(getDelta(angle, getThumbScrollerAngle())) < THUMB_GRAB_SLOP) { - setThumbAngleInt(angle); - } else { - if (useDirection) { - setThumbAngleAnimated(angle, 0, ccw); - } else { - setThumbAngleAnimated(angle, 0); - } - } - } - - private void setThumbAngleInt(int angle) { - mThumbAngle = angle; - int unoffsetAngle = angle + mZeroAngle; - int thumbCenterX = (int) (Math.cos(1f * unoffsetAngle / RADIAN_INT_MULTIPLIER) * - mThumbDistance) + mCenterX; - int thumbCenterY = (int) (Math.sin(1f * unoffsetAngle / RADIAN_INT_MULTIPLIER) * - mThumbDistance) * -1 + mCenterY; - - mThumbDrawable.setBounds(thumbCenterX - mThumbHalfWidth, - thumbCenterY - mThumbHalfHeight, - thumbCenterX + mThumbHalfWidth, - thumbCenterY + mThumbHalfHeight); - - if (mThumbArrowsToDraw > 0) { - setThumbArrowsAngle(angle); - } - - if (DRAW_TRAIL) { - double degrees; - degrees = Math.min(359.0, Math.abs(mAcculumalatedTrailAngle)); - int level = (int) (10000.0 * degrees / 360.0); - - mTrail.setLevel((int) (10000.0 * - (-Math.toDegrees(angle / (double) RADIAN_INT_MULTIPLIER) - - degrees + 90) / 360.0)); - ((RotateDrawable) mTrail).getDrawable().setLevel(level); - } - - invalidate(); - } - - /** - * - * @param angle - * @param duration The animation duration, or 0 for the default duration. - */ - public void setThumbAngleAnimated(int angle, int duration) { - // The angle when going from the current angle to the new angle - int deltaAngle = getDelta(mThumbAngle, angle); - setThumbAngleAnimated(angle, duration, deltaAngle > 0); - } - - public void setThumbAngleAnimated(int angle, int duration, boolean counterClockwise) { - if (mThumbScroller == null) { - mThumbScroller = new Scroller(mContext); - } - - int startAngle = mThumbAngle; - int endAngle = getValidAngle(angle); - int deltaAngle = getDelta(startAngle, endAngle, counterClockwise); - if (startAngle + deltaAngle < 0) { - // Keep our angles positive - startAngle += TWO_PI_INT_MULTIPLIED; - } - - if (!mThumbScroller.isFinished()) { - duration = mThumbScroller.getDuration() - mThumbScroller.timePassed(); - } else if (duration == 0) { - duration = getAnimationDuration(deltaAngle); - } - mThumbScroller.startScroll(startAngle, 0, deltaAngle, 0, duration); - onThumbScrollerStep(); - } - - private int getAnimationDuration(int deltaAngle) { - if (deltaAngle < 0) deltaAngle *= -1; - return 300 + deltaAngle * 300 / RADIAN_INT_MULTIPLIER; - } - - private void onThumbScrollerStep() { - if (!mThumbScroller.computeScrollOffset()) return; - setThumbAngleInt(getThumbScrollerAngle()); - mHandler.sendEmptyMessage(MSG_THUMB_SCROLLER_STEP); - } - - private int getThumbScrollerAngle() { - return mThumbScroller.getCurrX() % TWO_PI_INT_MULTIPLIED; - } - - public void resetThumbAngle() { - mPreviousCallbackTickAngle = 0; - setThumbAngleInt(0); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec), - resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec)); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, - int bottom) { - super.onLayout(changed, left, top, right, bottom); - - // Cache the center point - mCenterX = (right - left) / 2; - mCenterY = (bottom - top) / 2; - - // Done here since we now have center, which is needed to calculate some - // aux info for thumb angle - if (mThumbAngle == Integer.MIN_VALUE) { - resetThumbAngle(); - } - - if (DRAW_TRAIL) { - mTrail.setBounds(0, 0, right - left, bottom - top); - } - - // These drawables are the same size as the track - mZoomInArrowDrawable.setBounds(0, 0, right - left, bottom - top); - mZoomOutArrowDrawable.setBounds(0, 0, right - left, bottom - top); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - return handleTouch(event.getAction(), event.getEventTime(), - (int) event.getX(), (int) event.getY(), (int) event.getRawX(), - (int) event.getRawY()); - } - - private void resetToIdle() { - setMode(MODE_IDLE); - mPreviousWidgetDragX = mPreviousWidgetDragY = Integer.MIN_VALUE; - mAcculumalatedTrailAngle = 0.0; - } - - public void setTapDragMode(boolean tapDragMode, int x, int y) { - resetToIdle(); - if (tapDragMode) { - setMode(MODE_TAP_DRAG); - mCallback.onUserInteractionStarted(); - onThumbDragStarted(getAngle(x - mCenterX, y - mCenterY)); - } else { - onTouchUp(SystemClock.elapsedRealtime(), true); - } - } - - public boolean handleTouch(int action, long time, int x, int y, int rawX, int rawY) { - // local{X,Y} will be where the center of the widget is (0,0) - int localX = x - mCenterX; - int localY = y - mCenterY; - - /* - * If we are not drawing the thumb, there is no way for the user to be - * touching the thumb. Also, if this is the case, assume they are not - * touching the ring (so the user cannot absolute set the thumb, and - * there will be a larger touch region for going into the move-ring - * mode). - */ - boolean isTouchingThumb = mThumbVisible; - boolean isTouchingRing = mThumbVisible; - - int touchAngle = getAngle(localX, localY); - - int radiusSquared = localX * localX + localY * localY; - if (radiusSquared < mTrackInnerRadiusSquared || - radiusSquared > mTrackOuterRadiusSquared) { - // Out-of-bounds - isTouchingThumb = false; - isTouchingRing = false; - } - - if (isTouchingThumb) { - int deltaThumbAndTouch = getDelta(mThumbAngle, touchAngle); - int absoluteDeltaThumbAndTouch = deltaThumbAndTouch >= 0 ? - deltaThumbAndTouch : -deltaThumbAndTouch; - if (absoluteDeltaThumbAndTouch > THUMB_GRAB_SLOP) { - // Didn't grab close enough to the thumb - isTouchingThumb = false; - } - } - - switch (action) { - case MotionEvent.ACTION_DOWN: - if (!isTouchingRing && - (time - mPreviousCenterUpTime <= DOUBLE_TAP_DISMISS_TIMEOUT)) { - // Make sure the double-tap is in the center of the widget (and not on the ring) - mCallback.onZoomRingDismissed(); - onTouchUp(time, isTouchingRing); - - // Dismissing, so halt here - return true; - } - - resetToIdle(); - mCallback.onUserInteractionStarted(); - mPreviousDownX = x; - mPreviousDownY = y; - // Fall through to code below switch (since the down is used for - // jumping to the touched tick) - break; - - case MotionEvent.ACTION_MOVE: - // Fall through to code below switch - break; - - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - onTouchUp(time, isTouchingRing); - return true; - - default: - return false; - } - - if (mMode == MODE_IDLE) { - if (isTouchingThumb) { - // They grabbed the thumb - setMode(MODE_DRAG_THUMB); - onThumbDragStarted(touchAngle); - - } else if (isTouchingRing) { - // They tapped somewhere else on the ring - int tickAngle = getClosestTickAngle(touchAngle); - int deltaThumbAndTick = getDelta(mThumbAngle, tickAngle); - int boundAngle = getBoundIfExceeds(mThumbAngle, deltaThumbAndTick); - - if (mEnforceMaxAbsJump) { - // Enforcing the max jump - if (deltaThumbAndTick > MAX_ABS_JUMP_DELTA_ANGLE || - deltaThumbAndTick < -MAX_ABS_JUMP_DELTA_ANGLE) { - // Trying to jump too far, ignore this touch interaction - setMode(MODE_IGNORE_UNTIL_TOUCHES_THUMB); - return true; - } - - if (boundAngle != Integer.MIN_VALUE) { - // Cap the user's jump to the bound - tickAngle = boundAngle; - } - } else { - // Not enforcing the max jump, but we have to make sure - // we're getting to the tapped angle by going through the - // in-bounds region - if (boundAngle != Integer.MIN_VALUE) { - // Going this direction hits a bound, let's go the opposite direction - boolean oldDirectionIsCcw = deltaThumbAndTick > 0; - deltaThumbAndTick = getDelta(mThumbAngle, tickAngle, !oldDirectionIsCcw); - boundAngle = getBoundIfExceeds(mThumbAngle, deltaThumbAndTick); - if (boundAngle != Integer.MIN_VALUE) { - // Cannot get to the tapped location because it is out-of-bounds - setMode(MODE_IGNORE_UNTIL_TOUCHES_THUMB); - return true; - } - } - } - - setMode(MODE_WAITING_FOR_DRAG_THUMB_AFTER_JUMP); - mInitialGrabThumbAngle = touchAngle; - boolean ccw = deltaThumbAndTick > 0; - setThumbAngleAnimated(tickAngle, 0, ccw); - - /* - * Our thumb scrolling animation takes us from mThumbAngle to - * tickAngle, so manifest that as the user dragging the thumb - * there. - */ - onThumbDragStarted(mThumbAngle); - // We know which direction we want to go - onThumbDragged(tickAngle, true, ccw); - - } else { - // They tapped somewhere else on the widget - setMode(MODE_WAITING_FOR_MOVE_ZOOM_RING); - mCallback.onZoomRingSetMovableHintVisible(true); - } - - } else if (mMode == MODE_WAITING_FOR_DRAG_THUMB_AFTER_JUMP) { - int deltaDownAngle = getDelta(mInitialGrabThumbAngle, touchAngle); - if ((deltaDownAngle < -THUMB_DRAG_SLOP || deltaDownAngle > THUMB_DRAG_SLOP) && - isDeltaInBounds(mInitialGrabThumbAngle, deltaDownAngle)) { - setMode(MODE_DRAG_THUMB); - - // No need to call onThumbDragStarted, since that was done when they tapped-to-jump - } - - } else if (mMode == MODE_WAITING_FOR_MOVE_ZOOM_RING) { - if (Math.abs(x - mPreviousDownX) > mTouchSlop || - Math.abs(y - mPreviousDownY) > mTouchSlop) { - /* Make sure the user has moved the slop amount before going into that mode. */ - setMode(MODE_MOVE_ZOOM_RING); - mCallback.onZoomRingMovingStarted(); - // Move the zoom ring so it is under the finger where the user first touched - mCallback.onZoomRingMoved(x - mPreviousDownX, y - mPreviousDownY, rawX, rawY); - } - } else if (mMode == MODE_IGNORE_UNTIL_TOUCHES_THUMB) { - if (isTouchingThumb) { - // The user is back on the thumb, let's go back to the previous mode - setMode(mPreviousMode); - } - } - - // Purposefully not an "else if" - if (mMode == MODE_DRAG_THUMB || mMode == MODE_TAP_DRAG) { - if (isTouchingRing) { - onThumbDragged(touchAngle, false, false); - } - } else if (mMode == MODE_MOVE_ZOOM_RING) { - onZoomRingMoved(rawX, rawY); - } - - return true; - } - - private void onTouchUp(long time, boolean isTouchingRing) { - int mode = mMode; - if (mode == MODE_IGNORE_UNTIL_TOUCHES_THUMB) { - // For cleaning up, pretend like the user was still in the previous mode - mode = mPreviousMode; - } - - if (mode == MODE_MOVE_ZOOM_RING || mode == MODE_WAITING_FOR_MOVE_ZOOM_RING) { - mCallback.onZoomRingSetMovableHintVisible(false); - if (mode == MODE_MOVE_ZOOM_RING) { - mCallback.onZoomRingMovingStopped(); - } - } else if (mode == MODE_DRAG_THUMB || mode == MODE_TAP_DRAG || - mode == MODE_WAITING_FOR_DRAG_THUMB_AFTER_JUMP) { - onThumbDragStopped(); - - if (mode == MODE_DRAG_THUMB || mode == MODE_TAP_DRAG) { - // Animate back to a tick - setThumbAngleAnimated(mPreviousCallbackTickAngle, 0); - } - } - mCallback.onUserInteractionStopped(); - - if (!isTouchingRing) { - mPreviousCenterUpTime = time; - } - } - - private void setMode(int mode) { - if (mode != mMode) { - mPreviousMode = mMode; - mMode = mode; - } - } - - private boolean isDeltaInBounds(int startAngle, int deltaAngle) { - return getBoundIfExceeds(startAngle, deltaAngle) == Integer.MIN_VALUE; - } - - private int getBoundIfExceeds(int startAngle, int deltaAngle) { - if (deltaAngle > 0) { - // Counterclockwise movement - if (mThumbCcwBound != Integer.MIN_VALUE && - getDelta(startAngle, mThumbCcwBound, true) < deltaAngle) { - return mThumbCcwBound; - } - } else if (deltaAngle < 0) { - // Clockwise movement, both of these will be negative - int deltaThumbAndBound = getDelta(startAngle, mThumbCwBound, false); - if (mThumbCwBound != Integer.MIN_VALUE && - deltaThumbAndBound > deltaAngle) { - // Tapped outside of the bound in that direction - return mThumbCwBound; - } - } - - return Integer.MIN_VALUE; - } - - private int getDelta(int startAngle, int endAngle, boolean useDirection, boolean ccw) { - return useDirection ? getDelta(startAngle, endAngle, ccw) : getDelta(startAngle, endAngle); - } - - /** - * Gets the smallest delta between two angles, and infers the direction - * based on the shortest path between the two angles. If going from - * startAngle to endAngle is counterclockwise, the result will be positive. - * If it is clockwise, the result will be negative. - * - * @param startAngle The start angle. - * @param endAngle The end angle. - * @return The difference in angles. - */ - private int getDelta(int startAngle, int endAngle) { - int largerAngle, smallerAngle; - if (endAngle > startAngle) { - largerAngle = endAngle; - smallerAngle = startAngle; - } else { - largerAngle = startAngle; - smallerAngle = endAngle; - } - - int delta = largerAngle - smallerAngle; - if (delta <= PI_INT_MULTIPLIED) { - // If going clockwise, negate the delta - return startAngle == largerAngle ? -delta : delta; - } else { - // The other direction is the delta we want (it includes the - // discontinuous 0-2PI angle) - delta = TWO_PI_INT_MULTIPLIED - delta; - // If going clockwise, negate the delta - return startAngle == smallerAngle ? -delta : delta; - } - } - - /** - * Gets the delta between two angles in the direction specified. - * - * @param startAngle The start angle. - * @param endAngle The end angle. - * @param counterClockwise The direction to take when computing the delta. - * @return The difference in angles in the given direction. - */ - private int getDelta(int startAngle, int endAngle, boolean counterClockwise) { - int delta = endAngle - startAngle; - - if (!counterClockwise && delta > 0) { - // Crossed the discontinuous 0/2PI angle, take the leftover slice of - // the pie and negate it - return -TWO_PI_INT_MULTIPLIED + delta; - } else if (counterClockwise && delta < 0) { - // Crossed the discontinuous 0/2PI angle, take the leftover slice of - // the pie (and ensure it is positive) - return TWO_PI_INT_MULTIPLIED + delta; - } else { - return delta; - } - } - - private void onThumbDragStarted(int startAngle) { - setThumbArrowsVisible(false); - mThumbDragStartAngle = startAngle; - mCallback.onZoomRingThumbDraggingStarted(); - } - - private void onThumbDragged(int touchAngle, boolean useDirection, boolean ccw) { - boolean animateThumbToNewAngle = false; - - int totalDeltaAngle; - totalDeltaAngle = getDelta(mPreviousCallbackTickAngle, touchAngle, useDirection, ccw); - if (totalDeltaAngle >= mFuzzyTickDelta - || totalDeltaAngle <= -mFuzzyTickDelta) { - - if (!useDirection) { - // Set ccw to match the direction found by getDelta - ccw = totalDeltaAngle > 0; - } - - /* - * When the user slides the thumb through the tick that corresponds - * to a zoom bound, we don't want to abruptly stop there. Instead, - * let the user slide it to the next tick, and then animate it back - * to the original zoom bound tick. Because of this, we make sure - * the delta from the bound is more than halfway to the next tick. - * We make sure the bound is between the touch and the previous - * callback to ensure we just passed the bound. - */ - int oldTouchAngle = touchAngle; - if (ccw && mThumbCcwBound != Integer.MIN_VALUE) { - int deltaCcwBoundAndTouch = - getDelta(mThumbCcwBound, touchAngle, useDirection, true); - if (deltaCcwBoundAndTouch >= mTickDelta / 2) { - // The touch has past a bound - int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackTickAngle, - touchAngle, useDirection, true); - if (deltaPreviousCbAndTouch >= deltaCcwBoundAndTouch) { - // The bound is between the previous callback angle and the touch - touchAngle = mThumbCcwBound; - // We're moving the touch BACK to the bound, so opposite direction - ccw = false; - } - } - } else if (!ccw && mThumbCwBound != Integer.MIN_VALUE) { - // See block above for general comments - int deltaCwBoundAndTouch = - getDelta(mThumbCwBound, touchAngle, useDirection, false); - if (deltaCwBoundAndTouch <= -mTickDelta / 2) { - int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackTickAngle, - touchAngle, useDirection, false); - /* - * Both of these will be negative since we got delta in - * clockwise direction, and we want the magnitude of - * deltaPreviousCbAndTouch to be greater than the magnitude - * of deltaCwBoundAndTouch - */ - if (deltaPreviousCbAndTouch <= deltaCwBoundAndTouch) { - touchAngle = mThumbCwBound; - ccw = true; - } - } - } - if (touchAngle != oldTouchAngle) { - // We bounded the touch angle - totalDeltaAngle = getDelta(mPreviousCallbackTickAngle, touchAngle, useDirection, ccw); - animateThumbToNewAngle = true; - setMode(MODE_IGNORE_UNTIL_TOUCHES_THUMB); - } - - - // Prevent it from jumping too far - if (mEnforceMaxAbsJump) { - if (totalDeltaAngle <= -MAX_ABS_JUMP_DELTA_ANGLE) { - totalDeltaAngle = -MAX_ABS_JUMP_DELTA_ANGLE; - animateThumbToNewAngle = true; - } else if (totalDeltaAngle >= MAX_ABS_JUMP_DELTA_ANGLE) { - totalDeltaAngle = MAX_ABS_JUMP_DELTA_ANGLE; - animateThumbToNewAngle = true; - } - } - - /* - * We need to cover the edge case of a user grabbing the thumb, - * going into the center of the widget, and then coming out from the - * center to an angle that's slightly below the angle he's trying to - * hit. If we do int division, we'll end up with one level lower - * than the one he was going for. - */ - int deltaLevels = Math.round((float) totalDeltaAngle / mTickDelta); - if (deltaLevels != 0) { - boolean canStillZoom = mCallback.onZoomRingThumbDragged( - deltaLevels, mThumbDragStartAngle, touchAngle); - - if (mVibration) { - // TODO: we're trying the haptics to see how it goes with - // users, so we're ignoring the settings (for now) - performHapticFeedback(HapticFeedbackConstants.ZOOM_RING_TICK, - HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING | - HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); - } - - // Set the callback angle to the actual angle based on how many delta levels we gave - mPreviousCallbackTickAngle = getValidAngle( - mPreviousCallbackTickAngle + (deltaLevels * mTickDelta)); - } - } - - if (DRAW_TRAIL) { - int deltaAngle = getDelta(mThumbAngle, touchAngle, useDirection, ccw); - mAcculumalatedTrailAngle += Math.toDegrees(deltaAngle / (double) RADIAN_INT_MULTIPLIER); - } - - if (animateThumbToNewAngle) { - if (useDirection) { - setThumbAngleAnimated(touchAngle, 0, ccw); - } else { - setThumbAngleAnimated(touchAngle, 0); - } - } else { - setThumbAngleAuto(touchAngle, useDirection, ccw); - } - } -// private void onThumbDragged(int touchAngle, boolean useDirection, boolean ccw) { -// int deltaPrevCbAndTouch = getDelta(mPreviousCallbackAngle, touchAngle, useDirection, ccw); -// -// if (!useDirection) { -// // Set ccw to match the direction found by getDelta -// ccw = deltaPrevCbAndTouch > 0; -// useDirection = true; -// } -// -// boolean animateThumbToNewAngle = false; -// boolean animationCcw = ccw; -// -// if (deltaPrevCbAndTouch >= mFuzzyCallbackThreshold -// || deltaPrevCbAndTouch <= -mFuzzyCallbackThreshold) { -// -// /* -// * When the user slides the thumb through the tick that corresponds -// * to a zoom bound, we don't want to abruptly stop there. Instead, -// * let the user slide it to the next tick, and then animate it back -// * to the original zoom bound tick. Because of this, we make sure -// * the delta from the bound is more than halfway to the next tick. -// * We make sure the bound is between the touch and the previous -// * callback to ensure we JUST passed the bound. -// */ -// int oldTouchAngle = touchAngle; -// if (ccw && mThumbCcwBound != Integer.MIN_VALUE) { -// int deltaCcwBoundAndTouch = -// getDelta(mThumbCcwBound, touchAngle, true, ccw); -// if (deltaCcwBoundAndTouch >= mCallbackThreshold / 2) { -// // The touch has past far enough from the bound -// int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackAngle, -// touchAngle, true, ccw); -// if (deltaPreviousCbAndTouch >= deltaCcwBoundAndTouch) { -// // The bound is between the previous callback angle and the touch -// // Cap to the bound -// touchAngle = mThumbCcwBound; -// /* -// * We're moving the touch BACK to the bound, so animate -// * back in the opposite direction that passed the bound. -// */ -// animationCcw = false; -// } -// } -// } else if (!ccw && mThumbCwBound != Integer.MIN_VALUE) { -// // See block above for general comments -// int deltaCwBoundAndTouch = -// getDelta(mThumbCwBound, touchAngle, true, ccw); -// if (deltaCwBoundAndTouch <= -mCallbackThreshold / 2) { -// int deltaPreviousCbAndTouch = getDelta(mPreviousCallbackAngle, -// touchAngle, true, ccw); -// /* -// * Both of these will be negative since we got delta in -// * clockwise direction, and we want the magnitude of -// * deltaPreviousCbAndTouch to be greater than the magnitude -// * of deltaCwBoundAndTouch -// */ -// if (deltaPreviousCbAndTouch <= deltaCwBoundAndTouch) { -// touchAngle = mThumbCwBound; -// animationCcw = true; -// } -// } -// } -// if (touchAngle != oldTouchAngle) { -// // We bounded the touch angle -// deltaPrevCbAndTouch = getDelta(mPreviousCallbackAngle, touchAngle, true, ccw); -// // Animate back to the bound -// animateThumbToNewAngle = true; -// // Disallow movement now -// setMode(MODE_IGNORE_UNTIL_UP); -// } -// -// -// /* -// * Prevent it from jumping too far (this could happen if the user -// * goes through the center) -// */ -// -// if (mEnforceMaxAbsJump) { -// if (deltaPrevCbAndTouch <= -MAX_ABS_JUMP_DELTA_ANGLE) { -// deltaPrevCbAndTouch = -MAX_ABS_JUMP_DELTA_ANGLE; -// animateThumbToNewAngle = true; -// } else if (deltaPrevCbAndTouch >= MAX_ABS_JUMP_DELTA_ANGLE) { -// deltaPrevCbAndTouch = MAX_ABS_JUMP_DELTA_ANGLE; -// animateThumbToNewAngle = true; -// } -// } -// -// /* -// * We need to cover the edge case of a user grabbing the thumb, -// * going into the center of the widget, and then coming out from the -// * center to an angle that's slightly below the angle he's trying to -// * hit. If we do int division, we'll end up with one level lower -// * than the one he was going for. -// */ -// int deltaLevels = Math.round((float) deltaPrevCbAndTouch / mCallbackThreshold); -// if (deltaLevels != 0) { -// boolean canStillZoom = mCallback.onZoomRingThumbDragged( -// deltaLevels, mThumbDragStartAngle, touchAngle); -// -// if (mVibration) { -// // TODO: we're trying the haptics to see how it goes with -// // users, so we're ignoring the settings (for now) -// performHapticFeedback(HapticFeedbackConstants.ZOOM_RING_TICK, -// HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING | -// HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); -// -// } -// // Set the callback angle to the actual angle based on how many delta levels we gave -// mPreviousCallbackAngle = getValidAngle( -// mPreviousCallbackAngle + (deltaLevels * mCallbackThreshold)); -// } -// } -// -// if (DRAW_TRAIL) { -// int deltaAngle = getDelta(mThumbAngle, touchAngle, true, ccw); -// mAcculumalatedTrailAngle += Math.toDegrees(deltaAngle / (double) RADIAN_INT_MULTIPLIER); -// } -// -// if (animateThumbToNewAngle) { -// setThumbAngleAnimated(touchAngle, 0, animationCcw); -// } else { -// /* -// * Use regular ccw here because animationCcw will never have been -// * changed if animateThumbToNewAngle is false -// */ -// setThumbAngleAuto(touchAngle, true, ccw); -// } -// } - - private int getValidAngle(int invalidAngle) { - if (invalidAngle < 0) { - return (invalidAngle % TWO_PI_INT_MULTIPLIED) + TWO_PI_INT_MULTIPLIED; - } else if (invalidAngle >= TWO_PI_INT_MULTIPLIED) { - return invalidAngle % TWO_PI_INT_MULTIPLIED; - } else { - return invalidAngle; - } - } - - private int getClosestTickAngle(int angle) { - int smallerAngleDistance = angle % mTickDelta; - int smallerAngle = angle - smallerAngleDistance; - if (smallerAngleDistance < mTickDelta / 2) { - // Closer to the smaller angle - return smallerAngle; - } else { - // Closer to the bigger angle (premodding) - return (smallerAngle + mTickDelta) % TWO_PI_INT_MULTIPLIED; - } - } - - private void onThumbDragStopped() { - mCallback.onZoomRingThumbDraggingStopped(); - } - - private void onZoomRingMoved(int rawX, int rawY) { - if (mPreviousWidgetDragX != Integer.MIN_VALUE) { - int deltaX = rawX - mPreviousWidgetDragX; - int deltaY = rawY - mPreviousWidgetDragY; - - mCallback.onZoomRingMoved(deltaX, deltaY, rawX, rawY); - } - - mPreviousWidgetDragX = rawX; - mPreviousWidgetDragY = rawY; - } - - @Override - public void onWindowFocusChanged(boolean hasWindowFocus) { - super.onWindowFocusChanged(hasWindowFocus); - - if (!hasWindowFocus) { - mCallback.onZoomRingDismissed(); - } - } - - private int getAngle(int localX, int localY) { - int radians = (int) (Math.atan2(localY, localX) * RADIAN_INT_MULTIPLIER); - - // Convert from [-pi,pi] to {0,2pi] - if (radians < 0) { - radians = -radians; - } else if (radians > 0) { - radians = 2 * PI_INT_MULTIPLIED - radians; - } else { - radians = 0; - } - - radians = radians - mZeroAngle; - return radians >= 0 ? radians : radians + 2 * PI_INT_MULTIPLIED; - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - if (mThumbVisible) { - if (DRAW_TRAIL) { - mTrail.draw(canvas); - } - if ((mThumbArrowsToDraw & THUMB_ARROW_PLUS) != 0) { - mZoomInArrowDrawable.draw(canvas); - mZoomInArrowHintDrawable.draw(canvas); - } - if ((mThumbArrowsToDraw & THUMB_ARROW_MINUS) != 0) { - mZoomOutArrowDrawable.draw(canvas); - mZoomOutArrowHintDrawable.draw(canvas); - } - mThumbDrawable.draw(canvas); - } - } - - private void setThumbArrowsAngle(int angle) { - int level = -angle * 10000 / ZoomRing.TWO_PI_INT_MULTIPLIED; - mZoomInArrowDrawable.setLevel(level); - mZoomOutArrowDrawable.setLevel(level); - - // Assume it is a square - int halfSideLength = mZoomInArrowHintDrawable.getIntrinsicHeight() / 2; - int unoffsetAngle = angle + mZeroAngle; - - int plusCenterX = (int) (Math.cos(1f * (unoffsetAngle - mZoomArrowHintOffsetAngle) - / RADIAN_INT_MULTIPLIER) * mZoomArrowHintDistance) + mCenterX; - int plusCenterY = (int) (Math.sin(1f * (unoffsetAngle - mZoomArrowHintOffsetAngle) - / RADIAN_INT_MULTIPLIER) * mZoomArrowHintDistance) * -1 + mCenterY; - mZoomInArrowHintDrawable.setBounds(plusCenterX - halfSideLength, - plusCenterY - halfSideLength, - plusCenterX + halfSideLength, - plusCenterY + halfSideLength); - - int minusCenterX = (int) (Math.cos(1f * (unoffsetAngle + mZoomArrowHintOffsetAngle) - / RADIAN_INT_MULTIPLIER) * mZoomArrowHintDistance) + mCenterX; - int minusCenterY = (int) (Math.sin(1f * (unoffsetAngle + mZoomArrowHintOffsetAngle) - / RADIAN_INT_MULTIPLIER) * mZoomArrowHintDistance) * -1 + mCenterY; - mZoomOutArrowHintDrawable.setBounds(minusCenterX - halfSideLength, - minusCenterY - halfSideLength, - minusCenterX + halfSideLength, - minusCenterY + halfSideLength); - } - - void setThumbArrowsVisible(boolean visible) { - if (visible) { - mThumbArrowsAlpha = 255; - int callbackAngle = mPreviousCallbackTickAngle; - if (callbackAngle < mThumbCwBound - RADIAN_INT_ERROR || - callbackAngle > mThumbCwBound + RADIAN_INT_ERROR) { - mZoomInArrowDrawable.setAlpha(255); - mZoomInArrowHintDrawable.setAlpha(255); - mThumbArrowsToDraw |= THUMB_ARROW_PLUS; - } else { - mThumbArrowsToDraw &= ~THUMB_ARROW_PLUS; - } - if (callbackAngle < mThumbCcwBound - RADIAN_INT_ERROR || - callbackAngle > mThumbCcwBound + RADIAN_INT_ERROR) { - mZoomOutArrowDrawable.setAlpha(255); - mZoomOutArrowHintDrawable.setAlpha(255); - mThumbArrowsToDraw |= THUMB_ARROW_MINUS; - } else { - mThumbArrowsToDraw &= ~THUMB_ARROW_MINUS; - } - invalidate(); - } else if (mThumbArrowsAlpha == 255) { - // Only start fade if we're fully visible (otherwise another fade is happening already) - mThumbArrowsFadeStartTime = SystemClock.elapsedRealtime(); - onThumbArrowsFadeStep(); - } - } - - private void onThumbArrowsFadeStep() { - if (mThumbArrowsAlpha <= 0) { - mThumbArrowsToDraw = 0; - return; - } - - mThumbArrowsAlpha = (int) - (255 - (255 * (SystemClock.elapsedRealtime() - mThumbArrowsFadeStartTime) - / THUMB_ARROWS_FADE_DURATION)); - if (mThumbArrowsAlpha < 0) mThumbArrowsAlpha = 0; - if ((mThumbArrowsToDraw & THUMB_ARROW_PLUS) != 0) { - mZoomInArrowDrawable.setAlpha(mThumbArrowsAlpha); - mZoomInArrowHintDrawable.setAlpha(mThumbArrowsAlpha); - invalidateDrawable(mZoomInArrowHintDrawable); - invalidateDrawable(mZoomInArrowDrawable); - } - if ((mThumbArrowsToDraw & THUMB_ARROW_MINUS) != 0) { - mZoomOutArrowDrawable.setAlpha(mThumbArrowsAlpha); - mZoomOutArrowHintDrawable.setAlpha(mThumbArrowsAlpha); - invalidateDrawable(mZoomOutArrowHintDrawable); - invalidateDrawable(mZoomOutArrowDrawable); - } - - if (!mHandler.hasMessages(MSG_THUMB_ARROWS_FADE_STEP)) { - mHandler.sendEmptyMessage(MSG_THUMB_ARROWS_FADE_STEP); - } - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - setThumbArrowsAngle(mThumbAngle); - setThumbArrowsVisible(true); - } - - public interface OnZoomRingCallback { - void onZoomRingSetMovableHintVisible(boolean visible); - - void onZoomRingMovingStarted(); - boolean onZoomRingMoved(int deltaX, int deltaY, int rawX, int rawY); - void onZoomRingMovingStopped(); - - void onZoomRingThumbDraggingStarted(); - boolean onZoomRingThumbDragged(int numLevels, int startAngle, int curAngle); - void onZoomRingThumbDraggingStopped(); - - void onZoomRingDismissed(); - - void onUserInteractionStarted(); - void onUserInteractionStopped(); - } - - private static void printAngle(String angleName, int angle) { - Log.d(TAG, angleName + ": " + (long) angle * 180 / PI_INT_MULTIPLIED); - } -} diff --git a/core/java/android/widget/ZoomRingController.java b/core/java/android/widget/ZoomRingController.java deleted file mode 100644 index 3bf3b22..0000000 --- a/core/java/android/widget/ZoomRingController.java +++ /dev/null @@ -1,1383 +0,0 @@ -/* - * Copyright (C) 2009 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.DisplayMetrics; -import android.view.GestureDetector; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.LayoutInflater; -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; - -/** - * A controller to simplify the use of the zoom ring widget. - * <p> - * 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 { - - // Temporary methods for different zoom types - static int getZoomType(Context context) { - return Settings.System.getInt(context.getContentResolver(), "zoom", 1); - } - public static boolean useOldZoom(Context context) { - return getZoomType(context) == 0; - } - private static boolean useThisZoom(Context context) { - return getZoomType(context) == 1; - } - - /** The duration for the animation to re-center the zoom ring. */ - private static final int RECENTERING_DURATION = 500; - - /** The inactivity timeout for the zoom ring. */ - private static final int INACTIVITY_TIMEOUT = - (int) ViewConfiguration.getZoomControlsTimeout(); - - /** - * The delay when the user taps outside to dismiss the zoom ring. This is - * because the user can do a second-tap to recenter the owner view instead - * of dismissing the zoom ring. - */ - private static final int OUTSIDE_TAP_DISMISS_DELAY = - ViewConfiguration.getDoubleTapTimeout() / 2; - - /** - * When the zoom ring is on the edge, this is the delay before we actually - * start panning the owner. - * @see #mInitiatePanGap - */ - private static final int INITIATE_PAN_DELAY = 300; - - /** - * While already panning, if the zoom ring remains this close to an edge, - * the owner will continue to be panned. - */ - private int mPanGap; - - /** To begin a pan, the zoom ring must be this close to an edge. */ - private int mInitiatePanGap; - - /** Initialized from ViewConfiguration. */ - private int mScaledTouchSlop; - - /** - * The setting name that tracks whether we've shown the zoom ring toast. - */ - 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). - */ - /** The X where the tap-drag started. */ - private int mTapDragStartX; - /** The Y where the tap-drag started. */ - private int mTapDragStartY; - - /** The controller is idle */ - private static final int TOUCH_MODE_IDLE = 0; - /** - * In the middle of a second-tap interaction, waiting for either an up-touch - * or the user to start dragging to go into tap-drag mode. - */ - private static final int TOUCH_MODE_WAITING_FOR_TAP_DRAG_MOVEMENT = 2; - /** In the middle of a tap-drag. */ - private static final int TOUCH_MODE_FORWARDING_FOR_TAP_DRAG = 3; - private int mTouchMode; - - /** Whether the zoom ring is visible. */ - private boolean mIsZoomRingVisible; - - private ZoomRing mZoomRing; - /** Cached width of the zoom ring. */ - private int mZoomRingWidth; - /** Cached height of the zoom ring. */ - private int mZoomRingHeight; - - /** Invokes panning of owner view if the zoom ring is touching an edge. */ - private Panner mPanner; - /** The time when the zoom ring first touched the edge. */ - private long mTouchingEdgeStartTime; - /** Whether the user has already initiated the panning. */ - private boolean mPanningInitiated; - - /** - * When the finger moves the zoom ring to an edge, this is the horizontal - * accumulator for how much the finger has moved off of its original touch - * point on the zoom ring (OOB = out-of-bounds). If < 0, the finger has - * moved that many px to the left of its original touch point on the ring. - */ - private int mMovingZoomRingOobX; - /** Vertical accumulator, see {@link #mMovingZoomRingOobX} */ - private int mMovingZoomRingOobY; - - /** Arrows that hint that the zoom ring is movable. */ - private ImageView mPanningArrows; - /** The animation shown when the panning arrows are being shown. */ - private Animation mPanningArrowsEnterAnimation; - /** The animation shown when the panning arrows are being hidden. */ - private Animation mPanningArrowsExitAnimation; - - /** - * Temporary rectangle, only use from the UI thread (and ideally don't rely - * on it being unused across many method calls.) - */ - private Rect mTempRect = new Rect(); - - private OnZoomListener mCallback; - - /** - * 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). - * <p> - * 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); - - /** Listens for configuration changes so we can make sure we're still in a reasonable state. */ - 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_STEP = 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; - - /** - * If setVisible(true) is called and the owner view's window token is null, - * we delay the setVisible(true) call until it is not null. - */ - private static final int MSG_POST_SET_VISIBLE = 4; - - private Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_SCROLLER_STEP: - onScrollerStep(); - break; - - case MSG_POST_CONFIGURATION_CHANGED: - onPostConfigurationChanged(); - break; - - case MSG_DISMISS_ZOOM_RING: - setVisible(false); - break; - - case MSG_POST_SET_VISIBLE: - if (mOwnerView.getWindowToken() == null) { - // Doh, it is still null, throw an exception - throw new IllegalArgumentException( - "Cannot make the zoom ring visible if the owner view is " + - "not attached to a window."); - } - setVisible(true); - 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; - mContainerLayoutParams.windowAnimations = com.android.internal.R.style.Animation_ZoomRing; - - mContainer = new FrameLayout(context); - mContainer.setLayoutParams(mContainerLayoutParams); - mContainer.setMeasureAllChildren(true); - - mContainer.addView(mZoomRing); - mContainer.addView(mPanningArrows); - - mScroller = new Scroller(context, new DecelerateInterpolator()); - - ViewConfiguration vc = ViewConfiguration.get(context); - mScaledTouchSlop = vc.getScaledTouchSlop(); - - float density = context.getResources().getDisplayMetrics().density; - mPanGap = (int) (20 * density); - mInitiatePanGap = (int) (10 * density); - } - - private void createPanningArrows() { - mPanningArrows = new ImageView(mContext); - mPanningArrows.setImageDrawable(mZoomRing.getPanningArrowsDrawable()); - 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) between ticks. This is also the angle a user - * must move the thumb 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 angle The angle for the callback threshold, in radians - */ - public void setTickDelta(float angle) { - mZoomRing.setTickDelta((int) (angle * 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 setTrackDrawable(int drawable) { - mZoomRing.setBackgroundResource(drawable); - } - - /** - * Sets the callback for the zoom ring controller. - * - * @param callback The callback. - */ - public void setCallback(OnZoomListener callback) { - mCallback = callback; - } - - public void setVibration(boolean vibrate) { - mZoomRing.setVibration(vibrate); - } - - 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 setThumbVisible(boolean thumbVisible) { - mZoomRing.setThumbVisible(thumbVisible); - } - - 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 (!useThisZoom(mContext)) return; - - if (visible) { - if (mOwnerView.getWindowToken() == null) { - /* - * We need a window token to show ourselves, maybe the owner's - * window hasn't been created yet but it will have been by the - * time the looper is idle, so post the setVisible(true) call. - */ - if (!mHandler.hasMessages(MSG_POST_SET_VISIBLE)) { - mHandler.sendEmptyMessage(MSG_POST_SET_VISIBLE); - } - return; - } - - dismissZoomRingDelayed(INACTIVITY_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(); - refreshContainerLayout(); - - 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); - } - } - - } - - private void refreshContainerLayout() { - if (mIsZoomRingVisible) { - mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams); - } - } - - /** - * Returns the container of the zoom ring widget. The client can add views - * here to be shown alongside the zoom ring. See {@link #getZoomRingId()}. - * <p> - * Notes: - * <ul> - * <li> The controller dispatches touch events differently than the regular view - * framework. - * <li> Please ensure you set your view to INVISIBLE not GONE when hiding it. - * </ul> - * - * @return The layout used by the container. - */ - public FrameLayout getContainer() { - return mContainer; - } - - /** - * Returns the id of the zoom ring widget. - * - * @return The id of the zoom ring widget. - */ - 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). - * <p> - * In most cases, the client can use a {@link GestureDetector} and forward events from - * {@link GestureDetector.OnDoubleTapListener#onDoubleTapEvent(MotionEvent)}. - * - * @param event The event belonging to the second tap. - * @return Whether the event was consumed. - */ - public boolean handleDoubleTapEvent(MotionEvent event) { - if (!useThisZoom(mContext)) return false; - - 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) > mScaledTouchSlop || - Math.abs(y - mTapDragStartY) > - mScaledTouchSlop) { - 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, RECENTERING_DURATION); - mHandler.sendEmptyMessage(MSG_SCROLLER_STEP); - } - } - - 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); - } - - /** @hide */ - public void onZoomRingSetMovableHintVisible(boolean visible) { - setPanningArrowsVisible(visible); - } - - /** @hide */ - public void onUserInteractionStarted() { - mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); - } - - /** @hide */ - public void onUserInteractionStopped() { - dismissZoomRingDelayed(INACTIVITY_TIMEOUT); - } - - /** @hide */ - public void onZoomRingMovingStarted() { - mScroller.abortAnimation(); - mTouchingEdgeStartTime = 0; - mMovingZoomRingOobX = 0; - mMovingZoomRingOobY = 0; - if (mCallback != null) { - mCallback.onBeginPan(); - } - } - - private void setPanningArrowsVisible(boolean visible) { - mPanningArrows.startAnimation(visible ? mPanningArrowsEnterAnimation - : mPanningArrowsExitAnimation); - mPanningArrows.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); - } - - /** @hide */ - public boolean onZoomRingMoved(int deltaX, int deltaY, int rawX, int rawY) { - - if (mMovingZoomRingOobX != 0) { - /* - * The finger has moved off the point where it originally touched - * the zidget. - */ - boolean wasOobLeft = mMovingZoomRingOobX < 0; - mMovingZoomRingOobX += deltaX; - if ((wasOobLeft && mMovingZoomRingOobX > 0) || - (!wasOobLeft && mMovingZoomRingOobX < 0)) { - /* - * Woot, the finger is back on the original point. Infact, it - * went PAST its original point, so take the amount it passed - * and use that as the delta to move the zoom ring. - */ - deltaX = mMovingZoomRingOobX; - // No longer out-of-bounds, reset - mMovingZoomRingOobX = 0; - } else { - // The finger is still not back, eat this movement - deltaX = 0; - } - } - - if (mMovingZoomRingOobY != 0) { - // See above for comments - boolean wasOobUp = mMovingZoomRingOobY < 0; - mMovingZoomRingOobY += deltaY; - if ((wasOobUp && mMovingZoomRingOobY > 0) || (!wasOobUp && mMovingZoomRingOobY < 0)) { - deltaY = mMovingZoomRingOobY; - mMovingZoomRingOobY = 0; - } else { - deltaY = 0; - } - } - - 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; - - refreshContainerLayout(); - - // Check for pan - boolean horizontalPanning = true; - int leftGap = newZoomRingX - ownerBounds.left; - if (leftGap < mPanGap) { - if (leftGap == 0 && deltaX != 0 && mMovingZoomRingOobX == 0) { - // Future moves in this direction should be accumulated in mMovingZoomRingOobX - mMovingZoomRingOobX = deltaX / Math.abs(deltaX); - } - if (shouldPan(leftGap)) { - mPanner.setHorizontalStrength(-getStrengthFromGap(leftGap)); - } - } else { - int rightGap = ownerBounds.right - (lp.x + mZoomRingWidth + zoomRingLeft); - if (rightGap < mPanGap) { - if (rightGap == 0 && deltaX != 0 && mMovingZoomRingOobX == 0) { - mMovingZoomRingOobX = deltaX / Math.abs(deltaX); - } - if (shouldPan(rightGap)) { - mPanner.setHorizontalStrength(getStrengthFromGap(rightGap)); - } - } else { - mPanner.setHorizontalStrength(0); - horizontalPanning = false; - } - } - - int topGap = newZoomRingY - ownerBounds.top; - if (topGap < mPanGap) { - if (topGap == 0 && deltaY != 0 && mMovingZoomRingOobY == 0) { - mMovingZoomRingOobY = deltaY / Math.abs(deltaY); - } - if (shouldPan(topGap)) { - mPanner.setVerticalStrength(-getStrengthFromGap(topGap)); - } - } else { - int bottomGap = ownerBounds.bottom - (lp.y + mZoomRingHeight + zoomRingTop); - if (bottomGap < mPanGap) { - if (bottomGap == 0 && deltaY != 0 && mMovingZoomRingOobY == 0) { - mMovingZoomRingOobY = deltaY / Math.abs(deltaY); - } - 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; - mPanningInitiated = false; - mPanner.stop(); - } - } - } - - return true; - } - - private boolean shouldPan(int gap) { - if (mPanningInitiated) return true; - - if (gap < mInitiatePanGap) { - long time = SystemClock.elapsedRealtime(); - if (mTouchingEdgeStartTime != 0 && - mTouchingEdgeStartTime + INITIATE_PAN_DELAY < time) { - mPanningInitiated = 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; - } - - /** @hide */ - public void onZoomRingMovingStopped() { - mPanner.stop(); - setPanningArrowsVisible(false); - if (mCallback != null) { - mCallback.onEndPan(); - } - } - - private int getStrengthFromGap(int gap) { - return gap > mPanGap ? 0 : - (mPanGap - gap) * 100 / mPanGap; - } - - /** @hide */ - public void onZoomRingThumbDraggingStarted() { - if (mCallback != null) { - mCallback.onBeginDrag(); - } - } - - /** @hide */ - public boolean onZoomRingThumbDragged(int numLevels, int startAngle, int curAngle) { - if (mCallback != null) { - int deltaZoomLevel = -numLevels; - - return mCallback.onDragZoom(deltaZoomLevel, - getZoomRingCenterXInOwnerCoordinates(), - getZoomRingCenterYInOwnerCoordinates(), - (float) startAngle / ZoomRing.RADIAN_INT_MULTIPLIER, - (float) curAngle / ZoomRing.RADIAN_INT_MULTIPLIER); - } - - return false; - } - - private int getZoomRingCenterXInOwnerCoordinates() { - int globalZoomCenterX = mContainerLayoutParams.x + mZoomRing.getLeft() + - mZoomRingWidth / 2; - return globalZoomCenterX - mOwnerViewBounds.left; - } - - private int getZoomRingCenterYInOwnerCoordinates() { - int globalZoomCenterY = mContainerLayoutParams.y + mZoomRing.getTop() + - mZoomRingHeight / 2; - return globalZoomCenterY - mOwnerViewBounds.top; - } - - /** @hide */ - public void onZoomRingThumbDraggingStopped() { - if (mCallback != null) { - mCallback.onEndDrag(); - } - } - - /** @hide */ - public void onZoomRingDismissed() { - mHandler.removeMessages(MSG_DISMISS_ZOOM_RING); - setVisible(false); - } - - /** @hide */ - public void onRingDown(int tickAngle, int touchAngle) { - } - - /** @hide */ - 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 { -// dismissZoomRingDelayed(ZOOM_CONTROLS_TIMEOUT); - if (action == MotionEvent.ACTION_DOWN) { - dismissZoomRingDelayed(OUTSIDE_TAP_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 = mZoomRing.getTrackOuterRadius(); - 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. - * - * @hide - */ - 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(INACTIVITY_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, - getZoomRingCenterXInOwnerCoordinates(), - getZoomRingCenterYInOwnerCoordinates()); - } - - return true; - } - - return false; - } - - private void onScrollerStep() { - if (!mScroller.computeScrollOffset() || !mIsZoomRingVisible) return; - - mContainerLayoutParams.x = mScroller.getCurrX(); - mContainerLayoutParams.y = mScroller.getCurrY(); - refreshContainerLayout(); - - mHandler.sendEmptyMessage(MSG_SCROLLER_STEP); - } - - private void onPostConfigurationChanged() { - dismissZoomRingDelayed(INACTIVITY_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. - * <p> - * 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(); - } - - LayoutInflater layoutInflater = - (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - TextView textView = (TextView) layoutInflater.inflate( - com.android.internal.R.layout.alert_dialog_simple_text, null) - .findViewById(android.R.id.text1); - textView.setText(com.android.internal.R.string.tutorial_double_tap_to_zoom_message_short); - - sTutorialDialog = new AlertDialog.Builder(context) - .setView(textView) - .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(); - } - - /** @hide Should only be used by Android platform apps */ - public static void finishZoomTutorial(Context context, boolean userNotified) { - if (sTutorialDialog == null) return; - - sTutorialDialog.dismiss(); - sTutorialDialog = null; - - // Record that they have seen the tutorial - if (userNotified) { - try { - Settings.System.putInt(context.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 = context.getSharedPreferences("_zoom", Context.MODE_PRIVATE); - sp.edit().putInt(SETTING_NAME_SHOWN_TOAST, 1).commit(); - } - } - } - - /** @hide Should only be used by Android platform apps */ - public void finishZoomTutorial() { - finishZoomTutorial(mContext, true); - } - - /** - * Sets the initial velocity of a pan. - * - * @param startVelocity The initial velocity to move the owner view, in - * pixels per second. - */ - public void setPannerStartVelocity(float startVelocity) { - mPanner.mStartVelocity = startVelocity; - } - - /** - * Sets the accelartion of the pan. - * - * @param acceleration The acceleration, in pixels per second squared. - */ - public void setPannerAcceleration(float acceleration) { - mPanner.mAcceleration = acceleration; - } - - /** - * Sets the maximum velocity of a pan. - * - * @param maxVelocity The max velocity to move the owner view, in pixels per - * second. - */ - public void setPannerMaxVelocity(float maxVelocity) { - mPanner.mMaxVelocity = maxVelocity; - } - - /** - * Sets the duration before acceleration will be applied. - * - * @param duration The duration, in milliseconds. - */ - 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; - } - - } - - /** - * Interface used to inform the client of zoom events that the user - * triggers. - */ - public interface OnZoomListener { - /** - * Called when the user begins dragging the thumb on the zoom ring. - */ - void onBeginDrag(); - - /** - * Called when the user drags the thumb and passes a tick causing a - * zoom. - * - * @param deltaZoomLevel The number of levels to be zoomed. Positive to - * zoom in, negative to zoom out. - * @param centerX The point about which to zoom. The zoom should pin - * this point, leaving it at the same coordinate. This is - * relative to the owner view's upper-left. - * @param centerY The point about which to zoom. The zoom should pin - * this point, leaving it at the same coordinate. This is - * relative to the owner view's upper-left. - * @param startAngle The angle where the user started dragging the thumb. - * @param curAngle The current angle of the thumb. - * @return Whether the owner was zoomed. - */ - boolean onDragZoom(int deltaZoomLevel, int centerX, int centerY, float startAngle, - float curAngle); - - /** - * Called when the user releases the thumb. - */ - void onEndDrag(); - - /** - * Called when the user zooms via some other mechanism, for example - * arrow keys or a trackball. - * - * @param zoomIn Whether to zoom in (true) or out (false). - * @param centerX See {@link #onDragZoom(int, int, int, float, float)}. - * @param centerY See {@link #onDragZoom(int, int, int, float, float)}. - */ - void onSimpleZoom(boolean zoomIn, int centerX, int centerY); - - /** - * Called when the user begins moving the zoom ring in order to pan the - * owner. - */ - void onBeginPan(); - - /** - * Called when the owner should pan as a result of the user moving the zoom ring. - * - * @param deltaX The amount to pan horizontally. - * @param deltaY The amount to pan vertically. - * @return Whether the owner was panned. - */ - boolean onPan(int deltaX, int deltaY); - - /** - * Called when the user releases the zoom ring. - */ - void onEndPan(); - - /** - * Called when the client should center the owner on the given point. - * - * @param x The x to center on, relative to the owner view's upper-left. - * @param y The y to center on, relative to the owner view's upper-left. - */ - void onCenter(int x, int y); - - /** - * Called when the zoom ring's visibility changes. - * - * @param visible Whether the zoom ring is visible (true) or not (false). - */ - void onVisibilityChanged(boolean visible); - } -} diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 9030a3e..adec0a7 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -43,7 +43,7 @@ interface IInputMethodManager { in ResultReceiver resultReceiver); boolean hideSoftInput(in IInputMethodClient client, int flags, in ResultReceiver resultReceiver); - void windowGainedFocus(in IInputMethodClient client, + void windowGainedFocus(in IInputMethodClient client, in IBinder windowToken, boolean viewHasFocus, boolean isTextEditor, int softInputMode, boolean first, int windowFlags); |