diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-12-17 18:05:43 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-12-17 18:05:43 -0800 |
commit | f013e1afd1e68af5e3b868c26a653bbfb39538f8 (patch) | |
tree | 7ad6c8fd9c7b55f4b4017171dec1cb760bbd26bf /core/java/android/inputmethodservice | |
parent | e70cfafe580c6f2994c4827cd8a534aabf3eb05c (diff) | |
download | frameworks_base-f013e1afd1e68af5e3b868c26a653bbfb39538f8.zip frameworks_base-f013e1afd1e68af5e3b868c26a653bbfb39538f8.tar.gz frameworks_base-f013e1afd1e68af5e3b868c26a653bbfb39538f8.tar.bz2 |
Code drop from //branches/cupcake/...@124589
Diffstat (limited to 'core/java/android/inputmethodservice')
9 files changed, 3344 insertions, 0 deletions
diff --git a/core/java/android/inputmethodservice/AbstractInputMethodService.java b/core/java/android/inputmethodservice/AbstractInputMethodService.java new file mode 100644 index 0000000..7d02f65 --- /dev/null +++ b/core/java/android/inputmethodservice/AbstractInputMethodService.java @@ -0,0 +1,170 @@ +/* + * 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.inputmethodservice; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.inputmethod.InputMethod; +import android.view.inputmethod.InputMethodSession; + +/** + * AbstractInputMethodService provides a abstract base class for input methods. + * Normal input method implementations will not derive from this directly, + * instead building on top of {@link InputMethodService} or another more + * complete base class. Be sure to read {@link InputMethod} for more + * information on the basics of writing input methods. + * + * <p>This class combines a Service (representing the input method component + * to the system with the InputMethod interface that input methods must + * implement. This base class takes care of reporting your InputMethod from + * the service when clients bind to it, but provides no standard implementation + * of the InputMethod interface itself. Derived classes must implement that + * interface. + */ +public abstract class AbstractInputMethodService extends Service + implements KeyEvent.Callback { + private InputMethod mInputMethod; + + /** + * Base class for derived classes to implement their {@link InputMethod} + * interface. This takes care of basic maintenance of the input method, + * but most behavior must be implemented in a derived class. + */ + public abstract class AbstractInputMethodImpl implements InputMethod { + /** + * Instantiate a new client session for the input method, by calling + * back to {@link AbstractInputMethodService#onCreateInputMethodSessionInterface() + * AbstractInputMethodService.onCreateInputMethodSessionInterface()}. + */ + public void createSession(SessionCallback callback) { + callback.sessionCreated(onCreateInputMethodSessionInterface()); + } + + /** + * Take care of enabling or disabling an existing session by calling its + * {@link AbstractInputMethodSessionImpl#revokeSelf() + * AbstractInputMethodSessionImpl.setEnabled()} method. + */ + public void setSessionEnabled(InputMethodSession session, boolean enabled) { + ((AbstractInputMethodSessionImpl)session).setEnabled(enabled); + } + + /** + * Take care of killing an existing session by calling its + * {@link AbstractInputMethodSessionImpl#revokeSelf() + * AbstractInputMethodSessionImpl.revokeSelf()} method. + */ + public void revokeSession(InputMethodSession session) { + ((AbstractInputMethodSessionImpl)session).revokeSelf(); + } + } + + /** + * Base class for derived classes to implement their {@link InputMethodSession} + * interface. This takes care of basic maintenance of the session, + * but most behavior must be implemented in a derived class. + */ + public abstract class AbstractInputMethodSessionImpl implements InputMethodSession { + boolean mEnabled = true; + boolean mRevoked; + + /** + * Check whether this session has been enabled by the system. If not + * enabled, you should not execute any calls on to it. + */ + public boolean isEnabled() { + return mEnabled; + } + + /** + * Check whether this session has been revoked by the system. Revoked + * session is also always disabled, so there is generally no need to + * explicitly check for this. + */ + public boolean isRevoked() { + return mRevoked; + } + + /** + * Change the enabled state of the session. This only works if the + * session has not been revoked. + */ + public void setEnabled(boolean enabled) { + if (!mRevoked) { + mEnabled = enabled; + } + } + + /** + * Revoke the session from the client. This disabled the session, and + * prevents it from ever being enabled again. + */ + public void revokeSelf() { + mRevoked = true; + mEnabled = false; + } + + /** + * Take care of dispatching incoming key events to the appropriate + * callbacks on the service, and tell the client when this is done. + */ + public void dispatchKeyEvent(int seq, KeyEvent event, EventCallback callback) { + boolean handled = event.dispatch(AbstractInputMethodService.this); + if (callback != null) { + callback.finishedEvent(seq, handled); + } + } + + /** + * Take care of dispatching incoming trackball events to the appropriate + * callbacks on the service, and tell the client when this is done. + */ + public void dispatchTrackballEvent(int seq, MotionEvent event, EventCallback callback) { + boolean handled = onTrackballEvent(event); + if (callback != null) { + callback.finishedEvent(seq, handled); + } + } + } + + /** + * Called by the framework during initialization, when the InputMethod + * interface for this service needs to be created. + */ + public abstract AbstractInputMethodImpl onCreateInputMethodInterface(); + + /** + * Called by the framework when a new InputMethodSession interface is + * needed for a new client of the input method. + */ + public abstract AbstractInputMethodSessionImpl onCreateInputMethodSessionInterface(); + + @Override + final public IBinder onBind(Intent intent) { + if (mInputMethod == null) { + mInputMethod = onCreateInputMethodInterface(); + } + return new IInputMethodWrapper(this, mInputMethod); + } + + public boolean onTrackballEvent(MotionEvent event) { + return false; + } +} diff --git a/core/java/android/inputmethodservice/ExtractEditText.java b/core/java/android/inputmethodservice/ExtractEditText.java new file mode 100644 index 0000000..e59f38b --- /dev/null +++ b/core/java/android/inputmethodservice/ExtractEditText.java @@ -0,0 +1,23 @@ +package android.inputmethodservice; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.EditText; + +/*** + * Specialization of {@link EditText} for showing and interacting with the + * extracted text in a full-screen input method. + */ +public class ExtractEditText extends EditText { + public ExtractEditText(Context context) { + super(context, null); + } + + public ExtractEditText(Context context, AttributeSet attrs) { + super(context, attrs, com.android.internal.R.attr.editTextStyle); + } + + public ExtractEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } +} diff --git a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java new file mode 100644 index 0000000..40c03cd --- /dev/null +++ b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java @@ -0,0 +1,151 @@ +package android.inputmethodservice; + +import com.android.internal.os.HandlerCaller; +import com.android.internal.view.IInputMethodCallback; +import com.android.internal.view.IInputMethodSession; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.InputMethodSession; +import android.view.inputmethod.EditorInfo; + +class IInputMethodSessionWrapper extends IInputMethodSession.Stub + implements HandlerCaller.Callback { + private static final String TAG = "InputMethodWrapper"; + private static final boolean DEBUG = false; + + private static final int DO_FINISH_INPUT = 60; + private static final int DO_DISPLAY_COMPLETIONS = 65; + private static final int DO_UPDATE_EXTRACTED_TEXT = 67; + private static final int DO_DISPATCH_KEY_EVENT = 70; + private static final int DO_DISPATCH_TRACKBALL_EVENT = 80; + private static final int DO_UPDATE_SELECTION = 90; + private static final int DO_UPDATE_CURSOR = 95; + private static final int DO_APP_PRIVATE_COMMAND = 100; + + final HandlerCaller mCaller; + final InputMethodSession mInputMethodSession; + + // NOTE: we should have a cache of these. + static class InputMethodEventCallbackWrapper implements InputMethodSession.EventCallback { + final IInputMethodCallback mCb; + InputMethodEventCallbackWrapper(IInputMethodCallback cb) { + mCb = cb; + } + public void finishedEvent(int seq, boolean handled) { + try { + mCb.finishedEvent(seq, handled); + } catch (RemoteException e) { + } + } + } + + public IInputMethodSessionWrapper(Context context, + InputMethodSession inputMethodSession) { + mCaller = new HandlerCaller(context, this); + mInputMethodSession = inputMethodSession; + } + + public InputMethodSession getInternalInputMethodSession() { + return mInputMethodSession; + } + + public void executeMessage(Message msg) { + switch (msg.what) { + case DO_FINISH_INPUT: + mInputMethodSession.finishInput(); + return; + case DO_DISPLAY_COMPLETIONS: + mInputMethodSession.displayCompletions((CompletionInfo[])msg.obj); + return; + case DO_UPDATE_EXTRACTED_TEXT: + mInputMethodSession.updateExtractedText(msg.arg1, + (ExtractedText)msg.obj); + return; + case DO_DISPATCH_KEY_EVENT: { + HandlerCaller.SomeArgs args = (HandlerCaller.SomeArgs)msg.obj; + mInputMethodSession.dispatchKeyEvent(msg.arg1, + (KeyEvent)args.arg1, + new InputMethodEventCallbackWrapper( + (IInputMethodCallback)args.arg2)); + mCaller.recycleArgs(args); + return; + } + case DO_DISPATCH_TRACKBALL_EVENT: { + HandlerCaller.SomeArgs args = (HandlerCaller.SomeArgs)msg.obj; + mInputMethodSession.dispatchTrackballEvent(msg.arg1, + (MotionEvent)args.arg1, + new InputMethodEventCallbackWrapper( + (IInputMethodCallback)args.arg2)); + mCaller.recycleArgs(args); + return; + } + case DO_UPDATE_SELECTION: { + HandlerCaller.SomeArgs args = (HandlerCaller.SomeArgs)msg.obj; + mInputMethodSession.updateSelection(args.argi1, args.argi2, + args.argi3, args.argi4); + mCaller.recycleArgs(args); + return; + } + case DO_UPDATE_CURSOR: { + mInputMethodSession.updateCursor((Rect)msg.obj); + return; + } + case DO_APP_PRIVATE_COMMAND: { + HandlerCaller.SomeArgs args = (HandlerCaller.SomeArgs)msg.obj; + mInputMethodSession.appPrivateCommand((String)args.arg1, + (Bundle)args.arg2); + mCaller.recycleArgs(args); + return; + } + } + Log.w(TAG, "Unhandled message code: " + msg.what); + } + + public void finishInput() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_FINISH_INPUT)); + } + + public void displayCompletions(CompletionInfo[] completions) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO( + DO_DISPLAY_COMPLETIONS, completions)); + } + + public void updateExtractedText(int token, ExtractedText text) { + mCaller.executeOrSendMessage(mCaller.obtainMessageIO( + DO_UPDATE_EXTRACTED_TEXT, token, text)); + } + + public void dispatchKeyEvent(int seq, KeyEvent event, IInputMethodCallback callback) { + mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_DISPATCH_KEY_EVENT, seq, + event, callback)); + } + + public void dispatchTrackballEvent(int seq, MotionEvent event, IInputMethodCallback callback) { + mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_DISPATCH_TRACKBALL_EVENT, seq, + event, callback)); + } + + public void updateSelection(int oldSelStart, int oldSelEnd, + int newSelStart, int newSelEnd) { + mCaller.executeOrSendMessage(mCaller.obtainMessageIIII(DO_UPDATE_SELECTION, + oldSelStart, oldSelEnd, newSelStart, newSelEnd)); + } + + public void updateCursor(Rect newCursor) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_UPDATE_CURSOR, + newCursor)); + } + + public void appPrivateCommand(String action, Bundle data) { + mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_APP_PRIVATE_COMMAND, action, data)); + } +} diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java new file mode 100644 index 0000000..4108bdd --- /dev/null +++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java @@ -0,0 +1,172 @@ +package android.inputmethodservice; + +import com.android.internal.os.HandlerCaller; +import com.android.internal.view.IInputContext; +import com.android.internal.view.IInputMethod; +import com.android.internal.view.IInputMethodCallback; +import com.android.internal.view.IInputMethodSession; +import com.android.internal.view.InputConnectionWrapper; + +import android.content.Context; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputBinding; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethod; +import android.view.inputmethod.InputMethodSession; + +/** + * Implements the internal IInputMethod interface to convert incoming calls + * on to it back to calls on the public InputMethod interface, scheduling + * them on the main thread of the process. + */ +class IInputMethodWrapper extends IInputMethod.Stub + implements HandlerCaller.Callback { + private static final String TAG = "InputMethodWrapper"; + private static final boolean DEBUG = false; + + private static final int DO_ATTACH_TOKEN = 10; + private static final int DO_SET_INPUT_CONTEXT = 20; + private static final int DO_UNSET_INPUT_CONTEXT = 30; + private static final int DO_START_INPUT = 32; + private static final int DO_RESTART_INPUT = 34; + private static final int DO_CREATE_SESSION = 40; + private static final int DO_SET_SESSION_ENABLED = 45; + private static final int DO_REVOKE_SESSION = 50; + private static final int DO_SHOW_SOFT_INPUT = 60; + private static final int DO_HIDE_SOFT_INPUT = 70; + + final HandlerCaller mCaller; + final InputMethod mInputMethod; + + // NOTE: we should have a cache of these. + static class InputMethodSessionCallbackWrapper implements InputMethod.SessionCallback { + final Context mContext; + final IInputMethodCallback mCb; + InputMethodSessionCallbackWrapper(Context context, IInputMethodCallback cb) { + mContext = context; + mCb = cb; + } + public void sessionCreated(InputMethodSession session) { + try { + if (session != null) { + IInputMethodSessionWrapper wrap = + new IInputMethodSessionWrapper(mContext, session); + mCb.sessionCreated(wrap); + } else { + mCb.sessionCreated(null); + } + } catch (RemoteException e) { + } + } + } + + public IInputMethodWrapper(Context context, InputMethod inputMethod) { + mCaller = new HandlerCaller(context, this); + mInputMethod = inputMethod; + } + + public InputMethod getInternalInputMethod() { + return mInputMethod; + } + + public void executeMessage(Message msg) { + switch (msg.what) { + case DO_ATTACH_TOKEN: { + mInputMethod.attachToken((IBinder)msg.obj); + return; + } + case DO_SET_INPUT_CONTEXT: { + mInputMethod.bindInput((InputBinding)msg.obj); + return; + } + case DO_UNSET_INPUT_CONTEXT: + mInputMethod.unbindInput(); + return; + case DO_START_INPUT: + mInputMethod.startInput((EditorInfo)msg.obj); + return; + case DO_RESTART_INPUT: + mInputMethod.restartInput((EditorInfo)msg.obj); + return; + case DO_CREATE_SESSION: { + mInputMethod.createSession(new InputMethodSessionCallbackWrapper( + mCaller.mContext, (IInputMethodCallback)msg.obj)); + return; + } + case DO_SET_SESSION_ENABLED: + mInputMethod.setSessionEnabled((InputMethodSession)msg.obj, + msg.arg1 != 0); + return; + case DO_REVOKE_SESSION: + mInputMethod.revokeSession((InputMethodSession)msg.obj); + return; + case DO_SHOW_SOFT_INPUT: + mInputMethod.showSoftInput(); + return; + case DO_HIDE_SOFT_INPUT: + mInputMethod.hideSoftInput(); + return; + } + Log.w(TAG, "Unhandled message code: " + msg.what); + } + + public void attachToken(IBinder token) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_ATTACH_TOKEN, token)); + } + + public void bindInput(InputBinding binding) { + InputConnection ic = new InputConnectionWrapper( + IInputContext.Stub.asInterface(binding.getConnectionToken())); + InputBinding nu = new InputBinding(ic, binding); + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_INPUT_CONTEXT, nu)); + } + + public void unbindInput() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_UNSET_INPUT_CONTEXT)); + } + + public void startInput(EditorInfo attribute) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_START_INPUT, attribute)); + } + + public void restartInput(EditorInfo attribute) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_RESTART_INPUT, attribute)); + } + + public void createSession(IInputMethodCallback callback) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_CREATE_SESSION, callback)); + } + + public void setSessionEnabled(IInputMethodSession session, boolean enabled) { + try { + InputMethodSession ls = ((IInputMethodSessionWrapper) + session).getInternalInputMethodSession(); + mCaller.executeOrSendMessage(mCaller.obtainMessageIO( + DO_SET_SESSION_ENABLED, enabled ? 1 : 0, ls)); + } catch (ClassCastException e) { + Log.w(TAG, "Incoming session not of correct type: " + session, e); + } + } + + public void revokeSession(IInputMethodSession session) { + try { + InputMethodSession ls = ((IInputMethodSessionWrapper) + session).getInternalInputMethodSession(); + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_REVOKE_SESSION, ls)); + } catch (ClassCastException e) { + Log.w(TAG, "Incoming session not of correct type: " + session, e); + } + } + + public void showSoftInput() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_SHOW_SOFT_INPUT)); + } + + public void hideSoftInput() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_HIDE_SOFT_INPUT)); + } +} diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java new file mode 100644 index 0000000..9ebf127 --- /dev/null +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -0,0 +1,864 @@ +/* + * 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.inputmethodservice; + +import static android.view.ViewGroup.LayoutParams.FILL_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import android.app.Dialog; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputBinding; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethod; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.EditorInfo; +import android.widget.FrameLayout; + +/** + * InputMethodService provides a standard implementation of an InputMethod, + * which final implementations can derive from and customize. See the + * base class {@link AbstractInputMethodService} and the {@link InputMethod} + * interface for more information on the basics of writing input methods. + */ +public class InputMethodService extends AbstractInputMethodService { + static final String TAG = "InputMethodService"; + static final boolean DEBUG = false; + + LayoutInflater mInflater; + View mRootView; + SoftInputWindow mWindow; + boolean mWindowCreated; + boolean mWindowAdded; + boolean mWindowVisible; + FrameLayout mExtractFrame; + FrameLayout mCandidatesFrame; + FrameLayout mInputFrame; + + IBinder mToken; + + InputBinding mInputBinding; + InputConnection mInputConnection; + boolean mInputStarted; + EditorInfo mInputInfo; + + boolean mShowInputRequested; + boolean mShowCandidatesRequested; + + boolean mFullscreenApplied; + boolean mIsFullscreen; + View mExtractView; + ExtractEditText mExtractEditText; + ExtractedText mExtractedText; + int mExtractedToken; + + View mInputView; + boolean mIsInputViewShown; + + int mStatusIcon; + + final Insets mTmpInsets = new Insets(); + final int[] mTmpLocation = new int[2]; + + final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = + new ViewTreeObserver.OnComputeInternalInsetsListener() { + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) { + if (isFullscreenMode()) { + // In fullscreen mode, we just say the window isn't covering + // any content so we don't impact whatever is behind. + View decor = getWindow().getWindow().getDecorView(); + info.contentInsets.top = info.visibleInsets.top + = decor.getHeight(); + info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); + } else { + onComputeInsets(mTmpInsets); + info.contentInsets.top = mTmpInsets.contentTopInsets; + info.visibleInsets.top = mTmpInsets.visibleTopInsets; + info.setTouchableInsets(mTmpInsets.touchableInsets); + } + } + }; + + /** + * Concrete implementation of + * {@link AbstractInputMethodService.AbstractInputMethodImpl} that provides + * all of the standard behavior for an input method. + */ + public class InputMethodImpl extends AbstractInputMethodImpl { + /** + * Take care of attaching the given window token provided by the system. + */ + public void attachToken(IBinder token) { + if (mToken == null) { + mToken = token; + mWindow.setToken(token); + } + } + + /** + * Handle a new input binding, calling + * {@link InputMethodService#onBindInput InputMethodService.onBindInput()} + * when done. + */ + public void bindInput(InputBinding binding) { + mInputBinding = binding; + mInputConnection = binding.getConnection(); + onBindInput(); + } + + /** + * Clear the current input binding. + */ + public void unbindInput() { + mInputStarted = false; + mInputBinding = null; + mInputConnection = null; + } + + public void startInput(EditorInfo attribute) { + doStartInput(attribute, false); + } + + public void restartInput(EditorInfo attribute) { + doStartInput(attribute, false); + } + + /** + * Handle a request by the system to hide the soft input area. + */ + public void hideSoftInput() { + if (DEBUG) Log.v(TAG, "hideSoftInput()"); + mShowInputRequested = false; + hideWindow(); + } + + /** + * Handle a request by the system to show the soft input area. + */ + public void showSoftInput() { + if (DEBUG) Log.v(TAG, "showSoftInput()"); + showWindow(true); + } + } + + /** + * Concrete implementation of + * {@link AbstractInputMethodService.AbstractInputMethodSessionImpl} that provides + * all of the standard behavior for an input method session. + */ + public class InputMethodSessionImpl extends AbstractInputMethodSessionImpl { + public void finishInput() { + if (!isEnabled()) { + return; + } + onFinishInput(); + mInputStarted = false; + } + + /** + * Call {@link InputMethodService#onDisplayCompletions + * InputMethodService.onDisplayCompletions()}. + */ + public void displayCompletions(CompletionInfo[] completions) { + if (!isEnabled()) { + return; + } + onDisplayCompletions(completions); + } + + /** + * Call {@link InputMethodService#onUpdateExtractedText + * InputMethodService.onUpdateExtractedText()}. + */ + public void updateExtractedText(int token, ExtractedText text) { + if (!isEnabled()) { + return; + } + onUpdateExtractedText(token, text); + } + + /** + * Call {@link InputMethodService#onUpdateSelection + * InputMethodService.onUpdateSelection()}. + */ + public void updateSelection(int oldSelStart, int oldSelEnd, + int newSelStart, int newSelEnd) { + if (!isEnabled()) { + return; + } + InputMethodService.this.onUpdateSelection(oldSelStart, oldSelEnd, + newSelStart, newSelEnd); + } + + /** + * Call {@link InputMethodService#onUpdateCursor + * InputMethodService.onUpdateCursor()}. + */ + public void updateCursor(Rect newCursor) { + if (!isEnabled()) { + return; + } + InputMethodService.this.onUpdateCursor(newCursor); + } + + /** + * Call {@link InputMethodService#onAppPrivateCommand + * InputMethodService.onAppPrivateCommand()}. + */ + public void appPrivateCommand(String action, Bundle data) { + if (!isEnabled()) { + return; + } + InputMethodService.this.onAppPrivateCommand(action, data); + } + } + + /** + * Information about where interesting parts of the input method UI appear. + */ + public static final class Insets { + /** + * This is the top part of the UI that is the main content. It is + * used to determine the basic space needed, to resize/pan the + * application behind. It is assumed that this inset does not + * change very much, since any change will cause a full resize/pan + * of the application behind. This value is relative to the top edge + * of the input method window. + */ + int contentTopInsets; + + /** + * This is the top part of the UI that is visibly covering the + * application behind it. This provides finer-grained control over + * visibility, allowing you to change it relatively frequently (such + * as hiding or showing candidates) without disrupting the underlying + * UI too much. For example, this will never resize the application + * UI, will only pan if needed to make the current focus visible, and + * will not aggressively move the pan position when this changes unless + * needed to make the focus visible. This value is relative to the top edge + * of the input method window. + */ + int visibleTopInsets; + + /** + * Option for {@link #touchableInsets}: the entire window frame + * can be touched. + */ + public static final int TOUCHABLE_INSETS_FRAME + = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME; + + /** + * Option for {@link #touchableInsets}: the area inside of + * the content insets can be touched. + */ + public static final int TOUCHABLE_INSETS_CONTENT + = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT; + + /** + * Option for {@link #touchableInsets}: the area inside of + * the visible insets can be touched. + */ + public static final int TOUCHABLE_INSETS_VISIBLE + = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_VISIBLE; + + /** + * Determine which area of the window is touchable by the user. May + * be one of: {@link #TOUCHABLE_INSETS_FRAME}, + * {@link #TOUCHABLE_INSETS_CONTENT}, or {@link #TOUCHABLE_INSETS_VISIBLE}. + */ + public int touchableInsets; + } + + @Override public void onCreate() { + super.onCreate(); + mInflater = (LayoutInflater)getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + mWindow = new SoftInputWindow(this); + initViews(); + } + + void initViews() { + mWindowVisible = false; + mWindowCreated = false; + mShowInputRequested = false; + mShowCandidatesRequested = false; + + mRootView = mInflater.inflate( + com.android.internal.R.layout.input_method, null); + mWindow.setContentView(mRootView); + mRootView.getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsComputer); + + mExtractFrame = (FrameLayout)mRootView.findViewById(android.R.id.extractArea); + mExtractView = null; + mExtractEditText = null; + mFullscreenApplied = false; + + mCandidatesFrame = (FrameLayout)mRootView.findViewById(android.R.id.candidatesArea); + mInputFrame = (FrameLayout)mRootView.findViewById(android.R.id.inputArea); + mInputView = null; + mIsInputViewShown = false; + + mExtractFrame.setVisibility(View.GONE); + mCandidatesFrame.setVisibility(View.GONE); + mInputFrame.setVisibility(View.GONE); + } + + @Override public void onDestroy() { + super.onDestroy(); + mRootView.getViewTreeObserver().removeOnComputeInternalInsetsListener( + mInsetsComputer); + if (mWindowAdded) { + mWindow.dismiss(); + } + } + + /** + * Implement to return our standard {@link InputMethodImpl}. Subclasses + * can override to provide their own customized version. + */ + public AbstractInputMethodImpl onCreateInputMethodInterface() { + return new InputMethodImpl(); + } + + /** + * Implement to return our standard {@link InputMethodSessionImpl}. Subclasses + * can override to provide their own customized version. + */ + public AbstractInputMethodSessionImpl onCreateInputMethodSessionInterface() { + return new InputMethodSessionImpl(); + } + + public LayoutInflater getLayoutInflater() { + return mInflater; + } + + public Dialog getWindow() { + return mWindow; + } + + /** + * Return the currently active InputBinding for the input method, or + * null if there is none. + */ + public InputBinding getCurrentInputBinding() { + return mInputBinding; + } + + /** + * Retrieve the currently active InputConnection that is bound to + * the input method, or null if there is none. + */ + public InputConnection getCurrentInputConnection() { + return mInputConnection; + } + + public boolean getCurrentInputStarted() { + return mInputStarted; + } + + public EditorInfo getCurrentInputInfo() { + return mInputInfo; + } + + /** + * Re-evaluate whether the input method should be running in fullscreen + * mode, and update its UI if this has changed since the last time it + * was evaluated. This will call {@link #onEvaluateFullscreenMode()} to + * determine whether it should currently run in fullscreen mode. You + * can use {@link #isFullscreenMode()} to determine if the input method + * is currently running in fullscreen mode. + */ + public void updateFullscreenMode() { + boolean isFullscreen = onEvaluateFullscreenMode(); + if (mIsFullscreen != isFullscreen || !mFullscreenApplied) { + mIsFullscreen = isFullscreen; + mFullscreenApplied = true; + mWindow.getWindow().setBackgroundDrawable( + onCreateBackgroundDrawable()); + mExtractFrame.setVisibility(isFullscreen ? View.VISIBLE : View.GONE); + if (isFullscreen) { + if (mExtractView == null) { + View v = onCreateExtractTextView(); + if (v != null) { + setExtractView(v); + } + } + startExtractingText(); + mWindow.getWindow().setLayout(FILL_PARENT, FILL_PARENT); + } else { + mWindow.getWindow().setLayout(WRAP_CONTENT, WRAP_CONTENT); + } + } + } + + /** + * Return whether the input method is <em>currently</em> running in + * fullscreen mode. This is the mode that was last determined and + * applied by {@link #updateFullscreenMode()}. + */ + public boolean isFullscreenMode() { + return mIsFullscreen; + } + + /** + * Override this to control when the input method should run in + * fullscreen mode. The default implementation runs in fullsceen only + * when the screen is in landscape mode and the input view is being + * shown ({@link #onEvaluateInputViewShown} returns true). If you change what + * this returns, you will need to call {@link #updateFullscreenMode()} + * yourself whenever the returned value may have changed to have it + * re-evaluated and applied. + */ + public boolean onEvaluateFullscreenMode() { + Configuration config = getResources().getConfiguration(); + return config.orientation == Configuration.ORIENTATION_LANDSCAPE + && onEvaluateInputViewShown(); + } + + /** + * Compute the interesting insets into your UI. The default implementation + * uses the top of the candidates frame for the visible insets, and the + * top of the input frame for the content insets. The default touchable + * insets are {@link Insets#TOUCHABLE_INSETS_VISIBLE}. + * + * <p>Note that this method is not called when in fullscreen mode, since + * in that case the application is left as-is behind the input method and + * not impacted by anything in its UI. + * + * @param outInsets Fill in with the current UI insets. + */ + public void onComputeInsets(Insets outInsets) { + int[] loc = mTmpLocation; + if (mInputFrame.getVisibility() == View.VISIBLE) { + mInputFrame.getLocationInWindow(loc); + outInsets.contentTopInsets = loc[1]; + } + if (mCandidatesFrame.getVisibility() == View.VISIBLE) { + mCandidatesFrame.getLocationInWindow(loc); + outInsets.visibleTopInsets = loc[1]; + } else { + outInsets.visibleTopInsets = loc[1]; + } + outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_VISIBLE; + } + + /** + * Re-evaluate whether the soft input area should currently be shown, and + * update its UI if this has changed since the last time it + * was evaluated. This will call {@link #onEvaluateInputViewShown()} to + * determine whether the input view should currently be shown. You + * can use {@link #isInputViewShown()} to determine if the input view + * is currently shown. + */ + public void updateInputViewShown() { + boolean isShown = onEvaluateInputViewShown(); + if (mIsInputViewShown != isShown && mWindowVisible) { + mIsInputViewShown = isShown; + mInputFrame.setVisibility(isShown ? View.VISIBLE : View.GONE); + if (mInputView == null) { + View v = onCreateInputView(); + if (v != null) { + setInputView(v); + } + } + } + } + + /** + * Return whether the soft input view is <em>currently</em> shown to the + * user. This is the state that was last determined and + * applied by {@link #updateInputViewShown()}. + */ + public boolean isInputViewShown() { + return mIsInputViewShown; + } + + /** + * Override this to control when the soft input area should be shown to + * the user. The default implementation only shows the input view when + * there is no hard keyboard or the keyboard is hidden. If you change what + * this returns, you will need to call {@link #updateInputViewShown()} + * yourself whenever the returned value may have changed to have it + * re-evalauted and applied. + */ + public boolean onEvaluateInputViewShown() { + Configuration config = getResources().getConfiguration(); + return config.keyboard == Configuration.KEYBOARD_NOKEYS + || config.hardKeyboardHidden == Configuration.KEYBOARDHIDDEN_YES; + } + + /** + * Controls the visibility of the candidates display area. By default + * it is hidden. + */ + public void setCandidatesViewShown(boolean shown) { + if (mShowCandidatesRequested != shown) { + mCandidatesFrame.setVisibility(shown ? View.VISIBLE : View.INVISIBLE); + if (!mShowInputRequested) { + // If we are being asked to show the candidates view while the app + // has not asked for the input view to be shown, then we need + // to update whether the window is shown. + if (shown) { + showWindow(false); + } else { + hideWindow(); + } + } + mShowCandidatesRequested = shown; + } + } + + public void setStatusIcon(int iconResId) { + mStatusIcon = iconResId; + if (mInputConnection != null && mWindowVisible) { + mInputConnection.showStatusIcon(getPackageName(), iconResId); + } + } + + /** + * Force switch to a new input method, as identified by <var>id</var>. This + * input method will be destroyed, and the requested one started on the + * current input field. + * + * @param id Unique identifier of the new input method ot start. + */ + public void switchInputMethod(String id) { + ((InputMethodManager)getSystemService(INPUT_METHOD_SERVICE)) + .setInputMethod(mToken, id); + } + + public void setExtractView(View view) { + mExtractFrame.removeAllViews(); + mExtractFrame.addView(view, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + mExtractView = view; + if (view != null) { + mExtractEditText = (ExtractEditText)view.findViewById( + com.android.internal.R.id.inputExtractEditText); + startExtractingText(); + } else { + mExtractEditText = null; + } + } + + /** + * Replaces the current candidates view with a new one. You only need to + * call this when dynamically changing the view; normally, you should + * implement {@link #onCreateCandidatesView()} and create your view when + * first needed by the input method. + */ + public void setCandidatesView(View view) { + mCandidatesFrame.removeAllViews(); + mCandidatesFrame.addView(view, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + } + + /** + * Replaces the current input view with a new one. You only need to + * call this when dynamically changing the view; normally, you should + * implement {@link #onCreateInputView()} and create your view when + * first needed by the input method. + */ + public void setInputView(View view) { + mInputFrame.removeAllViews(); + mInputFrame.addView(view, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + mInputView = view; + } + + /** + * Called by the framework to create a Drawable for the background of + * the input method window. May return null for no background. The default + * implementation returns a non-null standard background only when in + * fullscreen mode. + */ + public Drawable onCreateBackgroundDrawable() { + if (isFullscreenMode()) { + return getResources().getDrawable( + com.android.internal.R.drawable.input_method_fullscreen_background); + } + return null; + } + + /** + * Called by the framework to create the layout for showing extacted text. + * Only called when in fullscreen mode. The returned view hierarchy must + * have an {@link ExtractEditText} whose ID is + * {@link android.R.id#inputExtractEditText}. + */ + public View onCreateExtractTextView() { + return mInflater.inflate( + com.android.internal.R.layout.input_method_extract_view, null); + } + + /** + * Create and return the view hierarchy used to show candidates. This will + * be called once, when the candidates are first displayed. You can return + * null to have no candidates view; the default implementation returns null. + * + * <p>To control when the candidates view is displayed, use + * {@link #setCandidatesViewShown(boolean)}. + * To change the candidates view after the first one is created by this + * function, use {@link #setCandidatesView(View)}. + */ + public View onCreateCandidatesView() { + return null; + } + + /** + * Create and return the view hierarchy used for the input area (such as + * a soft keyboard). This will be called once, when the input area is + * first displayed. You can return null to have no input area; the default + * implementation returns null. + * + * <p>To control when the input view is displayed, implement + * {@link #onEvaluateInputViewShown()}. + * To change the input view after the first one is created by this + * function, use {@link #setInputView(View)}. + */ + public View onCreateInputView() { + return null; + } + + /** + * Called when an input session is starting or restarting. + * + * @param info Description of the type of text being edited. + * @param restarting Set to true if we are restarting input on the + * same text field as before. + */ + public void onStartInputView(EditorInfo info, boolean restarting) { + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + boolean visible = mWindowVisible; + boolean showingInput = mShowInputRequested; + boolean showingCandidates = mShowCandidatesRequested; + initViews(); + if (visible) { + if (showingCandidates) { + setCandidatesViewShown(true); + } + showWindow(showingInput); + } + } + + public void showWindow(boolean showInput) { + if (DEBUG) Log.v(TAG, "Showing window: showInput=" + showInput + + " mShowInputRequested=" + mShowInputRequested + + " mWindowAdded=" + mWindowAdded + + " mWindowCreated=" + mWindowCreated + + " mWindowVisible=" + mWindowVisible + + " mInputStarted=" + mInputStarted); + boolean doShowInput = false; + boolean wasVisible = mWindowVisible; + mWindowVisible = true; + if (!mShowInputRequested) { + doShowInput = true; + mShowInputRequested = true; + } else { + showInput = true; + } + + if (doShowInput) { + if (DEBUG) Log.v(TAG, "showWindow: updating UI"); + updateFullscreenMode(); + updateInputViewShown(); + } + + if (!mWindowAdded || !mWindowCreated) { + mWindowAdded = true; + mWindowCreated = true; + View v = onCreateCandidatesView(); + if (DEBUG) Log.v(TAG, "showWindow: candidates=" + v); + if (v != null) { + setCandidatesView(v); + } + } + if (doShowInput) { + if (mInputStarted) { + if (DEBUG) Log.v(TAG, "showWindow: starting input view"); + onStartInputView(mInputInfo, false); + } + startExtractingText(); + } + + if (!wasVisible) { + if (DEBUG) Log.v(TAG, "showWindow: showing!"); + mWindow.show(); + if (mInputConnection != null) { + mInputConnection.showStatusIcon(getPackageName(), mStatusIcon); + } + } + } + + public void hideWindow() { + if (mWindowVisible) { + mWindow.hide(); + mWindowVisible = false; + if (mInputConnection != null) { + mInputConnection.hideStatusIcon(); + } + } + } + + public void onBindInput() { + } + + public void onStartInput(EditorInfo attribute, boolean restarting) { + } + + void doStartInput(EditorInfo attribute, boolean restarting) { + mInputStarted = true; + mInputInfo = attribute; + onStartInput(attribute, restarting); + if (mWindowVisible) { + if (mWindowCreated) { + onStartInputView(mInputInfo, restarting); + } + startExtractingText(); + } + } + + public void onFinishInput() { + } + + /** + * Called when the application has reported auto-completion candidates that + * it would like to have the input method displayed. Typically these are + * only used when an input method is running in full-screen mode, since + * otherwise the user can see and interact with the pop-up window of + * completions shown by the application. + * + * <p>The default implementation here does nothing. + */ + public void onDisplayCompletions(CompletionInfo[] completions) { + } + + /** + * Called when the application has reported new extracted text to be shown + * due to changes in its current text state. The default implementation + * here places the new text in the extract edit text, when the input + * method is running in fullscreen mode. + */ + public void onUpdateExtractedText(int token, ExtractedText text) { + if (mExtractedToken != token) { + return; + } + if (mExtractEditText != null && text != null) { + mExtractedText = text; + mExtractEditText.setExtractedText(text); + } + } + + /** + * Called when the application has reported a new selection region of + * the text. This is called whether or not the input method has requested + * extracted text updates, although if so it will not receive this call + * if the extracted text has changed as well. + * + * <p>The default implementation takes care of updating the cursor in + * the extract text, if it is being shown. + */ + public void onUpdateSelection(int oldSelStart, int oldSelEnd, + int newSelStart, int newSelEnd) { + if (mExtractEditText != null && mExtractedText != null) { + final int off = mExtractedText.startOffset; + mExtractEditText.setSelection(newSelStart-off, newSelEnd-off); + } + } + + /** + * Called when the application has reported a new location of its text + * cursor. This is only called if explicitly requested by the input method. + * The default implementation does nothing. + */ + public void onUpdateCursor(Rect newCursor) { + } + + /** + * Close this input method's soft input area, removing it from the display. + * The input method will continue running, but the user can no longer use + * it to generate input by touching the screen. + */ + public void dismissSoftInput() { + ((InputMethodManager)getSystemService(INPUT_METHOD_SERVICE)) + .hideSoftInputFromInputMethod(mToken); + } + + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (mWindowVisible && event.getKeyCode() == KeyEvent.KEYCODE_BACK + && event.getRepeatCount() == 0) { + dismissSoftInput(); + return true; + } + return false; + } + + public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { + return false; + } + + public boolean onKeyUp(int keyCode, KeyEvent event) { + return false; + } + + public boolean onTrackballEvent(MotionEvent event) { + return false; + } + + public void onAppPrivateCommand(String action, Bundle data) { + } + + void startExtractingText() { + if (mExtractEditText != null && getCurrentInputStarted() + && isFullscreenMode()) { + mExtractedToken++; + ExtractedTextRequest req = new ExtractedTextRequest(); + req.token = mExtractedToken; + req.hintMaxLines = 10; + req.hintMaxChars = 10000; + mExtractedText = mInputConnection.getExtractedText(req, + InputConnection.EXTRACTED_TEXT_MONITOR); + if (mExtractedText != null) { + mExtractEditText.setExtractedText(mExtractedText); + } + mExtractEditText.setInputType(getCurrentInputInfo().inputType); + mExtractEditText.setHint(mInputInfo.hintText); + } + } +} diff --git a/core/java/android/inputmethodservice/Keyboard.java b/core/java/android/inputmethodservice/Keyboard.java new file mode 100755 index 0000000..75a2911 --- /dev/null +++ b/core/java/android/inputmethodservice/Keyboard.java @@ -0,0 +1,756 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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.inputmethodservice; + +import org.xmlpull.v1.XmlPullParserException; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.Log; +import android.util.TypedValue; +import android.util.Xml; +import android.view.Display; +import android.view.WindowManager; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + + +/** + * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard + * consists of rows of keys. + * <p>The layout file for a keyboard contains XML that looks like the following snippet:</p> + * <pre> + * <Keyboard + * android:keyWidth="%10p" + * android:keyHeight="50px" + * android:horizontalGap="2px" + * android:verticalGap="2px" > + * <Row android:keyWidth="32px" > + * <Key android:keyLabel="A" /> + * ... + * </Row> + * ... + * </Keyboard> + * </pre> + * @attr ref android.R.styleable#Keyboard_keyWidth + * @attr ref android.R.styleable#Keyboard_keyHeight + * @attr ref android.R.styleable#Keyboard_horizontalGap + * @attr ref android.R.styleable#Keyboard_verticalGap + */ +public class Keyboard { + + static final String TAG = "Keyboard"; + + // Keyboard XML Tags + private static final String TAG_KEYBOARD = "Keyboard"; + private static final String TAG_ROW = "Row"; + private static final String TAG_KEY = "Key"; + + public static final int EDGE_LEFT = 0x01; + public static final int EDGE_RIGHT = 0x02; + public static final int EDGE_TOP = 0x04; + public static final int EDGE_BOTTOM = 0x08; + + public static final int KEYCODE_SHIFT = -1; + public static final int KEYCODE_MODE_CHANGE = -2; + public static final int KEYCODE_CANCEL = -3; + public static final int KEYCODE_DONE = -4; + public static final int KEYCODE_DELETE = -5; + public static final int KEYCODE_ALT = -6; + + /** Keyboard label **/ + private CharSequence mLabel; + + /** Horizontal gap default for all rows */ + private int mDefaultHorizontalGap; + + /** Default key width */ + private int mDefaultWidth; + + /** Default key height */ + private int mDefaultHeight; + + /** Default gap between rows */ + private int mDefaultVerticalGap; + + /** Is the keyboard in the shifted state */ + private boolean mShifted; + + /** Key instance for the shift key, if present */ + private Key mShiftKey; + + /** Key index for the shift key, if present */ + private int mShiftKeyIndex = -1; + + /** Current key width, while loading the keyboard */ + private int mKeyWidth; + + /** Current key height, while loading the keyboard */ + private int mKeyHeight; + + /** Total height of the keyboard, including the padding and keys */ + private int mTotalHeight; + + /** + * Total width of the keyboard, including left side gaps and keys, but not any gaps on the + * right side. + */ + private int mTotalWidth; + + /** List of keys in this keyboard */ + private List<Key> mKeys; + + /** List of modifier keys such as Shift & Alt, if any */ + private List<Key> mModifierKeys; + + /** Width of the screen available to fit the keyboard */ + private int mDisplayWidth; + + /** Height of the screen */ + private int mDisplayHeight; + + /** Keyboard mode, or zero, if none. */ + private int mKeyboardMode; + + /** + * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate. + * Some of the key size defaults can be overridden per row from what the {@link Keyboard} + * defines. + * @attr ref android.R.styleable#Keyboard_keyWidth + * @attr ref android.R.styleable#Keyboard_keyHeight + * @attr ref android.R.styleable#Keyboard_horizontalGap + * @attr ref android.R.styleable#Keyboard_verticalGap + * @attr ref android.R.styleable#Keyboard_Row_rowEdgeFlags + * @attr ref android.R.styleable#Keyboard_Row_keyboardMode + */ + public static class Row { + /** Default width of a key in this row. */ + public int defaultWidth; + /** Default height of a key in this row. */ + public int defaultHeight; + /** Default horizontal gap between keys in this row. */ + public int defaultHorizontalGap; + /** Vertical gap following this row. */ + public int verticalGap; + /** + * Edge flags for this row of keys. Possible values that can be assigned are + * {@link Keyboard#EDGE_TOP EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM EDGE_BOTTOM} + */ + public int rowEdgeFlags; + + /** The keyboard mode for this row */ + public int mode; + + private Keyboard parent; + + public Row(Keyboard parent) { + this.parent = parent; + } + + public Row(Resources res, Keyboard parent, XmlResourceParser parser) { + this.parent = parent; + TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), + com.android.internal.R.styleable.Keyboard); + defaultWidth = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyWidth, + parent.mDisplayWidth, parent.mDefaultWidth); + defaultHeight = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyHeight, + parent.mDisplayWidth, parent.mDefaultHeight); + defaultHorizontalGap = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_horizontalGap, + parent.mDisplayWidth, parent.mDefaultHorizontalGap); + verticalGap = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_verticalGap, + parent.mDisplayWidth, parent.mDefaultVerticalGap); + a.recycle(); + a = res.obtainAttributes(Xml.asAttributeSet(parser), + com.android.internal.R.styleable.Keyboard_Row); + rowEdgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Row_rowEdgeFlags, 0); + mode = a.getResourceId(com.android.internal.R.styleable.Keyboard_Row_keyboardMode, + 0); + } + } + + /** + * Class for describing the position and characteristics of a single key in the keyboard. + * + * @attr ref android.R.styleable#Keyboard_keyWidth + * @attr ref android.R.styleable#Keyboard_keyHeight + * @attr ref android.R.styleable#Keyboard_horizontalGap + * @attr ref android.R.styleable#Keyboard_Key_codes + * @attr ref android.R.styleable#Keyboard_Key_keyIcon + * @attr ref android.R.styleable#Keyboard_Key_keyLabel + * @attr ref android.R.styleable#Keyboard_Key_iconPreview + * @attr ref android.R.styleable#Keyboard_Key_isSticky + * @attr ref android.R.styleable#Keyboard_Key_isRepeatable + * @attr ref android.R.styleable#Keyboard_Key_isModifier + * @attr ref android.R.styleable#Keyboard_Key_popupKeyboard + * @attr ref android.R.styleable#Keyboard_Key_popupCharacters + * @attr ref android.R.styleable#Keyboard_Key_keyOutputText + * @attr ref android.R.styleable#Keyboard_Key_keyEdgeFlags + */ + public static class Key { + /** + * All the key codes (unicode or custom code) that this key could generate, zero'th + * being the most important. + */ + public int[] codes; + + /** Label to display */ + public CharSequence label; + + /** Icon to display instead of a label. Icon takes precedence over a label */ + public Drawable icon; + /** Preview version of the icon, for the preview popup */ + public Drawable iconPreview; + /** Width of the key, not including the gap */ + public int width; + /** Height of the key, not including the gap */ + public int height; + /** The horizontal gap before this key */ + public int gap; + /** Whether this key is sticky, i.e., a toggle key */ + public boolean sticky; + /** X coordinate of the key in the keyboard layout */ + public int x; + /** Y coordinate of the key in the keyboard layout */ + public int y; + /** The current pressed state of this key */ + public boolean pressed; + /** If this is a sticky key, is it on? */ + public boolean on; + /** Text to output when pressed. This can be multiple characters, like ".com" */ + public CharSequence text; + /** Popup characters */ + public CharSequence popupCharacters; + + /** + * Flags that specify the anchoring to edges of the keyboard for detecting touch events + * that are just out of the boundary of the key. This is a bit mask of + * {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT}, {@link Keyboard#EDGE_TOP} and + * {@link Keyboard#EDGE_BOTTOM}. + */ + public int edgeFlags; + /** Whether this is a modifier key, such as Shift or Alt */ + public boolean modifier; + /** The keyboard that this key belongs to */ + private Keyboard keyboard; + /** + * If this key pops up a mini keyboard, this is the resource id for the XML layout for that + * keyboard. + */ + public int popupResId; + /** Whether this key repeats itself when held down */ + public boolean repeatable; + + + private final static int[] KEY_STATE_NORMAL_ON = { + android.R.attr.state_checkable, + android.R.attr.state_checked + }; + + private final static int[] KEY_STATE_PRESSED_ON = { + android.R.attr.state_pressed, + android.R.attr.state_checkable, + android.R.attr.state_checked + }; + + private final static int[] KEY_STATE_NORMAL_OFF = { + android.R.attr.state_checkable + }; + + private final static int[] KEY_STATE_PRESSED_OFF = { + android.R.attr.state_pressed, + android.R.attr.state_checkable + }; + + private final static int[] KEY_STATE_NORMAL = { + }; + + private final static int[] KEY_STATE_PRESSED = { + android.R.attr.state_pressed + }; + + /** Create an empty key with no attributes. */ + public Key(Row parent) { + keyboard = parent.parent; + } + + /** Create a key with the given top-left coordinate and extract its attributes from + * the XML parser. + * @param res resources associated with the caller's context + * @param parent the row that this key belongs to. The row must already be attached to + * a {@link Keyboard}. + * @param x the x coordinate of the top-left + * @param y the y coordinate of the top-left + * @param parser the XML parser containing the attributes for this key + */ + public Key(Resources res, Row parent, int x, int y, XmlResourceParser parser) { + this(parent); + + this.x = x; + this.y = y; + + TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), + com.android.internal.R.styleable.Keyboard); + + width = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyWidth, + keyboard.mDisplayWidth, parent.defaultWidth); + height = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyHeight, + keyboard.mDisplayHeight, parent.defaultHeight); + gap = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_horizontalGap, + keyboard.mDisplayWidth, parent.defaultHorizontalGap); + a.recycle(); + a = res.obtainAttributes(Xml.asAttributeSet(parser), + com.android.internal.R.styleable.Keyboard_Key); + this.x += gap; + TypedValue codesValue = new TypedValue(); + a.getValue(com.android.internal.R.styleable.Keyboard_Key_codes, + codesValue); + if (codesValue.type == TypedValue.TYPE_INT_DEC + || codesValue.type == TypedValue.TYPE_INT_HEX) { + codes = new int[] { codesValue.data }; + } else if (codesValue.type == TypedValue.TYPE_STRING) { + codes = parseCSV(codesValue.string.toString()); + } + + iconPreview = a.getDrawable(com.android.internal.R.styleable.Keyboard_Key_iconPreview); + if (iconPreview != null) { + iconPreview.setBounds(0, 0, iconPreview.getIntrinsicWidth(), + iconPreview.getIntrinsicHeight()); + } + popupCharacters = a.getText( + com.android.internal.R.styleable.Keyboard_Key_popupCharacters); + popupResId = a.getResourceId( + com.android.internal.R.styleable.Keyboard_Key_popupKeyboard, 0); + repeatable = a.getBoolean( + com.android.internal.R.styleable.Keyboard_Key_isRepeatable, false); + modifier = a.getBoolean( + com.android.internal.R.styleable.Keyboard_Key_isModifier, false); + sticky = a.getBoolean( + com.android.internal.R.styleable.Keyboard_Key_isSticky, false); + edgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Key_keyEdgeFlags, 0); + edgeFlags |= parent.rowEdgeFlags; + + icon = a.getDrawable( + com.android.internal.R.styleable.Keyboard_Key_keyIcon); + if (icon != null) { + icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); + } + label = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyLabel); + text = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyOutputText); + + if (codes == null && !TextUtils.isEmpty(label)) { + codes = new int[] { label.charAt(0) }; + } + a.recycle(); + } + + /** + * Informs the key that it has been pressed, in case it needs to change its appearance or + * state. + * @see #onReleased(boolean) + */ + public void onPressed() { + pressed = !pressed; + } + + /** + * Changes the pressed state of the key. If it is a sticky key, it will also change the + * toggled state of the key if the finger was release inside. + * @param inside whether the finger was released inside the key + * @see #onPressed() + */ + public void onReleased(boolean inside) { + pressed = !pressed; + if (sticky) { + on = !on; + } + } + + int[] parseCSV(String value) { + int count = 0; + int lastIndex = 0; + if (value.length() > 0) { + count++; + while ((lastIndex = value.indexOf(",", lastIndex + 1)) > 0) { + count++; + } + } + int[] values = new int[count]; + count = 0; + StringTokenizer st = new StringTokenizer(value, ","); + while (st.hasMoreTokens()) { + try { + values[count++] = Integer.parseInt(st.nextToken()); + } catch (NumberFormatException nfe) { + Log.e(TAG, "Error parsing keycodes " + value); + } + } + return values; + } + + /** + * Detects if a point falls inside this key. + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @return whether or not the point falls inside the key. If the key is attached to an edge, + * it will assume that all points between the key and the edge are considered to be inside + * the key. + */ + public boolean isInside(int x, int y) { + boolean leftEdge = (edgeFlags & EDGE_LEFT) > 0; + boolean rightEdge = (edgeFlags & EDGE_RIGHT) > 0; + boolean topEdge = (edgeFlags & EDGE_TOP) > 0; + boolean bottomEdge = (edgeFlags & EDGE_BOTTOM) > 0; + if ((x >= this.x || (leftEdge && x <= this.x + this.width)) + && (x < this.x + this.width || (rightEdge && x >= this.x)) + && (y >= this.y || (topEdge && y <= this.y + this.height)) + && (y < this.y + this.height || (bottomEdge && y >= this.y))) { + return true; + } else { + return false; + } + } + + + /** + * Returns the square of the distance between the center of the key and the given point. + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @return the square of the distance of the point from the center of the key + */ + public int squaredDistanceFrom(int x, int y) { + float xDist = Math.abs((this.x + this.x + width) / 2f - x); + float yDist = Math.abs((this.y + this.y + height) / 2f - y); + return (int) (xDist * xDist + yDist * yDist); + } + + /** + * Returns the drawable state for the key, based on the current state and type of the key. + * @return the drawable state of the key. + * @see android.graphics.drawable.StateListDrawable#setState(int[]) + */ + public int[] getCurrentDrawableState() { + int[] states = KEY_STATE_NORMAL; + + if (on) { + if (pressed) { + states = KEY_STATE_PRESSED_ON; + } else { + states = KEY_STATE_NORMAL_ON; + } + } else { + if (sticky) { + if (pressed) { + states = KEY_STATE_PRESSED_OFF; + } else { + states = KEY_STATE_NORMAL_OFF; + } + } else { + if (pressed) { + states = KEY_STATE_PRESSED; + } + } + } + return states; + } + } + + /** + * Creates a keyboard from the given xml key layout file. + * @param context the application or service context + * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. + */ + public Keyboard(Context context, int xmlLayoutResId) { + this(context, xmlLayoutResId, 0); + } + + /** + * Creates a keyboard from the given xml key layout file. Weeds out rows + * that have a keyboard mode defined but don't match the specified mode. + * @param context the application or service context + * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. + * @param modeId keyboard mode identifier + */ + public Keyboard(Context context, int xmlLayoutResId, int modeId) { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + final Display display = wm.getDefaultDisplay(); + mDisplayWidth = display.getWidth(); + mDisplayHeight = display.getHeight(); + mDefaultHorizontalGap = 0; + mDefaultWidth = mDisplayWidth / 10; + mDefaultVerticalGap = 0; + mDefaultHeight = mDefaultWidth; + mKeys = new ArrayList<Key>(); + mModifierKeys = new ArrayList<Key>(); + mKeyboardMode = modeId; + loadKeyboard(context, context.getResources().getXml(xmlLayoutResId)); + } + + /** + * <p>Creates a blank keyboard from the given resource file and populates it with the specified + * characters in left-to-right, top-to-bottom fashion, using the specified number of columns. + * </p> + * <p>If the specified number of columns is -1, then the keyboard will fit as many keys as + * possible in each row.</p> + * @param context the application or service context + * @param layoutTemplateResId the layout template file, containing no keys. + * @param characters the list of characters to display on the keyboard. One key will be created + * for each character. + * @param columns the number of columns of keys to display. If this number is greater than the + * number of keys that can fit in a row, it will be ignored. If this number is -1, the + * keyboard will fit as many keys as possible in each row. + */ + public Keyboard(Context context, int layoutTemplateResId, + CharSequence characters, int columns, int horizontalPadding) { + this(context, layoutTemplateResId); + int x = 0; + int y = 0; + int column = 0; + mTotalWidth = 0; + + Row row = new Row(this); + row.defaultHeight = mDefaultHeight; + row.defaultWidth = mDefaultWidth; + row.defaultHorizontalGap = mDefaultHorizontalGap; + row.verticalGap = mDefaultVerticalGap; + row.rowEdgeFlags = EDGE_TOP | EDGE_BOTTOM; + + final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns; + for (int i = 0; i < characters.length(); i++) { + char c = characters.charAt(i); + if (column >= maxColumns + || x + mDefaultWidth + horizontalPadding > mDisplayWidth) { + x = 0; + y += mDefaultVerticalGap + mDefaultHeight; + column = 0; + } + final Key key = new Key(row); + key.x = x; + key.y = y; + key.width = mDefaultWidth; + key.height = mDefaultHeight; + key.gap = mDefaultHorizontalGap; + key.label = String.valueOf(c); + key.codes = new int[] { c }; + column++; + x += key.width + key.gap; + mKeys.add(key); + if (x > mTotalWidth) { + mTotalWidth = x; + } + } + mTotalHeight = y + mDefaultHeight; + } + + public List<Key> getKeys() { + return mKeys; + } + + public List<Key> getModifierKeys() { + return mModifierKeys; + } + + protected int getHorizontalGap() { + return mDefaultHorizontalGap; + } + + protected void setHorizontalGap(int gap) { + mDefaultHorizontalGap = gap; + } + + protected int getVerticalGap() { + return mDefaultVerticalGap; + } + + protected void setVerticalGap(int gap) { + mDefaultVerticalGap = gap; + } + + protected int getKeyHeight() { + return mDefaultHeight; + } + + protected void setKeyHeight(int height) { + mDefaultHeight = height; + } + + protected int getKeyWidth() { + return mDefaultWidth; + } + + protected void setKeyWidth(int width) { + mDefaultWidth = width; + } + + /** + * Returns the total height of the keyboard + * @return the total height of the keyboard + */ + public int getHeight() { + return mTotalHeight; + } + + public int getMinWidth() { + return mTotalWidth; + } + + public boolean setShifted(boolean shiftState) { + if (mShiftKey != null) { + mShiftKey.on = shiftState; + } + if (mShifted != shiftState) { + mShifted = shiftState; + return true; + } + return false; + } + + public boolean isShifted() { + return mShifted; + } + + public int getShiftKeyIndex() { + return mShiftKeyIndex; + } + + protected Row createRowFromXml(Resources res, XmlResourceParser parser) { + return new Row(res, this, parser); + } + + protected Key createKeyFromXml(Resources res, Row parent, int x, int y, + XmlResourceParser parser) { + return new Key(res, parent, x, y, parser); + } + + private void loadKeyboard(Context context, XmlResourceParser parser) { + boolean inKey = false; + boolean inRow = false; + boolean leftMostKey = false; + int row = 0; + int x = 0; + int y = 0; + Key key = null; + Row currentRow = null; + Resources res = context.getResources(); + boolean skipRow = false; + + try { + int event; + while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) { + if (event == XmlResourceParser.START_TAG) { + String tag = parser.getName(); + if (TAG_ROW.equals(tag)) { + inRow = true; + x = 0; + currentRow = createRowFromXml(res, parser); + skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode; + if (skipRow) { + skipToEndOfRow(parser); + inRow = false; + } + } else if (TAG_KEY.equals(tag)) { + inKey = true; + key = createKeyFromXml(res, currentRow, x, y, parser); + mKeys.add(key); + if (key.codes[0] == KEYCODE_SHIFT) { + mShiftKey = key; + mShiftKeyIndex = mKeys.size()-1; + mModifierKeys.add(key); + } else if (key.codes[0] == KEYCODE_ALT) { + mModifierKeys.add(key); + } + } else if (TAG_KEYBOARD.equals(tag)) { + parseKeyboardAttributes(res, parser); + } + } else if (event == XmlResourceParser.END_TAG) { + if (inKey) { + inKey = false; + x += key.gap + key.width; + if (x > mTotalWidth) { + mTotalWidth = x; + } + } else if (inRow) { + inRow = false; + y += currentRow.verticalGap; + y += currentRow.defaultHeight; + row++; + } else { + // TODO: error or extend? + } + } + } + } catch (Exception e) { + Log.e(TAG, "Parse error:" + e); + e.printStackTrace(); + } + mTotalHeight = y - mDefaultVerticalGap; + } + + private void skipToEndOfRow(XmlResourceParser parser) + throws XmlPullParserException, IOException { + int event; + while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) { + if (event == XmlResourceParser.END_TAG + && parser.getName().equals(TAG_ROW)) { + break; + } + } + } + + private void parseKeyboardAttributes(Resources res, XmlResourceParser parser) { + TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), + com.android.internal.R.styleable.Keyboard); + + mDefaultWidth = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyWidth, + mDisplayWidth, mDisplayWidth / 10); + mDefaultHeight = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_keyHeight, + mDisplayHeight, 50); + mDefaultHorizontalGap = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_horizontalGap, + mDisplayWidth, 0); + mDefaultVerticalGap = getDimensionOrFraction(a, + com.android.internal.R.styleable.Keyboard_verticalGap, + mDisplayHeight, 0); + a.recycle(); + } + + static int getDimensionOrFraction(TypedArray a, int index, int base, int defValue) { + TypedValue value = a.peekValue(index); + if (value == null) return defValue; + if (value.type == TypedValue.TYPE_DIMENSION) { + return a.getDimensionPixelOffset(index, defValue); + } else if (value.type == TypedValue.TYPE_FRACTION) { + return (int) a.getFraction(index, base, base, defValue); + } + return defValue; + } +} diff --git a/core/java/android/inputmethodservice/KeyboardView.java b/core/java/android/inputmethodservice/KeyboardView.java new file mode 100755 index 0000000..56473da --- /dev/null +++ b/core/java/android/inputmethodservice/KeyboardView.java @@ -0,0 +1,1049 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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.inputmethodservice; + +import com.android.internal.R; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Paint.Align; +import android.graphics.drawable.Drawable; +import android.inputmethodservice.Keyboard.Key; +import android.os.Handler; +import android.os.Message; +import android.os.Vibrator; +import android.preference.PreferenceManager; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup.LayoutParams; +import android.widget.Button; +import android.widget.PopupWindow; +import android.widget.TextView; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and + * detecting key presses and touch movements. + * + * @attr ref android.R.styleable#KeyboardView_keyBackground + * @attr ref android.R.styleable#KeyboardView_keyPreviewLayout + * @attr ref android.R.styleable#KeyboardView_keyPreviewOffset + * @attr ref android.R.styleable#KeyboardView_labelTextSize + * @attr ref android.R.styleable#KeyboardView_keyTextSize + * @attr ref android.R.styleable#KeyboardView_keyTextColor + * @attr ref android.R.styleable#KeyboardView_verticalCorrection + * @attr ref android.R.styleable#KeyboardView_popupLayout + */ +public class KeyboardView extends View implements View.OnClickListener { + + /** + * Listener for virtual keyboard events. + */ + public interface OnKeyboardActionListener { + /** + * Send a key press to the listener. + * @param primaryCode this is the key that was pressed + * @param keyCodes the codes for all the possible alternative keys + * with the primary code being the first. If the primary key code is + * a single character such as an alphabet or number or symbol, the alternatives + * will include other characters that may be on the same key or adjacent keys. + * These codes are useful to correct for accidental presses of a key adjacent to + * the intended key. + */ + void onKey(int primaryCode, int[] keyCodes); + + /** + * Called when the user quickly moves the finger from right to left. + */ + void swipeLeft(); + + /** + * Called when the user quickly moves the finger from left to right. + */ + void swipeRight(); + + /** + * Called when the user quickly moves the finger from up to down. + */ + void swipeDown(); + + /** + * Called when the user quickly moves the finger from down to up. + */ + void swipeUp(); + } + + private static final boolean DEBUG = false; + private static final int NOT_A_KEY = -1; + private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE }; + private static final int[] LONG_PRESSABLE_STATE_SET = { R.attr.state_long_pressable }; + + private Keyboard mKeyboard; + private int mCurrentKeyIndex = NOT_A_KEY; + private int mLabelTextSize; + private int mKeyTextSize; + private int mKeyTextColor; + + private TextView mPreviewText; + private PopupWindow mPreviewPopup; + private int mPreviewTextSizeLarge; + private int mPreviewOffset; + private int mPreviewHeight; + private int[] mOffsetInWindow; + + private PopupWindow mPopupKeyboard; + private View mMiniKeyboardContainer; + private KeyboardView mMiniKeyboard; + private boolean mMiniKeyboardOnScreen; + private View mPopupParent; + private int mMiniKeyboardOffsetX; + private int mMiniKeyboardOffsetY; + private Map<Key,View> mMiniKeyboardCache; + private int[] mWindowOffset; + + /** Listener for {@link OnKeyboardActionListener}. */ + private OnKeyboardActionListener mKeyboardActionListener; + + private static final int MSG_REMOVE_PREVIEW = 1; + private static final int MSG_REPEAT = 2; + + private int mVerticalCorrection; + private int mProximityThreshold; + + private boolean mPreviewCentered = false; + private boolean mShowPreview = true; + private boolean mShowTouchPoints = false; + private int mPopupPreviewX; + private int mPopupPreviewY; + + private int mLastX; + private int mLastY; + private int mStartX; + private int mStartY; + + private boolean mVibrateOn; + private boolean mSoundOn; + private boolean mProximityCorrectOn; + + private Paint mPaint; + private Rect mPadding; + + private long mDownTime; + private long mLastMoveTime; + private int mLastKey; + private int mLastCodeX; + private int mLastCodeY; + private int mCurrentKey = NOT_A_KEY; + private long mLastKeyTime; + private long mCurrentKeyTime; + private int[] mKeyIndices = new int[12]; + private GestureDetector mGestureDetector; + private int mPopupX; + private int mPopupY; + private int mRepeatKeyIndex = NOT_A_KEY; + private int mPopupLayout; + private boolean mAbortKey; + + private Drawable mKeyBackground; + + private static final String PREF_VIBRATE_ON = "vibrate_on"; + private static final String PREF_SOUND_ON = "sound_on"; + private static final String PREF_PROXIMITY_CORRECTION = "hit_correction"; + + private static final int REPEAT_INTERVAL = 50; // ~20 keys per second + private static final int REPEAT_START_DELAY = 400; + + private Vibrator mVibrator; + private long[] mVibratePattern = new long[] {1, 20}; + + private static int MAX_NEARBY_KEYS = 12; + private int[] mDistances = new int[MAX_NEARBY_KEYS]; + + // For multi-tap + private int mLastSentIndex; + private int mTapCount; + private long mLastTapTime; + private boolean mInMultiTap; + private static final int MULTITAP_INTERVAL = 800; // milliseconds + private StringBuilder mPreviewLabel = new StringBuilder(1); + + Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_REMOVE_PREVIEW: + mPreviewText.setVisibility(INVISIBLE); + break; + case MSG_REPEAT: + if (repeatKey()) { + Message repeat = Message.obtain(this, MSG_REPEAT); + sendMessageDelayed(repeat, REPEAT_INTERVAL); + } + break; + } + + } + }; + + public KeyboardView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.keyboardViewStyle); + } + + public KeyboardView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = + context.obtainStyledAttributes( + attrs, android.R.styleable.KeyboardView, defStyle, 0); + + LayoutInflater inflate = + (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + int previewLayout = 0; + int keyTextSize = 0; + + int n = a.getIndexCount(); + + for (int i = 0; i < n; i++) { + int attr = a.getIndex(i); + + switch (attr) { + case com.android.internal.R.styleable.KeyboardView_keyBackground: + mKeyBackground = a.getDrawable(attr); + break; + case com.android.internal.R.styleable.KeyboardView_verticalCorrection: + mVerticalCorrection = a.getDimensionPixelOffset(attr, 0); + break; + case com.android.internal.R.styleable.KeyboardView_keyPreviewLayout: + previewLayout = a.getResourceId(attr, 0); + break; + case com.android.internal.R.styleable.KeyboardView_keyPreviewOffset: + mPreviewOffset = a.getDimensionPixelOffset(attr, 0); + break; + case com.android.internal.R.styleable.KeyboardView_keyPreviewHeight: + mPreviewHeight = a.getDimensionPixelSize(attr, 80); + break; + case com.android.internal.R.styleable.KeyboardView_keyTextSize: + mKeyTextSize = a.getDimensionPixelSize(attr, 18); + break; + case com.android.internal.R.styleable.KeyboardView_keyTextColor: + mKeyTextColor = a.getColor(attr, 0xFF000000); + break; + case com.android.internal.R.styleable.KeyboardView_labelTextSize: + mLabelTextSize = a.getDimensionPixelSize(attr, 14); + break; + case com.android.internal.R.styleable.KeyboardView_popupLayout: + mPopupLayout = a.getResourceId(attr, 0); + break; + } + } + + // Get the settings preferences + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); + mVibrateOn = sp.getBoolean(PREF_VIBRATE_ON, mVibrateOn); + mSoundOn = sp.getBoolean(PREF_SOUND_ON, mSoundOn); + mProximityCorrectOn = sp.getBoolean(PREF_PROXIMITY_CORRECTION, true); + + mPreviewPopup = new PopupWindow(context); + if (previewLayout != 0) { + mPreviewText = (TextView) inflate.inflate(previewLayout, null); + mPreviewTextSizeLarge = (int) mPreviewText.getTextSize(); + mPreviewPopup.setContentView(mPreviewText); + mPreviewPopup.setBackgroundDrawable(null); + } else { + mShowPreview = false; + } + + mPreviewPopup.setTouchable(false); + + mPopupKeyboard = new PopupWindow(context); + mPopupKeyboard.setBackgroundDrawable(null); + //mPopupKeyboard.setClippingEnabled(false); + + mPopupParent = this; + //mPredicting = true; + + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setTextSize(keyTextSize); + mPaint.setTextAlign(Align.CENTER); + + mPadding = new Rect(0, 0, 0, 0); + mMiniKeyboardCache = new HashMap<Key,View>(); + mKeyBackground.getPadding(mPadding); + + resetMultiTap(); + initGestureDetector(); + } + + private void initGestureDetector() { + mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onFling(MotionEvent me1, MotionEvent me2, + float velocityX, float velocityY) { + if (velocityX > 400 && Math.abs(velocityY) < 400) { + swipeRight(); + return true; + } else if (velocityX < -400 && Math.abs(velocityY) < 400) { + swipeLeft(); + return true; + } else if (velocityY < -400 && Math.abs(velocityX) < 400) { + swipeUp(); + return true; + } else if (velocityY > 400 && Math.abs(velocityX) < 400) { + swipeDown(); + return true; + } + return false; + } + + @Override + public void onLongPress(MotionEvent me) { + openPopupIfRequired(me); + } + }); + } + + public void setOnKeyboardActionListener(OnKeyboardActionListener listener) { + mKeyboardActionListener = listener; + } + + /** + * Returns the {@link OnKeyboardActionListener} object. + * @return the listener attached to this keyboard + */ + protected OnKeyboardActionListener getOnKeyboardActionListener() { + return mKeyboardActionListener; + } + + /** + * Attaches a keyboard to this view. The keyboard can be switched at any time and the + * view will re-layout itself to accommodate the keyboard. + * @see Keyboard + * @see #getKeyboard() + * @param keyboard the keyboard to display in this view + */ + public void setKeyboard(Keyboard keyboard) { + mKeyboard = keyboard; + requestLayout(); + invalidate(); + computeProximityThreshold(keyboard); + } + + /** + * Returns the current keyboard being displayed by this view. + * @return the currently attached keyboard + * @see #setKeyboard(Keyboard) + */ + public Keyboard getKeyboard() { + return mKeyboard; + } + + /** + * Sets the state of the shift key of the keyboard, if any. + * @param shifted whether or not to enable the state of the shift key + * @return true if the shift key state changed, false if there was no change + * @see KeyboardView#isShifted() + */ + public boolean setShifted(boolean shifted) { + if (mKeyboard != null) { + if (mKeyboard.setShifted(shifted)) { + // The whole keyboard probably needs to be redrawn + invalidate(); + return true; + } + } + return false; + } + + /** + * Returns the state of the shift key of the keyboard, if any. + * @return true if the shift is in a pressed state, false otherwise. If there is + * no shift key on the keyboard or there is no keyboard attached, it returns false. + * @see KeyboardView#setShifted(boolean) + */ + public boolean isShifted() { + if (mKeyboard != null) { + return mKeyboard.isShifted(); + } + return false; + } + + /** + * Enables or disables the key feedback popup. This is a popup that shows a magnified + * version of the depressed key. By default the preview is enabled. + * @param previewEnabled whether or not to enable the key feedback popup + * @see #isPreviewEnabled() + */ + public void setPreviewEnabled(boolean previewEnabled) { + mShowPreview = previewEnabled; + } + + /** + * Returns the enabled state of the key feedback popup. + * @return whether or not the key feedback popup is enabled + * @see #setPreviewEnabled(boolean) + */ + public boolean isPreviewEnabled() { + return mShowPreview; + } + + public void setVerticalCorrection(int verticalOffset) { + + } + public void setPopupParent(View v) { + mPopupParent = v; + } + + public void setPopupOffset(int x, int y) { + mMiniKeyboardOffsetX = x; + mMiniKeyboardOffsetY = y; + if (mPreviewPopup.isShowing()) { + mPreviewPopup.dismiss(); + } + } + + /** + * Popup keyboard close button clicked. + * @hide + */ + public void onClick(View v) { + dismissPopupKeyboard(); + } + + private CharSequence adjustCase(CharSequence label) { + if (mKeyboard.isShifted() && label != null && label.length() == 1 + && Character.isLowerCase(label.charAt(0))) { + label = label.toString().toUpperCase(); + } + return label; + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Round up a little + if (mKeyboard == null) { + setMeasuredDimension(mPaddingLeft + mPaddingRight, mPaddingTop + mPaddingBottom); + } else { + int width = mKeyboard.getMinWidth() + mPaddingLeft + mPaddingRight; + if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) { + width = MeasureSpec.getSize(widthMeasureSpec); + } + setMeasuredDimension(width, mKeyboard.getHeight() + mPaddingTop + mPaddingBottom); + } + } + + /** + * Compute the average distance between adjacent keys (horizontally and vertically) + * and square it to get the proximity threshold. We use a square here and in computing + * the touch distance from a key's center to avoid taking a square root. + * @param keyboard + */ + private void computeProximityThreshold(Keyboard keyboard) { + if (keyboard == null) return; + List<Key> keys = keyboard.getKeys(); + if (keys == null) return; + int length = keys.size(); + int dimensionSum = 0; + for (int i = 0; i < length; i++) { + Key key = keys.get(i); + dimensionSum += key.width + key.gap + key.height; + } + if (dimensionSum < 0 || length == 0) return; + mProximityThreshold = dimensionSum / (length * 2); + mProximityThreshold *= mProximityThreshold; // Square it + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mKeyboard == null) return; + + final Paint paint = mPaint; + //final int descent = (int) paint.descent(); + final Drawable keyBackground = mKeyBackground; + final Rect padding = mPadding; + final int kbdPaddingLeft = mPaddingLeft; + final int kbdPaddingTop = mPaddingTop; + List<Key> keys = mKeyboard.getKeys(); + //canvas.translate(0, mKeyboardPaddingTop); + paint.setAlpha(255); + paint.setColor(mKeyTextColor); + + final int keyCount = keys.size(); + for (int i = 0; i < keyCount; i++) { + final Key key = keys.get(i); + int[] drawableState = key.getCurrentDrawableState(); + keyBackground.setState(drawableState); + + // Switch the character to uppercase if shift is pressed + String label = key.label == null? null : adjustCase(key.label).toString(); + + final Rect bounds = keyBackground.getBounds(); + if (key.width != bounds.right || + key.height != bounds.bottom) { + keyBackground.setBounds(0, 0, key.width, key.height); + } + canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop); + keyBackground.draw(canvas); + + if (label != null) { + // For characters, use large font. For labels like "Done", use small font. + if (label.length() > 1 && key.codes.length < 2) { + paint.setTextSize(mLabelTextSize); + paint.setFakeBoldText(true); + } else { + paint.setTextSize(mKeyTextSize); + paint.setFakeBoldText(false); + } + // Draw a drop shadow for the text + paint.setShadowLayer(3f, 0, 0, 0xCC000000); + // Draw the text + canvas.drawText(label, + (key.width - padding.left - padding.right) / 2 + + padding.left, + (key.height - padding.top - padding.bottom) / 2 + + (paint.getTextSize() - paint.descent()) / 2 + padding.top, + paint); + // Turn off drop shadow + paint.setShadowLayer(0, 0, 0, 0); + } else if (key.icon != null) { + final int drawableX = (key.width - padding.left - padding.right + - key.icon.getIntrinsicWidth()) / 2 + padding.left; + final int drawableY = (key.height - padding.top - padding.bottom + - key.icon.getIntrinsicHeight()) / 2 + padding.top; + canvas.translate(drawableX, drawableY); + key.icon.setBounds(0, 0, + key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight()); + key.icon.draw(canvas); + canvas.translate(-drawableX, -drawableY); + } + canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop); + } + + // Overlay a dark rectangle to dim the keyboard + if (mMiniKeyboardOnScreen) { + paint.setColor(0xA0000000); + canvas.drawRect(0, 0, getWidth(), getHeight(), paint); + } + + if (DEBUG && mShowTouchPoints) { + paint.setAlpha(128); + paint.setColor(0xFFFF0000); + canvas.drawCircle(mStartX, mStartY, 3, paint); + canvas.drawLine(mStartX, mStartY, mLastX, mLastY, paint); + paint.setColor(0xFF0000FF); + canvas.drawCircle(mLastX, mLastY, 3, paint); + paint.setColor(0xFF00FF00); + canvas.drawCircle((mStartX + mLastX) / 2, (mStartY + mLastY) / 2, 2, paint); + } + } + + private void playKeyClick() { + if (mSoundOn) { + playSoundEffect(0); + } + } + + private void vibrate() { + if (!mVibrateOn) { + return; + } + if (mVibrator == null) { + mVibrator = new Vibrator(); + } + mVibrator.vibrate(mVibratePattern, -1); + } + + private int getKeyIndices(int x, int y, int[] allKeys) { + final List<Key> keys = mKeyboard.getKeys(); + final boolean shifted = mKeyboard.isShifted(); + int primaryIndex = NOT_A_KEY; + int closestKey = NOT_A_KEY; + int closestKeyDist = mProximityThreshold + 1; + java.util.Arrays.fill(mDistances, Integer.MAX_VALUE); + final int keyCount = keys.size(); + for (int i = 0; i < keyCount; i++) { + final Key key = keys.get(i); + int dist = 0; + boolean isInside = key.isInside(x,y); + if (((mProximityCorrectOn + && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold) + || isInside) + && key.codes[0] > 32) { + // Find insertion point + final int nCodes = key.codes.length; + if (dist < closestKeyDist) { + closestKeyDist = dist; + closestKey = i; + } + + if (allKeys == null) continue; + + for (int j = 0; j < mDistances.length; j++) { + if (mDistances[j] > dist) { + // Make space for nCodes codes + System.arraycopy(mDistances, j, mDistances, j + nCodes, + mDistances.length - j - nCodes); + System.arraycopy(allKeys, j, allKeys, j + nCodes, + allKeys.length - j - nCodes); + for (int c = 0; c < nCodes; c++) { + allKeys[j + c] = key.codes[c]; + if (shifted) { + //allKeys[j + c] = Character.toUpperCase(key.codes[c]); + } + mDistances[j + c] = dist; + } + break; + } + } + } + + if (isInside) { + primaryIndex = i; + } + } + if (primaryIndex == NOT_A_KEY) { + primaryIndex = closestKey; + } + return primaryIndex; + } + + private void detectAndSendKey(int x, int y, long eventTime) { + int index = mCurrentKey; + if (index != NOT_A_KEY) { + vibrate(); + final Key key = mKeyboard.getKeys().get(index); + if (key.text != null) { + for (int i = 0; i < key.text.length(); i++) { + mKeyboardActionListener.onKey(key.text.charAt(i), key.codes); + } + } else { + int code = key.codes[0]; + //TextEntryState.keyPressedAt(key, x, y); + int[] codes = new int[MAX_NEARBY_KEYS]; + Arrays.fill(codes, NOT_A_KEY); + getKeyIndices(x, y, codes); + // Multi-tap + if (mInMultiTap) { + if (mTapCount != -1) { + mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE); + } else { + mTapCount = 0; + } + code = key.codes[mTapCount]; + } + mKeyboardActionListener.onKey(code, codes); + } + mLastSentIndex = index; + mLastTapTime = eventTime; + } + } + + /** + * Handle multi-tap keys by producing the key label for the current multi-tap state. + */ + private CharSequence getPreviewText(Key key) { + if (mInMultiTap) { + // Multi-tap + mPreviewLabel.setLength(0); + mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]); + return adjustCase(mPreviewLabel); + } else { + return adjustCase(key.label); + } + } + + private void showPreview(int keyIndex) { + int oldKeyIndex = mCurrentKeyIndex; + final PopupWindow previewPopup = mPreviewPopup; + + mCurrentKeyIndex = keyIndex; + // Release the old key and press the new key + final List<Key> keys = mKeyboard.getKeys(); + if (oldKeyIndex != mCurrentKeyIndex) { + if (oldKeyIndex != NOT_A_KEY && keys.size() > oldKeyIndex) { + keys.get(oldKeyIndex).onReleased(mCurrentKeyIndex == NOT_A_KEY); + invalidateKey(oldKeyIndex); + } + if (mCurrentKeyIndex != NOT_A_KEY && keys.size() > mCurrentKeyIndex) { + keys.get(mCurrentKeyIndex).onPressed(); + invalidateKey(mCurrentKeyIndex); + } + } + // If key changed and preview is on ... + if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) { + if (previewPopup.isShowing()) { + if (keyIndex == NOT_A_KEY) { + mHandler.sendMessageDelayed(mHandler + .obtainMessage(MSG_REMOVE_PREVIEW), 60); + } + } + if (keyIndex != NOT_A_KEY) { + Key key = keys.get(keyIndex); + if (key.icon != null) { + mPreviewText.setCompoundDrawables(null, null, null, + key.iconPreview != null ? key.iconPreview : key.icon); + mPreviewText.setText(null); + } else { + mPreviewText.setCompoundDrawables(null, null, null, null); + mPreviewText.setText(getPreviewText(key)); + if (key.label.length() > 1 && key.codes.length < 2) { + mPreviewText.setTextSize(mLabelTextSize); + } else { + mPreviewText.setTextSize(mPreviewTextSizeLarge); + } + } + mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width + + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight()); + final int popupHeight = mPreviewHeight; + LayoutParams lp = mPreviewText.getLayoutParams(); + if (lp != null) { + lp.width = popupWidth; + lp.height = popupHeight; + } + previewPopup.setWidth(popupWidth); + previewPopup.setHeight(popupHeight); + if (!mPreviewCentered) { + mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + mPaddingLeft; + mPopupPreviewY = key.y - popupHeight + mPreviewOffset; + } else { + // TODO: Fix this if centering is brought back + mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2; + mPopupPreviewY = - mPreviewText.getMeasuredHeight(); + } + mHandler.removeMessages(MSG_REMOVE_PREVIEW); + if (mOffsetInWindow == null) { + mOffsetInWindow = new int[2]; + getLocationInWindow(mOffsetInWindow); + mOffsetInWindow[0] += mMiniKeyboardOffsetX; // Offset may be zero + mOffsetInWindow[1] += mMiniKeyboardOffsetY; // Offset may be zero + } + // Set the preview background state + mPreviewText.getBackground().setState( + key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); + if (previewPopup.isShowing()) { + previewPopup.update(mPopupPreviewX + mOffsetInWindow[0], + mPopupPreviewY + mOffsetInWindow[1], + popupWidth, popupHeight); + } else { + previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY, + mPopupPreviewX + mOffsetInWindow[0], + mPopupPreviewY + mOffsetInWindow[1]); + } + mPreviewText.setVisibility(VISIBLE); + } + } + } + + private void invalidateKey(int keyIndex) { + if (keyIndex < 0 || keyIndex >= mKeyboard.getKeys().size()) { + return; + } + final Key key = mKeyboard.getKeys().get(keyIndex); + invalidate(key.x + mPaddingLeft, key.y + mPaddingTop, + key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop); + } + + private boolean openPopupIfRequired(MotionEvent me) { + // Check if we have a popup layout specified first. + if (mPopupLayout == 0) { + return false; + } + if (mCurrentKey < 0 || mCurrentKey >= mKeyboard.getKeys().size()) { + return false; + } + + Key popupKey = mKeyboard.getKeys().get(mCurrentKey); + boolean result = onLongPress(popupKey); + if (result) { + mAbortKey = true; + showPreview(NOT_A_KEY); + } + return result; + } + + /** + * Called when a key is long pressed. By default this will open any popup keyboard associated + * with this key through the attributes popupLayout and popupCharacters. + * @param popupKey the key that was long pressed + * @return true if the long press is handled, false otherwise. Subclasses should call the + * method on the base class if the subclass doesn't wish to handle the call. + */ + protected boolean onLongPress(Key popupKey) { + int popupKeyboardId = popupKey.popupResId; + + if (popupKeyboardId != 0) { + mMiniKeyboardContainer = mMiniKeyboardCache.get(popupKey); + if (mMiniKeyboardContainer == null) { + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null); + mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById( + com.android.internal.R.id.keyboardView); + View closeButton = mMiniKeyboardContainer.findViewById( + com.android.internal.R.id.button_close); + if (closeButton != null) closeButton.setOnClickListener(this); + mMiniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() { + public void onKey(int primaryCode, int[] keyCodes) { + mKeyboardActionListener.onKey(primaryCode, keyCodes); + dismissPopupKeyboard(); + } + + public void swipeLeft() { } + public void swipeRight() { } + public void swipeUp() { } + public void swipeDown() { } + }); + //mInputView.setSuggest(mSuggest); + Keyboard keyboard; + if (popupKey.popupCharacters != null) { + keyboard = new Keyboard(getContext(), popupKeyboardId, + popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight()); + } else { + keyboard = new Keyboard(getContext(), popupKeyboardId); + } + mMiniKeyboard.setKeyboard(keyboard); + mMiniKeyboard.setPopupParent(this); + mMiniKeyboardContainer.measure( + MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); + + mMiniKeyboardCache.put(popupKey, mMiniKeyboardContainer); + } else { + mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById( + com.android.internal.R.id.keyboardView); + } + if (mWindowOffset == null) { + mWindowOffset = new int[2]; + getLocationInWindow(mWindowOffset); + } + mPopupX = popupKey.x + mPaddingLeft; + mPopupY = popupKey.y + mPaddingTop; + mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.getMeasuredWidth(); + mPopupY = mPopupY - mMiniKeyboardContainer.getMeasuredHeight(); + final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mWindowOffset[0]; + final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mWindowOffset[1]; + mMiniKeyboard.setPopupOffset(x < 0 ? 0 : x, y); + mMiniKeyboard.setShifted(isShifted()); + mPopupKeyboard.setContentView(mMiniKeyboardContainer); + mPopupKeyboard.setWidth(mMiniKeyboardContainer.getMeasuredWidth()); + mPopupKeyboard.setHeight(mMiniKeyboardContainer.getMeasuredHeight()); + mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y); + mMiniKeyboardOnScreen = true; + //mMiniKeyboard.onTouchEvent(getTranslatedEvent(me)); + invalidate(); + return true; + } + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent me) { + int touchX = (int) me.getX() - mPaddingLeft; + int touchY = (int) me.getY() + mVerticalCorrection - mPaddingTop; + int action = me.getAction(); + long eventTime = me.getEventTime(); + int keyIndex = getKeyIndices(touchX, touchY, null); + + if (mGestureDetector.onTouchEvent(me)) { + showPreview(NOT_A_KEY); + mHandler.removeMessages(MSG_REPEAT); + return true; + } + + // Needs to be called after the gesture detector gets a turn, as it may have + // displayed the mini keyboard + if (mMiniKeyboardOnScreen) { + return true; + } + + switch (action) { + case MotionEvent.ACTION_DOWN: + mAbortKey = false; + mStartX = touchX; + mStartY = touchY; + mLastCodeX = touchX; + mLastCodeY = touchY; + mLastKeyTime = 0; + mCurrentKeyTime = 0; + mLastKey = NOT_A_KEY; + mCurrentKey = keyIndex; + mDownTime = me.getEventTime(); + mLastMoveTime = mDownTime; + checkMultiTap(eventTime, keyIndex); + if (mCurrentKey >= 0 && mKeyboard.getKeys().get(mCurrentKey).repeatable) { + mRepeatKeyIndex = mCurrentKey; + repeatKey(); + Message msg = mHandler.obtainMessage(MSG_REPEAT); + mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY); + } + showPreview(keyIndex); + playKeyClick(); + vibrate(); + break; + + case MotionEvent.ACTION_MOVE: + if (keyIndex != NOT_A_KEY) { + if (mCurrentKey == NOT_A_KEY) { + mCurrentKey = keyIndex; + mCurrentKeyTime = eventTime - mDownTime; + } else { + if (keyIndex == mCurrentKey) { + mCurrentKeyTime += eventTime - mLastMoveTime; + } else { + resetMultiTap(); + mLastKey = mCurrentKey; + mLastCodeX = mLastX; + mLastCodeY = mLastY; + mLastKeyTime = + mCurrentKeyTime + eventTime - mLastMoveTime; + mCurrentKey = keyIndex; + mCurrentKeyTime = 0; + } + } + if (keyIndex != mRepeatKeyIndex) { + mHandler.removeMessages(MSG_REPEAT); + mRepeatKeyIndex = NOT_A_KEY; + } + } + showPreview(keyIndex); + break; + + case MotionEvent.ACTION_UP: + mHandler.removeMessages(MSG_REPEAT); + if (keyIndex == mCurrentKey) { + mCurrentKeyTime += eventTime - mLastMoveTime; + } else { + resetMultiTap(); + mLastKey = mCurrentKey; + mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime; + mCurrentKey = keyIndex; + mCurrentKeyTime = 0; + } + if (mCurrentKeyTime < mLastKeyTime && mLastKey != NOT_A_KEY) { + mCurrentKey = mLastKey; + touchX = mLastCodeX; + touchY = mLastCodeY; + } + showPreview(NOT_A_KEY); + Arrays.fill(mKeyIndices, NOT_A_KEY); + invalidateKey(keyIndex); + // If we're not on a repeating key (which sends on a DOWN event) + if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) { + detectAndSendKey(touchX, touchY, eventTime); + } + mRepeatKeyIndex = NOT_A_KEY; + break; + } + mLastX = touchX; + mLastY = touchY; + return true; + } + + private boolean repeatKey() { + Key key = mKeyboard.getKeys().get(mRepeatKeyIndex); + detectAndSendKey(key.x, key.y, mLastTapTime); + return true; + } + + protected void swipeRight() { + mKeyboardActionListener.swipeRight(); + } + + protected void swipeLeft() { + mKeyboardActionListener.swipeLeft(); + } + + protected void swipeUp() { + mKeyboardActionListener.swipeUp(); + } + + protected void swipeDown() { + mKeyboardActionListener.swipeDown(); + } + + public void closing() { + if (mPreviewPopup.isShowing()) { + mPreviewPopup.dismiss(); + } + dismissPopupKeyboard(); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + closing(); + } + + private void dismissPopupKeyboard() { + if (mPopupKeyboard.isShowing()) { + mPopupKeyboard.dismiss(); + mMiniKeyboardOnScreen = false; + invalidate(); + } + } + + public boolean handleBack() { + if (mPopupKeyboard.isShowing()) { + dismissPopupKeyboard(); + return true; + } + return false; + } + + private void resetMultiTap() { + mLastSentIndex = NOT_A_KEY; + mTapCount = 0; + mLastTapTime = -1; + mInMultiTap = false; + } + + private void checkMultiTap(long eventTime, int keyIndex) { + if (keyIndex == NOT_A_KEY) return; + Key key = mKeyboard.getKeys().get(keyIndex); + if (key.codes.length > 1) { + mInMultiTap = true; + if (eventTime < mLastTapTime + MULTITAP_INTERVAL + && keyIndex == mLastSentIndex) { + mTapCount = (mTapCount + 1) % key.codes.length; + return; + } else { + mTapCount = -1; + return; + } + } + if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) { + resetMultiTap(); + } + } +} diff --git a/core/java/android/inputmethodservice/SoftInputWindow.java b/core/java/android/inputmethodservice/SoftInputWindow.java new file mode 100644 index 0000000..9ff1665 --- /dev/null +++ b/core/java/android/inputmethodservice/SoftInputWindow.java @@ -0,0 +1,151 @@ +/* + * 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.inputmethodservice; + +import android.app.Dialog; +import android.content.Context; +import android.os.IBinder; +import android.view.Gravity; +import android.view.WindowManager; + +/** + * A SoftInputWindow is a Dialog that is intended to be used for a top-level input + * method window. It will be displayed along the edge of the screen, moving + * the application user interface away from it so that the focused item is + * always visible. + */ +class SoftInputWindow extends Dialog { + + /** + * Create a DockWindow that uses the default style. + * + * @param context The Context the DockWindow is to run it. In particular, it + * uses the window manager and theme in this context to present its + * UI. + */ + public SoftInputWindow(Context context) { + super(context, com.android.internal.R.style.Theme_InputMethod); + initDockWindow(); + } + + public void setToken(IBinder token) { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + lp.token = token; + getWindow().setAttributes(lp); + } + + /** + * Create a DockWindow that uses a custom style. + * + * @param context The Context in which the DockWindow should run. In + * particular, it uses the window manager and theme from this context + * to present its UI. + * @param theme A style resource describing the theme to use for the window. + * See <a href="{@docRoot}reference/available-resources.html#stylesandthemes">Style + * and Theme Resources</a> for more information about defining and + * using styles. This theme is applied on top of the current theme in + * <var>context</var>. If 0, the default dialog theme will be used. + */ + public SoftInputWindow(Context context, int theme) { + super(context, theme); + initDockWindow(); + } + + /** + * Get the size of the DockWindow. + * + * @return If the DockWindow sticks to the top or bottom of the screen, the + * return value is the height of the DockWindow, and its width is + * equal to the width of the screen; If the DockWindow sticks to the + * left or right of the screen, the return value is the width of the + * DockWindow, and its height is equal to the height of the screen. + */ + public int getSize() { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + + if (lp.gravity == Gravity.TOP || lp.gravity == Gravity.BOTTOM) { + return lp.height; + } else { + return lp.width; + } + } + + /** + * Set the size of the DockWindow. + * + * @param size If the DockWindow sticks to the top or bottom of the screen, + * <var>size</var> is the height of the DockWindow, and its width is + * equal to the width of the screen; If the DockWindow sticks to the + * left or right of the screen, <var>size</var> is the width of the + * DockWindow, and its height is equal to the height of the screen. + */ + public void setSize(int size) { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + + if (lp.gravity == Gravity.TOP || lp.gravity == Gravity.BOTTOM) { + lp.width = -1; + lp.height = size; + } else { + lp.width = size; + lp.height = -1; + } + getWindow().setAttributes(lp); + } + + /** + * Set which boundary of the screen the DockWindow sticks to. + * + * @param gravity The boundary of the screen to stick. See {#link + * android.view.Gravity.LEFT}, {#link android.view.Gravity.TOP}, + * {#link android.view.Gravity.BOTTOM}, {#link + * android.view.Gravity.RIGHT}. + */ + public void setGravity(int gravity) { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + + boolean oldIsVertical = (lp.gravity == Gravity.TOP || lp.gravity == Gravity.BOTTOM); + + lp.gravity = gravity; + + boolean newIsVertical = (lp.gravity == Gravity.TOP || lp.gravity == Gravity.BOTTOM); + + if (oldIsVertical != newIsVertical) { + int tmp = lp.width; + lp.width = lp.height; + lp.height = tmp; + getWindow().setAttributes(lp); + } + } + + private void initDockWindow() { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + + lp.type = WindowManager.LayoutParams.TYPE_INPUT_METHOD; + lp.setTitle("InputMethod"); + + lp.gravity = Gravity.BOTTOM; + lp.width = -1; + + getWindow().setAttributes(lp); + getWindow().setFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_DIM_BEHIND); + } +} diff --git a/core/java/android/inputmethodservice/package.html b/core/java/android/inputmethodservice/package.html new file mode 100644 index 0000000..164349b --- /dev/null +++ b/core/java/android/inputmethodservice/package.html @@ -0,0 +1,8 @@ +<html> +<body> +Base classes for writing input methods. These APIs are not for use by +normal applications, they are a framework specifically for writing input +method components. Implementations will typically derive from +{@link android.inputmethodservice.InputMethodService}. +</body> +</html> |