| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.inputmethod.pinyin; |
| |
| import android.app.AlertDialog; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.Intent; |
| import android.content.ServiceConnection; |
| import android.content.res.Configuration; |
| import android.inputmethodservice.InputMethodService; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.preference.PreferenceManager; |
| import android.util.Log; |
| import android.view.Gravity; |
| import android.view.GestureDetector; |
| import android.view.LayoutInflater; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.Window; |
| import android.view.WindowManager; |
| import android.view.View.MeasureSpec; |
| import android.view.ViewGroup.LayoutParams; |
| import android.view.inputmethod.CompletionInfo; |
| import android.view.inputmethod.InputConnection; |
| import android.view.inputmethod.EditorInfo; |
| import android.view.inputmethod.InputMethodManager; |
| import android.widget.LinearLayout; |
| import android.widget.PopupWindow; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Vector; |
| |
| /** |
| * Main class of the Pinyin input method. |
| */ |
| public class PinyinIME extends InputMethodService { |
| /** |
| * TAG for debug. |
| */ |
| static final String TAG = "PinyinIME"; |
| |
| /** |
| * If is is true, IME will simulate key events for delete key, and send the |
| * events back to the application. |
| */ |
| private static final boolean SIMULATE_KEY_DELETE = true; |
| |
| /** |
| * Necessary environment configurations like screen size for this IME. |
| */ |
| private Environment mEnvironment; |
| |
| /** |
| * Used to switch input mode. |
| */ |
| private InputModeSwitcher mInputModeSwitcher; |
| |
| /** |
| * Soft keyboard container view to host real soft keyboard view. |
| */ |
| private SkbContainer mSkbContainer; |
| |
| /** |
| * The floating container which contains the composing view. If necessary, |
| * some other view like candiates container can also be put here. |
| */ |
| private LinearLayout mFloatingContainer; |
| |
| /** |
| * View to show the composing string. |
| */ |
| private ComposingView mComposingView; |
| |
| /** |
| * Window to show the composing string. |
| */ |
| private PopupWindow mFloatingWindow; |
| |
| /** |
| * Used to show the floating window. |
| */ |
| private PopupTimer mFloatingWindowTimer = new PopupTimer(); |
| |
| /** |
| * View to show candidates list. |
| */ |
| private CandidatesContainer mCandidatesContainer; |
| |
| /** |
| * Balloon used when user presses a candidate. |
| */ |
| private BalloonHint mCandidatesBalloon; |
| |
| /** |
| * Used to notify the input method when the user touch a candidate. |
| */ |
| private ChoiceNotifier mChoiceNotifier; |
| |
| /** |
| * Used to notify gestures from soft keyboard. |
| */ |
| private OnGestureListener mGestureListenerSkb; |
| |
| /** |
| * Used to notify gestures from candidates view. |
| */ |
| private OnGestureListener mGestureListenerCandidates; |
| |
| /** |
| * The on-screen movement gesture detector for soft keyboard. |
| */ |
| private GestureDetector mGestureDetectorSkb; |
| |
| /** |
| * The on-screen movement gesture detector for candidates view. |
| */ |
| private GestureDetector mGestureDetectorCandidates; |
| |
| /** |
| * Option dialog to choose settings and other IMEs. |
| */ |
| private AlertDialog mOptionsDialog; |
| |
| /** |
| * Connection used to bind the decoding service. |
| */ |
| private PinyinDecoderServiceConnection mPinyinDecoderServiceConnection; |
| |
| /** |
| * The current IME status. |
| * |
| * @see com.android.inputmethod.pinyin.PinyinIME.ImeState |
| */ |
| private ImeState mImeState = ImeState.STATE_IDLE; |
| |
| /** |
| * The decoding information, include spelling(Pinyin) string, decoding |
| * result, etc. |
| */ |
| private DecodingInfo mDecInfo = new DecodingInfo(); |
| |
| /** |
| * For English input. |
| */ |
| private EnglishInputProcessor mImEn; |
| |
| // receive ringer mode changes |
| private BroadcastReceiver mReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| SoundManager.getInstance(context).updateRingerMode(); |
| } |
| }; |
| |
| @Override |
| public void onCreate() { |
| mEnvironment = Environment.getInstance(); |
| if (mEnvironment.needDebug()) { |
| Log.d(TAG, "onCreate."); |
| } |
| super.onCreate(); |
| |
| startPinyinDecoderService(); |
| mImEn = new EnglishInputProcessor(); |
| Settings.getInstance(PreferenceManager |
| .getDefaultSharedPreferences(getApplicationContext())); |
| |
| mInputModeSwitcher = new InputModeSwitcher(this); |
| mChoiceNotifier = new ChoiceNotifier(this); |
| mGestureListenerSkb = new OnGestureListener(false); |
| mGestureListenerCandidates = new OnGestureListener(true); |
| mGestureDetectorSkb = new GestureDetector(this, mGestureListenerSkb); |
| mGestureDetectorCandidates = new GestureDetector(this, |
| mGestureListenerCandidates); |
| |
| mEnvironment.onConfigurationChanged(getResources().getConfiguration(), |
| this); |
| } |
| |
| @Override |
| public void onDestroy() { |
| if (mEnvironment.needDebug()) { |
| Log.d(TAG, "onDestroy."); |
| } |
| unbindService(mPinyinDecoderServiceConnection); |
| Settings.releaseInstance(); |
| super.onDestroy(); |
| } |
| |
| @Override |
| public void onConfigurationChanged(Configuration newConfig) { |
| Environment env = Environment.getInstance(); |
| if (mEnvironment.needDebug()) { |
| Log.d(TAG, "onConfigurationChanged"); |
| Log.d(TAG, "--last config: " + env.getConfiguration().toString()); |
| Log.d(TAG, "---new config: " + newConfig.toString()); |
| } |
| // We need to change the local environment first so that UI components |
| // can get the environment instance to handle size issues. When |
| // super.onConfigurationChanged() is called, onCreateCandidatesView() |
| // and onCreateInputView() will be executed if necessary. |
| env.onConfigurationChanged(newConfig, this); |
| |
| // Clear related UI of the previous configuration. |
| if (null != mSkbContainer) { |
| mSkbContainer.dismissPopups(); |
| } |
| if (null != mCandidatesBalloon) { |
| mCandidatesBalloon.dismiss(); |
| } |
| super.onConfigurationChanged(newConfig); |
| resetToIdleState(false); |
| } |
| |
| @Override |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| if (processKey(event, 0 != event.getRepeatCount())) return true; |
| return super.onKeyDown(keyCode, event); |
| } |
| |
| @Override |
| public boolean onKeyUp(int keyCode, KeyEvent event) { |
| if (processKey(event, true)) return true; |
| return super.onKeyUp(keyCode, event); |
| } |
| |
| private boolean processKey(KeyEvent event, boolean realAction) { |
| if (ImeState.STATE_BYPASS == mImeState) return false; |
| |
| int keyCode = event.getKeyCode(); |
| // SHIFT-SPACE is used to switch between Chinese and English |
| // when HKB is on. |
| if (KeyEvent.KEYCODE_SPACE == keyCode && event.isShiftPressed()) { |
| if (!realAction) return true; |
| |
| updateIcon(mInputModeSwitcher.switchLanguageWithHkb()); |
| resetToIdleState(false); |
| |
| int allMetaState = KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON |
| | KeyEvent.META_ALT_RIGHT_ON | KeyEvent.META_SHIFT_ON |
| | KeyEvent.META_SHIFT_LEFT_ON |
| | KeyEvent.META_SHIFT_RIGHT_ON | KeyEvent.META_SYM_ON; |
| getCurrentInputConnection().clearMetaKeyStates(allMetaState); |
| return true; |
| } |
| |
| // If HKB is on to input English, by-pass the key event so that |
| // default key listener will handle it. |
| if (mInputModeSwitcher.isEnglishWithHkb()) { |
| return false; |
| } |
| |
| if (processFunctionKeys(keyCode, realAction)) { |
| return true; |
| } |
| |
| int keyChar = 0; |
| if (keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z) { |
| keyChar = keyCode - KeyEvent.KEYCODE_A + 'a'; |
| } else if (keyCode >= KeyEvent.KEYCODE_0 |
| && keyCode <= KeyEvent.KEYCODE_9) { |
| keyChar = keyCode - KeyEvent.KEYCODE_0 + '0'; |
| } else if (keyCode == KeyEvent.KEYCODE_COMMA) { |
| keyChar = ','; |
| } else if (keyCode == KeyEvent.KEYCODE_PERIOD) { |
| keyChar = '.'; |
| } else if (keyCode == KeyEvent.KEYCODE_SPACE) { |
| keyChar = ' '; |
| } else if (keyCode == KeyEvent.KEYCODE_APOSTROPHE) { |
| keyChar = '\''; |
| } |
| |
| if (mInputModeSwitcher.isEnglishWithSkb()) { |
| return mImEn.processKey(getCurrentInputConnection(), event, |
| mInputModeSwitcher.isEnglishUpperCaseWithSkb(), realAction); |
| } else if (mInputModeSwitcher.isChineseText()) { |
| if (mImeState == ImeState.STATE_IDLE || |
| mImeState == ImeState.STATE_APP_COMPLETION) { |
| mImeState = ImeState.STATE_IDLE; |
| return processStateIdle(keyChar, keyCode, event, realAction); |
| } else if (mImeState == ImeState.STATE_INPUT) { |
| return processStateInput(keyChar, keyCode, event, realAction); |
| } else if (mImeState == ImeState.STATE_PREDICT) { |
| return processStatePredict(keyChar, keyCode, event, realAction); |
| } else if (mImeState == ImeState.STATE_COMPOSING) { |
| return processStateEditComposing(keyChar, keyCode, event, |
| realAction); |
| } |
| } else { |
| if (0 != keyChar && realAction) { |
| commitResultText(String.valueOf((char) keyChar)); |
| } |
| } |
| |
| return false; |
| } |
| |
| // keyCode can be from both hard key or soft key. |
| private boolean processFunctionKeys(int keyCode, boolean realAction) { |
| // Back key is used to dismiss all popup UI in a soft keyboard. |
| if (keyCode == KeyEvent.KEYCODE_BACK) { |
| if (isInputViewShown()) { |
| if (mSkbContainer.handleBack(realAction)) return true; |
| } |
| } |
| |
| // Chinese related input is handle separately. |
| if (mInputModeSwitcher.isChineseText()) { |
| return false; |
| } |
| |
| if (null != mCandidatesContainer && mCandidatesContainer.isShown() |
| && !mDecInfo.isCandidatesListEmpty()) { |
| if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { |
| if (!realAction) return true; |
| |
| chooseCandidate(-1); |
| return true; |
| } |
| |
| if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { |
| if (!realAction) return true; |
| mCandidatesContainer.activeCurseBackward(); |
| return true; |
| } |
| |
| if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { |
| if (!realAction) return true; |
| mCandidatesContainer.activeCurseForward(); |
| return true; |
| } |
| |
| if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { |
| if (!realAction) return true; |
| mCandidatesContainer.pageBackward(false, true); |
| return true; |
| } |
| |
| if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { |
| if (!realAction) return true; |
| mCandidatesContainer.pageForward(false, true); |
| return true; |
| } |
| |
| if (keyCode == KeyEvent.KEYCODE_DEL && |
| ImeState.STATE_PREDICT == mImeState) { |
| if (!realAction) return true; |
| resetToIdleState(false); |
| return true; |
| } |
| } else { |
| if (keyCode == KeyEvent.KEYCODE_DEL) { |
| if (!realAction) return true; |
| if (SIMULATE_KEY_DELETE) { |
| simulateKeyEventDownUp(keyCode); |
| } else { |
| getCurrentInputConnection().deleteSurroundingText(1, 0); |
| } |
| return true; |
| } |
| if (keyCode == KeyEvent.KEYCODE_ENTER) { |
| if (!realAction) return true; |
| sendKeyChar('\n'); |
| return true; |
| } |
| if (keyCode == KeyEvent.KEYCODE_SPACE) { |
| if (!realAction) return true; |
| sendKeyChar(' '); |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| private boolean processStateIdle(int keyChar, int keyCode, KeyEvent event, |
| boolean realAction) { |
| // In this status, when user presses keys in [a..z], the status will |
| // change to input state. |
| if (keyChar >= 'a' && keyChar <= 'z' && !event.isAltPressed()) { |
| if (!realAction) return true; |
| mDecInfo.addSplChar((char) keyChar, true); |
| chooseAndUpdate(-1); |
| return true; |
| } else if (keyCode == KeyEvent.KEYCODE_DEL) { |
| if (!realAction) return true; |
| if (SIMULATE_KEY_DELETE) { |
| simulateKeyEventDownUp(keyCode); |
| } else { |
| getCurrentInputConnection().deleteSurroundingText(1, 0); |
| } |
| return true; |
| } else if (keyCode == KeyEvent.KEYCODE_ENTER) { |
| if (!realAction) return true; |
| sendKeyChar('\n'); |
| return true; |
| } else if (keyCode == KeyEvent.KEYCODE_ALT_LEFT |
| || keyCode == KeyEvent.KEYCODE_ALT_RIGHT |
| || keyCode == KeyEvent.KEYCODE_SHIFT_LEFT |
| || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { |
| return true; |
| } else if (event.isAltPressed()) { |
| char fullwidth_char = KeyMapDream.getChineseLabel(keyCode); |
| if (0 != fullwidth_char) { |
| if (realAction) { |
| String result = String.valueOf(fullwidth_char); |
| commitResultText(result); |
| } |
| return true; |
| } else { |
| if (keyCode >= KeyEvent.KEYCODE_A |
| && keyCode <= KeyEvent.KEYCODE_Z) { |
| return true; |
| } |
| } |
| } else if (keyChar != 0 && keyChar != '\t') { |
| if (realAction) { |
| if (keyChar == ',' || keyChar == '.') { |
| inputCommaPeriod("", keyChar, false, ImeState.STATE_IDLE); |
| } else { |
| if (0 != keyChar) { |
| String result = String.valueOf((char) keyChar); |
| commitResultText(result); |
| } |
| } |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean processStateInput(int keyChar, int keyCode, KeyEvent event, |
| boolean realAction) { |
| // If ALT key is pressed, input alternative key. But if the |
| // alternative key is quote key, it will be used for input a splitter |
| // in Pinyin string. |
| if (event.isAltPressed()) { |
| if ('\'' != event.getUnicodeChar(event.getMetaState())) { |
| if (realAction) { |
| char fullwidth_char = KeyMapDream.getChineseLabel(keyCode); |
| if (0 != fullwidth_char) { |
| commitResultText(mDecInfo |
| .getCurrentFullSent(mCandidatesContainer |
| .getActiveCandiatePos()) + |
| String.valueOf(fullwidth_char)); |
| resetToIdleState(false); |
| } |
| } |
| return true; |
| } else { |
| keyChar = '\''; |
| } |
| } |
| |
| if (keyChar >= 'a' && keyChar <= 'z' || keyChar == '\'' |
| && !mDecInfo.charBeforeCursorIsSeparator() |
| || keyCode == KeyEvent.KEYCODE_DEL) { |
| if (!realAction) return true; |
| return processSurfaceChange(keyChar, keyCode); |
| } else if (keyChar == ',' || keyChar == '.') { |
| if (!realAction) return true; |
| inputCommaPeriod(mDecInfo.getCurrentFullSent(mCandidatesContainer |
| .getActiveCandiatePos()), keyChar, true, |
| ImeState.STATE_IDLE); |
| return true; |
| } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP |
| || keyCode == KeyEvent.KEYCODE_DPAD_DOWN |
| || keyCode == KeyEvent.KEYCODE_DPAD_LEFT |
| || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { |
| if (!realAction) return true; |
| |
| if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { |
| mCandidatesContainer.activeCurseBackward(); |
| } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { |
| mCandidatesContainer.activeCurseForward(); |
| } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { |
| // If it has been the first page, a up key will shift |
| // the state to edit composing string. |
| if (!mCandidatesContainer.pageBackward(false, true)) { |
| mCandidatesContainer.enableActiveHighlight(false); |
| changeToStateComposing(true); |
| updateComposingText(true); |
| } |
| } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { |
| mCandidatesContainer.pageForward(false, true); |
| } |
| return true; |
| } else if (keyCode >= KeyEvent.KEYCODE_1 |
| && keyCode <= KeyEvent.KEYCODE_9) { |
| if (!realAction) return true; |
| |
| int activePos = keyCode - KeyEvent.KEYCODE_1; |
| int currentPage = mCandidatesContainer.getCurrentPage(); |
| if (activePos < mDecInfo.getCurrentPageSize(currentPage)) { |
| activePos = activePos |
| + mDecInfo.getCurrentPageStart(currentPage); |
| if (activePos >= 0) { |
| chooseAndUpdate(activePos); |
| } |
| } |
| return true; |
| } else if (keyCode == KeyEvent.KEYCODE_ENTER) { |
| if (!realAction) return true; |
| if (mInputModeSwitcher.isEnterNoramlState()) { |
| commitResultText(mDecInfo.getOrigianlSplStr().toString()); |
| resetToIdleState(false); |
| } else { |
| commitResultText(mDecInfo |
| .getCurrentFullSent(mCandidatesContainer |
| .getActiveCandiatePos())); |
| sendKeyChar('\n'); |
| resetToIdleState(false); |
| } |
| return true; |
| } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER |
| || keyCode == KeyEvent.KEYCODE_SPACE) { |
| if (!realAction) return true; |
| chooseCandidate(-1); |
| return true; |
| } else if (keyCode == KeyEvent.KEYCODE_BACK) { |
| if (!realAction) return true; |
| resetToIdleState(false); |
| requestHideSelf(0); |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean processStatePredict(int keyChar, int keyCode, |
| KeyEvent event, boolean realAction) { |
| if (!realAction) return true; |
| |
| // If ALT key is pressed, input alternative key. |
| if (event.isAltPressed()) { |
| char fullwidth_char = KeyMapDream.getChineseLabel(keyCode); |
| if (0 != fullwidth_char) { |
| commitResultText(mDecInfo.getCandidate(mCandidatesContainer |
| .getActiveCandiatePos()) + |
| String.valueOf(fullwidth_char)); |
| resetToIdleState(false); |
| } |
| return true; |
| } |
| |
| // In this status, when user presses keys in [a..z], the status will |
| // change to input state. |
| if (keyChar >= 'a' && keyChar <= 'z') { |
| changeToStateInput(true); |
| mDecInfo.addSplChar((char) keyChar, true); |
| chooseAndUpdate(-1); |
| } else if (keyChar == ',' || keyChar == '.') { |
| inputCommaPeriod("", keyChar, true, ImeState.STATE_IDLE); |
| } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP |
| || keyCode == KeyEvent.KEYCODE_DPAD_DOWN |
| || keyCode == KeyEvent.KEYCODE_DPAD_LEFT |
| || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { |
| if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { |
| mCandidatesContainer.activeCurseBackward(); |
| } |
| if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { |
| mCandidatesContainer.activeCurseForward(); |
| } |
| if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { |
| mCandidatesContainer.pageBackward(false, true); |
| } |
| if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { |
| mCandidatesContainer.pageForward(false, true); |
| } |
| } else if (keyCode == KeyEvent.KEYCODE_DEL) { |
| resetToIdleState(false); |
| } else if (keyCode == KeyEvent.KEYCODE_BACK) { |
| resetToIdleState(false); |
| requestHideSelf(0); |
| } else if (keyCode >= KeyEvent.KEYCODE_1 |
| && keyCode <= KeyEvent.KEYCODE_9) { |
| int activePos = keyCode - KeyEvent.KEYCODE_1; |
| int currentPage = mCandidatesContainer.getCurrentPage(); |
| if (activePos < mDecInfo.getCurrentPageSize(currentPage)) { |
| activePos = activePos |
| + mDecInfo.getCurrentPageStart(currentPage); |
| if (activePos >= 0) { |
| chooseAndUpdate(activePos); |
| } |
| } |
| } else if (keyCode == KeyEvent.KEYCODE_ENTER) { |
| sendKeyChar('\n'); |
| resetToIdleState(false); |
| } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER |
| || keyCode == KeyEvent.KEYCODE_SPACE) { |
| chooseCandidate(-1); |
| } |
| |
| return true; |
| } |
| |
| private boolean processStateEditComposing(int keyChar, int keyCode, |
| KeyEvent event, boolean realAction) { |
| if (!realAction) return true; |
| |
| ComposingView.ComposingStatus cmpsvStatus = |
| mComposingView.getComposingStatus(); |
| |
| // If ALT key is pressed, input alternative key. But if the |
| // alternative key is quote key, it will be used for input a splitter |
| // in Pinyin string. |
| if (event.isAltPressed()) { |
| if ('\'' != event.getUnicodeChar(event.getMetaState())) { |
| char fullwidth_char = KeyMapDream.getChineseLabel(keyCode); |
| if (0 != fullwidth_char) { |
| String retStr; |
| if (ComposingView.ComposingStatus.SHOW_STRING_LOWERCASE == |
| cmpsvStatus) { |
| retStr = mDecInfo.getOrigianlSplStr().toString(); |
| } else { |
| retStr = mDecInfo.getComposingStr(); |
| } |
| commitResultText(retStr + String.valueOf(fullwidth_char)); |
| resetToIdleState(false); |
| } |
| return true; |
| } else { |
| keyChar = '\''; |
| } |
| } |
| |
| if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { |
| if (!mDecInfo.selectionFinished()) { |
| changeToStateInput(true); |
| } |
| } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT |
| || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { |
| mComposingView.moveCursor(keyCode); |
| } else if ((keyCode == KeyEvent.KEYCODE_ENTER && mInputModeSwitcher |
| .isEnterNoramlState()) |
| || keyCode == KeyEvent.KEYCODE_DPAD_CENTER |
| || keyCode == KeyEvent.KEYCODE_SPACE) { |
| if (ComposingView.ComposingStatus.SHOW_STRING_LOWERCASE == cmpsvStatus) { |
| String str = mDecInfo.getOrigianlSplStr().toString(); |
| if (!tryInputRawUnicode(str)) { |
| commitResultText(str); |
| } |
| } else if (ComposingView.ComposingStatus.EDIT_PINYIN == cmpsvStatus) { |
| String str = mDecInfo.getComposingStr(); |
| if (!tryInputRawUnicode(str)) { |
| commitResultText(str); |
| } |
| } else { |
| commitResultText(mDecInfo.getComposingStr()); |
| } |
| resetToIdleState(false); |
| } else if (keyCode == KeyEvent.KEYCODE_ENTER |
| && !mInputModeSwitcher.isEnterNoramlState()) { |
| String retStr; |
| if (!mDecInfo.isCandidatesListEmpty()) { |
| retStr = mDecInfo.getCurrentFullSent(mCandidatesContainer |
| .getActiveCandiatePos()); |
| } else { |
| retStr = mDecInfo.getComposingStr(); |
| } |
| commitResultText(retStr); |
| sendKeyChar('\n'); |
| resetToIdleState(false); |
| } else if (keyCode == KeyEvent.KEYCODE_BACK) { |
| resetToIdleState(false); |
| requestHideSelf(0); |
| return true; |
| } else { |
| return processSurfaceChange(keyChar, keyCode); |
| } |
| return true; |
| } |
| |
| private boolean tryInputRawUnicode(String str) { |
| if (str.length() > 7) { |
| if (str.substring(0, 7).compareTo("unicode") == 0) { |
| try { |
| String digitStr = str.substring(7); |
| int startPos = 0; |
| int radix = 10; |
| if (digitStr.length() > 2 && digitStr.charAt(0) == '0' |
| && digitStr.charAt(1) == 'x') { |
| startPos = 2; |
| radix = 16; |
| } |
| digitStr = digitStr.substring(startPos); |
| int unicode = Integer.parseInt(digitStr, radix); |
| if (unicode > 0) { |
| char low = (char) (unicode & 0x0000ffff); |
| char high = (char) ((unicode & 0xffff0000) >> 16); |
| commitResultText(String.valueOf(low)); |
| if (0 != high) { |
| commitResultText(String.valueOf(high)); |
| } |
| } |
| return true; |
| } catch (NumberFormatException e) { |
| return false; |
| } |
| } else if (str.substring(str.length() - 7, str.length()).compareTo( |
| "unicode") == 0) { |
| String resultStr = ""; |
| for (int pos = 0; pos < str.length() - 7; pos++) { |
| if (pos > 0) { |
| resultStr += " "; |
| } |
| |
| resultStr += "0x" + Integer.toHexString(str.charAt(pos)); |
| } |
| commitResultText(String.valueOf(resultStr)); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean processSurfaceChange(int keyChar, int keyCode) { |
| if (mDecInfo.isSplStrFull() && KeyEvent.KEYCODE_DEL != keyCode) { |
| return true; |
| } |
| |
| if ((keyChar >= 'a' && keyChar <= 'z') |
| || (keyChar == '\'' && !mDecInfo.charBeforeCursorIsSeparator()) |
| || (((keyChar >= '0' && keyChar <= '9') || keyChar == ' ') && ImeState.STATE_COMPOSING == mImeState)) { |
| mDecInfo.addSplChar((char) keyChar, false); |
| chooseAndUpdate(-1); |
| } else if (keyCode == KeyEvent.KEYCODE_DEL) { |
| mDecInfo.prepareDeleteBeforeCursor(); |
| chooseAndUpdate(-1); |
| } |
| return true; |
| } |
| |
| private void changeToStateComposing(boolean updateUi) { |
| mImeState = ImeState.STATE_COMPOSING; |
| if (!updateUi) return; |
| |
| if (null != mSkbContainer && mSkbContainer.isShown()) { |
| mSkbContainer.toggleCandidateMode(true); |
| } |
| } |
| |
| private void changeToStateInput(boolean updateUi) { |
| mImeState = ImeState.STATE_INPUT; |
| if (!updateUi) return; |
| |
| if (null != mSkbContainer && mSkbContainer.isShown()) { |
| mSkbContainer.toggleCandidateMode(true); |
| } |
| showCandidateWindow(true); |
| } |
| |
| private void simulateKeyEventDownUp(int keyCode) { |
| InputConnection ic = getCurrentInputConnection(); |
| if (null == ic) return; |
| |
| ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); |
| ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode)); |
| } |
| |
| private void commitResultText(String resultText) { |
| InputConnection ic = getCurrentInputConnection(); |
| if (null != ic) ic.commitText(resultText, 1); |
| if (null != mComposingView) { |
| mComposingView.setVisibility(View.INVISIBLE); |
| mComposingView.invalidate(); |
| } |
| } |
| |
| private void updateComposingText(boolean visible) { |
| if (!visible) { |
| mComposingView.setVisibility(View.INVISIBLE); |
| } else { |
| mComposingView.setDecodingInfo(mDecInfo, mImeState); |
| mComposingView.setVisibility(View.VISIBLE); |
| } |
| mComposingView.invalidate(); |
| } |
| |
| private void inputCommaPeriod(String preEdit, int keyChar, |
| boolean dismissCandWindow, ImeState nextState) { |
| if (keyChar == ',') |
| preEdit += '\uff0c'; |
| else if (keyChar == '.') |
| preEdit += '\u3002'; |
| else |
| return; |
| commitResultText(preEdit); |
| if (dismissCandWindow) resetCandidateWindow(); |
| mImeState = nextState; |
| } |
| |
| private void resetToIdleState(boolean resetInlineText) { |
| if (ImeState.STATE_IDLE == mImeState) return; |
| |
| mImeState = ImeState.STATE_IDLE; |
| mDecInfo.reset(); |
| |
| if (null != mComposingView) mComposingView.reset(); |
| if (resetInlineText) commitResultText(""); |
| resetCandidateWindow(); |
| } |
| |
| private void chooseAndUpdate(int candId) { |
| if (!mInputModeSwitcher.isChineseText()) { |
| String choice = mDecInfo.getCandidate(candId); |
| if (null != choice) { |
| commitResultText(choice); |
| } |
| resetToIdleState(false); |
| return; |
| } |
| |
| if (ImeState.STATE_PREDICT != mImeState) { |
| // Get result candidate list, if choice_id < 0, do a new decoding. |
| // If choice_id >=0, select the candidate, and get the new candidate |
| // list. |
| mDecInfo.chooseDecodingCandidate(candId); |
| } else { |
| // Choose a prediction item. |
| mDecInfo.choosePredictChoice(candId); |
| } |
| |
| if (mDecInfo.getComposingStr().length() > 0) { |
| String resultStr; |
| resultStr = mDecInfo.getComposingStrActivePart(); |
| |
| // choiceId >= 0 means user finishes a choice selection. |
| if (candId >= 0 && mDecInfo.canDoPrediction()) { |
| commitResultText(resultStr); |
| mImeState = ImeState.STATE_PREDICT; |
| if (null != mSkbContainer && mSkbContainer.isShown()) { |
| mSkbContainer.toggleCandidateMode(false); |
| } |
| // Try to get the prediction list. |
| if (Settings.getPrediction()) { |
| InputConnection ic = getCurrentInputConnection(); |
| if (null != ic) { |
| CharSequence cs = ic.getTextBeforeCursor(3, 0); |
| if (null != cs) { |
| mDecInfo.preparePredicts(cs); |
| } |
| } |
| } else { |
| mDecInfo.resetCandidates(); |
| } |
| |
| if (mDecInfo.mCandidatesList.size() > 0) { |
| showCandidateWindow(false); |
| } else { |
| resetToIdleState(false); |
| } |
| } else { |
| if (ImeState.STATE_IDLE == mImeState) { |
| if (mDecInfo.getSplStrDecodedLen() == 0) { |
| changeToStateComposing(true); |
| } else { |
| changeToStateInput(true); |
| } |
| } else { |
| if (mDecInfo.selectionFinished()) { |
| changeToStateComposing(true); |
| } |
| } |
| showCandidateWindow(true); |
| } |
| } else { |
| resetToIdleState(false); |
| } |
| } |
| |
| // If activeCandNo is less than 0, get the current active candidate number |
| // from candidate view, otherwise use activeCandNo. |
| private void chooseCandidate(int activeCandNo) { |
| if (activeCandNo < 0) { |
| activeCandNo = mCandidatesContainer.getActiveCandiatePos(); |
| } |
| if (activeCandNo >= 0) { |
| chooseAndUpdate(activeCandNo); |
| } |
| } |
| |
| private boolean startPinyinDecoderService() { |
| if (null == mDecInfo.mIPinyinDecoderService) { |
| Intent serviceIntent = new Intent(); |
| serviceIntent.setClass(this, PinyinDecoderService.class); |
| |
| if (null == mPinyinDecoderServiceConnection) { |
| mPinyinDecoderServiceConnection = new PinyinDecoderServiceConnection(); |
| } |
| |
| // Bind service |
| if (bindService(serviceIntent, mPinyinDecoderServiceConnection, |
| Context.BIND_AUTO_CREATE)) { |
| return true; |
| } else { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| @Override |
| public View onCreateCandidatesView() { |
| if (mEnvironment.needDebug()) { |
| Log.d(TAG, "onCreateCandidatesView."); |
| } |
| |
| LayoutInflater inflater = getLayoutInflater(); |
| // Inflate the floating container view |
| mFloatingContainer = (LinearLayout) inflater.inflate( |
| R.layout.floating_container, null); |
| |
| // The first child is the composing view. |
| mComposingView = (ComposingView) mFloatingContainer.getChildAt(0); |
| |
| mCandidatesContainer = (CandidatesContainer) inflater.inflate( |
| R.layout.candidates_container, null); |
| |
| // Create balloon hint for candidates view. |
| mCandidatesBalloon = new BalloonHint(this, mCandidatesContainer, |
| MeasureSpec.UNSPECIFIED); |
| mCandidatesBalloon.setBalloonBackground(getResources().getDrawable( |
| R.drawable.candidate_balloon_bg)); |
| mCandidatesContainer.initialize(mChoiceNotifier, mCandidatesBalloon, |
| mGestureDetectorCandidates); |
| |
| // The floating window |
| if (null != mFloatingWindow && mFloatingWindow.isShowing()) { |
| mFloatingWindowTimer.cancelShowing(); |
| mFloatingWindow.dismiss(); |
| } |
| mFloatingWindow = new PopupWindow(this); |
| mFloatingWindow.setClippingEnabled(false); |
| mFloatingWindow.setBackgroundDrawable(null); |
| mFloatingWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); |
| mFloatingWindow.setContentView(mFloatingContainer); |
| |
| setCandidatesViewShown(true); |
| return mCandidatesContainer; |
| } |
| |
| public void responseSoftKeyEvent(SoftKey sKey) { |
| if (null == sKey) return; |
| |
| InputConnection ic = getCurrentInputConnection(); |
| if (ic == null) return; |
| |
| int keyCode = sKey.getKeyCode(); |
| // Process some general keys, including KEYCODE_DEL, KEYCODE_SPACE, |
| // KEYCODE_ENTER and KEYCODE_DPAD_CENTER. |
| if (sKey.isKeyCodeKey()) { |
| if (processFunctionKeys(keyCode, true)) return; |
| } |
| |
| if (sKey.isUserDefKey()) { |
| updateIcon(mInputModeSwitcher.switchModeForUserKey(keyCode)); |
| resetToIdleState(false); |
| mSkbContainer.updateInputMode(); |
| } else { |
| if (sKey.isKeyCodeKey()) { |
| KeyEvent eDown = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, |
| keyCode, 0, 0, 0, 0, KeyEvent.FLAG_SOFT_KEYBOARD); |
| KeyEvent eUp = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, |
| 0, 0, 0, 0, KeyEvent.FLAG_SOFT_KEYBOARD); |
| |
| onKeyDown(keyCode, eDown); |
| onKeyUp(keyCode, eUp); |
| } else if (sKey.isUniStrKey()) { |
| boolean kUsed = false; |
| String keyLabel = sKey.getKeyLabel(); |
| if (mInputModeSwitcher.isChineseTextWithSkb() |
| && (ImeState.STATE_INPUT == mImeState || ImeState.STATE_COMPOSING == mImeState)) { |
| if (mDecInfo.length() > 0 && keyLabel.length() == 1 |
| && keyLabel.charAt(0) == '\'') { |
| processSurfaceChange('\'', 0); |
| kUsed = true; |
| } |
| } |
| if (!kUsed) { |
| if (ImeState.STATE_INPUT == mImeState) { |
| commitResultText(mDecInfo |
| .getCurrentFullSent(mCandidatesContainer |
| .getActiveCandiatePos())); |
| } else if (ImeState.STATE_COMPOSING == mImeState) { |
| commitResultText(mDecInfo.getComposingStr()); |
| } |
| commitResultText(keyLabel); |
| resetToIdleState(false); |
| } |
| } |
| |
| // If the current soft keyboard is not sticky, IME needs to go |
| // back to the previous soft keyboard automatically. |
| if (!mSkbContainer.isCurrentSkbSticky()) { |
| updateIcon(mInputModeSwitcher.requestBackToPreviousSkb()); |
| resetToIdleState(false); |
| mSkbContainer.updateInputMode(); |
| } |
| } |
| } |
| |
| private void showCandidateWindow(boolean showComposingView) { |
| if (mEnvironment.needDebug()) { |
| Log.d(TAG, "Candidates window is shown. Parent = " |
| + mCandidatesContainer); |
| } |
| |
| setCandidatesViewShown(true); |
| |
| if (null != mSkbContainer) mSkbContainer.requestLayout(); |
| |
| if (null == mCandidatesContainer) { |
| resetToIdleState(false); |
| return; |
| } |
| |
| updateComposingText(showComposingView); |
| mCandidatesContainer.showCandidates(mDecInfo, |
| ImeState.STATE_COMPOSING != mImeState); |
| mFloatingWindowTimer.postShowFloatingWindow(); |
| } |
| |
| private void dismissCandidateWindow() { |
| if (mEnvironment.needDebug()) { |
| Log.d(TAG, "Candidates window is to be dismissed"); |
| } |
| if (null == mCandidatesContainer) return; |
| try { |
| mFloatingWindowTimer.cancelShowing(); |
| mFloatingWindow.dismiss(); |
| } catch (Exception e) { |
| Log.e(TAG, "Fail to show the PopupWindow."); |
| } |
| setCandidatesViewShown(false); |
| |
| if (null != mSkbContainer && mSkbContainer.isShown()) { |
| mSkbContainer.toggleCandidateMode(false); |
| } |
| } |
| |
| private void resetCandidateWindow() { |
| if (mEnvironment.needDebug()) { |
| Log.d(TAG, "Candidates window is to be reset"); |
| } |
| if (null == mCandidatesContainer) return; |
| try { |
| mFloatingWindowTimer.cancelShowing(); |
| mFloatingWindow.dismiss(); |
| } catch (Exception e) { |
| Log.e(TAG, "Fail to show the PopupWindow."); |
| } |
| |
| if (null != mSkbContainer && mSkbContainer.isShown()) { |
| mSkbContainer.toggleCandidateMode(false); |
| } |
| |
| mDecInfo.resetCandidates(); |
| |
| if (null != mCandidatesContainer && mCandidatesContainer.isShown()) { |
| showCandidateWindow(false); |
| } |
| } |
| |
| private void updateIcon(int iconId) { |
| if (iconId > 0) { |
| showStatusIcon(iconId); |
| } else { |
| hideStatusIcon(); |
| } |
| } |
| |
| @Override |
| public View onCreateInputView() { |
| if (mEnvironment.needDebug()) { |
| Log.d(TAG, "onCreateInputView."); |
| } |
| LayoutInflater inflater = getLayoutInflater(); |
| mSkbContainer = (SkbContainer) inflater.inflate(R.layout.skb_container, |
| null); |
| mSkbContainer.setService(this); |
| mSkbContainer.setInputModeSwitcher(mInputModeSwitcher); |
| mSkbContainer.setGestureDetector(mGestureDetectorSkb); |
| return mSkbContainer; |
| } |
| |
| @Override |
| public void onStartInput(EditorInfo editorInfo, boolean restarting) { |
| if (mEnvironment.needDebug()) { |
| Log.d(TAG, "onStartInput " + " ccontentType: " |
| + String.valueOf(editorInfo.inputType) + " Restarting:" |
| + String.valueOf(restarting)); |
| } |
| updateIcon(mInputModeSwitcher.requestInputWithHkb(editorInfo)); |
| resetToIdleState(false); |
| } |
| |
| @Override |
| public void onStartInputView(EditorInfo editorInfo, boolean restarting) { |
| if (mEnvironment.needDebug()) { |
| Log.d(TAG, "onStartInputView " + " contentType: " |
| + String.valueOf(editorInfo.inputType) + " Restarting:" |
| + String.valueOf(restarting)); |
| } |
| updateIcon(mInputModeSwitcher.requestInputWithSkb(editorInfo)); |
| resetToIdleState(false); |
| mSkbContainer.updateInputMode(); |
| setCandidatesViewShown(false); |
| } |
| |
| @Override |
| public void onFinishInputView(boolean finishingInput) { |
| if (mEnvironment.needDebug()) { |
| Log.d(TAG, "onFinishInputView."); |
| } |
| resetToIdleState(false); |
| super.onFinishInputView(finishingInput); |
| } |
| |
| @Override |
| public void onFinishInput() { |
| if (mEnvironment.needDebug()) { |
| Log.d(TAG, "onFinishInput."); |
| } |
| resetToIdleState(false); |
| super.onFinishInput(); |
| } |
| |
| @Override |
| public void onFinishCandidatesView(boolean finishingInput) { |
| if (mEnvironment.needDebug()) { |
| Log.d(TAG, "onFinishCandidateView."); |
| } |
| resetToIdleState(false); |
| super.onFinishCandidatesView(finishingInput); |
| } |
| |
| @Override public void onDisplayCompletions(CompletionInfo[] completions) { |
| if (!isFullscreenMode()) return; |
| if (null == completions || completions.length <= 0) return; |
| if (null == mSkbContainer || !mSkbContainer.isShown()) return; |
| |
| if (!mInputModeSwitcher.isChineseText() || |
| ImeState.STATE_IDLE == mImeState || |
| ImeState.STATE_PREDICT == mImeState) { |
| mImeState = ImeState.STATE_APP_COMPLETION; |
| mDecInfo.prepareAppCompletions(completions); |
| showCandidateWindow(false); |
| } |
| } |
| |
| private void onChoiceTouched(int activeCandNo) { |
| if (mImeState == ImeState.STATE_COMPOSING) { |
| changeToStateInput(true); |
| } else if (mImeState == ImeState.STATE_INPUT |
| || mImeState == ImeState.STATE_PREDICT) { |
| chooseCandidate(activeCandNo); |
| } else if (mImeState == ImeState.STATE_APP_COMPLETION) { |
| if (null != mDecInfo.mAppCompletions && activeCandNo >= 0 && |
| activeCandNo < mDecInfo.mAppCompletions.length) { |
| CompletionInfo ci = mDecInfo.mAppCompletions[activeCandNo]; |
| if (null != ci) { |
| InputConnection ic = getCurrentInputConnection(); |
| ic.commitCompletion(ci); |
| } |
| } |
| resetToIdleState(false); |
| } |
| } |
| |
| @Override |
| public void requestHideSelf(int flags) { |
| if (mEnvironment.needDebug()) { |
| Log.d(TAG, "DimissSoftInput."); |
| } |
| dismissCandidateWindow(); |
| if (null != mSkbContainer && mSkbContainer.isShown()) { |
| mSkbContainer.dismissPopups(); |
| } |
| super.requestHideSelf(flags); |
| } |
| |
| public void showOptionsMenu() { |
| AlertDialog.Builder builder = new AlertDialog.Builder(this); |
| builder.setCancelable(true); |
| builder.setIcon(R.drawable.app_icon); |
| builder.setNegativeButton(android.R.string.cancel, null); |
| CharSequence itemSettings = getString(R.string.ime_settings_activity_name); |
| CharSequence itemInputMethod = getString(com.android.internal.R.string.inputMethod); |
| builder.setItems(new CharSequence[] {itemSettings, itemInputMethod}, |
| new DialogInterface.OnClickListener() { |
| |
| public void onClick(DialogInterface di, int position) { |
| di.dismiss(); |
| switch (position) { |
| case 0: |
| launchSettings(); |
| break; |
| case 1: |
| InputMethodManager.getInstance(PinyinIME.this) |
| .showInputMethodPicker(); |
| break; |
| } |
| } |
| }); |
| builder.setTitle(getString(R.string.ime_name)); |
| mOptionsDialog = builder.create(); |
| Window window = mOptionsDialog.getWindow(); |
| WindowManager.LayoutParams lp = window.getAttributes(); |
| lp.token = mSkbContainer.getWindowToken(); |
| lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; |
| window.setAttributes(lp); |
| window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); |
| mOptionsDialog.show(); |
| } |
| |
| private void launchSettings() { |
| Intent intent = new Intent(); |
| intent.setClass(PinyinIME.this, SettingsActivity.class); |
| intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| startActivity(intent); |
| } |
| |
| private class PopupTimer extends Handler implements Runnable { |
| private int mParentLocation[] = new int[2]; |
| |
| void postShowFloatingWindow() { |
| mFloatingContainer.measure(LayoutParams.WRAP_CONTENT, |
| LayoutParams.WRAP_CONTENT); |
| mFloatingWindow.setWidth(mFloatingContainer.getMeasuredWidth()); |
| mFloatingWindow.setHeight(mFloatingContainer.getMeasuredHeight()); |
| post(this); |
| } |
| |
| void cancelShowing() { |
| if (mFloatingWindow.isShowing()) { |
| mFloatingWindow.dismiss(); |
| } |
| removeCallbacks(this); |
| } |
| |
| public void run() { |
| mCandidatesContainer.getLocationInWindow(mParentLocation); |
| |
| if (!mFloatingWindow.isShowing()) { |
| mFloatingWindow.showAtLocation(mCandidatesContainer, |
| Gravity.LEFT | Gravity.TOP, mParentLocation[0], |
| mParentLocation[1] -mFloatingWindow.getHeight()); |
| } else { |
| mFloatingWindow |
| .update(mParentLocation[0], |
| mParentLocation[1] - mFloatingWindow.getHeight(), |
| mFloatingWindow.getWidth(), |
| mFloatingWindow.getHeight()); |
| } |
| } |
| } |
| |
| /** |
| * Used to notify IME that the user selects a candidate or performs an |
| * gesture. |
| */ |
| public class ChoiceNotifier extends Handler implements |
| CandidateViewListener { |
| PinyinIME mIme; |
| |
| ChoiceNotifier(PinyinIME ime) { |
| mIme = ime; |
| } |
| |
| public void onClickChoice(int choiceId) { |
| if (choiceId >= 0) { |
| mIme.onChoiceTouched(choiceId); |
| } |
| } |
| |
| public void onToLeftGesture() { |
| if (ImeState.STATE_COMPOSING == mImeState) { |
| changeToStateInput(true); |
| } |
| mCandidatesContainer.pageForward(true, false); |
| } |
| |
| public void onToRightGesture() { |
| if (ImeState.STATE_COMPOSING == mImeState) { |
| changeToStateInput(true); |
| } |
| mCandidatesContainer.pageBackward(true, false); |
| } |
| |
| public void onToTopGesture() { |
| } |
| |
| public void onToBottomGesture() { |
| } |
| } |
| |
| public class OnGestureListener extends |
| GestureDetector.SimpleOnGestureListener { |
| /** |
| * When user presses and drags, the minimum x-distance to make a |
| * response to the drag event. |
| */ |
| private static final int MIN_X_FOR_DRAG = 60; |
| |
| /** |
| * When user presses and drags, the minimum y-distance to make a |
| * response to the drag event. |
| */ |
| private static final int MIN_Y_FOR_DRAG = 40; |
| |
| /** |
| * Velocity threshold for a screen-move gesture. If the minimum |
| * x-velocity is less than it, no gesture. |
| */ |
| static private final float VELOCITY_THRESHOLD_X1 = 0.3f; |
| |
| /** |
| * Velocity threshold for a screen-move gesture. If the maximum |
| * x-velocity is less than it, no gesture. |
| */ |
| static private final float VELOCITY_THRESHOLD_X2 = 0.7f; |
| |
| /** |
| * Velocity threshold for a screen-move gesture. If the minimum |
| * y-velocity is less than it, no gesture. |
| */ |
| static private final float VELOCITY_THRESHOLD_Y1 = 0.2f; |
| |
| /** |
| * Velocity threshold for a screen-move gesture. If the maximum |
| * y-velocity is less than it, no gesture. |
| */ |
| static private final float VELOCITY_THRESHOLD_Y2 = 0.45f; |
| |
| /** If it false, we will not response detected gestures. */ |
| private boolean mReponseGestures; |
| |
| /** The minimum X velocity observed in the gesture. */ |
| private float mMinVelocityX = Float.MAX_VALUE; |
| |
| /** The minimum Y velocity observed in the gesture. */ |
| private float mMinVelocityY = Float.MAX_VALUE; |
| |
| /** The first down time for the series of touch events for an action. */ |
| private long mTimeDown; |
| |
| /** The last time when onScroll() is called. */ |
| private long mTimeLastOnScroll; |
| |
| /** This flag used to indicate that this gesture is not a gesture. */ |
| private boolean mNotGesture; |
| |
| /** This flag used to indicate that this gesture has been recognized. */ |
| private boolean mGestureRecognized; |
| |
| public OnGestureListener(boolean reponseGestures) { |
| mReponseGestures = reponseGestures; |
| } |
| |
| @Override |
| public boolean onDown(MotionEvent e) { |
| mMinVelocityX = Integer.MAX_VALUE; |
| mMinVelocityY = Integer.MAX_VALUE; |
| mTimeDown = e.getEventTime(); |
| mTimeLastOnScroll = mTimeDown; |
| mNotGesture = false; |
| mGestureRecognized = false; |
| return false; |
| } |
| |
| @Override |
| public boolean onScroll(MotionEvent e1, MotionEvent e2, |
| float distanceX, float distanceY) { |
| if (mNotGesture) return false; |
| if (mGestureRecognized) return true; |
| |
| if (Math.abs(e1.getX() - e2.getX()) < MIN_X_FOR_DRAG |
| && Math.abs(e1.getY() - e2.getY()) < MIN_Y_FOR_DRAG) |
| return false; |
| |
| long timeNow = e2.getEventTime(); |
| long spanTotal = timeNow - mTimeDown; |
| long spanThis = timeNow - mTimeLastOnScroll; |
| if (0 == spanTotal) spanTotal = 1; |
| if (0 == spanThis) spanThis = 1; |
| |
| float vXTotal = (e2.getX() - e1.getX()) / spanTotal; |
| float vYTotal = (e2.getY() - e1.getY()) / spanTotal; |
| |
| // The distances are from the current point to the previous one. |
| float vXThis = -distanceX / spanThis; |
| float vYThis = -distanceY / spanThis; |
| |
| float kX = vXTotal * vXThis; |
| float kY = vYTotal * vYThis; |
| float k1 = kX + kY; |
| float k2 = Math.abs(kX) + Math.abs(kY); |
| |
| if (k1 / k2 < 0.8) { |
| mNotGesture = true; |
| return false; |
| } |
| float absVXTotal = Math.abs(vXTotal); |
| float absVYTotal = Math.abs(vYTotal); |
| if (absVXTotal < mMinVelocityX) { |
| mMinVelocityX = absVXTotal; |
| } |
| if (absVYTotal < mMinVelocityY) { |
| mMinVelocityY = absVYTotal; |
| } |
| |
| if (mMinVelocityX < VELOCITY_THRESHOLD_X1 |
| && mMinVelocityY < VELOCITY_THRESHOLD_Y1) { |
| mNotGesture = true; |
| return false; |
| } |
| |
| if (vXTotal > VELOCITY_THRESHOLD_X2 |
| && absVYTotal < VELOCITY_THRESHOLD_Y2) { |
| if (mReponseGestures) onDirectionGesture(Gravity.RIGHT); |
| mGestureRecognized = true; |
| } else if (vXTotal < -VELOCITY_THRESHOLD_X2 |
| && absVYTotal < VELOCITY_THRESHOLD_Y2) { |
| if (mReponseGestures) onDirectionGesture(Gravity.LEFT); |
| mGestureRecognized = true; |
| } else if (vYTotal > VELOCITY_THRESHOLD_Y2 |
| && absVXTotal < VELOCITY_THRESHOLD_X2) { |
| if (mReponseGestures) onDirectionGesture(Gravity.BOTTOM); |
| mGestureRecognized = true; |
| } else if (vYTotal < -VELOCITY_THRESHOLD_Y2 |
| && absVXTotal < VELOCITY_THRESHOLD_X2) { |
| if (mReponseGestures) onDirectionGesture(Gravity.TOP); |
| mGestureRecognized = true; |
| } |
| |
| mTimeLastOnScroll = timeNow; |
| return mGestureRecognized; |
| } |
| |
| @Override |
| public boolean onFling(MotionEvent me1, MotionEvent me2, |
| float velocityX, float velocityY) { |
| return mGestureRecognized; |
| } |
| |
| public void onDirectionGesture(int gravity) { |
| if (Gravity.NO_GRAVITY == gravity) { |
| return; |
| } |
| |
| if (Gravity.LEFT == gravity || Gravity.RIGHT == gravity) { |
| if (mCandidatesContainer.isShown()) { |
| if (Gravity.LEFT == gravity) { |
| mCandidatesContainer.pageForward(true, true); |
| } else { |
| mCandidatesContainer.pageBackward(true, true); |
| } |
| return; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Connection used for binding to the Pinyin decoding service. |
| */ |
| public class PinyinDecoderServiceConnection implements ServiceConnection { |
| public void onServiceConnected(ComponentName name, IBinder service) { |
| mDecInfo.mIPinyinDecoderService = IPinyinDecoderService.Stub |
| .asInterface(service); |
| } |
| |
| public void onServiceDisconnected(ComponentName name) { |
| } |
| } |
| |
| public enum ImeState { |
| STATE_BYPASS, STATE_IDLE, STATE_INPUT, STATE_COMPOSING, STATE_PREDICT, |
| STATE_APP_COMPLETION |
| } |
| |
| public class DecodingInfo { |
| /** |
| * Maximum length of the Pinyin string |
| */ |
| private static final int PY_STRING_MAX = 28; |
| |
| /** |
| * Maximum number of candidates to display in one page. |
| */ |
| private static final int MAX_PAGE_SIZE_DISPLAY = 10; |
| |
| /** |
| * Spelling (Pinyin) string. |
| */ |
| private StringBuffer mSurface; |
| |
| /** |
| * Byte buffer used as the Pinyin string parameter for native function |
| * call. |
| */ |
| private byte mPyBuf[]; |
| |
| /** |
| * The length of surface string successfully decoded by engine. |
| */ |
| private int mSurfaceDecodedLen; |
| |
| /** |
| * Composing string. |
| */ |
| private String mComposingStr; |
| |
| /** |
| * Length of the active composing string. |
| */ |
| private int mActiveCmpsLen; |
| |
| /** |
| * Composing string for display, it is copied from mComposingStr, and |
| * add spaces between spellings. |
| **/ |
| private String mComposingStrDisplay; |
| |
| /** |
| * Length of the active composing string for display. |
| */ |
| private int mActiveCmpsDisplayLen; |
| |
| /** |
| * The first full sentence choice. |
| */ |
| private String mFullSent; |
| |
| /** |
| * Number of characters which have been fixed. |
| */ |
| private int mFixedLen; |
| |
| /** |
| * If this flag is true, selection is finished. |
| */ |
| private boolean mFinishSelection; |
| |
| /** |
| * The starting position for each spelling. The first one is the number |
| * of the real starting position elements. |
| */ |
| private int mSplStart[]; |
| |
| /** |
| * Editing cursor in mSurface. |
| */ |
| private int mCursorPos; |
| |
| /** |
| * Remote Pinyin-to-Hanzi decoding engine service. |
| */ |
| private IPinyinDecoderService mIPinyinDecoderService; |
| |
| /** |
| * The complication information suggested by application. |
| */ |
| private CompletionInfo[] mAppCompletions; |
| |
| /** |
| * The total number of choices for display. The list may only contains |
| * the first part. If user tries to navigate to next page which is not |
| * in the result list, we need to get these items. |
| **/ |
| public int mTotalChoicesNum; |
| |
| /** |
| * Candidate list. The first one is the full-sentence candidate. |
| */ |
| public List<String> mCandidatesList = new Vector<String>(); |
| |
| /** |
| * Element i stores the starting position of page i. |
| */ |
| public Vector<Integer> mPageStart = new Vector<Integer>(); |
| |
| /** |
| * Element i stores the number of characters to page i. |
| */ |
| public Vector<Integer> mCnToPage = new Vector<Integer>(); |
| |
| /** |
| * The position to delete in Pinyin string. If it is less than 0, IME |
| * will do an incremental search, otherwise IME will do a deletion |
| * operation. if {@link #mIsPosInSpl} is true, IME will delete the whole |
| * string for mPosDelSpl-th spelling, otherwise it will only delete |
| * mPosDelSpl-th character in the Pinyin string. |
| */ |
| public int mPosDelSpl = -1; |
| |
| /** |
| * If {@link #mPosDelSpl} is big than or equal to 0, this member is used |
| * to indicate that whether the postion is counted in spelling id or |
| * character. |
| */ |
| public boolean mIsPosInSpl; |
| |
| public DecodingInfo() { |
| mSurface = new StringBuffer(); |
| mSurfaceDecodedLen = 0; |
| } |
| |
| public void reset() { |
| mSurface.delete(0, mSurface.length()); |
| mSurfaceDecodedLen = 0; |
| mCursorPos = 0; |
| mFullSent = ""; |
| mFixedLen = 0; |
| mFinishSelection = false; |
| mComposingStr = ""; |
| mComposingStrDisplay = ""; |
| mActiveCmpsLen = 0; |
| mActiveCmpsDisplayLen = 0; |
| |
| resetCandidates(); |
| } |
| |
| public boolean isCandidatesListEmpty() { |
| return mCandidatesList.size() == 0; |
| } |
| |
| public boolean isSplStrFull() { |
| if (mSurface.length() >= PY_STRING_MAX - 1) return true; |
| return false; |
| } |
| |
| public void addSplChar(char ch, boolean reset) { |
| if (reset) { |
| mSurface.delete(0, mSurface.length()); |
| mSurfaceDecodedLen = 0; |
| mCursorPos = 0; |
| try { |
| mIPinyinDecoderService.imResetSearch(); |
| } catch (RemoteException e) { |
| } |
| } |
| mSurface.insert(mCursorPos, ch); |
| mCursorPos++; |
| } |
| |
| // Prepare to delete before cursor. We may delete a spelling char if |
| // the cursor is in the range of unfixed part, delete a whole spelling |
| // if the cursor in inside the range of the fixed part. |
| // This function only marks the position used to delete. |
| public void prepareDeleteBeforeCursor() { |
| if (mCursorPos > 0) { |
| int pos; |
| for (pos = 0; pos < mFixedLen; pos++) { |
| if (mSplStart[pos + 2] >= mCursorPos |
| && mSplStart[pos + 1] < mCursorPos) { |
| mPosDelSpl = pos; |
| mCursorPos = mSplStart[pos + 1]; |
| mIsPosInSpl = true; |
| break; |
| } |
| } |
| if (mPosDelSpl < 0) { |
| mPosDelSpl = mCursorPos - 1; |
| mCursorPos--; |
| mIsPosInSpl = false; |
| } |
| } |
| } |
| |
| public int length() { |
| return mSurface.length(); |
| } |
| |
| public char charAt(int index) { |
| return mSurface.charAt(index); |
| } |
| |
| public StringBuffer getOrigianlSplStr() { |
| return mSurface; |
| } |
| |
| public int getSplStrDecodedLen() { |
| return mSurfaceDecodedLen; |
| } |
| |
| public int[] getSplStart() { |
| return mSplStart; |
| } |
| |
| public String getComposingStr() { |
| return mComposingStr; |
| } |
| |
| public String getComposingStrActivePart() { |
| assert (mActiveCmpsLen <= mComposingStr.length()); |
| return mComposingStr.substring(0, mActiveCmpsLen); |
| } |
| |
| public int getActiveCmpsLen() { |
| return mActiveCmpsLen; |
| } |
| |
| public String getComposingStrForDisplay() { |
| return mComposingStrDisplay; |
| } |
| |
| public int getActiveCmpsDisplayLen() { |
| return mActiveCmpsDisplayLen; |
| } |
| |
| public String getFullSent() { |
| return mFullSent; |
| } |
| |
| public String getCurrentFullSent(int activeCandPos) { |
| try { |
| String retStr = mFullSent.substring(0, mFixedLen); |
| retStr += mCandidatesList.get(activeCandPos); |
| return retStr; |
| } catch (Exception e) { |
| return ""; |
| } |
| } |
| |
| public void resetCandidates() { |
| mCandidatesList.clear(); |
| mTotalChoicesNum = 0; |
| |
| mPageStart.clear(); |
| mPageStart.add(0); |
| mCnToPage.clear(); |
| mCnToPage.add(0); |
| } |
| |
| public boolean candidatesFromApp() { |
| return ImeState.STATE_APP_COMPLETION == mImeState; |
| } |
| |
| public boolean canDoPrediction() { |
| return mComposingStr.length() == mFixedLen; |
| } |
| |
| public boolean selectionFinished() { |
| return mFinishSelection; |
| } |
| |
| // After the user chooses a candidate, input method will do a |
| // re-decoding and give the new candidate list. |
| // If candidate id is less than 0, means user is inputting Pinyin, |
| // not selecting any choice. |
| private void chooseDecodingCandidate(int candId) { |
| if (mImeState != ImeState.STATE_PREDICT) { |
| resetCandidates(); |
| int totalChoicesNum = 0; |
| try { |
| if (candId < 0) { |
| if (length() == 0) { |
| totalChoicesNum = 0; |
| } else { |
| if (mPyBuf == null) |
| mPyBuf = new byte[PY_STRING_MAX]; |
| for (int i = 0; i < length(); i++) |
| mPyBuf[i] = (byte) charAt(i); |
| mPyBuf[length()] = 0; |
| |
| if (mPosDelSpl < 0) { |
| totalChoicesNum = mIPinyinDecoderService |
| .imSearch(mPyBuf, length()); |
| } else { |
| boolean clear_fixed_this_step = true; |
| if (ImeState.STATE_COMPOSING == mImeState) { |
| clear_fixed_this_step = false; |
| } |
| totalChoicesNum = mIPinyinDecoderService |
| .imDelSearch(mPosDelSpl, mIsPosInSpl, |
| clear_fixed_this_step); |
| mPosDelSpl = -1; |
| } |
| } |
| } else { |
| totalChoicesNum = mIPinyinDecoderService |
| .imChoose(candId); |
| } |
| } catch (RemoteException e) { |
| } |
| updateDecInfoForSearch(totalChoicesNum); |
| } |
| } |
| |
| private void updateDecInfoForSearch(int totalChoicesNum) { |
| mTotalChoicesNum = totalChoicesNum; |
| if (mTotalChoicesNum < 0) { |
| mTotalChoicesNum = 0; |
| return; |
| } |
| |
| try { |
| String pyStr; |
| |
| mSplStart = mIPinyinDecoderService.imGetSplStart(); |
| pyStr = mIPinyinDecoderService.imGetPyStr(false); |
| mSurfaceDecodedLen = mIPinyinDecoderService.imGetPyStrLen(true); |
| assert (mSurfaceDecodedLen <= pyStr.length()); |
| |
| mFullSent = mIPinyinDecoderService.imGetChoice(0); |
| mFixedLen = mIPinyinDecoderService.imGetFixedLen(); |
| |
| // Update the surface string to the one kept by engine. |
| mSurface.replace(0, mSurface.length(), pyStr); |
| |
| if (mCursorPos > mSurface.length()) |
| mCursorPos = mSurface.length(); |
| mComposingStr = mFullSent.substring(0, mFixedLen) |
| + mSurface.substring(mSplStart[mFixedLen + 1]); |
| |
| mActiveCmpsLen = mComposingStr.length(); |
| if (mSurfaceDecodedLen > 0) { |
| mActiveCmpsLen = mActiveCmpsLen |
| - (mSurface.length() - mSurfaceDecodedLen); |
| } |
| |
| // Prepare the display string. |
| if (0 == mSurfaceDecodedLen) { |
| mComposingStrDisplay = mComposingStr; |
| mActiveCmpsDisplayLen = mComposingStr.length(); |
| } else { |
| mComposingStrDisplay = mFullSent.substring(0, mFixedLen); |
| for (int pos = mFixedLen + 1; pos < mSplStart.length - 1; pos++) { |
| mComposingStrDisplay += mSurface.substring( |
| mSplStart[pos], mSplStart[pos + 1]); |
| if (mSplStart[pos + 1] < mSurfaceDecodedLen) { |
| mComposingStrDisplay += " "; |
| } |
| } |
| mActiveCmpsDisplayLen = mComposingStrDisplay.length(); |
| if (mSurfaceDecodedLen < mSurface.length()) { |
| mComposingStrDisplay += mSurface |
| .substring(mSurfaceDecodedLen); |
| } |
| } |
| |
| if (mSplStart.length == mFixedLen + 2) { |
| mFinishSelection = true; |
| } else { |
| mFinishSelection = false; |
| } |
| } catch (RemoteException e) { |
| Log.w(TAG, "PinyinDecoderService died", e); |
| } catch (Exception e) { |
| mTotalChoicesNum = 0; |
| mComposingStr = ""; |
| } |
| // Prepare page 0. |
| if (!mFinishSelection) { |
| preparePage(0); |
| } |
| } |
| |
| private void choosePredictChoice(int choiceId) { |
| if (ImeState.STATE_PREDICT != mImeState || choiceId < 0 |
| || choiceId >= mTotalChoicesNum) { |
| return; |
| } |
| |
| String tmp = mCandidatesList.get(choiceId); |
| |
| resetCandidates(); |
| |
| mCandidatesList.add(tmp); |
| mTotalChoicesNum = 1; |
| |
| mSurface.replace(0, mSurface.length(), ""); |
| mCursorPos = 0; |
| mFullSent = tmp; |
| mFixedLen = tmp.length(); |
| mComposingStr = mFullSent; |
| mActiveCmpsLen = mFixedLen; |
| |
| mFinishSelection = true; |
| } |
| |
| public String getCandidate(int candId) { |
| // Only loaded items can be gotten, so we use mCandidatesList.size() |
| // instead mTotalChoiceNum. |
| if (candId < 0 || candId > mCandidatesList.size()) { |
| return null; |
| } |
| return mCandidatesList.get(candId); |
| } |
| |
| private void getCandiagtesForCache() { |
| int fetchStart = mCandidatesList.size(); |
| int fetchSize = mTotalChoicesNum - fetchStart; |
| if (fetchSize > MAX_PAGE_SIZE_DISPLAY) { |
| fetchSize = MAX_PAGE_SIZE_DISPLAY; |
| } |
| try { |
| List<String> newList = null; |
| if (ImeState.STATE_INPUT == mImeState || |
| ImeState.STATE_IDLE == mImeState || |
| ImeState.STATE_COMPOSING == mImeState){ |
| newList = mIPinyinDecoderService.imGetChoiceList( |
| fetchStart, fetchSize, mFixedLen); |
| } else if (ImeState.STATE_PREDICT == mImeState) { |
| newList = mIPinyinDecoderService.imGetPredictList( |
| fetchStart, fetchSize); |
| } else if (ImeState.STATE_APP_COMPLETION == mImeState) { |
| newList = new ArrayList<String>(); |
| if (null != mAppCompletions) { |
| for (int pos = fetchStart; pos < fetchSize; pos++) { |
| CompletionInfo ci = mAppCompletions[pos]; |
| if (null != ci) { |
| CharSequence s = ci.getText(); |
| if (null != s) newList.add(s.toString()); |
| } |
| } |
| } |
| } |
| mCandidatesList.addAll(newList); |
| } catch (RemoteException e) { |
| Log.w(TAG, "PinyinDecoderService died", e); |
| } |
| } |
| |
| public boolean pageReady(int pageNo) { |
| // If the page number is less than 0, return false |
| if (pageNo < 0) return false; |
| |
| // Page pageNo's ending information is not ready. |
| if (mPageStart.size() <= pageNo + 1) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| public boolean preparePage(int pageNo) { |
| // If the page number is less than 0, return false |
| if (pageNo < 0) return false; |
| |
| // Make sure the starting information for page pageNo is ready. |
| if (mPageStart.size() <= pageNo) { |
| return false; |
| } |
| |
| // Page pageNo's ending information is also ready. |
| if (mPageStart.size() > pageNo + 1) { |
| return true; |
| } |
| |
| // If cached items is enough for page pageNo. |
| if (mCandidatesList.size() - mPageStart.elementAt(pageNo) >= MAX_PAGE_SIZE_DISPLAY) { |
| return true; |
| } |
| |
| // Try to get more items from engine |
| getCandiagtesForCache(); |
| |
| // Try to find if there are available new items to display. |
| // If no new item, return false; |
| if (mPageStart.elementAt(pageNo) >= mCandidatesList.size()) { |
| return false; |
| } |
| |
| // If there are new items, return true; |
| return true; |
| } |
| |
| public void preparePredicts(CharSequence history) { |
| if (null == history) return; |
| |
| resetCandidates(); |
| |
| if (Settings.getPrediction()) { |
| String preEdit = history.toString(); |
| int predictNum = 0; |
| if (null != preEdit) { |
| try { |
| mTotalChoicesNum = mIPinyinDecoderService |
| .imGetPredictsNum(preEdit); |
| } catch (RemoteException e) { |
| return; |
| } |
| } |
| } |
| |
| preparePage(0); |
| mFinishSelection = false; |
| } |
| |
| private void prepareAppCompletions(CompletionInfo completions[]) { |
| resetCandidates(); |
| mAppCompletions = completions; |
| mTotalChoicesNum = completions.length; |
| preparePage(0); |
| mFinishSelection = false; |
| return; |
| } |
| |
| public int getCurrentPageSize(int currentPage) { |
| if (mPageStart.size() <= currentPage + 1) return 0; |
| return mPageStart.elementAt(currentPage + 1) |
| - mPageStart.elementAt(currentPage); |
| } |
| |
| public int getCurrentPageStart(int currentPage) { |
| if (mPageStart.size() < currentPage + 1) return mTotalChoicesNum; |
| return mPageStart.elementAt(currentPage); |
| } |
| |
| public boolean pageForwardable(int currentPage) { |
| if (mPageStart.size() <= currentPage + 1) return false; |
| if (mPageStart.elementAt(currentPage + 1) >= mTotalChoicesNum) { |
| return false; |
| } |
| return true; |
| } |
| |
| public boolean pageBackwardable(int currentPage) { |
| if (currentPage > 0) return true; |
| return false; |
| } |
| |
| public boolean charBeforeCursorIsSeparator() { |
| int len = mSurface.length(); |
| if (mCursorPos > len) return false; |
| if (mCursorPos > 0 && mSurface.charAt(mCursorPos - 1) == '\'') { |
| return true; |
| } |
| return false; |
| } |
| |
| public int getCursorPos() { |
| return mCursorPos; |
| } |
| |
| public int getCursorPosInCmps() { |
| int cursorPos = mCursorPos; |
| int fixedLen = 0; |
| |
| for (int hzPos = 0; hzPos < mFixedLen; hzPos++) { |
| if (mCursorPos >= mSplStart[hzPos + 2]) { |
| cursorPos -= mSplStart[hzPos + 2] - mSplStart[hzPos + 1]; |
| cursorPos += 1; |
| } |
| } |
| return cursorPos; |
| } |
| |
| public int getCursorPosInCmpsDisplay() { |
| int cursorPos = getCursorPosInCmps(); |
| // +2 is because: one for mSplStart[0], which is used for other |
| // purpose(The length of the segmentation string), and another |
| // for the first spelling which does not need a space before it. |
| for (int pos = mFixedLen + 2; pos < mSplStart.length - 1; pos++) { |
| if (mCursorPos <= mSplStart[pos]) { |
| break; |
| } else { |
| cursorPos++; |
| } |
| } |
| return cursorPos; |
| } |
| |
| public void moveCursorToEdge(boolean left) { |
| if (left) |
| mCursorPos = 0; |
| else |
| mCursorPos = mSurface.length(); |
| } |
| |
| // Move cursor. If offset is 0, this function can be used to adjust |
| // the cursor into the bounds of the string. |
| public void moveCursor(int offset) { |
| if (offset > 1 || offset < -1) return; |
| |
| if (offset != 0) { |
| int hzPos = 0; |
| for (hzPos = 0; hzPos <= mFixedLen; hzPos++) { |
| if (mCursorPos == mSplStart[hzPos + 1]) { |
| if (offset < 0) { |
| if (hzPos > 0) { |
| offset = mSplStart[hzPos] |
| - mSplStart[hzPos + 1]; |
| } |
| } else { |
| if (hzPos < mFixedLen) { |
| offset = mSplStart[hzPos + 2] |
| - mSplStart[hzPos + 1]; |
| } |
| } |
| break; |
| } |
| } |
| } |
| mCursorPos += offset; |
| if (mCursorPos < 0) { |
| mCursorPos = 0; |
| } else if (mCursorPos > mSurface.length()) { |
| mCursorPos = mSurface.length(); |
| } |
| } |
| |
| public int getSplNum() { |
| return mSplStart[0]; |
| } |
| |
| public int getFixedLen() { |
| return mFixedLen; |
| } |
| } |
| } |