summaryrefslogtreecommitdiff
path: root/src/com/android/inputmethod/pinyin
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/inputmethod/pinyin')
-rw-r--r--src/com/android/inputmethod/pinyin/BalloonHint.java472
-rw-r--r--src/com/android/inputmethod/pinyin/CandidateView.java760
-rw-r--r--src/com/android/inputmethod/pinyin/CandidateViewListener.java33
-rw-r--r--src/com/android/inputmethod/pinyin/CandidatesContainer.java474
-rw-r--r--src/com/android/inputmethod/pinyin/ComposingView.java280
-rw-r--r--src/com/android/inputmethod/pinyin/EnglishInputProcessor.java95
-rw-r--r--src/com/android/inputmethod/pinyin/Environment.java221
-rw-r--r--src/com/android/inputmethod/pinyin/InputModeSwitcher.java825
-rw-r--r--src/com/android/inputmethod/pinyin/KeyMapDream.java106
-rw-r--r--src/com/android/inputmethod/pinyin/PinyinDecoderService.java326
-rw-r--r--src/com/android/inputmethod/pinyin/PinyinIME.java2134
-rw-r--r--src/com/android/inputmethod/pinyin/Settings.java101
-rw-r--r--src/com/android/inputmethod/pinyin/SettingsActivity.java115
-rw-r--r--src/com/android/inputmethod/pinyin/SkbContainer.java642
-rw-r--r--src/com/android/inputmethod/pinyin/SkbPool.java88
-rw-r--r--src/com/android/inputmethod/pinyin/SkbTemplate.java233
-rw-r--r--src/com/android/inputmethod/pinyin/SoftKey.java230
-rw-r--r--src/com/android/inputmethod/pinyin/SoftKeyToggle.java283
-rw-r--r--src/com/android/inputmethod/pinyin/SoftKeyboard.java520
-rw-r--r--src/com/android/inputmethod/pinyin/SoftKeyboardView.java478
-rw-r--r--src/com/android/inputmethod/pinyin/SoundManager.java63
-rw-r--r--src/com/android/inputmethod/pinyin/XmlKeyboardLoader.java835
22 files changed, 9314 insertions, 0 deletions
diff --git a/src/com/android/inputmethod/pinyin/BalloonHint.java b/src/com/android/inputmethod/pinyin/BalloonHint.java
new file mode 100644
index 0000000..919dbf1
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/BalloonHint.java
@@ -0,0 +1,472 @@
+/*
+ * 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.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.view.Gravity;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.widget.PopupWindow;
+
+/**
+ * Subclass of PopupWindow used as the feedback when user presses on a soft key
+ * or a candidate.
+ */
+public class BalloonHint extends PopupWindow {
+ /**
+ * Delayed time to show the balloon hint.
+ */
+ public static final int TIME_DELAY_SHOW = 0;
+
+ /**
+ * Delayed time to dismiss the balloon hint.
+ */
+ public static final int TIME_DELAY_DISMISS = 200;
+
+ /**
+ * The padding information of the balloon. Because PopupWindow's background
+ * can not be changed unless it is dismissed and shown again, we set the
+ * real background drawable to the content view, and make the PopupWindow's
+ * background transparent. So actually this padding information is for the
+ * content view.
+ */
+ private Rect mPaddingRect = new Rect();
+
+ /**
+ * The context used to create this balloon hint object.
+ */
+ private Context mContext;
+
+ /**
+ * Parent used to show the balloon window.
+ */
+ private View mParent;
+
+ /**
+ * The content view of the balloon.
+ */
+ BalloonView mBalloonView;
+
+ /**
+ * The measuring specification used to determine its size. Key-press
+ * balloons and candidates balloons have different measuring specifications.
+ */
+ private int mMeasureSpecMode;
+
+ /**
+ * Used to indicate whether the balloon needs to be dismissed forcibly.
+ */
+ private boolean mForceDismiss;
+
+ /**
+ * Timer used to show/dismiss the balloon window with some time delay.
+ */
+ private BalloonTimer mBalloonTimer;
+
+ private int mParentLocationInWindow[] = new int[2];
+
+ public BalloonHint(Context context, View parent, int measureSpecMode) {
+ super(context);
+ mParent = parent;
+ mMeasureSpecMode = measureSpecMode;
+
+ setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ setTouchable(false);
+ setBackgroundDrawable(new ColorDrawable(0));
+
+ mBalloonView = new BalloonView(context);
+ mBalloonView.setClickable(false);
+ setContentView(mBalloonView);
+
+ mBalloonTimer = new BalloonTimer();
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ public Rect getPadding() {
+ return mPaddingRect;
+ }
+
+ public void setBalloonBackground(Drawable drawable) {
+ // We usually pick up a background from a soft keyboard template,
+ // and the object may has been set to this balloon before.
+ if (mBalloonView.getBackground() == drawable) return;
+ mBalloonView.setBackgroundDrawable(drawable);
+
+ if (null != drawable) {
+ drawable.getPadding(mPaddingRect);
+ } else {
+ mPaddingRect.set(0, 0, 0, 0);
+ }
+ }
+
+ /**
+ * Set configurations to show text label in this balloon.
+ *
+ * @param label The text label to show in the balloon.
+ * @param textSize The text size used to show label.
+ * @param textBold Used to indicate whether the label should be bold.
+ * @param textColor The text color used to show label.
+ * @param width The desired width of the balloon. The real width is
+ * determined by the desired width and balloon's measuring
+ * specification.
+ * @param height The desired width of the balloon. The real width is
+ * determined by the desired width and balloon's measuring
+ * specification.
+ */
+ public void setBalloonConfig(String label, float textSize,
+ boolean textBold, int textColor, int width, int height) {
+ mBalloonView.setTextConfig(label, textSize, textBold, textColor);
+ setBalloonSize(width, height);
+ }
+
+ /**
+ * Set configurations to show text label in this balloon.
+ *
+ * @param icon The icon used to shown in this balloon.
+ * @param width The desired width of the balloon. The real width is
+ * determined by the desired width and balloon's measuring
+ * specification.
+ * @param height The desired width of the balloon. The real width is
+ * determined by the desired width and balloon's measuring
+ * specification.
+ */
+ public void setBalloonConfig(Drawable icon, int width, int height) {
+ mBalloonView.setIcon(icon);
+ setBalloonSize(width, height);
+ }
+
+
+ public boolean needForceDismiss() {
+ return mForceDismiss;
+ }
+
+ public int getPaddingLeft() {
+ return mPaddingRect.left;
+ }
+
+ public int getPaddingTop() {
+ return mPaddingRect.top;
+ }
+
+ public int getPaddingRight() {
+ return mPaddingRect.right;
+ }
+
+ public int getPaddingBottom() {
+ return mPaddingRect.bottom;
+ }
+
+ public void delayedShow(long delay, int locationInParent[]) {
+ if (mBalloonTimer.isPending()) {
+ mBalloonTimer.removeTimer();
+ }
+ if (delay <= 0) {
+ mParent.getLocationInWindow(mParentLocationInWindow);
+ showAtLocation(mParent, Gravity.LEFT | Gravity.TOP,
+ locationInParent[0], locationInParent[1]
+ + mParentLocationInWindow[1]);
+ } else {
+ mBalloonTimer.startTimer(delay, BalloonTimer.ACTION_SHOW,
+ locationInParent, -1, -1);
+ }
+ }
+
+ public void delayedUpdate(long delay, int locationInParent[],
+ int width, int height) {
+ mBalloonView.invalidate();
+ if (mBalloonTimer.isPending()) {
+ mBalloonTimer.removeTimer();
+ }
+ if (delay <= 0) {
+ mParent.getLocationInWindow(mParentLocationInWindow);
+ update(locationInParent[0], locationInParent[1]
+ + mParentLocationInWindow[1], width, height);
+ } else {
+ mBalloonTimer.startTimer(delay, BalloonTimer.ACTION_UPDATE,
+ locationInParent, width, height);
+ }
+ }
+
+ public void delayedDismiss(long delay) {
+ if (mBalloonTimer.isPending()) {
+ mBalloonTimer.removeTimer();
+ int pendingAction = mBalloonTimer.getAction();
+ if (0 != delay && BalloonTimer.ACTION_HIDE != pendingAction) {
+ mBalloonTimer.run();
+ }
+ }
+ if (delay <= 0) {
+ dismiss();
+ } else {
+ mBalloonTimer.startTimer(delay, BalloonTimer.ACTION_HIDE, null, -1,
+ -1);
+ }
+ }
+
+ public void removeTimer() {
+ if (mBalloonTimer.isPending()) {
+ mBalloonTimer.removeTimer();
+ }
+ }
+
+ private void setBalloonSize(int width, int height) {
+ int widthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
+ mMeasureSpecMode);
+ int heightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
+ mMeasureSpecMode);
+ mBalloonView.measure(widthMeasureSpec, heightMeasureSpec);
+
+ int oldWidth = getWidth();
+ int oldHeight = getHeight();
+ int newWidth = mBalloonView.getMeasuredWidth() + getPaddingLeft()
+ + getPaddingRight();
+ int newHeight = mBalloonView.getMeasuredHeight() + getPaddingTop()
+ + getPaddingBottom();
+ setWidth(newWidth);
+ setHeight(newHeight);
+
+ // If update() is called to update both size and position, the system
+ // will first MOVE the PopupWindow to the new position, and then
+ // perform a size-updating operation, so there will be a flash in
+ // PopupWindow if user presses a key and moves finger to next one whose
+ // size is different.
+ // PopupWindow will handle the updating issue in one go in the future,
+ // but before that, if we find the size is changed, a mandatory dismiss
+ // operation is required. In our UI design, normal QWERTY keys' width
+ // can be different in 1-pixel, and we do not dismiss the balloon when
+ // user move between QWERTY keys.
+ mForceDismiss = false;
+ if (isShowing()) {
+ mForceDismiss = oldWidth - newWidth > 1 || newWidth - oldWidth > 1;
+ }
+ }
+
+
+ private class BalloonTimer extends Handler implements Runnable {
+ public static final int ACTION_SHOW = 1;
+ public static final int ACTION_HIDE = 2;
+ public static final int ACTION_UPDATE = 3;
+
+ /**
+ * The pending action.
+ */
+ private int mAction;
+
+ private int mPositionInParent[] = new int[2];
+ private int mWidth;
+ private int mHeight;
+
+ private boolean mTimerPending = false;
+
+ public void startTimer(long time, int action, int positionInParent[],
+ int width, int height) {
+ mAction = action;
+ if (ACTION_HIDE != action) {
+ mPositionInParent[0] = positionInParent[0];
+ mPositionInParent[1] = positionInParent[1];
+ }
+ mWidth = width;
+ mHeight = height;
+ postDelayed(this, time);
+ mTimerPending = true;
+ }
+
+ public boolean isPending() {
+ return mTimerPending;
+ }
+
+ public boolean removeTimer() {
+ if (mTimerPending) {
+ mTimerPending = false;
+ removeCallbacks(this);
+ return true;
+ }
+
+ return false;
+ }
+
+ public int getAction() {
+ return mAction;
+ }
+
+ public void run() {
+ switch (mAction) {
+ case ACTION_SHOW:
+ mParent.getLocationInWindow(mParentLocationInWindow);
+ showAtLocation(mParent, Gravity.LEFT | Gravity.TOP,
+ mPositionInParent[0], mPositionInParent[1]
+ + mParentLocationInWindow[1]);
+ break;
+ case ACTION_HIDE:
+ dismiss();
+ break;
+ case ACTION_UPDATE:
+ mParent.getLocationInWindow(mParentLocationInWindow);
+ update(mPositionInParent[0], mPositionInParent[1]
+ + mParentLocationInWindow[1], mWidth, mHeight);
+ }
+ mTimerPending = false;
+ }
+ }
+
+ private class BalloonView extends View {
+ /**
+ * Suspension points used to display long items.
+ */
+ private static final String SUSPENSION_POINTS = "...";
+
+ /**
+ * The icon to be shown. If it is not null, {@link #mLabel} will be
+ * ignored.
+ */
+ private Drawable mIcon;
+
+ /**
+ * The label to be shown. It is enabled only if {@link #mIcon} is null.
+ */
+ private String mLabel;
+
+ private int mLabeColor = 0xff000000;
+ private Paint mPaintLabel;
+ private FontMetricsInt mFmi;
+
+ /**
+ * The width to show suspension points.
+ */
+ private float mSuspensionPointsWidth;
+
+
+ public BalloonView(Context context) {
+ super(context);
+ mPaintLabel = new Paint();
+ mPaintLabel.setColor(mLabeColor);
+ mPaintLabel.setAntiAlias(true);
+ mPaintLabel.setFakeBoldText(true);
+ mFmi = mPaintLabel.getFontMetricsInt();
+ }
+
+ public void setIcon(Drawable icon) {
+ mIcon = icon;
+ }
+
+ public void setTextConfig(String label, float fontSize,
+ boolean textBold, int textColor) {
+ // Icon should be cleared so that the label will be enabled.
+ mIcon = null;
+ mLabel = label;
+ mPaintLabel.setTextSize(fontSize);
+ mPaintLabel.setFakeBoldText(textBold);
+ mPaintLabel.setColor(textColor);
+ mFmi = mPaintLabel.getFontMetricsInt();
+ mSuspensionPointsWidth = mPaintLabel.measureText(SUSPENSION_POINTS);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (widthMode == MeasureSpec.EXACTLY) {
+ setMeasuredDimension(widthSize, heightSize);
+ return;
+ }
+
+ int measuredWidth = mPaddingLeft + mPaddingRight;
+ int measuredHeight = mPaddingTop + mPaddingBottom;
+ if (null != mIcon) {
+ measuredWidth += mIcon.getIntrinsicWidth();
+ measuredHeight += mIcon.getIntrinsicHeight();
+ } else if (null != mLabel) {
+ measuredWidth += (int) (mPaintLabel.measureText(mLabel));
+ measuredHeight += mFmi.bottom - mFmi.top;
+ }
+ if (widthSize > measuredWidth || widthMode == MeasureSpec.AT_MOST) {
+ measuredWidth = widthSize;
+ }
+
+ if (heightSize > measuredHeight
+ || heightMode == MeasureSpec.AT_MOST) {
+ measuredHeight = heightSize;
+ }
+
+ int maxWidth = Environment.getInstance().getScreenWidth() -
+ mPaddingLeft - mPaddingRight;
+ if (measuredWidth > maxWidth) {
+ measuredWidth = maxWidth;
+ }
+ setMeasuredDimension(measuredWidth, measuredHeight);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ int width = getWidth();
+ int height = getHeight();
+ if (null != mIcon) {
+ int marginLeft = (width - mIcon.getIntrinsicWidth()) / 2;
+ int marginRight = width - mIcon.getIntrinsicWidth()
+ - marginLeft;
+ int marginTop = (height - mIcon.getIntrinsicHeight()) / 2;
+ int marginBottom = height - mIcon.getIntrinsicHeight()
+ - marginTop;
+ mIcon.setBounds(marginLeft, marginTop, width - marginRight,
+ height - marginBottom);
+ mIcon.draw(canvas);
+ } else if (null != mLabel) {
+ float labelMeasuredWidth = mPaintLabel.measureText(mLabel);
+ float x = mPaddingLeft;
+ x += (width - labelMeasuredWidth - mPaddingLeft - mPaddingRight) / 2.0f;
+ String labelToDraw = mLabel;
+ if (x < mPaddingLeft) {
+ x = mPaddingLeft;
+ labelToDraw = getLimitedLabelForDrawing(mLabel,
+ width - mPaddingLeft - mPaddingRight);
+ }
+
+ int fontHeight = mFmi.bottom - mFmi.top;
+ float marginY = (height - fontHeight) / 2.0f;
+ float y = marginY - mFmi.top;
+ canvas.drawText(labelToDraw, x, y, mPaintLabel);
+ }
+ }
+
+ private String getLimitedLabelForDrawing(String rawLabel,
+ float widthToDraw) {
+ int subLen = rawLabel.length();
+ if (subLen <= 1) return rawLabel;
+ do {
+ subLen--;
+ float width = mPaintLabel.measureText(rawLabel, 0, subLen);
+ if (width + mSuspensionPointsWidth <= widthToDraw || 1 >= subLen) {
+ return rawLabel.substring(0, subLen) +
+ SUSPENSION_POINTS;
+ }
+ } while (true);
+ }
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/CandidateView.java b/src/com/android/inputmethod/pinyin/CandidateView.java
new file mode 100644
index 0000000..8dc1bf1
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/CandidateView.java
@@ -0,0 +1,760 @@
+/*
+ * 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 com.android.inputmethod.pinyin.PinyinIME.DecodingInfo;
+
+import java.util.Vector;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * View to show candidate list. There two candidate view instances which are
+ * used to show animation when user navigates between pages.
+ */
+public class CandidateView extends View {
+ /**
+ * The minimum width to show a item.
+ */
+ private static final float MIN_ITEM_WIDTH = 22;
+
+ /**
+ * Suspension points used to display long items.
+ */
+ private static final String SUSPENSION_POINTS = "...";
+
+ /**
+ * The width to draw candidates.
+ */
+ private int mContentWidth;
+
+ /**
+ * The height to draw candidate content.
+ */
+ private int mContentHeight;
+
+ /**
+ * Whether footnotes are displayed. Footnote is shown when hardware keyboard
+ * is available.
+ */
+ private boolean mShowFootnote = true;
+
+ /**
+ * Balloon hint for candidate press/release.
+ */
+ private BalloonHint mBalloonHint;
+
+ /**
+ * Desired position of the balloon to the input view.
+ */
+ private int mHintPositionToInputView[] = new int[2];
+
+ /**
+ * Decoding result to show.
+ */
+ private DecodingInfo mDecInfo;
+
+ /**
+ * Listener used to notify IME that user clicks a candidate, or navigate
+ * between them.
+ */
+ private CandidateViewListener mCvListener;
+
+ /**
+ * Used to notify the container to update the status of forward/backward
+ * arrows.
+ */
+ private ArrowUpdater mArrowUpdater;
+
+ /**
+ * If true, update the arrow status when drawing candidates.
+ */
+ private boolean mUpdateArrowStatusWhenDraw = false;
+
+ /**
+ * Page number of the page displayed in this view.
+ */
+ private int mPageNo;
+
+ /**
+ * Active candidate position in this page.
+ */
+ private int mActiveCandInPage;
+
+ /**
+ * Used to decided whether the active candidate should be highlighted or
+ * not. If user changes focus to composing view (The view to show Pinyin
+ * string), the highlight in candidate view should be removed.
+ */
+ private boolean mEnableActiveHighlight = true;
+
+ /**
+ * The page which is just calculated.
+ */
+ private int mPageNoCalculated = -1;
+
+ /**
+ * The Drawable used to display as the background of the high-lighted item.
+ */
+ private Drawable mActiveCellDrawable;
+
+ /**
+ * The Drawable used to display as separators between candidates.
+ */
+ private Drawable mSeparatorDrawable;
+
+ /**
+ * Color to draw normal candidates generated by IME.
+ */
+ private int mImeCandidateColor;
+
+ /**
+ * Color to draw normal candidates Recommended by application.
+ */
+ private int mRecommendedCandidateColor;
+
+ /**
+ * Color to draw the normal(not highlighted) candidates, it can be one of
+ * {@link #mImeCandidateColor} or {@link #mRecommendedCandidateColor}.
+ */
+ private int mNormalCandidateColor;
+
+ /**
+ * Color to draw the active(highlighted) candidates, including candidates
+ * from IME and candidates from application.
+ */
+ private int mActiveCandidateColor;
+
+ /**
+ * Text size to draw candidates generated by IME.
+ */
+ private int mImeCandidateTextSize;
+
+ /**
+ * Text size to draw candidates recommended by application.
+ */
+ private int mRecommendedCandidateTextSize;
+
+ /**
+ * The current text size to draw candidates. It can be one of
+ * {@link #mImeCandidateTextSize} or {@link #mRecommendedCandidateTextSize}.
+ */
+ private int mCandidateTextSize;
+
+ /**
+ * Paint used to draw candidates.
+ */
+ private Paint mCandidatesPaint;
+
+ /**
+ * Used to draw footnote.
+ */
+ private Paint mFootnotePaint;
+
+ /**
+ * The width to show suspension points.
+ */
+ private float mSuspensionPointsWidth;
+
+ /**
+ * Rectangle used to draw the active candidate.
+ */
+ private RectF mActiveCellRect;
+
+ /**
+ * Left and right margins for a candidate. It is specified in xml, and is
+ * the minimum margin for a candidate. The actual gap between two candidates
+ * is 2 * {@link #mCandidateMargin} + {@link #mSeparatorDrawable}.
+ * getIntrinsicWidth(). Because length of candidate is not fixed, there can
+ * be some extra space after the last candidate in the current page. In
+ * order to achieve best look-and-feel, this extra space will be divided and
+ * allocated to each candidates.
+ */
+ private float mCandidateMargin;
+
+ /**
+ * Left and right extra margins for a candidate.
+ */
+ private float mCandidateMarginExtra;
+
+ /**
+ * Rectangles for the candidates in this page.
+ **/
+ private Vector<RectF> mCandRects;
+
+ /**
+ * FontMetricsInt used to measure the size of candidates.
+ */
+ private FontMetricsInt mFmiCandidates;
+
+ /**
+ * FontMetricsInt used to measure the size of footnotes.
+ */
+ private FontMetricsInt mFmiFootnote;
+
+ private PressTimer mTimer = new PressTimer();
+
+ private GestureDetector mGestureDetector;
+
+ private int mLocationTmp[] = new int[2];
+
+ public CandidateView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ Resources r = context.getResources();
+
+ Configuration conf = r.getConfiguration();
+ if (conf.keyboard == Configuration.KEYBOARD_NOKEYS
+ || conf.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) {
+ mShowFootnote = false;
+ }
+
+ mActiveCellDrawable = r.getDrawable(R.drawable.candidate_hl_bg);
+ mSeparatorDrawable = r.getDrawable(R.drawable.candidates_vertical_line);
+ mCandidateMargin = r.getDimension(R.dimen.candidate_margin_left_right);
+
+ mImeCandidateColor = r.getColor(R.color.candidate_color);
+ mRecommendedCandidateColor = r.getColor(R.color.recommended_candidate_color);
+ mNormalCandidateColor = mImeCandidateColor;
+ mActiveCandidateColor = r.getColor(R.color.active_candidate_color);
+
+ mCandidatesPaint = new Paint();
+ mCandidatesPaint.setAntiAlias(true);
+
+ mFootnotePaint = new Paint();
+ mFootnotePaint.setAntiAlias(true);
+ mFootnotePaint.setColor(r.getColor(R.color.footnote_color));
+ mActiveCellRect = new RectF();
+
+ mCandRects = new Vector<RectF>();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int mOldWidth = mMeasuredWidth;
+ int mOldHeight = mMeasuredHeight;
+
+ setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),
+ widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(),
+ heightMeasureSpec));
+
+ if (mOldWidth != mMeasuredWidth || mOldHeight != mMeasuredHeight) {
+ onSizeChanged();
+ }
+ }
+
+ public void initialize(ArrowUpdater arrowUpdater, BalloonHint balloonHint,
+ GestureDetector gestureDetector, CandidateViewListener cvListener) {
+ mArrowUpdater = arrowUpdater;
+ mBalloonHint = balloonHint;
+ mGestureDetector = gestureDetector;
+ mCvListener = cvListener;
+ }
+
+ public void setDecodingInfo(DecodingInfo decInfo) {
+ if (null == decInfo) return;
+ mDecInfo = decInfo;
+ mPageNoCalculated = -1;
+
+ if (mDecInfo.candidatesFromApp()) {
+ mNormalCandidateColor = mRecommendedCandidateColor;
+ mCandidateTextSize = mRecommendedCandidateTextSize;
+ } else {
+ mNormalCandidateColor = mImeCandidateColor;
+ mCandidateTextSize = mImeCandidateTextSize;
+ }
+ if (mCandidatesPaint.getTextSize() != mCandidateTextSize) {
+ mCandidatesPaint.setTextSize(mCandidateTextSize);
+ mFmiCandidates = mCandidatesPaint.getFontMetricsInt();
+ mSuspensionPointsWidth =
+ mCandidatesPaint.measureText(SUSPENSION_POINTS);
+ }
+
+ // Remove any pending timer for the previous list.
+ mTimer.removeTimer();
+ }
+
+ public int getActiveCandiatePosInPage() {
+ return mActiveCandInPage;
+ }
+
+ public int getActiveCandiatePosGlobal() {
+ return mDecInfo.mPageStart.get(mPageNo) + mActiveCandInPage;
+ }
+
+ /**
+ * Show a page in the decoding result set previously.
+ *
+ * @param pageNo Which page to show.
+ * @param activeCandInPage Which candidate should be set as active item.
+ * @param enableActiveHighlight When false, active item will not be
+ * highlighted.
+ */
+ public void showPage(int pageNo, int activeCandInPage,
+ boolean enableActiveHighlight) {
+ if (null == mDecInfo) return;
+ mPageNo = pageNo;
+ mActiveCandInPage = activeCandInPage;
+ if (mEnableActiveHighlight != enableActiveHighlight) {
+ mEnableActiveHighlight = enableActiveHighlight;
+ }
+
+ if (!calculatePage(mPageNo)) {
+ mUpdateArrowStatusWhenDraw = true;
+ } else {
+ mUpdateArrowStatusWhenDraw = false;
+ }
+
+ invalidate();
+ }
+
+ public void enableActiveHighlight(boolean enableActiveHighlight) {
+ if (enableActiveHighlight == mEnableActiveHighlight) return;
+
+ mEnableActiveHighlight = enableActiveHighlight;
+ invalidate();
+ }
+
+ public boolean activeCursorForward() {
+ if (!mDecInfo.pageReady(mPageNo)) return false;
+ int pageSize = mDecInfo.mPageStart.get(mPageNo + 1)
+ - mDecInfo.mPageStart.get(mPageNo);
+ if (mActiveCandInPage + 1 < pageSize) {
+ showPage(mPageNo, mActiveCandInPage + 1, true);
+ return true;
+ }
+ return false;
+ }
+
+ public boolean activeCurseBackward() {
+ if (mActiveCandInPage > 0) {
+ showPage(mPageNo, mActiveCandInPage - 1, true);
+ return true;
+ }
+ return false;
+ }
+
+ private void onSizeChanged() {
+ mContentWidth = mMeasuredWidth - mPaddingLeft - mPaddingRight;
+ mContentHeight = (int) ((mMeasuredHeight - mPaddingTop - mPaddingBottom) * 0.95f);
+ /**
+ * How to decide the font size if the height for display is given?
+ * Now it is implemented in a stupid way.
+ */
+ int textSize = 1;
+ mCandidatesPaint.setTextSize(textSize);
+ mFmiCandidates = mCandidatesPaint.getFontMetricsInt();
+ while (mFmiCandidates.bottom - mFmiCandidates.top < mContentHeight) {
+ textSize++;
+ mCandidatesPaint.setTextSize(textSize);
+ mFmiCandidates = mCandidatesPaint.getFontMetricsInt();
+ }
+
+ mImeCandidateTextSize = textSize;
+ mRecommendedCandidateTextSize = textSize * 3 / 4;
+ if (null == mDecInfo) {
+ mCandidateTextSize = mImeCandidateTextSize;
+ mCandidatesPaint.setTextSize(mCandidateTextSize);
+ mFmiCandidates = mCandidatesPaint.getFontMetricsInt();
+ mSuspensionPointsWidth =
+ mCandidatesPaint.measureText(SUSPENSION_POINTS);
+ } else {
+ // Reset the decoding information to update members for painting.
+ setDecodingInfo(mDecInfo);
+ }
+
+ textSize = 1;
+ mFootnotePaint.setTextSize(textSize);
+ mFmiFootnote = mFootnotePaint.getFontMetricsInt();
+ while (mFmiFootnote.bottom - mFmiFootnote.top < mContentHeight / 2) {
+ textSize++;
+ mFootnotePaint.setTextSize(textSize);
+ mFmiFootnote = mFootnotePaint.getFontMetricsInt();
+ }
+ textSize--;
+ mFootnotePaint.setTextSize(textSize);
+ mFmiFootnote = mFootnotePaint.getFontMetricsInt();
+
+ // When the size is changed, the first page will be displayed.
+ mPageNo = 0;
+ mActiveCandInPage = 0;
+ }
+
+ private boolean calculatePage(int pageNo) {
+ if (pageNo == mPageNoCalculated) return true;
+
+ mContentWidth = mMeasuredWidth - mPaddingLeft - mPaddingRight;
+ mContentHeight = (int) ((mMeasuredHeight - mPaddingTop - mPaddingBottom) * 0.95f);
+
+ if (mContentWidth <= 0 || mContentHeight <= 0) return false;
+
+ int candSize = mDecInfo.mCandidatesList.size();
+
+ // If the size of page exists, only calculate the extra margin.
+ boolean onlyExtraMargin = false;
+ int fromPage = mDecInfo.mPageStart.size() - 1;
+ if (mDecInfo.mPageStart.size() > pageNo + 1) {
+ onlyExtraMargin = true;
+ fromPage = pageNo;
+ }
+
+ // If the previous pages have no information, calculate them first.
+ for (int p = fromPage; p <= pageNo; p++) {
+ int pStart = mDecInfo.mPageStart.get(p);
+ int pSize = 0;
+ int charNum = 0;
+ float lastItemWidth = 0;
+
+ float xPos;
+ xPos = 0;
+ xPos += mSeparatorDrawable.getIntrinsicWidth();
+ while (xPos < mContentWidth && pStart + pSize < candSize) {
+ int itemPos = pStart + pSize;
+ String itemStr = mDecInfo.mCandidatesList.get(itemPos);
+ float itemWidth = mCandidatesPaint.measureText(itemStr);
+ if (itemWidth < MIN_ITEM_WIDTH) itemWidth = MIN_ITEM_WIDTH;
+
+ itemWidth += mCandidateMargin * 2;
+ itemWidth += mSeparatorDrawable.getIntrinsicWidth();
+ if (xPos + itemWidth < mContentWidth || 0 == pSize) {
+ xPos += itemWidth;
+ lastItemWidth = itemWidth;
+ pSize++;
+ charNum += itemStr.length();
+ } else {
+ break;
+ }
+ }
+ if (!onlyExtraMargin) {
+ mDecInfo.mPageStart.add(pStart + pSize);
+ mDecInfo.mCnToPage.add(mDecInfo.mCnToPage.get(p) + charNum);
+ }
+
+ float marginExtra = (mContentWidth - xPos) / pSize / 2;
+
+ if (mContentWidth - xPos > lastItemWidth) {
+ // Must be the last page, because if there are more items,
+ // the next item's width must be less than lastItemWidth.
+ // In this case, if the last margin is less than the current
+ // one, the last margin can be used, so that the
+ // look-and-feeling will be the same as the previous page.
+ if (mCandidateMarginExtra <= marginExtra) {
+ marginExtra = mCandidateMarginExtra;
+ }
+ } else if (pSize == 1) {
+ marginExtra = 0;
+ }
+ mCandidateMarginExtra = marginExtra;
+ }
+ mPageNoCalculated = pageNo;
+ return true;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ // The invisible candidate view(the one which is not in foreground) can
+ // also be called to drawn, but its decoding result and candidate list
+ // may be empty.
+ if (null == mDecInfo || mDecInfo.isCandidatesListEmpty()) return;
+
+ // Calculate page. If the paging information is ready, the function will
+ // return at once.
+ calculatePage(mPageNo);
+
+ int pStart = mDecInfo.mPageStart.get(mPageNo);
+ int pSize = mDecInfo.mPageStart.get(mPageNo + 1) - pStart;
+ float candMargin = mCandidateMargin + mCandidateMarginExtra;
+ if (mActiveCandInPage > pSize - 1) {
+ mActiveCandInPage = pSize - 1;
+ }
+
+ mCandRects.removeAllElements();
+
+ float xPos = mPaddingLeft;
+ int yPos = (getMeasuredHeight() -
+ (mFmiCandidates.bottom - mFmiCandidates.top)) / 2
+ - mFmiCandidates.top;
+ xPos += drawVerticalSeparator(canvas, xPos);
+ for (int i = 0; i < pSize; i++) {
+ float footnoteSize = 0;
+ String footnote = null;
+ if (mShowFootnote) {
+ footnote = Integer.toString(i + 1);
+ footnoteSize = mFootnotePaint.measureText(footnote);
+ assert (footnoteSize < candMargin);
+ }
+ String cand = mDecInfo.mCandidatesList.get(pStart + i);
+ float candidateWidth = mCandidatesPaint.measureText(cand);
+ float centerOffset = 0;
+ if (candidateWidth < MIN_ITEM_WIDTH) {
+ centerOffset = (MIN_ITEM_WIDTH - candidateWidth) / 2;
+ candidateWidth = MIN_ITEM_WIDTH;
+ }
+
+ float itemTotalWidth = candidateWidth + 2 * candMargin;
+
+ if (mActiveCandInPage == i && mEnableActiveHighlight) {
+ mActiveCellRect.set(xPos, mPaddingTop + 1, xPos
+ + itemTotalWidth, getHeight() - mPaddingBottom - 1);
+ mActiveCellDrawable.setBounds((int) mActiveCellRect.left,
+ (int) mActiveCellRect.top, (int) mActiveCellRect.right,
+ (int) mActiveCellRect.bottom);
+ mActiveCellDrawable.draw(canvas);
+ }
+
+ if (mCandRects.size() < pSize) mCandRects.add(new RectF());
+ mCandRects.elementAt(i).set(xPos - 1, yPos + mFmiCandidates.top,
+ xPos + itemTotalWidth + 1, yPos + mFmiCandidates.bottom);
+
+ // Draw footnote
+ if (mShowFootnote) {
+ canvas.drawText(footnote, xPos + (candMargin - footnoteSize)
+ / 2, yPos, mFootnotePaint);
+ }
+
+ // Left margin
+ xPos += candMargin;
+ if (candidateWidth > mContentWidth - xPos - centerOffset) {
+ cand = getLimitedCandidateForDrawing(cand,
+ mContentWidth - xPos - centerOffset);
+ }
+ if (mActiveCandInPage == i && mEnableActiveHighlight) {
+ mCandidatesPaint.setColor(mActiveCandidateColor);
+ } else {
+ mCandidatesPaint.setColor(mNormalCandidateColor);
+ }
+ canvas.drawText(cand, xPos + centerOffset, yPos,
+ mCandidatesPaint);
+
+ // Candidate and right margin
+ xPos += candidateWidth + candMargin;
+
+ // Draw the separator between candidates.
+ xPos += drawVerticalSeparator(canvas, xPos);
+ }
+
+ // Update the arrow status of the container.
+ if (null != mArrowUpdater && mUpdateArrowStatusWhenDraw) {
+ mArrowUpdater.updateArrowStatus();
+ mUpdateArrowStatusWhenDraw = false;
+ }
+ }
+
+ private String getLimitedCandidateForDrawing(String rawCandidate,
+ float widthToDraw) {
+ int subLen = rawCandidate.length();
+ if (subLen <= 1) return rawCandidate;
+ do {
+ subLen--;
+ float width = mCandidatesPaint.measureText(rawCandidate, 0, subLen);
+ if (width + mSuspensionPointsWidth <= widthToDraw || 1 >= subLen) {
+ return rawCandidate.substring(0, subLen) +
+ SUSPENSION_POINTS;
+ }
+ } while (true);
+ }
+
+ private float drawVerticalSeparator(Canvas canvas, float xPos) {
+ mSeparatorDrawable.setBounds((int) xPos, mPaddingTop, (int) xPos
+ + mSeparatorDrawable.getIntrinsicWidth(), getMeasuredHeight()
+ - mPaddingBottom);
+ mSeparatorDrawable.draw(canvas);
+ return mSeparatorDrawable.getIntrinsicWidth();
+ }
+
+ private int mapToItemInPage(int x, int y) {
+ // mCandRects.size() == 0 happens when the page is set, but
+ // touch events occur before onDraw(). It usually happens with
+ // monkey test.
+ if (!mDecInfo.pageReady(mPageNo) || mPageNoCalculated != mPageNo
+ || mCandRects.size() == 0) {
+ return -1;
+ }
+
+ int pageStart = mDecInfo.mPageStart.get(mPageNo);
+ int pageSize = mDecInfo.mPageStart.get(mPageNo + 1) - pageStart;
+ if (mCandRects.size() < pageSize) {
+ return -1;
+ }
+
+ // If not found, try to find the nearest one.
+ float nearestDis = Float.MAX_VALUE;
+ int nearest = -1;
+ for (int i = 0; i < pageSize; i++) {
+ RectF r = mCandRects.elementAt(i);
+ if (r.left < x && r.right > x && r.top < y && r.bottom > y) {
+ return i;
+ }
+ float disx = (r.left + r.right) / 2 - x;
+ float disy = (r.top + r.bottom) / 2 - y;
+ float dis = disx * disx + disy * disy;
+ if (dis < nearestDis) {
+ nearestDis = dis;
+ nearest = i;
+ }
+ }
+
+ return nearest;
+ }
+
+ // Because the candidate view under the current focused one may also get
+ // touching events. Here we just bypass the event to the container and let
+ // it decide which view should handle the event.
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ return super.onTouchEvent(event);
+ }
+
+ public boolean onTouchEventReal(MotionEvent event) {
+ // The page in the background can also be touched.
+ if (null == mDecInfo || !mDecInfo.pageReady(mPageNo)
+ || mPageNoCalculated != mPageNo) return true;
+
+ int x, y;
+ x = (int) event.getX();
+ y = (int) event.getY();
+
+ if (mGestureDetector.onTouchEvent(event)) {
+ mTimer.removeTimer();
+ mBalloonHint.delayedDismiss(0);
+ return true;
+ }
+
+ int clickedItemInPage = -1;
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_UP:
+ clickedItemInPage = mapToItemInPage(x, y);
+ if (clickedItemInPage >= 0) {
+ invalidate();
+ mCvListener.onClickChoice(clickedItemInPage
+ + mDecInfo.mPageStart.get(mPageNo));
+ }
+ mBalloonHint.delayedDismiss(BalloonHint.TIME_DELAY_DISMISS);
+ break;
+
+ case MotionEvent.ACTION_DOWN:
+ clickedItemInPage = mapToItemInPage(x, y);
+ if (clickedItemInPage >= 0) {
+ showBalloon(clickedItemInPage, true);
+ mTimer.startTimer(BalloonHint.TIME_DELAY_SHOW, mPageNo,
+ clickedItemInPage);
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ clickedItemInPage = mapToItemInPage(x, y);
+ if (clickedItemInPage >= 0
+ && (clickedItemInPage != mTimer.getActiveCandOfPageToShow() || mPageNo != mTimer
+ .getPageToShow())) {
+ showBalloon(clickedItemInPage, true);
+ mTimer.startTimer(BalloonHint.TIME_DELAY_SHOW, mPageNo,
+ clickedItemInPage);
+ }
+ }
+ return true;
+ }
+
+ private void showBalloon(int candPos, boolean delayedShow) {
+ mBalloonHint.removeTimer();
+
+ RectF r = mCandRects.elementAt(candPos);
+ int desired_width = (int) (r.right - r.left);
+ int desired_height = (int) (r.bottom - r.top);
+ mBalloonHint.setBalloonConfig(mDecInfo.mCandidatesList
+ .get(mDecInfo.mPageStart.get(mPageNo) + candPos), 44, true,
+ mImeCandidateColor, desired_width, desired_height);
+
+ getLocationOnScreen(mLocationTmp);
+ mHintPositionToInputView[0] = mLocationTmp[0]
+ + (int) (r.left - (mBalloonHint.getWidth() - desired_width) / 2);
+ mHintPositionToInputView[1] = -mBalloonHint.getHeight();
+
+ long delay = BalloonHint.TIME_DELAY_SHOW;
+ if (!delayedShow) delay = 0;
+ mBalloonHint.dismiss();
+ if (!mBalloonHint.isShowing()) {
+ mBalloonHint.delayedShow(delay, mHintPositionToInputView);
+ } else {
+ mBalloonHint.delayedUpdate(0, mHintPositionToInputView, -1, -1);
+ }
+ }
+
+ private class PressTimer extends Handler implements Runnable {
+ private boolean mTimerPending = false;
+ private int mPageNoToShow;
+ private int mActiveCandOfPage;
+
+ public PressTimer() {
+ super();
+ }
+
+ public void startTimer(long afterMillis, int pageNo, int activeInPage) {
+ mTimer.removeTimer();
+ postDelayed(this, afterMillis);
+ mTimerPending = true;
+ mPageNoToShow = pageNo;
+ mActiveCandOfPage = activeInPage;
+ }
+
+ public int getPageToShow() {
+ return mPageNoToShow;
+ }
+
+ public int getActiveCandOfPageToShow() {
+ return mActiveCandOfPage;
+ }
+
+ public boolean removeTimer() {
+ if (mTimerPending) {
+ mTimerPending = false;
+ removeCallbacks(this);
+ return true;
+ }
+ return false;
+ }
+
+ public boolean isPending() {
+ return mTimerPending;
+ }
+
+ public void run() {
+ if (mPageNoToShow >= 0 && mActiveCandOfPage >= 0) {
+ // Always enable to highlight the clicked one.
+ showPage(mPageNoToShow, mActiveCandOfPage, true);
+ invalidate();
+ }
+ mTimerPending = false;
+ }
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/CandidateViewListener.java b/src/com/android/inputmethod/pinyin/CandidateViewListener.java
new file mode 100644
index 0000000..795d119
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/CandidateViewListener.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * Interface to notify the input method when the user clicks a candidate or
+ * makes a direction-gesture on candidate view.
+ */
+public interface CandidateViewListener {
+ public void onClickChoice(int choiceId);
+
+ public void onToLeftGesture();
+
+ public void onToRightGesture();
+
+ public void onToTopGesture();
+
+ public void onToBottomGesture();
+}
diff --git a/src/com/android/inputmethod/pinyin/CandidatesContainer.java b/src/com/android/inputmethod/pinyin/CandidatesContainer.java
new file mode 100644
index 0000000..5b2a999
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/CandidatesContainer.java
@@ -0,0 +1,474 @@
+/*
+ * 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 com.android.inputmethod.pinyin.PinyinIME.DecodingInfo;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.TranslateAnimation;
+import android.view.animation.Animation.AnimationListener;
+import android.widget.ImageButton;
+import android.widget.RelativeLayout;
+import android.widget.ViewFlipper;
+
+interface ArrowUpdater {
+ void updateArrowStatus();
+}
+
+
+/**
+ * Container used to host the two candidate views. When user drags on candidate
+ * view, animation is used to dismiss the current candidate view and show a new
+ * one. These two candidate views and their parent are hosted by this container.
+ * <p>
+ * Besides the candidate views, there are two arrow views to show the page
+ * forward/backward arrows.
+ * </p>
+ */
+public class CandidatesContainer extends RelativeLayout implements
+ OnTouchListener, AnimationListener, ArrowUpdater {
+ /**
+ * Alpha value to show an enabled arrow.
+ */
+ private static int ARROW_ALPHA_ENABLED = 0xff;
+
+ /**
+ * Alpha value to show an disabled arrow.
+ */
+ private static int ARROW_ALPHA_DISABLED = 0x40;
+
+ /**
+ * Animation time to show a new candidate view and dismiss the old one.
+ */
+ private static int ANIMATION_TIME = 200;
+
+ /**
+ * Listener used to notify IME that user clicks a candidate, or navigate
+ * between them.
+ */
+ private CandidateViewListener mCvListener;
+
+ /**
+ * The left arrow button used to show previous page.
+ */
+ private ImageButton mLeftArrowBtn;
+
+ /**
+ * The right arrow button used to show next page.
+ */
+ private ImageButton mRightArrowBtn;
+
+ /**
+ * Decoding result to show.
+ */
+ private DecodingInfo mDecInfo;
+
+ /**
+ * The animation view used to show candidates. It contains two views.
+ * Normally, the candidates are shown one of them. When user navigates to
+ * another page, animation effect will be performed.
+ */
+ private ViewFlipper mFlipper;
+
+ /**
+ * The x offset of the flipper in this container.
+ */
+ private int xOffsetForFlipper;
+
+ /**
+ * Animation used by the incoming view when the user navigates to a left
+ * page.
+ */
+ private Animation mInAnimPushLeft;
+
+ /**
+ * Animation used by the incoming view when the user navigates to a right
+ * page.
+ */
+ private Animation mInAnimPushRight;
+
+ /**
+ * Animation used by the incoming view when the user navigates to a page
+ * above. If the page navigation is triggered by DOWN key, this animation is
+ * used.
+ */
+ private Animation mInAnimPushUp;
+
+ /**
+ * Animation used by the incoming view when the user navigates to a page
+ * below. If the page navigation is triggered by UP key, this animation is
+ * used.
+ */
+ private Animation mInAnimPushDown;
+
+ /**
+ * Animation used by the outgoing view when the user navigates to a left
+ * page.
+ */
+ private Animation mOutAnimPushLeft;
+
+ /**
+ * Animation used by the outgoing view when the user navigates to a right
+ * page.
+ */
+ private Animation mOutAnimPushRight;
+
+ /**
+ * Animation used by the outgoing view when the user navigates to a page
+ * above. If the page navigation is triggered by DOWN key, this animation is
+ * used.
+ */
+ private Animation mOutAnimPushUp;
+
+ /**
+ * Animation used by the incoming view when the user navigates to a page
+ * below. If the page navigation is triggered by UP key, this animation is
+ * used.
+ */
+ private Animation mOutAnimPushDown;
+
+ /**
+ * Animation object which is used for the incoming view currently.
+ */
+ private Animation mInAnimInUse;
+
+ /**
+ * Animation object which is used for the outgoing view currently.
+ */
+ private Animation mOutAnimInUse;
+
+ /**
+ * Current page number in display.
+ */
+ private int mCurrentPage = -1;
+
+ public CandidatesContainer(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void initialize(CandidateViewListener cvListener,
+ BalloonHint balloonHint, GestureDetector gestureDetector) {
+ mCvListener = cvListener;
+
+ mLeftArrowBtn = (ImageButton) findViewById(R.id.arrow_left_btn);
+ mRightArrowBtn = (ImageButton) findViewById(R.id.arrow_right_btn);
+ mLeftArrowBtn.setOnTouchListener(this);
+ mRightArrowBtn.setOnTouchListener(this);
+
+ mFlipper = (ViewFlipper) findViewById(R.id.candidate_flipper);
+ mFlipper.setMeasureAllChildren(true);
+
+ invalidate();
+ requestLayout();
+
+ for (int i = 0; i < mFlipper.getChildCount(); i++) {
+ CandidateView cv = (CandidateView) mFlipper.getChildAt(i);
+ cv.initialize(this, balloonHint, gestureDetector, mCvListener);
+ }
+ }
+
+ public void showCandidates(PinyinIME.DecodingInfo decInfo,
+ boolean enableActiveHighlight) {
+ if (null == decInfo) return;
+ mDecInfo = decInfo;
+ mCurrentPage = 0;
+
+ if (decInfo.isCandidatesListEmpty()) {
+ showArrow(mLeftArrowBtn, false);
+ showArrow(mRightArrowBtn, false);
+ } else {
+ showArrow(mLeftArrowBtn, true);
+ showArrow(mRightArrowBtn, true);
+ }
+
+ for (int i = 0; i < mFlipper.getChildCount(); i++) {
+ CandidateView cv = (CandidateView) mFlipper.getChildAt(i);
+ cv.setDecodingInfo(mDecInfo);
+ }
+ stopAnimation();
+
+ CandidateView cv = (CandidateView) mFlipper.getCurrentView();
+ cv.showPage(mCurrentPage, 0, enableActiveHighlight);
+
+ updateArrowStatus();
+ invalidate();
+ }
+
+ public int getCurrentPage() {
+ return mCurrentPage;
+ }
+
+ public void enableActiveHighlight(boolean enableActiveHighlight) {
+ CandidateView cv = (CandidateView) mFlipper.getCurrentView();
+ cv.enableActiveHighlight(enableActiveHighlight);
+ invalidate();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ Environment env = Environment.getInstance();
+ int measuredWidth = env.getScreenWidth();
+ int measuredHeight = getPaddingTop();
+ measuredHeight += env.getHeightForCandidates();
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth,
+ MeasureSpec.EXACTLY);
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(measuredHeight,
+ MeasureSpec.EXACTLY);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ if (null != mLeftArrowBtn) {
+ xOffsetForFlipper = mLeftArrowBtn.getMeasuredWidth();
+ }
+ }
+
+ public boolean activeCurseBackward() {
+ if (mFlipper.isFlipping() || null == mDecInfo) {
+ return false;
+ }
+
+ CandidateView cv = (CandidateView) mFlipper.getCurrentView();
+
+ if (cv.activeCurseBackward()) {
+ cv.invalidate();
+ return true;
+ } else {
+ return pageBackward(true, true);
+ }
+ }
+
+ public boolean activeCurseForward() {
+ if (mFlipper.isFlipping() || null == mDecInfo) {
+ return false;
+ }
+
+ CandidateView cv = (CandidateView) mFlipper.getCurrentView();
+
+ if (cv.activeCursorForward()) {
+ cv.invalidate();
+ return true;
+ } else {
+ return pageForward(true, true);
+ }
+ }
+
+ public boolean pageBackward(boolean animLeftRight,
+ boolean enableActiveHighlight) {
+ if (null == mDecInfo) return false;
+
+ if (mFlipper.isFlipping() || 0 == mCurrentPage) return false;
+
+ int child = mFlipper.getDisplayedChild();
+ int childNext = (child + 1) % 2;
+ CandidateView cv = (CandidateView) mFlipper.getChildAt(child);
+ CandidateView cvNext = (CandidateView) mFlipper.getChildAt(childNext);
+
+ mCurrentPage--;
+ int activeCandInPage = cv.getActiveCandiatePosInPage();
+ if (animLeftRight)
+ activeCandInPage = mDecInfo.mPageStart.elementAt(mCurrentPage + 1)
+ - mDecInfo.mPageStart.elementAt(mCurrentPage) - 1;
+
+ cvNext.showPage(mCurrentPage, activeCandInPage, enableActiveHighlight);
+ loadAnimation(animLeftRight, false);
+ startAnimation();
+
+ updateArrowStatus();
+ return true;
+ }
+
+ public boolean pageForward(boolean animLeftRight,
+ boolean enableActiveHighlight) {
+ if (null == mDecInfo) return false;
+
+ if (mFlipper.isFlipping() || !mDecInfo.preparePage(mCurrentPage + 1)) {
+ return false;
+ }
+
+ int child = mFlipper.getDisplayedChild();
+ int childNext = (child + 1) % 2;
+ CandidateView cv = (CandidateView) mFlipper.getChildAt(child);
+ int activeCandInPage = cv.getActiveCandiatePosInPage();
+ cv.enableActiveHighlight(enableActiveHighlight);
+
+ CandidateView cvNext = (CandidateView) mFlipper.getChildAt(childNext);
+ mCurrentPage++;
+ if (animLeftRight) activeCandInPage = 0;
+
+ cvNext.showPage(mCurrentPage, activeCandInPage, enableActiveHighlight);
+ loadAnimation(animLeftRight, true);
+ startAnimation();
+
+ updateArrowStatus();
+ return true;
+ }
+
+ public int getActiveCandiatePos() {
+ if (null == mDecInfo) return -1;
+ CandidateView cv = (CandidateView) mFlipper.getCurrentView();
+ return cv.getActiveCandiatePosGlobal();
+ }
+
+ public void updateArrowStatus() {
+ if (mCurrentPage < 0) return;
+ boolean forwardEnabled = mDecInfo.pageForwardable(mCurrentPage);
+ boolean backwardEnabled = mDecInfo.pageBackwardable(mCurrentPage);
+
+ if (backwardEnabled) {
+ enableArrow(mLeftArrowBtn, true);
+ } else {
+ enableArrow(mLeftArrowBtn, false);
+ }
+ if (forwardEnabled) {
+ enableArrow(mRightArrowBtn, true);
+ } else {
+ enableArrow(mRightArrowBtn, false);
+ }
+ }
+
+ private void enableArrow(ImageButton arrowBtn, boolean enabled) {
+ arrowBtn.setEnabled(enabled);
+ if (enabled)
+ arrowBtn.setAlpha(ARROW_ALPHA_ENABLED);
+ else
+ arrowBtn.setAlpha(ARROW_ALPHA_DISABLED);
+ }
+
+ private void showArrow(ImageButton arrowBtn, boolean show) {
+ if (show)
+ arrowBtn.setVisibility(View.VISIBLE);
+ else
+ arrowBtn.setVisibility(View.INVISIBLE);
+ }
+
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ if (v == mLeftArrowBtn) {
+ mCvListener.onToRightGesture();
+ } else if (v == mRightArrowBtn) {
+ mCvListener.onToLeftGesture();
+ }
+ } else if (event.getAction() == MotionEvent.ACTION_UP) {
+ CandidateView cv = (CandidateView) mFlipper.getCurrentView();
+ cv.enableActiveHighlight(true);
+ }
+
+ return false;
+ }
+
+ // The reason why we handle candiate view's touch events here is because
+ // that the view under the focused view may get touch events instead of the
+ // focused one.
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ event.offsetLocation(-xOffsetForFlipper, 0);
+ CandidateView cv = (CandidateView) mFlipper.getCurrentView();
+ cv.onTouchEventReal(event);
+ return true;
+ }
+
+ public void loadAnimation(boolean animLeftRight, boolean forward) {
+ if (animLeftRight) {
+ if (forward) {
+ if (null == mInAnimPushLeft) {
+ mInAnimPushLeft = createAnimation(1.0f, 0, 0, 0, 0, 1.0f,
+ ANIMATION_TIME);
+ mOutAnimPushLeft = createAnimation(0, -1.0f, 0, 0, 1.0f, 0,
+ ANIMATION_TIME);
+ }
+ mInAnimInUse = mInAnimPushLeft;
+ mOutAnimInUse = mOutAnimPushLeft;
+ } else {
+ if (null == mInAnimPushRight) {
+ mInAnimPushRight = createAnimation(-1.0f, 0, 0, 0, 0, 1.0f,
+ ANIMATION_TIME);
+ mOutAnimPushRight = createAnimation(0, 1.0f, 0, 0, 1.0f, 0,
+ ANIMATION_TIME);
+ }
+ mInAnimInUse = mInAnimPushRight;
+ mOutAnimInUse = mOutAnimPushRight;
+ }
+ } else {
+ if (forward) {
+ if (null == mInAnimPushUp) {
+ mInAnimPushUp = createAnimation(0, 0, 1.0f, 0, 0, 1.0f,
+ ANIMATION_TIME);
+ mOutAnimPushUp = createAnimation(0, 0, 0, -1.0f, 1.0f, 0,
+ ANIMATION_TIME);
+ }
+ mInAnimInUse = mInAnimPushUp;
+ mOutAnimInUse = mOutAnimPushUp;
+ } else {
+ if (null == mInAnimPushDown) {
+ mInAnimPushDown = createAnimation(0, 0, -1.0f, 0, 0, 1.0f,
+ ANIMATION_TIME);
+ mOutAnimPushDown = createAnimation(0, 0, 0, 1.0f, 1.0f, 0,
+ ANIMATION_TIME);
+ }
+ mInAnimInUse = mInAnimPushDown;
+ mOutAnimInUse = mOutAnimPushDown;
+ }
+ }
+
+ mInAnimInUse.setAnimationListener(this);
+
+ mFlipper.setInAnimation(mInAnimInUse);
+ mFlipper.setOutAnimation(mOutAnimInUse);
+ }
+
+ private Animation createAnimation(float xFrom, float xTo, float yFrom,
+ float yTo, float alphaFrom, float alphaTo, long duration) {
+ AnimationSet animSet = new AnimationSet(getContext(), null);
+ Animation trans = new TranslateAnimation(Animation.RELATIVE_TO_SELF,
+ xFrom, Animation.RELATIVE_TO_SELF, xTo,
+ Animation.RELATIVE_TO_SELF, yFrom, Animation.RELATIVE_TO_SELF,
+ yTo);
+ Animation alpha = new AlphaAnimation(alphaFrom, alphaTo);
+ animSet.addAnimation(trans);
+ animSet.addAnimation(alpha);
+ animSet.setDuration(duration);
+ return animSet;
+ }
+
+ private void startAnimation() {
+ mFlipper.showNext();
+ }
+
+ private void stopAnimation() {
+ mFlipper.stopFlipping();
+ }
+
+ public void onAnimationEnd(Animation animation) {
+ if (!mLeftArrowBtn.isPressed() && !mRightArrowBtn.isPressed()) {
+ CandidateView cv = (CandidateView) mFlipper.getCurrentView();
+ cv.enableActiveHighlight(true);
+ }
+ }
+
+ public void onAnimationRepeat(Animation animation) {
+ }
+
+ public void onAnimationStart(Animation animation) {
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/ComposingView.java b/src/com/android/inputmethod/pinyin/ComposingView.java
new file mode 100644
index 0000000..f70af45
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/ComposingView.java
@@ -0,0 +1,280 @@
+/*
+ * 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.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+
+/**
+ * View used to show composing string (The Pinyin string for the unselected
+ * syllables and the Chinese string for the selected syllables.)
+ */
+public class ComposingView extends View {
+ /**
+ * <p>
+ * There are three statuses for the composing view.
+ * </p>
+ *
+ * <p>
+ * {@link #SHOW_PINYIN} is used to show the current Pinyin string without
+ * highlighted effect. When user inputs Pinyin characters one by one, the
+ * Pinyin string will be shown in this mode.
+ * </p>
+ * <p>
+ * {@link #SHOW_STRING_LOWERCASE} is used to show the Pinyin string in
+ * lowercase with highlighted effect. When user presses UP key and there is
+ * no fixed Chinese characters, composing view will switch from
+ * {@link #SHOW_PINYIN} to this mode, and in this mode, user can press
+ * confirm key to input the lower-case string, so that user can input
+ * English letter in Chinese mode.
+ * </p>
+ * <p>
+ * {@link #EDIT_PINYIN} is used to edit the Pinyin string (shown with
+ * highlighted effect). When current status is {@link #SHOW_PINYIN} and user
+ * presses UP key, if there are fixed Characters, the input method will
+ * switch to {@link #EDIT_PINYIN} thus user can modify some characters in
+ * the middle of the Pinyin string. If the current status is
+ * {@link #SHOW_STRING_LOWERCASE} and user presses LEFT and RIGHT key, it
+ * will also switch to {@link #EDIT_PINYIN}.
+ * </p>
+ * <p>
+ * Whenever user presses down key, the status switches to
+ * {@link #SHOW_PINYIN}.
+ * </p>
+ * <p>
+ * When composing view's status is {@link #SHOW_PINYIN}, the IME's status is
+ * {@link PinyinIME.ImeState#STATE_INPUT}, otherwise, the IME's status
+ * should be {@link PinyinIME.ImeState#STATE_COMPOSING}.
+ * </p>
+ */
+ public enum ComposingStatus {
+ SHOW_PINYIN, SHOW_STRING_LOWERCASE, EDIT_PINYIN,
+ }
+
+ private static final int LEFT_RIGHT_MARGIN = 5;
+
+ /**
+ * Used to draw composing string. When drawing the active and idle part of
+ * the spelling(Pinyin) string, the color may be changed.
+ */
+ private Paint mPaint;
+
+ /**
+ * Drawable used to draw highlight effect.
+ */
+ private Drawable mHlDrawable;
+
+ /**
+ * Drawable used to draw cursor for editing mode.
+ */
+ private Drawable mCursor;
+
+ /**
+ * Used to estimate dimensions to show the string .
+ */
+ private FontMetricsInt mFmi;
+
+ private int mStrColor;
+ private int mStrColorHl;
+ private int mStrColorIdle;
+
+ private int mFontSize;
+
+ private ComposingStatus mComposingStatus;
+
+ PinyinIME.DecodingInfo mDecInfo;
+
+ public ComposingView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ Resources r = context.getResources();
+ mHlDrawable = r.getDrawable(R.drawable.composing_hl_bg);
+ mCursor = r.getDrawable(R.drawable.composing_area_cursor);
+
+ mStrColor = r.getColor(R.color.composing_color);
+ mStrColorHl = r.getColor(R.color.composing_color_hl);
+ mStrColorIdle = r.getColor(R.color.composing_color_idle);
+
+ mFontSize = r.getDimensionPixelSize(R.dimen.composing_height);
+
+ mPaint = new Paint();
+ mPaint.setColor(mStrColor);
+ mPaint.setAntiAlias(true);
+ mPaint.setTextSize(mFontSize);
+
+ mFmi = mPaint.getFontMetricsInt();
+ }
+
+ public void reset() {
+ mComposingStatus = ComposingStatus.SHOW_PINYIN;
+ }
+
+ /**
+ * Set the composing string to show. If the IME status is
+ * {@link PinyinIME.ImeState#STATE_INPUT}, the composing view's status will
+ * be set to {@link ComposingStatus#SHOW_PINYIN}, otherwise the composing
+ * view will set its status to {@link ComposingStatus#SHOW_STRING_LOWERCASE}
+ * or {@link ComposingStatus#EDIT_PINYIN} automatically.
+ */
+ public void setDecodingInfo(PinyinIME.DecodingInfo decInfo,
+ PinyinIME.ImeState imeStatus) {
+ mDecInfo = decInfo;
+
+ if (PinyinIME.ImeState.STATE_INPUT == imeStatus) {
+ mComposingStatus = ComposingStatus.SHOW_PINYIN;
+ mDecInfo.moveCursorToEdge(false);
+ } else {
+ if (decInfo.getFixedLen() != 0
+ || ComposingStatus.EDIT_PINYIN == mComposingStatus) {
+ mComposingStatus = ComposingStatus.EDIT_PINYIN;
+ } else {
+ mComposingStatus = ComposingStatus.SHOW_STRING_LOWERCASE;
+ }
+ mDecInfo.moveCursor(0);
+ }
+
+ measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ requestLayout();
+ invalidate();
+ }
+
+ public boolean moveCursor(int keyCode) {
+ if (keyCode != KeyEvent.KEYCODE_DPAD_LEFT
+ && keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) return false;
+
+ if (ComposingStatus.EDIT_PINYIN == mComposingStatus) {
+ int offset = 0;
+ if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT)
+ offset = -1;
+ else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) offset = 1;
+ mDecInfo.moveCursor(offset);
+ } else if (ComposingStatus.SHOW_STRING_LOWERCASE == mComposingStatus) {
+ if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
+ || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
+ mComposingStatus = ComposingStatus.EDIT_PINYIN;
+
+ measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ requestLayout();
+ }
+
+ }
+ invalidate();
+ return true;
+ }
+
+ public ComposingStatus getComposingStatus() {
+ return mComposingStatus;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ float width;
+ int height;
+ height = mFmi.bottom - mFmi.top + mPaddingTop + mPaddingBottom;
+
+ if (null == mDecInfo) {
+ width = 0;
+ } else {
+ width = mPaddingLeft + mPaddingRight + LEFT_RIGHT_MARGIN * 2;
+
+ String str;
+ if (ComposingStatus.SHOW_STRING_LOWERCASE == mComposingStatus) {
+ str = mDecInfo.getOrigianlSplStr().toString();
+ } else {
+ str = mDecInfo.getComposingStrForDisplay();
+ }
+ width += mPaint.measureText(str, 0, str.length());
+ }
+ setMeasuredDimension((int) (width + 0.5f), height);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (ComposingStatus.EDIT_PINYIN == mComposingStatus
+ || ComposingStatus.SHOW_PINYIN == mComposingStatus) {
+ drawForPinyin(canvas);
+ return;
+ }
+
+ float x, y;
+ x = mPaddingLeft + LEFT_RIGHT_MARGIN;
+ y = -mFmi.top + mPaddingTop;
+
+ mPaint.setColor(mStrColorHl);
+ mHlDrawable.setBounds(mPaddingLeft, mPaddingTop, getWidth()
+ - mPaddingRight, getHeight() - mPaddingBottom);
+ mHlDrawable.draw(canvas);
+
+ String splStr = mDecInfo.getOrigianlSplStr().toString();
+ canvas.drawText(splStr, 0, splStr.length(), x, y, mPaint);
+ }
+
+ private void drawCursor(Canvas canvas, float x) {
+ mCursor.setBounds((int) x, mPaddingTop, (int) x
+ + mCursor.getIntrinsicWidth(), getHeight() - mPaddingBottom);
+ mCursor.draw(canvas);
+ }
+
+ private void drawForPinyin(Canvas canvas) {
+ float x, y;
+ x = mPaddingLeft + LEFT_RIGHT_MARGIN;
+ y = -mFmi.top + mPaddingTop;
+
+ mPaint.setColor(mStrColor);
+
+ int cursorPos = mDecInfo.getCursorPosInCmpsDisplay();
+ int cmpsPos = cursorPos;
+ String cmpsStr = mDecInfo.getComposingStrForDisplay();
+ int activeCmpsLen = mDecInfo.getActiveCmpsDisplayLen();
+ if (cursorPos > activeCmpsLen) cmpsPos = activeCmpsLen;
+ canvas.drawText(cmpsStr, 0, cmpsPos, x, y, mPaint);
+ x += mPaint.measureText(cmpsStr, 0, cmpsPos);
+ if (cursorPos <= activeCmpsLen) {
+ if (ComposingStatus.EDIT_PINYIN == mComposingStatus) {
+ drawCursor(canvas, x);
+ }
+ canvas.drawText(cmpsStr, cmpsPos, activeCmpsLen, x, y, mPaint);
+ }
+
+ x += mPaint.measureText(cmpsStr, cmpsPos, activeCmpsLen);
+
+ if (cmpsStr.length() > activeCmpsLen) {
+ mPaint.setColor(mStrColorIdle);
+ int oriPos = activeCmpsLen;
+ if (cursorPos > activeCmpsLen) {
+ if (cursorPos > cmpsStr.length()) cursorPos = cmpsStr.length();
+ canvas.drawText(cmpsStr, oriPos, cursorPos, x, y, mPaint);
+ x += mPaint.measureText(cmpsStr, oriPos, cursorPos);
+
+ if (ComposingStatus.EDIT_PINYIN == mComposingStatus) {
+ drawCursor(canvas, x);
+ }
+
+ oriPos = cursorPos;
+ }
+ canvas.drawText(cmpsStr, oriPos, cmpsStr.length(), x, y, mPaint);
+ }
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/EnglishInputProcessor.java b/src/com/android/inputmethod/pinyin/EnglishInputProcessor.java
new file mode 100644
index 0000000..6d61119
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/EnglishInputProcessor.java
@@ -0,0 +1,95 @@
+/*
+ * 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.view.KeyEvent;
+import android.view.inputmethod.InputConnection;
+
+/**
+ * Class to handle English input.
+ */
+public class EnglishInputProcessor {
+
+ private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
+
+ public boolean processKey(InputConnection inputContext, KeyEvent event,
+ boolean upperCase, boolean realAction) {
+ if (null == inputContext || null == event) return false;
+
+ int keyCode = event.getKeyCode();
+
+ CharSequence prefix = null;
+ prefix = inputContext.getTextBeforeCursor(2, 0);
+
+ int keyChar;
+ keyChar = 0;
+ if (keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z) {
+ keyChar = keyCode - KeyEvent.KEYCODE_A + 'a';
+ if (upperCase) {
+ keyChar = keyChar + '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_APOSTROPHE)
+ keyChar = '\'';
+ else if (keyCode == KeyEvent.KEYCODE_AT)
+ keyChar = '@';
+ else if (keyCode == KeyEvent.KEYCODE_SLASH) keyChar = '/';
+
+ if (0 == keyChar) {
+ mLastKeyCode = keyCode;
+
+ String insert = null;
+ if (KeyEvent.KEYCODE_DEL == keyCode) {
+ if (realAction) {
+ inputContext.deleteSurroundingText(1, 0);
+ }
+ } else if (KeyEvent.KEYCODE_ENTER == keyCode) {
+ insert = "\n";
+ } else if (KeyEvent.KEYCODE_SPACE == keyCode) {
+ insert = " ";
+ } else {
+ return false;
+ }
+
+ if (null != insert && realAction)
+ inputContext.commitText(insert, insert.length());
+
+ return true;
+ }
+
+ if (!realAction)
+ return true;
+
+ if (KeyEvent.KEYCODE_SHIFT_LEFT == mLastKeyCode
+ || KeyEvent.KEYCODE_SHIFT_LEFT == mLastKeyCode) {
+ if (keyChar >= 'a' && keyChar <= 'z')
+ keyChar = keyChar - 'a' + 'A';
+ } else if (KeyEvent.KEYCODE_ALT_LEFT == mLastKeyCode) {
+ }
+
+ String result = String.valueOf((char) keyChar);
+ inputContext.commitText(result, result.length());
+ mLastKeyCode = keyCode;
+ return true;
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/Environment.java b/src/com/android/inputmethod/pinyin/Environment.java
new file mode 100644
index 0000000..8869294
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/Environment.java
@@ -0,0 +1,221 @@
+/*
+ * 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.content.Context;
+import android.content.res.Configuration;
+import android.view.Display;
+import android.view.WindowManager;
+
+/**
+ * Global environment configurations for showing soft keyboard and candidate
+ * view. All original dimension values are defined in float, and the real size
+ * is calculated from the float values of and screen size. In this way, this
+ * input method can work even when screen size is changed.
+ */
+public class Environment {
+ /**
+ * The key height for portrait mode. It is relative to the screen height.
+ */
+ private static final float KEY_HEIGHT_RATIO_PORTRAIT = 0.105f;
+
+ /**
+ * The key height for landscape mode. It is relative to the screen height.
+ */
+ private static final float KEY_HEIGHT_RATIO_LANDSCAPE = 0.147f;
+
+ /**
+ * The height of the candidates area for portrait mode. It is relative to
+ * screen height.
+ */
+ private static final float CANDIDATES_AREA_HEIGHT_RATIO_PORTRAIT = 0.084f;
+
+ /**
+ * The height of the candidates area for portrait mode. It is relative to
+ * screen height.
+ */
+ private static final float CANDIDATES_AREA_HEIGHT_RATIO_LANDSCAPE = 0.125f;
+
+ /**
+ * How much should the balloon width be larger than width of the real key.
+ * It is relative to the smaller one of screen width and height.
+ */
+ private static final float KEY_BALLOON_WIDTH_PLUS_RATIO = 0.08f;
+
+ /**
+ * How much should the balloon height be larger than that of the real key.
+ * It is relative to the smaller one of screen width and height.
+ */
+ private static final float KEY_BALLOON_HEIGHT_PLUS_RATIO = 0.07f;
+
+ /**
+ * The text size for normal keys. It is relative to the smaller one of
+ * screen width and height.
+ */
+ private static final float NORMAL_KEY_TEXT_SIZE_RATIO = 0.075f;
+
+ /**
+ * The text size for function keys. It is relative to the smaller one of
+ * screen width and height.
+ */
+ private static final float FUNCTION_KEY_TEXT_SIZE_RATIO = 0.055f;
+
+ /**
+ * The text size balloons of normal keys. It is relative to the smaller one
+ * of screen width and height.
+ */
+ private static final float NORMAL_BALLOON_TEXT_SIZE_RATIO = 0.14f;
+
+ /**
+ * The text size balloons of function keys. It is relative to the smaller
+ * one of screen width and height.
+ */
+ private static final float FUNCTION_BALLOON_TEXT_SIZE_RATIO = 0.085f;
+
+ /**
+ * The configurations are managed in a singleton.
+ */
+ private static Environment mInstance;
+
+ private int mScreenWidth;
+ private int mScreenHeight;
+ private int mKeyHeight;
+ private int mCandidatesAreaHeight;
+ private int mKeyBalloonWidthPlus;
+ private int mKeyBalloonHeightPlus;
+ private int mNormalKeyTextSize;
+ private int mFunctionKeyTextSize;
+ private int mNormalBalloonTextSize;
+ private int mFunctionBalloonTextSize;
+ private Configuration mConfig = new Configuration();
+ private boolean mDebug = false;
+
+ private Environment() {
+ }
+
+ public static Environment getInstance() {
+ if (null == mInstance) {
+ mInstance = new Environment();
+ }
+ return mInstance;
+ }
+
+ public void onConfigurationChanged(Configuration newConfig, Context context) {
+ if (mConfig.orientation != newConfig.orientation) {
+ WindowManager wm = (WindowManager) context
+ .getSystemService(Context.WINDOW_SERVICE);
+ Display d = wm.getDefaultDisplay();
+ mScreenWidth = d.getWidth();
+ mScreenHeight = d.getHeight();
+
+ int scale;
+ if (mScreenHeight > mScreenWidth) {
+ mKeyHeight = (int) (mScreenHeight * KEY_HEIGHT_RATIO_PORTRAIT);
+ mCandidatesAreaHeight = (int) (mScreenHeight * CANDIDATES_AREA_HEIGHT_RATIO_PORTRAIT);
+ scale = mScreenWidth;
+ } else {
+ mKeyHeight = (int) (mScreenHeight * KEY_HEIGHT_RATIO_LANDSCAPE);
+ mCandidatesAreaHeight = (int) (mScreenHeight * CANDIDATES_AREA_HEIGHT_RATIO_LANDSCAPE);
+ scale = mScreenHeight;
+ }
+ mNormalKeyTextSize = (int) (scale * NORMAL_KEY_TEXT_SIZE_RATIO);
+ mFunctionKeyTextSize = (int) (scale * FUNCTION_KEY_TEXT_SIZE_RATIO);
+ mNormalBalloonTextSize = (int) (scale * NORMAL_BALLOON_TEXT_SIZE_RATIO);
+ mFunctionBalloonTextSize = (int) (scale * FUNCTION_BALLOON_TEXT_SIZE_RATIO);
+ mKeyBalloonWidthPlus = (int) (scale * KEY_BALLOON_WIDTH_PLUS_RATIO);
+ mKeyBalloonHeightPlus = (int) (scale * KEY_BALLOON_HEIGHT_PLUS_RATIO);
+ }
+
+ mConfig.updateFrom(newConfig);
+ }
+
+ public Configuration getConfiguration() {
+ return mConfig;
+ }
+
+ public int getScreenWidth() {
+ return mScreenWidth;
+ }
+
+ public int getScreenHeight() {
+ return mScreenHeight;
+ }
+
+ public int getHeightForCandidates() {
+ return mCandidatesAreaHeight;
+ }
+
+ public float getKeyXMarginFactor() {
+ return 1.0f;
+ }
+
+ public float getKeyYMarginFactor() {
+ if (Configuration.ORIENTATION_LANDSCAPE == mConfig.orientation) {
+ return 0.7f;
+ }
+ return 1.0f;
+ }
+
+ public int getKeyHeight() {
+ return mKeyHeight;
+ }
+
+ public int getKeyBalloonWidthPlus() {
+ return mKeyBalloonWidthPlus;
+ }
+
+ public int getKeyBalloonHeightPlus() {
+ return mKeyBalloonHeightPlus;
+ }
+
+ public int getSkbHeight() {
+ if (Configuration.ORIENTATION_PORTRAIT == mConfig.orientation) {
+ return mKeyHeight * 4;
+ } else if (Configuration.ORIENTATION_LANDSCAPE == mConfig.orientation) {
+ return mKeyHeight * 4;
+ }
+ return 0;
+ }
+
+ public int getKeyTextSize(boolean isFunctionKey) {
+ if (isFunctionKey) {
+ return mFunctionKeyTextSize;
+ } else {
+ return mNormalKeyTextSize;
+ }
+ }
+
+ public int getBalloonTextSize(boolean isFunctionKey) {
+ if (isFunctionKey) {
+ return mFunctionBalloonTextSize;
+ } else {
+ return mNormalBalloonTextSize;
+ }
+ }
+
+ public boolean hasHardKeyboard() {
+ if (mConfig.keyboard == Configuration.KEYBOARD_NOKEYS
+ || mConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) {
+ return false;
+ }
+ return true;
+ }
+
+ public boolean needDebug() {
+ return mDebug;
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/InputModeSwitcher.java b/src/com/android/inputmethod/pinyin/InputModeSwitcher.java
new file mode 100644
index 0000000..7167182
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/InputModeSwitcher.java
@@ -0,0 +1,825 @@
+/*
+ * 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 com.android.inputmethod.pinyin.SoftKeyboard.KeyRow;
+
+import android.content.res.Resources;
+import android.view.inputmethod.EditorInfo;
+
+/**
+ * Switcher used to switching input mode between Chinese, English, symbol,etc.
+ */
+public class InputModeSwitcher {
+ /**
+ * User defined key code, used by soft keyboard.
+ */
+ private static final int USERDEF_KEYCODE_SHIFT_1 = -1;
+
+ /**
+ * User defined key code, used by soft keyboard.
+ */
+ private static final int USERDEF_KEYCODE_LANG_2 = -2;
+
+ /**
+ * User defined key code, used by soft keyboard.
+ */
+ private static final int USERDEF_KEYCODE_SYM_3 = -3;
+
+ /**
+ * User defined key code, used by soft keyboard.
+ */
+ public static final int USERDEF_KEYCODE_PHONE_SYM_4 = -4;
+
+ /**
+ * User defined key code, used by soft keyboard.
+ */
+ private static final int USERDEF_KEYCODE_MORE_SYM_5 = -5;
+
+ /**
+ * User defined key code, used by soft keyboard.
+ */
+ private static final int USERDEF_KEYCODE_SMILEY_6 = -6;
+
+
+ /**
+ * Bits used to indicate soft keyboard layout. If none bit is set, the
+ * current input mode does not require a soft keyboard.
+ **/
+ private static final int MASK_SKB_LAYOUT = 0xf0000000;
+
+ /**
+ * A kind of soft keyboard layout. An input mode should be anded with
+ * {@link #MASK_SKB_LAYOUT} to get its soft keyboard layout.
+ */
+ private static final int MASK_SKB_LAYOUT_QWERTY = 0x10000000;
+
+ /**
+ * A kind of soft keyboard layout. An input mode should be anded with
+ * {@link #MASK_SKB_LAYOUT} to get its soft keyboard layout.
+ */
+ private static final int MASK_SKB_LAYOUT_SYMBOL1 = 0x20000000;
+
+ /**
+ * A kind of soft keyboard layout. An input mode should be anded with
+ * {@link #MASK_SKB_LAYOUT} to get its soft keyboard layout.
+ */
+ private static final int MASK_SKB_LAYOUT_SYMBOL2 = 0x30000000;
+
+ /**
+ * A kind of soft keyboard layout. An input mode should be anded with
+ * {@link #MASK_SKB_LAYOUT} to get its soft keyboard layout.
+ */
+ private static final int MASK_SKB_LAYOUT_SMILEY = 0x40000000;
+
+ /**
+ * A kind of soft keyboard layout. An input mode should be anded with
+ * {@link #MASK_SKB_LAYOUT} to get its soft keyboard layout.
+ */
+ private static final int MASK_SKB_LAYOUT_PHONE = 0x50000000;
+
+ /**
+ * Used to indicate which language the current input mode is in. If the
+ * current input mode works with a none-QWERTY soft keyboard, these bits are
+ * also used to get language information. For example, a Chinese symbol soft
+ * keyboard and an English one are different in an icon which is used to
+ * tell user the language information. BTW, the smiley soft keyboard mode
+ * should be set with {@link #MASK_LANGUAGE_CN} because it can only be
+ * launched from Chinese QWERTY soft keyboard, and it has Chinese icon on
+ * soft keyboard.
+ */
+ private static final int MASK_LANGUAGE = 0x0f000000;
+
+ /**
+ * Used to indicate the current language. An input mode should be anded with
+ * {@link #MASK_LANGUAGE} to get this information.
+ */
+ private static final int MASK_LANGUAGE_CN = 0x01000000;
+
+ /**
+ * Used to indicate the current language. An input mode should be anded with
+ * {@link #MASK_LANGUAGE} to get this information.
+ */
+ private static final int MASK_LANGUAGE_EN = 0x02000000;
+
+ /**
+ * Used to indicate which case the current input mode is in. For example,
+ * English QWERTY has lowercase and uppercase. For the Chinese QWERTY, these
+ * bits are ignored. For phone keyboard layout, these bits can be
+ * {@link #MASK_CASE_UPPER} to request symbol page for phone soft keyboard.
+ */
+ private static final int MASK_CASE = 0x00f00000;
+
+ /**
+ * Used to indicate the current case information. An input mode should be
+ * anded with {@link #MASK_CASE} to get this information.
+ */
+ private static final int MASK_CASE_LOWER = 0x00100000;
+
+ /**
+ * Used to indicate the current case information. An input mode should be
+ * anded with {@link #MASK_CASE} to get this information.
+ */
+ private static final int MASK_CASE_UPPER = 0x00200000;
+
+ /**
+ * Mode for inputing Chinese with soft keyboard.
+ */
+ public static final int MODE_SKB_CHINESE = (MASK_SKB_LAYOUT_QWERTY | MASK_LANGUAGE_CN);
+
+ /**
+ * Mode for inputing basic symbols for Chinese mode with soft keyboard.
+ */
+ public static final int MODE_SKB_SYMBOL1_CN = (MASK_SKB_LAYOUT_SYMBOL1 | MASK_LANGUAGE_CN);
+
+ /**
+ * Mode for inputing more symbols for Chinese mode with soft keyboard.
+ */
+ public static final int MODE_SKB_SYMBOL2_CN = (MASK_SKB_LAYOUT_SYMBOL2 | MASK_LANGUAGE_CN);
+
+ /**
+ * Mode for inputing English lower characters with soft keyboard.
+ */
+ public static final int MODE_SKB_ENGLISH_LOWER = (MASK_SKB_LAYOUT_QWERTY
+ | MASK_LANGUAGE_EN | MASK_CASE_LOWER);
+
+ /**
+ * Mode for inputing English upper characters with soft keyboard.
+ */
+ public static final int MODE_SKB_ENGLISH_UPPER = (MASK_SKB_LAYOUT_QWERTY
+ | MASK_LANGUAGE_EN | MASK_CASE_UPPER);
+
+ /**
+ * Mode for inputing basic symbols for English mode with soft keyboard.
+ */
+ public static final int MODE_SKB_SYMBOL1_EN = (MASK_SKB_LAYOUT_SYMBOL1 | MASK_LANGUAGE_EN);
+
+ /**
+ * Mode for inputing more symbols for English mode with soft keyboard.
+ */
+ public static final int MODE_SKB_SYMBOL2_EN = (MASK_SKB_LAYOUT_SYMBOL2 | MASK_LANGUAGE_EN);
+
+ /**
+ * Mode for inputing smileys with soft keyboard.
+ */
+ public static final int MODE_SKB_SMILEY = (MASK_SKB_LAYOUT_SMILEY | MASK_LANGUAGE_CN);
+
+ /**
+ * Mode for inputing phone numbers.
+ */
+ public static final int MODE_SKB_PHONE_NUM = (MASK_SKB_LAYOUT_PHONE);
+
+ /**
+ * Mode for inputing phone numbers.
+ */
+ public static final int MODE_SKB_PHONE_SYM = (MASK_SKB_LAYOUT_PHONE | MASK_CASE_UPPER);
+
+ /**
+ * Mode for inputing Chinese with a hardware keyboard.
+ */
+ public static final int MODE_HKB_CHINESE = (MASK_LANGUAGE_CN);
+
+ /**
+ * Mode for inputing English with a hardware keyboard
+ */
+ public static final int MODE_HKB_ENGLISH = (MASK_LANGUAGE_EN);
+
+ /**
+ * Unset mode.
+ */
+ public static final int MODE_UNSET = 0;
+
+ /**
+ * Maximum toggle states for a soft keyboard.
+ */
+ public static final int MAX_TOGGLE_STATES = 4;
+
+ /**
+ * The input mode for the current edit box.
+ */
+ private int mInputMode = MODE_UNSET;
+
+ /**
+ * Used to remember previous input mode. When user enters an edit field, the
+ * previous input mode will be tried. If the previous mode can not be used
+ * for the current situation (For example, previous mode is a soft keyboard
+ * mode to input symbols, and we have a hardware keyboard for the current
+ * situation), {@link #mRecentLauageInputMode} will be tried.
+ **/
+ private int mPreviousInputMode = MODE_SKB_CHINESE;
+
+ /**
+ * Used to remember recent mode to input language.
+ */
+ private int mRecentLauageInputMode = MODE_SKB_CHINESE;
+
+ /**
+ * Editor information of the current edit box.
+ */
+ private EditorInfo mEditorInfo;
+
+ /**
+ * Used to indicate required toggling operations.
+ */
+ private ToggleStates mToggleStates = new ToggleStates();
+
+ /**
+ * The current field is a short message field?
+ */
+ private boolean mShortMessageField;
+
+ /**
+ * Is return key in normal state?
+ */
+ private boolean mEnterKeyNormal = true;
+
+ /**
+ * Current icon. 0 for none icon.
+ */
+ int mInputIcon = R.drawable.ime_pinyin;
+
+ /**
+ * IME service.
+ */
+ private PinyinIME mImeService;
+
+ /**
+ * Key toggling state for Chinese mode.
+ */
+ private int mToggleStateCn;
+
+ /**
+ * Key toggling state for Chinese mode with candidates.
+ */
+ private int mToggleStateCnCand;
+
+ /**
+ * Key toggling state for English lowwercase mode.
+ */
+ private int mToggleStateEnLower;
+
+ /**
+ * Key toggling state for English upppercase mode.
+ */
+ private int mToggleStateEnUpper;
+
+ /**
+ * Key toggling state for English symbol mode for the first page.
+ */
+ private int mToggleStateEnSym1;
+
+ /**
+ * Key toggling state for English symbol mode for the second page.
+ */
+ private int mToggleStateEnSym2;
+
+ /**
+ * Key toggling state for smiley mode.
+ */
+ private int mToggleStateSmiley;
+
+ /**
+ * Key toggling state for phone symbol mode.
+ */
+ private int mToggleStatePhoneSym;
+
+ /**
+ * Key toggling state for GO action of ENTER key.
+ */
+ private int mToggleStateGo;
+
+ /**
+ * Key toggling state for SEARCH action of ENTER key.
+ */
+ private int mToggleStateSearch;
+
+ /**
+ * Key toggling state for SEND action of ENTER key.
+ */
+ private int mToggleStateSend;
+
+ /**
+ * Key toggling state for NEXT action of ENTER key.
+ */
+ private int mToggleStateNext;
+
+ /**
+ * Key toggling state for SEND action of ENTER key.
+ */
+ private int mToggleStateDone;
+
+ /**
+ * QWERTY row toggling state for Chinese input.
+ */
+ private int mToggleRowCn;
+
+ /**
+ * QWERTY row toggling state for English input.
+ */
+ private int mToggleRowEn;
+
+ /**
+ * QWERTY row toggling state for URI input.
+ */
+ private int mToggleRowUri;
+
+ /**
+ * QWERTY row toggling state for email address input.
+ */
+ private int mToggleRowEmailAddress;
+
+ class ToggleStates {
+ /**
+ * If it is true, this soft keyboard is a QWERTY one.
+ */
+ boolean mQwerty;
+
+ /**
+ * If {@link #mQwerty} is true, this variable is used to decide the
+ * letter case of the QWERTY keyboard.
+ */
+ boolean mQwertyUpperCase;
+
+ /**
+ * The id of enabled row in the soft keyboard. Refer to
+ * {@link com.android.inputmethod.pinyin.SoftKeyboard.KeyRow} for
+ * details.
+ */
+ public int mRowIdToEnable;
+
+ /**
+ * Used to store all other toggle states for the current input mode.
+ */
+ public int mKeyStates[] = new int[MAX_TOGGLE_STATES];
+
+ /**
+ * Number of states to toggle.
+ */
+ public int mKeyStatesNum;
+ }
+
+ public InputModeSwitcher(PinyinIME imeService) {
+ mImeService = imeService;
+ Resources r = mImeService.getResources();
+ mToggleStateCn = Integer.parseInt(r.getString(R.string.toggle_cn));
+ mToggleStateCnCand = Integer.parseInt(r
+ .getString(R.string.toggle_cn_cand));
+ mToggleStateEnLower = Integer.parseInt(r
+ .getString(R.string.toggle_en_lower));
+ mToggleStateEnUpper = Integer.parseInt(r
+ .getString(R.string.toggle_en_upper));
+ mToggleStateEnSym1 = Integer.parseInt(r
+ .getString(R.string.toggle_en_sym1));
+ mToggleStateEnSym2 = Integer.parseInt(r
+ .getString(R.string.toggle_en_sym2));
+ mToggleStateSmiley = Integer.parseInt(r
+ .getString(R.string.toggle_smiley));
+ mToggleStatePhoneSym = Integer.parseInt(r
+ .getString(R.string.toggle_phone_sym));
+
+ mToggleStateGo = Integer
+ .parseInt(r.getString(R.string.toggle_enter_go));
+ mToggleStateSearch = Integer.parseInt(r
+ .getString(R.string.toggle_enter_search));
+ mToggleStateSend = Integer.parseInt(r
+ .getString(R.string.toggle_enter_send));
+ mToggleStateNext = Integer.parseInt(r
+ .getString(R.string.toggle_enter_next));
+ mToggleStateDone = Integer.parseInt(r
+ .getString(R.string.toggle_enter_done));
+
+ mToggleRowCn = Integer.parseInt(r.getString(R.string.toggle_row_cn));
+ mToggleRowEn = Integer.parseInt(r.getString(R.string.toggle_row_en));
+ mToggleRowUri = Integer.parseInt(r.getString(R.string.toggle_row_uri));
+ mToggleRowEmailAddress = Integer.parseInt(r
+ .getString(R.string.toggle_row_emailaddress));
+ }
+
+ public int getInputMode() {
+ return mInputMode;
+ }
+
+ public ToggleStates getToggleStates() {
+ return mToggleStates;
+ }
+
+ public int getSkbLayout() {
+ int layout = (mInputMode & MASK_SKB_LAYOUT);
+
+ switch (layout) {
+ case MASK_SKB_LAYOUT_QWERTY:
+ return R.xml.skb_qwerty;
+ case MASK_SKB_LAYOUT_SYMBOL1:
+ return R.xml.skb_sym1;
+ case MASK_SKB_LAYOUT_SYMBOL2:
+ return R.xml.skb_sym2;
+ case MASK_SKB_LAYOUT_SMILEY:
+ return R.xml.skb_smiley;
+ case MASK_SKB_LAYOUT_PHONE:
+ return R.xml.skb_phone;
+ }
+ return 0;
+ }
+
+ // Return the icon to update.
+ public int switchLanguageWithHkb() {
+ int newInputMode = MODE_HKB_CHINESE;
+ mInputIcon = R.drawable.ime_pinyin;
+
+ if (MODE_HKB_CHINESE == mInputMode) {
+ newInputMode = MODE_HKB_ENGLISH;
+ mInputIcon = R.drawable.ime_en;
+ }
+
+ saveInputMode(newInputMode);
+ return mInputIcon;
+ }
+
+ // Return the icon to update.
+ public int switchModeForUserKey(int userKey) {
+ int newInputMode = MODE_UNSET;
+
+ if (USERDEF_KEYCODE_LANG_2 == userKey) {
+ if (MODE_SKB_CHINESE == mInputMode) {
+ newInputMode = MODE_SKB_ENGLISH_LOWER;
+ } else if (MODE_SKB_ENGLISH_LOWER == mInputMode
+ || MODE_SKB_ENGLISH_UPPER == mInputMode) {
+ newInputMode = MODE_SKB_CHINESE;
+ } else if (MODE_SKB_SYMBOL1_CN == mInputMode) {
+ newInputMode = MODE_SKB_SYMBOL1_EN;
+ } else if (MODE_SKB_SYMBOL1_EN == mInputMode) {
+ newInputMode = MODE_SKB_SYMBOL1_CN;
+ } else if (MODE_SKB_SYMBOL2_CN == mInputMode) {
+ newInputMode = MODE_SKB_SYMBOL2_EN;
+ } else if (MODE_SKB_SYMBOL2_EN == mInputMode) {
+ newInputMode = MODE_SKB_SYMBOL2_CN;
+ } else if (MODE_SKB_SMILEY == mInputMode) {
+ newInputMode = MODE_SKB_CHINESE;
+ }
+ } else if (USERDEF_KEYCODE_SYM_3 == userKey) {
+ if (MODE_SKB_CHINESE == mInputMode) {
+ newInputMode = MODE_SKB_SYMBOL1_CN;
+ } else if (MODE_SKB_ENGLISH_UPPER == mInputMode
+ || MODE_SKB_ENGLISH_LOWER == mInputMode) {
+ newInputMode = MODE_SKB_SYMBOL1_EN;
+ } else if (MODE_SKB_SYMBOL1_EN == mInputMode
+ || MODE_SKB_SYMBOL2_EN == mInputMode) {
+ newInputMode = MODE_SKB_ENGLISH_LOWER;
+ } else if (MODE_SKB_SYMBOL1_CN == mInputMode
+ || MODE_SKB_SYMBOL2_CN == mInputMode) {
+ newInputMode = MODE_SKB_CHINESE;
+ } else if (MODE_SKB_SMILEY == mInputMode) {
+ newInputMode = MODE_SKB_SYMBOL1_CN;
+ }
+ } else if (USERDEF_KEYCODE_SHIFT_1 == userKey) {
+ if (MODE_SKB_ENGLISH_LOWER == mInputMode) {
+ newInputMode = MODE_SKB_ENGLISH_UPPER;
+ } else if (MODE_SKB_ENGLISH_UPPER == mInputMode) {
+ newInputMode = MODE_SKB_ENGLISH_LOWER;
+ }
+ } else if (USERDEF_KEYCODE_MORE_SYM_5 == userKey) {
+ int sym = (MASK_SKB_LAYOUT & mInputMode);
+ if (MASK_SKB_LAYOUT_SYMBOL1 == sym) {
+ sym = MASK_SKB_LAYOUT_SYMBOL2;
+ } else {
+ sym = MASK_SKB_LAYOUT_SYMBOL1;
+ }
+ newInputMode = ((mInputMode & (~MASK_SKB_LAYOUT)) | sym);
+ } else if (USERDEF_KEYCODE_SMILEY_6 == userKey) {
+ if (MODE_SKB_CHINESE == mInputMode) {
+ newInputMode = MODE_SKB_SMILEY;
+ } else {
+ newInputMode = MODE_SKB_CHINESE;
+ }
+ } else if (USERDEF_KEYCODE_PHONE_SYM_4 == userKey) {
+ if (MODE_SKB_PHONE_NUM == mInputMode) {
+ newInputMode = MODE_SKB_PHONE_SYM;
+ } else {
+ newInputMode = MODE_SKB_PHONE_NUM;
+ }
+ }
+
+ if (newInputMode == mInputMode || MODE_UNSET == newInputMode) {
+ return mInputIcon;
+ }
+
+ saveInputMode(newInputMode);
+ prepareToggleStates(true);
+ return mInputIcon;
+ }
+
+ // Return the icon to update.
+ public int requestInputWithHkb(EditorInfo editorInfo) {
+ mShortMessageField = false;
+ boolean english = false;
+ int newInputMode = MODE_HKB_CHINESE;
+
+ switch (editorInfo.inputType & EditorInfo.TYPE_MASK_CLASS) {
+ case EditorInfo.TYPE_CLASS_NUMBER:
+ case EditorInfo.TYPE_CLASS_PHONE:
+ case EditorInfo.TYPE_CLASS_DATETIME:
+ english = true;
+ break;
+ case EditorInfo.TYPE_CLASS_TEXT:
+ int v = editorInfo.inputType & EditorInfo.TYPE_MASK_VARIATION;
+ if (v == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+ || v == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
+ || v == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
+ || v == EditorInfo.TYPE_TEXT_VARIATION_URI) {
+ english = true;
+ } else if (v == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
+ mShortMessageField = true;
+ }
+ break;
+ default:
+ }
+
+ if (english) {
+ // If the application request English mode, we switch to it.
+ newInputMode = MODE_HKB_ENGLISH;
+ } else {
+ // If the application do not request English mode, we will
+ // try to keep the previous mode to input language text.
+ // Because there is not soft keyboard, we need discard all
+ // soft keyboard related information from the previous language
+ // mode.
+ if ((mRecentLauageInputMode & MASK_LANGUAGE) == MASK_LANGUAGE_CN) {
+ newInputMode = MODE_HKB_CHINESE;
+ } else {
+ newInputMode = MODE_HKB_ENGLISH;
+ }
+ }
+ mEditorInfo = editorInfo;
+ saveInputMode(newInputMode);
+ prepareToggleStates(false);
+ return mInputIcon;
+ }
+
+ // Return the icon to update.
+ public int requestInputWithSkb(EditorInfo editorInfo) {
+ mShortMessageField = false;
+
+ int newInputMode = MODE_SKB_CHINESE;
+
+ switch (editorInfo.inputType & EditorInfo.TYPE_MASK_CLASS) {
+ case EditorInfo.TYPE_CLASS_NUMBER:
+ case EditorInfo.TYPE_CLASS_DATETIME:
+ newInputMode = MODE_SKB_SYMBOL1_EN;
+ break;
+ case EditorInfo.TYPE_CLASS_PHONE:
+ newInputMode = MODE_SKB_PHONE_NUM;
+ break;
+ case EditorInfo.TYPE_CLASS_TEXT:
+ int v = editorInfo.inputType & EditorInfo.TYPE_MASK_VARIATION;
+ if (v == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+ || v == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
+ || v == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
+ || v == EditorInfo.TYPE_TEXT_VARIATION_URI) {
+ // If the application request English mode, we switch to it.
+ newInputMode = MODE_SKB_ENGLISH_LOWER;
+ } else {
+ if (v == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
+ mShortMessageField = true;
+ }
+ // If the application do not request English mode, we will
+ // try to keep the previous mode.
+ int skbLayout = (mInputMode & MASK_SKB_LAYOUT);
+ newInputMode = mInputMode;
+ if (0 == skbLayout) {
+ if ((mInputMode & MASK_LANGUAGE) == MASK_LANGUAGE_CN) {
+ newInputMode = MODE_SKB_CHINESE;
+ } else {
+ newInputMode = MODE_SKB_ENGLISH_LOWER;
+ }
+ }
+ }
+ break;
+ default:
+ // Try to keep the previous mode.
+ int skbLayout = (mInputMode & MASK_SKB_LAYOUT);
+ newInputMode = mInputMode;
+ if (0 == skbLayout) {
+ if ((mInputMode & MASK_LANGUAGE) == MASK_LANGUAGE_CN) {
+ newInputMode = MODE_SKB_CHINESE;
+ } else {
+ newInputMode = MODE_SKB_ENGLISH_LOWER;
+ }
+ }
+ break;
+ }
+
+ mEditorInfo = editorInfo;
+ saveInputMode(newInputMode);
+ prepareToggleStates(true);
+ return mInputIcon;
+ }
+
+ // Return the icon to update.
+ public int requestBackToPreviousSkb() {
+ int layout = (mInputMode & MASK_SKB_LAYOUT);
+ int lastLayout = (mPreviousInputMode & MASK_SKB_LAYOUT);
+ if (0 != layout && 0 != lastLayout) {
+ mInputMode = mPreviousInputMode;
+ saveInputMode(mInputMode);
+ prepareToggleStates(true);
+ return mInputIcon;
+ }
+ return 0;
+ }
+
+ public int getTooggleStateForCnCand() {
+ return mToggleStateCnCand;
+ }
+
+ public boolean isEnglishWithHkb() {
+ return MODE_HKB_ENGLISH == mInputMode;
+ }
+
+ public boolean isEnglishWithSkb() {
+ return MODE_SKB_ENGLISH_LOWER == mInputMode
+ || MODE_SKB_ENGLISH_UPPER == mInputMode;
+ }
+
+ public boolean isEnglishUpperCaseWithSkb() {
+ return MODE_SKB_ENGLISH_UPPER == mInputMode;
+ }
+
+ public boolean isChineseText() {
+ int skbLayout = (mInputMode & MASK_SKB_LAYOUT);
+ if (MASK_SKB_LAYOUT_QWERTY == skbLayout || 0 == skbLayout) {
+ int language = (mInputMode & MASK_LANGUAGE);
+ if (MASK_LANGUAGE_CN == language) return true;
+ }
+ return false;
+ }
+
+ public boolean isChineseTextWithHkb() {
+ int skbLayout = (mInputMode & MASK_SKB_LAYOUT);
+ if (0 == skbLayout) {
+ int language = (mInputMode & MASK_LANGUAGE);
+ if (MASK_LANGUAGE_CN == language) return true;
+ }
+ return false;
+ }
+
+ public boolean isChineseTextWithSkb() {
+ int skbLayout = (mInputMode & MASK_SKB_LAYOUT);
+ if (MASK_SKB_LAYOUT_QWERTY == skbLayout) {
+ int language = (mInputMode & MASK_LANGUAGE);
+ if (MASK_LANGUAGE_CN == language) return true;
+ }
+ return false;
+ }
+
+ public boolean isSymbolWithSkb() {
+ int skbLayout = (mInputMode & MASK_SKB_LAYOUT);
+ if (MASK_SKB_LAYOUT_SYMBOL1 == skbLayout
+ || MASK_SKB_LAYOUT_SYMBOL2 == skbLayout) {
+ return true;
+ }
+ return false;
+ }
+
+ public boolean isEnterNoramlState() {
+ return mEnterKeyNormal;
+ }
+
+ public boolean tryHandleLongPressSwitch(int keyCode) {
+ if (USERDEF_KEYCODE_LANG_2 == keyCode
+ || USERDEF_KEYCODE_PHONE_SYM_4 == keyCode) {
+ mImeService.showOptionsMenu();
+ return true;
+ }
+ return false;
+ }
+
+ private void saveInputMode(int newInputMode) {
+ mPreviousInputMode = mInputMode;
+ mInputMode = newInputMode;
+
+ int skbLayout = (mInputMode & MASK_SKB_LAYOUT);
+ if (MASK_SKB_LAYOUT_QWERTY == skbLayout || 0 == skbLayout) {
+ mRecentLauageInputMode = mInputMode;
+ }
+
+ mInputIcon = R.drawable.ime_pinyin;
+ if (isEnglishWithHkb()) {
+ mInputIcon = R.drawable.ime_en;
+ } else if (isChineseTextWithHkb()) {
+ mInputIcon = R.drawable.ime_pinyin;
+ }
+
+ if (!Environment.getInstance().hasHardKeyboard()) {
+ mInputIcon = 0;
+ }
+ }
+
+ private void prepareToggleStates(boolean needSkb) {
+ mEnterKeyNormal = true;
+ if (!needSkb) return;
+
+ mToggleStates.mQwerty = false;
+ mToggleStates.mKeyStatesNum = 0;
+
+ int states[] = mToggleStates.mKeyStates;
+ int statesNum = 0;
+ // Toggle state for language.
+ int language = (mInputMode & MASK_LANGUAGE);
+ int layout = (mInputMode & MASK_SKB_LAYOUT);
+ int charcase = (mInputMode & MASK_CASE);
+ int variation = mEditorInfo.inputType & EditorInfo.TYPE_MASK_VARIATION;
+
+ if (MASK_SKB_LAYOUT_PHONE != layout) {
+ if (MASK_LANGUAGE_CN == language) {
+ // Chinese and Chinese symbol are always the default states,
+ // do not add a toggling operation.
+ if (MASK_SKB_LAYOUT_QWERTY == layout) {
+ mToggleStates.mQwerty = true;
+ mToggleStates.mQwertyUpperCase = true;
+ if (mShortMessageField) {
+ states[statesNum] = mToggleStateSmiley;
+ statesNum++;
+ }
+ }
+ } else if (MASK_LANGUAGE_EN == language) {
+ if (MASK_SKB_LAYOUT_QWERTY == layout) {
+ mToggleStates.mQwerty = true;
+ mToggleStates.mQwertyUpperCase = false;
+ states[statesNum] = mToggleStateEnLower;
+ if (MASK_CASE_UPPER == charcase) {
+ mToggleStates.mQwertyUpperCase = true;
+ states[statesNum] = mToggleStateEnUpper;
+ }
+ statesNum++;
+ } else if (MASK_SKB_LAYOUT_SYMBOL1 == layout) {
+ states[statesNum] = mToggleStateEnSym1;
+ statesNum++;
+ } else if (MASK_SKB_LAYOUT_SYMBOL2 == layout) {
+ states[statesNum] = mToggleStateEnSym2;
+ statesNum++;
+ }
+ }
+
+ // Toggle rows for QWERTY.
+ mToggleStates.mRowIdToEnable = KeyRow.DEFAULT_ROW_ID;
+ if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) {
+ mToggleStates.mRowIdToEnable = mToggleRowEmailAddress;
+ } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_URI) {
+ mToggleStates.mRowIdToEnable = mToggleRowUri;
+ } else if (MASK_LANGUAGE_CN == language) {
+ mToggleStates.mRowIdToEnable = mToggleRowCn;
+ } else if (MASK_LANGUAGE_EN == language) {
+ mToggleStates.mRowIdToEnable = mToggleRowEn;
+ }
+ } else {
+ if (MASK_CASE_UPPER == charcase) {
+ states[statesNum] = mToggleStatePhoneSym;
+ statesNum++;
+ }
+ }
+
+ // Toggle state for enter key.
+ int action = mEditorInfo.imeOptions
+ & (EditorInfo.IME_MASK_ACTION | EditorInfo.IME_FLAG_NO_ENTER_ACTION);
+
+ if (action == EditorInfo.IME_ACTION_GO) {
+ states[statesNum] = mToggleStateGo;
+ statesNum++;
+ mEnterKeyNormal = false;
+ } else if (action == EditorInfo.IME_ACTION_SEARCH) {
+ states[statesNum] = mToggleStateSearch;
+ statesNum++;
+ mEnterKeyNormal = false;
+ } else if (action == EditorInfo.IME_ACTION_SEND) {
+ states[statesNum] = mToggleStateSend;
+ statesNum++;
+ mEnterKeyNormal = false;
+ } else if (action == EditorInfo.IME_ACTION_NEXT) {
+ int f = mEditorInfo.inputType & EditorInfo.TYPE_MASK_FLAGS;
+ if (f != EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) {
+ states[statesNum] = mToggleStateNext;
+ statesNum++;
+ mEnterKeyNormal = false;
+ }
+ } else if (action == EditorInfo.IME_ACTION_DONE) {
+ states[statesNum] = mToggleStateDone;
+ statesNum++;
+ mEnterKeyNormal = false;
+ }
+ mToggleStates.mKeyStatesNum = statesNum;
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/KeyMapDream.java b/src/com/android/inputmethod/pinyin/KeyMapDream.java
new file mode 100644
index 0000000..5a95c6f
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/KeyMapDream.java
@@ -0,0 +1,106 @@
+/*
+ * 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.view.KeyEvent;
+
+/**
+ * Class used to map the symbols on Dream's hardware keyboard to corresponding
+ * Chinese full-width symbols.
+ */
+public class KeyMapDream {
+ // Number of shift bits to store full-width symbols
+ private static final int SHIFT_FWCH = 8;
+ private static final int[] mKeyMap = {
+ KeyEvent.KEYCODE_UNKNOWN,
+ KeyEvent.KEYCODE_SOFT_LEFT,
+ KeyEvent.KEYCODE_SOFT_RIGHT,
+ KeyEvent.KEYCODE_HOME,
+ KeyEvent.KEYCODE_BACK,
+ KeyEvent.KEYCODE_CALL,
+ KeyEvent.KEYCODE_ENDCALL,
+ KeyEvent.KEYCODE_0 | ('\uff09' << SHIFT_FWCH), // )
+ KeyEvent.KEYCODE_1 | ('\uff01' << SHIFT_FWCH), // !
+ KeyEvent.KEYCODE_2 | ('\uff20' << SHIFT_FWCH), // @
+ KeyEvent.KEYCODE_3 | ('\uff03' << SHIFT_FWCH), // #
+ KeyEvent.KEYCODE_4 | ('\uffe5' << SHIFT_FWCH), // $ - fullwidth Yuan
+ KeyEvent.KEYCODE_5 | ('\uff05' << SHIFT_FWCH), // %
+ KeyEvent.KEYCODE_6 | ('\u2026' << SHIFT_FWCH), // ^ - Apostrophe
+ KeyEvent.KEYCODE_7 | ('\uff06' << SHIFT_FWCH), // &
+ KeyEvent.KEYCODE_8 | ('\uff0a' << SHIFT_FWCH), // *
+ KeyEvent.KEYCODE_9 | ('\uff08' << SHIFT_FWCH), // (
+ KeyEvent.KEYCODE_STAR,
+ KeyEvent.KEYCODE_POUND,
+ KeyEvent.KEYCODE_DPAD_UP,
+ KeyEvent.KEYCODE_DPAD_DOWN,
+ KeyEvent.KEYCODE_DPAD_LEFT,
+ KeyEvent.KEYCODE_DPAD_RIGHT,
+ KeyEvent.KEYCODE_DPAD_CENTER,
+ KeyEvent.KEYCODE_VOLUME_UP,
+ KeyEvent.KEYCODE_VOLUME_DOWN,
+ KeyEvent.KEYCODE_POWER,
+ KeyEvent.KEYCODE_CAMERA,
+ KeyEvent.KEYCODE_CLEAR,
+ KeyEvent.KEYCODE_A,
+ KeyEvent.KEYCODE_B | ('\uff3d' << SHIFT_FWCH), // ]
+ KeyEvent.KEYCODE_C | ('\u00a9' << SHIFT_FWCH), // copyright
+ KeyEvent.KEYCODE_D | ('\u3001' << SHIFT_FWCH), // \\
+ KeyEvent.KEYCODE_E | ('_' << SHIFT_FWCH), // _
+ KeyEvent.KEYCODE_F | ('\uff5b' << SHIFT_FWCH), // {
+ KeyEvent.KEYCODE_G | ('\uff5d' << SHIFT_FWCH), // }
+ KeyEvent.KEYCODE_H | ('\uff1a' << SHIFT_FWCH), // :
+ KeyEvent.KEYCODE_I | ('\uff0d' << SHIFT_FWCH), // -
+ KeyEvent.KEYCODE_J | ('\uff1b' << SHIFT_FWCH), // ;
+ KeyEvent.KEYCODE_K | ('\u201c' << SHIFT_FWCH), // "
+ KeyEvent.KEYCODE_L | ('\u2019' << SHIFT_FWCH), // '
+ KeyEvent.KEYCODE_M | ('\u300b' << SHIFT_FWCH), // > - French quotes
+ KeyEvent.KEYCODE_N | ('\u300a' << SHIFT_FWCH), // < - French quotes
+ KeyEvent.KEYCODE_O | ('\uff0b' << SHIFT_FWCH), // +
+ KeyEvent.KEYCODE_P | ('\uff1d' << SHIFT_FWCH), // =
+ KeyEvent.KEYCODE_Q | ('\t' << SHIFT_FWCH), // \t
+ KeyEvent.KEYCODE_R | ('\u00ae' << SHIFT_FWCH), // trademark
+ KeyEvent.KEYCODE_S | ('\uff5c' << SHIFT_FWCH), // |
+ KeyEvent.KEYCODE_T | ('\u20ac' << SHIFT_FWCH), //
+ KeyEvent.KEYCODE_U | ('\u00d7' << SHIFT_FWCH), // multiplier
+ KeyEvent.KEYCODE_V | ('\uff3b' << SHIFT_FWCH), // [
+ KeyEvent.KEYCODE_W | ('\uff40' << SHIFT_FWCH), // `
+ KeyEvent.KEYCODE_X, KeyEvent.KEYCODE_Y | ('\u00f7' << SHIFT_FWCH),
+ KeyEvent.KEYCODE_Z,
+ KeyEvent.KEYCODE_COMMA | ('\uff1f' << SHIFT_FWCH),
+ KeyEvent.KEYCODE_PERIOD | ('\uff0f' << SHIFT_FWCH),
+ KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_RIGHT,
+ KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_RIGHT,
+ KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_SYM,
+ KeyEvent.KEYCODE_EXPLORER, KeyEvent.KEYCODE_ENVELOPE,
+ KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_DEL,
+ KeyEvent.KEYCODE_GRAVE, KeyEvent.KEYCODE_MINUS,
+ KeyEvent.KEYCODE_EQUALS, KeyEvent.KEYCODE_LEFT_BRACKET,
+ KeyEvent.KEYCODE_RIGHT_BRACKET, KeyEvent.KEYCODE_BACKSLASH,
+ KeyEvent.KEYCODE_SEMICOLON, KeyEvent.KEYCODE_APOSTROPHE,
+ KeyEvent.KEYCODE_SLASH,
+ KeyEvent.KEYCODE_AT | ('\uff5e' << SHIFT_FWCH),
+ KeyEvent.KEYCODE_NUM, KeyEvent.KEYCODE_HEADSETHOOK,
+ KeyEvent.KEYCODE_FOCUS, KeyEvent.KEYCODE_PLUS,
+ KeyEvent.KEYCODE_MENU, KeyEvent.KEYCODE_NOTIFICATION,
+ KeyEvent.KEYCODE_SEARCH,};
+
+ static public char getChineseLabel(int keyCode) {
+ if (keyCode <= 0 || keyCode >= KeyEvent.getMaxKeyCode()) return 0;
+ assert ((mKeyMap[keyCode] & 0x000000ff) == keyCode);
+ return (char) (mKeyMap[keyCode] >> SHIFT_FWCH);
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/PinyinDecoderService.java b/src/com/android/inputmethod/pinyin/PinyinDecoderService.java
new file mode 100644
index 0000000..a4a3ac4
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/PinyinDecoderService.java
@@ -0,0 +1,326 @@
+/*
+ * 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 com.android.inputmethod.pinyin.IPinyinDecoderService;
+
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.List;
+import java.util.Vector;
+
+import android.app.Service;
+import android.content.Intent;
+import android.content.res.AssetFileDescriptor;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * This class is used to separate the input method kernel in an individual
+ * service so that both IME and IME-syncer can use it.
+ */
+public class PinyinDecoderService extends Service {
+ native static boolean nativeImOpenDecoder(byte fn_sys_dict[],
+ byte fn_usr_dict[]);
+
+ native static boolean nativeImOpenDecoderFd(FileDescriptor fd,
+ long startOffset, long length, byte fn_usr_dict[]);
+
+ native static void nativeImSetMaxLens(int maxSpsLen, int maxHzsLen);
+
+ native static boolean nativeImCloseDecoder();
+
+ native static int nativeImSearch(byte pyBuf[], int pyLen);
+
+ native static int nativeImDelSearch(int pos, boolean is_pos_in_splid,
+ boolean clear_fixed_this_step);
+
+ native static void nativeImResetSearch();
+
+ native static int nativeImAddLetter(byte ch);
+
+ native static String nativeImGetPyStr(boolean decoded);
+
+ native static int nativeImGetPyStrLen(boolean decoded);
+
+ native static int[] nativeImGetSplStart();
+
+ native static String nativeImGetChoice(int choiceId);
+
+ native static int nativeImChoose(int choiceId);
+
+ native static int nativeImCancelLastChoice();
+
+ native static int nativeImGetFixedLen();
+
+ native static boolean nativeImCancelInput();
+
+ native static boolean nativeImFlushCache();
+
+ native static int nativeImGetPredictsNum(String fixedStr);
+
+ native static String nativeImGetPredictItem(int predictNo);
+
+ // Sync related
+ native static String nativeSyncUserDict(byte[] user_dict, String tomerge);
+
+ native static boolean nativeSyncBegin(byte[] user_dict);
+
+ native static boolean nativeSyncFinish();
+
+ native static String nativeSyncGetLemmas();
+
+ native static int nativeSyncPutLemmas(String tomerge);
+
+ native static int nativeSyncGetLastCount();
+
+ native static int nativeSyncGetTotalCount();
+
+ native static boolean nativeSyncClearLastGot();
+
+ native static int nativeSyncGetCapacity();
+
+ private final static int MAX_PATH_FILE_LENGTH = 100;
+ private static boolean inited = false;
+
+ private String mUsr_dict_file;
+
+ static {
+ try {
+ System.loadLibrary("jni_pinyinime");
+ } catch (UnsatisfiedLinkError ule) {
+ Log.e("PinyinDecoderService",
+ "WARNING: Could not load jni_pinyinime natives");
+ }
+ }
+
+ // Get file name of the specified dictionary
+ private boolean getUsrDictFileName(byte usr_dict[]) {
+ if (null == usr_dict) {
+ return false;
+ }
+
+ for (int i = 0; i < mUsr_dict_file.length(); i++)
+ usr_dict[i] = (byte) mUsr_dict_file.charAt(i);
+ usr_dict[mUsr_dict_file.length()] = 0;
+
+ return true;
+ }
+
+ private void initPinyinEngine() {
+ byte usr_dict[];
+ usr_dict = new byte[MAX_PATH_FILE_LENGTH];
+
+ // Here is how we open a built-in dictionary for access through
+ // a file descriptor...
+ AssetFileDescriptor afd = getResources().openRawResourceFd(
+ R.raw.dict_pinyin);
+ if (Environment.getInstance().needDebug()) {
+ Log
+ .i("foo", "Dict: start=" + afd.getStartOffset()
+ + ", length=" + afd.getLength() + ", fd="
+ + afd.getParcelFileDescriptor());
+ }
+ if (getUsrDictFileName(usr_dict)) {
+ inited = nativeImOpenDecoderFd(afd.getFileDescriptor(), afd
+ .getStartOffset(), afd.getLength(), usr_dict);
+ }
+ try {
+ afd.close();
+ } catch (IOException e) {
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mUsr_dict_file = getFileStreamPath("usr_dict.dat").getPath();
+ // This is a hack to make sure our "files" directory has been
+ // created.
+ try {
+ openFileOutput("dummy", 0).close();
+ } catch (FileNotFoundException e) {
+ } catch (IOException e) {
+ }
+
+ initPinyinEngine();
+ }
+
+ @Override
+ public void onDestroy() {
+ nativeImCloseDecoder();
+ inited = false;
+ super.onDestroy();
+ }
+
+ private final IPinyinDecoderService.Stub mBinder = new IPinyinDecoderService.Stub() {
+ public int getInt() {
+ return 12345;
+ }
+
+ public void setMaxLens(int maxSpsLen, int maxHzsLen) {
+ nativeImSetMaxLens(maxSpsLen, maxHzsLen);
+ }
+
+ public int imSearch(byte[] pyBuf, int pyLen) {
+ return nativeImSearch(pyBuf, pyLen);
+ }
+
+ public int imDelSearch(int pos, boolean is_pos_in_splid,
+ boolean clear_fixed_this_step) {
+ return nativeImDelSearch(pos, is_pos_in_splid,
+ clear_fixed_this_step);
+ }
+
+ public void imResetSearch() {
+ nativeImResetSearch();
+ }
+
+ public int imAddLetter(byte ch) {
+ return nativeImAddLetter(ch);
+ }
+
+ public String imGetPyStr(boolean decoded) {
+ return nativeImGetPyStr(decoded);
+ }
+
+ public int imGetPyStrLen(boolean decoded) {
+ return nativeImGetPyStrLen(decoded);
+ }
+
+ public int[] imGetSplStart() {
+ return nativeImGetSplStart();
+ }
+
+ public String imGetChoice(int choiceId) {
+ return nativeImGetChoice(choiceId);
+ }
+
+ public String imGetChoices(int choicesNum) {
+ String retStr = null;
+ for (int i = 0; i < choicesNum; i++) {
+ if (null == retStr)
+ retStr = nativeImGetChoice(i);
+ else
+ retStr += " " + nativeImGetChoice(i);
+ }
+ return retStr;
+ }
+
+ public List<String> imGetChoiceList(int choicesStart, int choicesNum,
+ int sentFixedLen) {
+ Vector<String> choiceList = new Vector<String>();
+ for (int i = choicesStart; i < choicesStart + choicesNum; i++) {
+ String retStr = nativeImGetChoice(i);
+ if (0 == i) retStr = retStr.substring(sentFixedLen);
+ choiceList.add(retStr);
+ }
+ return choiceList;
+ }
+
+ public int imChoose(int choiceId) {
+ return nativeImChoose(choiceId);
+ }
+
+ public int imCancelLastChoice() {
+ return nativeImCancelLastChoice();
+ }
+
+ public int imGetFixedLen() {
+ return nativeImGetFixedLen();
+ }
+
+ public boolean imCancelInput() {
+ return nativeImCancelInput();
+ }
+
+ public void imFlushCache() {
+ nativeImFlushCache();
+ }
+
+ public int imGetPredictsNum(String fixedStr) {
+ return nativeImGetPredictsNum(fixedStr);
+ }
+
+ public String imGetPredictItem(int predictNo) {
+ return nativeImGetPredictItem(predictNo);
+ }
+
+ public List<String> imGetPredictList(int predictsStart, int predictsNum) {
+ Vector<String> predictList = new Vector<String>();
+ for (int i = predictsStart; i < predictsStart + predictsNum; i++) {
+ predictList.add(nativeImGetPredictItem(i));
+ }
+ return predictList;
+ }
+
+ public String syncUserDict(String tomerge) {
+ byte usr_dict[];
+ usr_dict = new byte[MAX_PATH_FILE_LENGTH];
+
+ if (getUsrDictFileName(usr_dict)) {
+ return nativeSyncUserDict(usr_dict, tomerge);
+ }
+ return null;
+ }
+
+ public boolean syncBegin() {
+ byte usr_dict[];
+ usr_dict = new byte[MAX_PATH_FILE_LENGTH];
+
+ if (getUsrDictFileName(usr_dict)) {
+ return nativeSyncBegin(usr_dict);
+ }
+ return false;
+ }
+
+ public void syncFinish() {
+ nativeSyncFinish();
+ }
+
+ public int syncPutLemmas(String tomerge) {
+ return nativeSyncPutLemmas(tomerge);
+ }
+
+ public String syncGetLemmas() {
+ return nativeSyncGetLemmas();
+ }
+
+ public int syncGetLastCount() {
+ return nativeSyncGetLastCount();
+ }
+
+ public int syncGetTotalCount() {
+ return nativeSyncGetTotalCount();
+ }
+
+ public void syncClearLastGot() {
+ nativeSyncClearLastGot();
+ }
+
+ public int imSyncGetCapacity() {
+ return nativeSyncGetCapacity();
+ }
+ };
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/PinyinIME.java b/src/com/android/inputmethod/pinyin/PinyinIME.java
new file mode 100644
index 0000000..9ac2c2d
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/PinyinIME.java
@@ -0,0 +1,2134 @@
+/*
+ * 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;
+ }
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/Settings.java b/src/com/android/inputmethod/pinyin/Settings.java
new file mode 100644
index 0000000..c05f605
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/Settings.java
@@ -0,0 +1,101 @@
+/*
+ * 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.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+
+/**
+ * Class used to maintain settings.
+ */
+public class Settings {
+ private static final String ANDPY_CONFS_KEYSOUND_KEY = "Sound";
+ private static final String ANDPY_CONFS_VIBRATE_KEY = "Vibrate";
+ private static final String ANDPY_CONFS_PREDICTION_KEY = "Prediction";
+
+ private static boolean mKeySound;
+ private static boolean mVibrate;
+ private static boolean mPrediction;
+
+ private static Settings mInstance = null;
+
+ private static int mRefCount = 0;
+
+ private static SharedPreferences mSharedPref = null;
+
+ protected Settings(SharedPreferences pref) {
+ mSharedPref = pref;
+ initConfs();
+ }
+
+ public static Settings getInstance(SharedPreferences pref) {
+ if (mInstance == null) {
+ mInstance = new Settings(pref);
+ }
+ assert (pref == mSharedPref);
+ mRefCount++;
+ return mInstance;
+ }
+
+ public static void writeBack() {
+ Editor editor = mSharedPref.edit();
+ editor.putBoolean(ANDPY_CONFS_VIBRATE_KEY, mVibrate);
+ editor.putBoolean(ANDPY_CONFS_KEYSOUND_KEY, mKeySound);
+ editor.putBoolean(ANDPY_CONFS_PREDICTION_KEY, mPrediction);
+ editor.commit();
+ }
+
+ public static void releaseInstance() {
+ mRefCount--;
+ if (mRefCount == 0) {
+ mInstance = null;
+ }
+ }
+
+ private void initConfs() {
+ mKeySound = mSharedPref.getBoolean(ANDPY_CONFS_KEYSOUND_KEY, true);
+ mVibrate = mSharedPref.getBoolean(ANDPY_CONFS_VIBRATE_KEY, false);
+ mPrediction = mSharedPref.getBoolean(ANDPY_CONFS_PREDICTION_KEY, true);
+ }
+
+ public static boolean getKeySound() {
+ return mKeySound;
+ }
+
+ public static void setKeySound(boolean v) {
+ if (mKeySound == v) return;
+ mKeySound = v;
+ }
+
+ public static boolean getVibrate() {
+ return mVibrate;
+ }
+
+ public static void setVibrate(boolean v) {
+ if (mVibrate == v) return;
+ mVibrate = v;
+ }
+
+ public static boolean getPrediction() {
+ return mPrediction;
+ }
+
+ public static void setPrediction(boolean v) {
+ if (mPrediction == v) return;
+ mPrediction = v;
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/SettingsActivity.java b/src/com/android/inputmethod/pinyin/SettingsActivity.java
new file mode 100644
index 0000000..7d23d8e
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/SettingsActivity.java
@@ -0,0 +1,115 @@
+/*
+ * 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 java.util.List;
+
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceGroup;
+import android.preference.PreferenceManager;
+import android.preference.PreferenceScreen;
+import com.android.inputmethod.pinyin.Settings;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+
+/**
+ * Setting activity of Pinyin IME.
+ */
+public class SettingsActivity extends PreferenceActivity implements
+ Preference.OnPreferenceChangeListener {
+
+ private static String TAG = "SettingsActivity";
+
+ private CheckBoxPreference mKeySoundPref;
+ private CheckBoxPreference mVibratePref;
+ private CheckBoxPreference mPredictionPref;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.settings);
+
+ PreferenceScreen prefSet = getPreferenceScreen();
+
+ mKeySoundPref = (CheckBoxPreference) prefSet
+ .findPreference(getString(R.string.setting_sound_key));
+ mVibratePref = (CheckBoxPreference) prefSet
+ .findPreference(getString(R.string.setting_vibrate_key));
+ mPredictionPref = (CheckBoxPreference) prefSet
+ .findPreference(getString(R.string.setting_prediction_key));
+
+ prefSet.setOnPreferenceChangeListener(this);
+
+ Settings.getInstance(PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext()));
+
+ updatePreference(prefSet, getString(R.string.setting_advanced_key));
+
+ updateWidgets();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ updateWidgets();
+ }
+
+ @Override
+ protected void onDestroy() {
+ Settings.releaseInstance();
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ Settings.setKeySound(mKeySoundPref.isChecked());
+ Settings.setVibrate(mVibratePref.isChecked());
+ Settings.setPrediction(mPredictionPref.isChecked());
+
+ Settings.writeBack();
+ }
+
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ return true;
+ }
+
+ private void updateWidgets() {
+ mKeySoundPref.setChecked(Settings.getKeySound());
+ mVibratePref.setChecked(Settings.getVibrate());
+ mPredictionPref.setChecked(Settings.getPrediction());
+ }
+
+ public void updatePreference(PreferenceGroup parentPref, String prefKey) {
+ Preference preference = parentPref.findPreference(prefKey);
+ if (preference == null) {
+ return;
+ }
+ Intent intent = preference.getIntent();
+ if (intent != null) {
+ PackageManager pm = getPackageManager();
+ List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
+ int listSize = list.size();
+ if (listSize == 0)
+ parentPref.removePreference(preference);
+ }
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/SkbContainer.java b/src/com/android/inputmethod/pinyin/SkbContainer.java
new file mode 100644
index 0000000..2294860
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/SkbContainer.java
@@ -0,0 +1,642 @@
+/*
+ * 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.content.Context;
+import android.content.res.Resources;
+import android.inputmethodservice.InputMethodService;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.widget.PopupWindow;
+import android.widget.RelativeLayout;
+import android.widget.ViewFlipper;
+
+/**
+ * The top container to host soft keyboard view(s).
+ */
+public class SkbContainer extends RelativeLayout implements OnTouchListener {
+ /**
+ * For finger touch, user tends to press the bottom part of the target key,
+ * or he/she even presses the area out of it, so it is necessary to make a
+ * simple bias correction. If the input method runs on emulator, no bias
+ * correction will be used.
+ */
+ private static final int Y_BIAS_CORRECTION = -10;
+
+ /**
+ * Used to skip these move events whose position is too close to the
+ * previous touch events.
+ */
+ private static final int MOVE_TOLERANCE = 6;
+
+ /**
+ * If this member is true, PopupWindow is used to show on-key highlight
+ * effect.
+ */
+ private static boolean POPUPWINDOW_FOR_PRESSED_UI = false;
+
+ /**
+ * The current soft keyboard layout.
+ *
+ * @see com.android.inputmethod.pinyin.InputModeSwitcher for detailed layout
+ * definitions.
+ */
+ private int mSkbLayout = 0;
+
+ /**
+ * The input method service.
+ */
+ private InputMethodService mService;
+
+ /**
+ * Input mode switcher used to switch between different modes like Chinese,
+ * English, etc.
+ */
+ private InputModeSwitcher mInputModeSwitcher;
+
+ /**
+ * The gesture detector.
+ */
+ private GestureDetector mGestureDetector;
+
+ private Environment mEnvironment;
+
+ private ViewFlipper mSkbFlipper;
+
+ /**
+ * The popup balloon hint for key press/release.
+ */
+ private BalloonHint mBalloonPopup;
+
+ /**
+ * The on-key balloon hint for key press/release.
+ */
+ private BalloonHint mBalloonOnKey = null;
+
+ /** The major sub soft keyboard. */
+ private SoftKeyboardView mMajorView;
+
+ /**
+ * The last parameter when function {@link #toggleCandidateMode(boolean)}
+ * was called.
+ */
+ private boolean mLastCandidatesShowing;
+
+ /** Used to indicate whether a popup soft keyboard is shown. */
+ private boolean mPopupSkbShow = false;
+
+ /**
+ * Used to indicate whether a popup soft keyboard is just shown, and waits
+ * for the touch event to release. After the release, the popup window can
+ * response to touch events.
+ **/
+ private boolean mPopupSkbNoResponse = false;
+
+ /** Popup sub keyboard. */
+ private PopupWindow mPopupSkb;
+
+ /** The view of the popup sub soft keyboard. */
+ private SoftKeyboardView mPopupSkbView;
+
+ private int mPopupX;
+
+ private int mPopupY;
+
+ /**
+ * When user presses a key, a timer is started, when it times out, it is
+ * necessary to detect whether user still holds the key.
+ */
+ private volatile boolean mWaitForTouchUp = false;
+
+ /**
+ * When user drags on the soft keyboard and the distance is enough, this
+ * drag will be recognized as a gesture and a gesture-based action will be
+ * taken, in this situation, ignore the consequent events.
+ */
+ private volatile boolean mDiscardEvent = false;
+
+ /**
+ * For finger touch, user tends to press the bottom part of the target key,
+ * or he/she even presses the area out of it, so it is necessary to make a
+ * simple bias correction in Y.
+ */
+ private int mYBiasCorrection = 0;
+
+ /**
+ * The x coordination of the last touch event.
+ */
+ private int mXLast;
+
+ /**
+ * The y coordination of the last touch event.
+ */
+ private int mYLast;
+
+ /**
+ * The soft keyboard view.
+ */
+ private SoftKeyboardView mSkv;
+
+ /**
+ * The position of the soft keyboard view in the container.
+ */
+ private int mSkvPosInContainer[] = new int[2];
+
+ /**
+ * The key pressed by user.
+ */
+ private SoftKey mSoftKeyDown = null;
+
+ /**
+ * Used to timeout a press if user holds the key for a long time.
+ */
+ private LongPressTimer mLongPressTimer;
+
+ /**
+ * For temporary use.
+ */
+ private int mXyPosTmp[] = new int[2];
+
+ public SkbContainer(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mEnvironment = Environment.getInstance();
+
+ mLongPressTimer = new LongPressTimer(this);
+
+ // If it runs on an emulator, no bias correction
+ if ("1".equals(SystemProperties.get("ro.kernel.qemu"))) {
+ mYBiasCorrection = 0;
+ } else {
+ mYBiasCorrection = Y_BIAS_CORRECTION;
+ }
+ mBalloonPopup = new BalloonHint(context, this, MeasureSpec.AT_MOST);
+ if (POPUPWINDOW_FOR_PRESSED_UI) {
+ mBalloonOnKey = new BalloonHint(context, this, MeasureSpec.AT_MOST);
+ }
+
+ mPopupSkb = new PopupWindow(mContext);
+ mPopupSkb.setBackgroundDrawable(null);
+ mPopupSkb.setClippingEnabled(false);
+ }
+
+ public void setService(InputMethodService service) {
+ mService = service;
+ }
+
+ public void setInputModeSwitcher(InputModeSwitcher inputModeSwitcher) {
+ mInputModeSwitcher = inputModeSwitcher;
+ }
+
+ public void setGestureDetector(GestureDetector gestureDetector) {
+ mGestureDetector = gestureDetector;
+ }
+
+ public boolean isCurrentSkbSticky() {
+ if (null == mMajorView) return true;
+ SoftKeyboard skb = mMajorView.getSoftKeyboard();
+ if (null != skb) {
+ return skb.getStickyFlag();
+ }
+ return true;
+ }
+
+ public void toggleCandidateMode(boolean candidatesShowing) {
+ if (null == mMajorView || !mInputModeSwitcher.isChineseText()
+ || mLastCandidatesShowing == candidatesShowing) return;
+ mLastCandidatesShowing = candidatesShowing;
+
+ SoftKeyboard skb = mMajorView.getSoftKeyboard();
+ if (null == skb) return;
+
+ int state = mInputModeSwitcher.getTooggleStateForCnCand();
+ if (!candidatesShowing) {
+ skb.disableToggleState(state, false);
+ skb.enableToggleStates(mInputModeSwitcher.getToggleStates());
+ } else {
+ skb.enableToggleState(state, false);
+ }
+
+ mMajorView.invalidate();
+ }
+
+ public void updateInputMode() {
+ int skbLayout = mInputModeSwitcher.getSkbLayout();
+ if (mSkbLayout != skbLayout) {
+ mSkbLayout = skbLayout;
+ updateSkbLayout();
+ }
+
+ mLastCandidatesShowing = false;
+
+ if (null == mMajorView) return;
+
+ SoftKeyboard skb = mMajorView.getSoftKeyboard();
+ if (null == skb) return;
+ skb.enableToggleStates(mInputModeSwitcher.getToggleStates());
+ invalidate();
+ return;
+ }
+
+ private void updateSkbLayout() {
+ int screenWidth = mEnvironment.getScreenWidth();
+ int keyHeight = mEnvironment.getKeyHeight();
+ int skbHeight = mEnvironment.getSkbHeight();
+
+ Resources r = mContext.getResources();
+ if (null == mSkbFlipper) {
+ mSkbFlipper = (ViewFlipper) findViewById(R.id.alpha_floatable);
+ }
+ mMajorView = (SoftKeyboardView) mSkbFlipper.getChildAt(0);
+
+ SoftKeyboard majorSkb = null;
+ SkbPool skbPool = SkbPool.getInstance();
+
+ switch (mSkbLayout) {
+ case R.xml.skb_qwerty:
+ majorSkb = skbPool.getSoftKeyboard(R.xml.skb_qwerty,
+ R.xml.skb_qwerty, screenWidth, skbHeight, mContext);
+ break;
+
+ case R.xml.skb_sym1:
+ majorSkb = skbPool.getSoftKeyboard(R.xml.skb_sym1, R.xml.skb_sym1,
+ screenWidth, skbHeight, mContext);
+ break;
+
+ case R.xml.skb_sym2:
+ majorSkb = skbPool.getSoftKeyboard(R.xml.skb_sym2, R.xml.skb_sym2,
+ screenWidth, skbHeight, mContext);
+ break;
+
+ case R.xml.skb_smiley:
+ majorSkb = skbPool.getSoftKeyboard(R.xml.skb_smiley,
+ R.xml.skb_smiley, screenWidth, skbHeight, mContext);
+ break;
+
+ case R.xml.skb_phone:
+ majorSkb = skbPool.getSoftKeyboard(R.xml.skb_phone,
+ R.xml.skb_phone, screenWidth, skbHeight, mContext);
+ break;
+ default:
+ }
+
+ if (null == majorSkb || !mMajorView.setSoftKeyboard(majorSkb)) {
+ return;
+ }
+ mMajorView.setBalloonHint(mBalloonOnKey, mBalloonPopup, false);
+ mMajorView.invalidate();
+ }
+
+ private void responseKeyEvent(SoftKey sKey) {
+ if (null == sKey) return;
+ ((PinyinIME) mService).responseSoftKeyEvent(sKey);
+ return;
+ }
+
+ private SoftKeyboardView inKeyboardView(int x, int y,
+ int positionInParent[]) {
+ if (mPopupSkbShow) {
+ if (mPopupX <= x && mPopupX + mPopupSkb.getWidth() > x
+ && mPopupY <= y && mPopupY + mPopupSkb.getHeight() > y) {
+ positionInParent[0] = mPopupX;
+ positionInParent[1] = mPopupY;
+ mPopupSkbView.setOffsetToSkbContainer(positionInParent);
+ return mPopupSkbView;
+ }
+ return null;
+ }
+
+ return mMajorView;
+ }
+
+ private void popupSymbols() {
+ int popupResId = mSoftKeyDown.getPopupResId();
+ if (popupResId > 0) {
+ int skbContainerWidth = getWidth();
+ int skbContainerHeight = getHeight();
+ // The paddings of the background are not included.
+ int miniSkbWidth = (int) (skbContainerWidth * 0.8);
+ int miniSkbHeight = (int) (skbContainerHeight * 0.23);
+
+ SkbPool skbPool = SkbPool.getInstance();
+ SoftKeyboard skb = skbPool.getSoftKeyboard(popupResId, popupResId,
+ miniSkbWidth, miniSkbHeight, mContext);
+ if (null == skb) return;
+
+ mPopupX = (skbContainerWidth - skb.getSkbTotalWidth()) / 2;
+ mPopupY = (skbContainerHeight - skb.getSkbTotalHeight()) / 2;
+
+ if (null == mPopupSkbView) {
+ mPopupSkbView = new SoftKeyboardView(mContext, null);
+ mPopupSkbView.onMeasure(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ }
+ mPopupSkbView.setOnTouchListener(this);
+ mPopupSkbView.setSoftKeyboard(skb);
+ mPopupSkbView.setBalloonHint(mBalloonOnKey, mBalloonPopup, true);
+
+ mPopupSkb.setContentView(mPopupSkbView);
+ mPopupSkb.setWidth(skb.getSkbCoreWidth()
+ + mPopupSkbView.getPaddingLeft()
+ + mPopupSkbView.getPaddingRight());
+ mPopupSkb.setHeight(skb.getSkbCoreHeight()
+ + mPopupSkbView.getPaddingTop()
+ + mPopupSkbView.getPaddingBottom());
+
+ getLocationInWindow(mXyPosTmp);
+ mPopupSkb.showAtLocation(this, Gravity.NO_GRAVITY, mPopupX, mPopupY
+ + mXyPosTmp[1]);
+ mPopupSkbShow = true;
+ mPopupSkbNoResponse = true;
+ // Invalidate itself to dim the current soft keyboards.
+ dimSoftKeyboard(true);
+ resetKeyPress(0);
+ }
+ }
+
+ private void dimSoftKeyboard(boolean dimSkb) {
+ mMajorView.dimSoftKeyboard(dimSkb);
+ }
+
+ private void dismissPopupSkb() {
+ mPopupSkb.dismiss();
+ mPopupSkbShow = false;
+ dimSoftKeyboard(false);
+ resetKeyPress(0);
+ }
+
+ private void resetKeyPress(long delay) {
+ mLongPressTimer.removeTimer();
+
+ if (null != mSkv) {
+ mSkv.resetKeyPress(delay);
+ }
+ }
+
+ public boolean handleBack(boolean realAction) {
+ if (mPopupSkbShow) {
+ if (!realAction) return true;
+
+ dismissPopupSkb();
+ mDiscardEvent = true;
+ return true;
+ }
+ return false;
+ }
+
+ public void dismissPopups() {
+ handleBack(true);
+ resetKeyPress(0);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ Environment env = Environment.getInstance();
+ int measuredWidth = env.getScreenWidth();
+ int measuredHeight = getPaddingTop();
+ measuredHeight += env.getSkbHeight();
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth,
+ MeasureSpec.EXACTLY);
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(measuredHeight,
+ MeasureSpec.EXACTLY);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ super.onTouchEvent(event);
+
+ if (mSkbFlipper.isFlipping()) {
+ resetKeyPress(0);
+ return true;
+ }
+
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+ // Bias correction
+ y = y + mYBiasCorrection;
+
+ // Ignore short-distance movement event to get better performance.
+ if (event.getAction() == MotionEvent.ACTION_MOVE) {
+ if (Math.abs(x - mXLast) <= MOVE_TOLERANCE
+ && Math.abs(y - mYLast) <= MOVE_TOLERANCE) {
+ return true;
+ }
+ }
+
+ mXLast = x;
+ mYLast = y;
+
+ if (!mPopupSkbShow) {
+ if (mGestureDetector.onTouchEvent(event)) {
+ resetKeyPress(0);
+ mDiscardEvent = true;
+ return true;
+ }
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ resetKeyPress(0);
+
+ mWaitForTouchUp = true;
+ mDiscardEvent = false;
+
+ mSkv = null;
+ mSoftKeyDown = null;
+ mSkv = inKeyboardView(x, y, mSkvPosInContainer);
+ if (null != mSkv) {
+ mSoftKeyDown = mSkv.onKeyPress(x - mSkvPosInContainer[0], y
+ - mSkvPosInContainer[1], mLongPressTimer, false);
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) {
+ break;
+ }
+ if (mDiscardEvent) {
+ resetKeyPress(0);
+ break;
+ }
+
+ if (mPopupSkbShow && mPopupSkbNoResponse) {
+ break;
+ }
+
+ SoftKeyboardView skv = inKeyboardView(x, y, mSkvPosInContainer);
+ if (null != skv) {
+ if (skv != mSkv) {
+ mSkv = skv;
+ mSoftKeyDown = mSkv.onKeyPress(x - mSkvPosInContainer[0], y
+ - mSkvPosInContainer[1], mLongPressTimer, true);
+ } else if (null != skv) {
+ if (null != mSkv) {
+ mSoftKeyDown = mSkv.onKeyMove(
+ x - mSkvPosInContainer[0], y
+ - mSkvPosInContainer[1]);
+ if (null == mSoftKeyDown) {
+ mDiscardEvent = true;
+ }
+ }
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ if (mDiscardEvent) {
+ resetKeyPress(0);
+ break;
+ }
+
+ mWaitForTouchUp = false;
+
+ // The view which got the {@link MotionEvent#ACTION_DOWN} event is
+ // always used to handle this event.
+ if (null != mSkv) {
+ mSkv.onKeyRelease(x - mSkvPosInContainer[0], y
+ - mSkvPosInContainer[1]);
+ }
+
+ if (!mPopupSkbShow || !mPopupSkbNoResponse) {
+ responseKeyEvent(mSoftKeyDown);
+ }
+
+ if (mSkv == mPopupSkbView && !mPopupSkbNoResponse) {
+ dismissPopupSkb();
+ }
+ mPopupSkbNoResponse = false;
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ break;
+ }
+
+ if (null == mSkv) {
+ return false;
+ }
+
+ return true;
+ }
+
+ // Function for interface OnTouchListener, it is used to handle touch events
+ // which will be delivered to the popup soft keyboard view.
+ public boolean onTouch(View v, MotionEvent event) {
+ // Translate the event to fit to the container.
+ MotionEvent newEv = MotionEvent.obtain(event.getDownTime(), event
+ .getEventTime(), event.getAction(), event.getX() + mPopupX,
+ event.getY() + mPopupY, event.getPressure(), event.getSize(),
+ event.getMetaState(), event.getXPrecision(), event
+ .getYPrecision(), event.getDeviceId(), event
+ .getEdgeFlags());
+ boolean ret = onTouchEvent(newEv);
+ return ret;
+ }
+
+ class LongPressTimer extends Handler implements Runnable {
+ /**
+ * When user presses a key for a long time, the timeout interval to
+ * generate first {@link #LONG_PRESS_KEYNUM1} key events.
+ */
+ public static final int LONG_PRESS_TIMEOUT1 = 500;
+
+ /**
+ * When user presses a key for a long time, after the first
+ * {@link #LONG_PRESS_KEYNUM1} key events, this timeout interval will be
+ * used.
+ */
+ private static final int LONG_PRESS_TIMEOUT2 = 100;
+
+ /**
+ * When user presses a key for a long time, after the first
+ * {@link #LONG_PRESS_KEYNUM2} key events, this timeout interval will be
+ * used.
+ */
+ private static final int LONG_PRESS_TIMEOUT3 = 100;
+
+ /**
+ * When user presses a key for a long time, after the first
+ * {@link #LONG_PRESS_KEYNUM1} key events, timeout interval
+ * {@link #LONG_PRESS_TIMEOUT2} will be used instead.
+ */
+ public static final int LONG_PRESS_KEYNUM1 = 1;
+
+ /**
+ * When user presses a key for a long time, after the first
+ * {@link #LONG_PRESS_KEYNUM2} key events, timeout interval
+ * {@link #LONG_PRESS_TIMEOUT3} will be used instead.
+ */
+ public static final int LONG_PRESS_KEYNUM2 = 3;
+
+ SkbContainer mSkbContainer;
+
+ private int mResponseTimes = 0;
+
+ public LongPressTimer(SkbContainer skbContainer) {
+ mSkbContainer = skbContainer;
+ }
+
+ public void startTimer() {
+ postAtTime(this, SystemClock.uptimeMillis() + LONG_PRESS_TIMEOUT1);
+ mResponseTimes = 0;
+ }
+
+ public boolean removeTimer() {
+ removeCallbacks(this);
+ return true;
+ }
+
+ public void run() {
+ if (mWaitForTouchUp) {
+ mResponseTimes++;
+ if (mSoftKeyDown.repeatable()) {
+ if (mSoftKeyDown.isUserDefKey()) {
+ if (1 == mResponseTimes) {
+ if (mInputModeSwitcher
+ .tryHandleLongPressSwitch(mSoftKeyDown.mKeyCode)) {
+ mDiscardEvent = true;
+ resetKeyPress(0);
+ }
+ }
+ } else {
+ responseKeyEvent(mSoftKeyDown);
+ long timeout;
+ if (mResponseTimes < LONG_PRESS_KEYNUM1) {
+ timeout = LONG_PRESS_TIMEOUT1;
+ } else if (mResponseTimes < LONG_PRESS_KEYNUM2) {
+ timeout = LONG_PRESS_TIMEOUT2;
+ } else {
+ timeout = LONG_PRESS_TIMEOUT3;
+ }
+ postAtTime(this, SystemClock.uptimeMillis() + timeout);
+ }
+ } else {
+ if (1 == mResponseTimes) {
+ popupSymbols();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/SkbPool.java b/src/com/android/inputmethod/pinyin/SkbPool.java
new file mode 100644
index 0000000..4c46951
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/SkbPool.java
@@ -0,0 +1,88 @@
+/*
+ * 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 java.util.Vector;
+
+import android.content.Context;
+
+/**
+ * Class used to cache previously loaded soft keyboard layouts.
+ */
+public class SkbPool {
+ private static SkbPool mInstance = null;
+
+ private Vector<SkbTemplate> mSkbTemplates = new Vector<SkbTemplate>();
+ private Vector<SoftKeyboard> mSoftKeyboards = new Vector<SoftKeyboard>();
+
+ private SkbPool() {
+ }
+
+ public static SkbPool getInstance() {
+ if (null == mInstance) mInstance = new SkbPool();
+ return mInstance;
+ }
+
+ public void resetCachedSkb() {
+ mSoftKeyboards.clear();
+ }
+
+ public SkbTemplate getSkbTemplate(int skbTemplateId, Context context) {
+ for (int i = 0; i < mSkbTemplates.size(); i++) {
+ SkbTemplate t = mSkbTemplates.elementAt(i);
+ if (t.getSkbTemplateId() == skbTemplateId) {
+ return t;
+ }
+ }
+
+ if (null != context) {
+ XmlKeyboardLoader xkbl = new XmlKeyboardLoader(context);
+ SkbTemplate t = xkbl.loadSkbTemplate(skbTemplateId);
+ if (null != t) {
+ mSkbTemplates.add(t);
+ return t;
+ }
+ }
+ return null;
+ }
+
+ // Try to find the keyboard in the pool with the cache id. If there is no
+ // keyboard found, try to load it with the given xml id.
+ public SoftKeyboard getSoftKeyboard(int skbCacheId, int skbXmlId,
+ int skbWidth, int skbHeight, Context context) {
+ for (int i = 0; i < mSoftKeyboards.size(); i++) {
+ SoftKeyboard skb = mSoftKeyboards.elementAt(i);
+ if (skb.getCacheId() == skbCacheId && skb.getSkbXmlId() == skbXmlId) {
+ skb.setSkbCoreSize(skbWidth, skbHeight);
+ skb.setNewlyLoadedFlag(false);
+ return skb;
+ }
+ }
+ if (null != context) {
+ XmlKeyboardLoader xkbl = new XmlKeyboardLoader(context);
+ SoftKeyboard skb = xkbl.loadKeyboard(skbXmlId, skbWidth, skbHeight);
+ if (skb != null) {
+ if (skb.getCacheFlag()) {
+ skb.setCacheId(skbCacheId);
+ mSoftKeyboards.add(skb);
+ }
+ }
+ return skb;
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/SkbTemplate.java b/src/com/android/inputmethod/pinyin/SkbTemplate.java
new file mode 100644
index 0000000..9ab53ff
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/SkbTemplate.java
@@ -0,0 +1,233 @@
+/*
+ * 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.graphics.drawable.Drawable;
+
+import java.util.Vector;
+
+/**
+ * Key icon definition. It is defined in soft keyboard template. A soft keyboard
+ * can refer to such an icon in its xml file directly to improve performance.
+ */
+class KeyIconRecord {
+ int keyCode;
+ Drawable icon;
+ Drawable iconPopup;
+}
+
+
+/**
+ * Default definition for a certain key. It is defined in soft keyboard
+ * template. A soft keyboard can refer to a default key in its xml file. Nothing
+ * of the key can be overwritten, including the size.
+ */
+class KeyRecord {
+ int keyId;
+ SoftKey softKey;
+}
+
+
+/**
+ * Soft keyboard template used by soft keyboards to share common resources. In
+ * this way, memory cost is reduced.
+ */
+public class SkbTemplate {
+ private int mSkbTemplateId;
+ private Drawable mSkbBg;
+ private Drawable mBalloonBg;
+ private Drawable mPopupBg;
+ private float mXMargin = 0;
+ private float mYMargin = 0;
+ /** Key type list. */
+ private Vector<SoftKeyType> mKeyTypeList = new Vector<SoftKeyType>();
+
+ /**
+ * Default key icon list. It is only for keys which do not have popup icons.
+ */
+ private Vector<KeyIconRecord> mKeyIconRecords = new Vector<KeyIconRecord>();
+
+ /**
+ * Default key list.
+ */
+ private Vector<KeyRecord> mKeyRecords = new Vector<KeyRecord>();
+
+ public SkbTemplate(int skbTemplateId) {
+ mSkbTemplateId = skbTemplateId;
+ }
+
+ public int getSkbTemplateId() {
+ return mSkbTemplateId;
+ }
+
+ public void setBackgrounds(Drawable skbBg, Drawable balloonBg,
+ Drawable popupBg) {
+ mSkbBg = skbBg;
+ mBalloonBg = balloonBg;
+ mPopupBg = popupBg;
+ }
+
+ public Drawable getSkbBackground() {
+ return mSkbBg;
+ }
+
+ public Drawable getBalloonBackground() {
+ return mBalloonBg;
+ }
+
+ public Drawable getPopupBackground() {
+ return mPopupBg;
+ }
+
+ public void setMargins(float xMargin, float yMargin) {
+ mXMargin = xMargin;
+ mYMargin = yMargin;
+ }
+
+ public float getXMargin() {
+ return mXMargin;
+ }
+
+ public float getYMargin() {
+ return mYMargin;
+ }
+
+ public SoftKeyType createKeyType(int id, Drawable bg, Drawable hlBg) {
+ return new SoftKeyType(id, bg, hlBg);
+ }
+
+ public boolean addKeyType(SoftKeyType keyType) {
+ // The newly added item should have the right id.
+ if (mKeyTypeList.size() != keyType.mKeyTypeId) return false;
+ mKeyTypeList.add(keyType);
+ return true;
+ }
+
+ public SoftKeyType getKeyType(int typeId) {
+ if (typeId < 0 || typeId > mKeyTypeList.size()) return null;
+ return mKeyTypeList.elementAt(typeId);
+ }
+
+ public void addDefaultKeyIcons(int keyCode, Drawable icon,
+ Drawable iconPopup) {
+ if (null == icon || null == iconPopup) return;
+
+ KeyIconRecord iconRecord = new KeyIconRecord();
+ iconRecord.icon = icon;
+ iconRecord.iconPopup = iconPopup;
+ iconRecord.keyCode = keyCode;
+
+ int size = mKeyIconRecords.size();
+ int pos = 0;
+ while (pos < size) {
+ if (mKeyIconRecords.get(pos).keyCode >= keyCode) break;
+ pos++;
+ }
+ mKeyIconRecords.add(pos, iconRecord);
+ }
+
+ public Drawable getDefaultKeyIcon(int keyCode) {
+ int size = mKeyIconRecords.size();
+ int pos = 0;
+ while (pos < size) {
+ KeyIconRecord iconRecord = mKeyIconRecords.get(pos);
+ if (iconRecord.keyCode < keyCode) {
+ pos++;
+ continue;
+ }
+ if (iconRecord.keyCode == keyCode) {
+ return iconRecord.icon;
+ }
+ return null;
+ }
+ return null;
+ }
+
+ public Drawable getDefaultKeyIconPopup(int keyCode) {
+ int size = mKeyIconRecords.size();
+ int pos = 0;
+ while (pos < size) {
+ KeyIconRecord iconRecord = mKeyIconRecords.get(pos);
+ if (iconRecord.keyCode < keyCode) {
+ pos++;
+ continue;
+ }
+ if (iconRecord.keyCode == keyCode) {
+ return iconRecord.iconPopup;
+ }
+ return null;
+ }
+ return null;
+ }
+
+ public void addDefaultKey(int keyId, SoftKey softKey) {
+ if (null == softKey) return;
+
+ KeyRecord keyRecord = new KeyRecord();
+ keyRecord.keyId = keyId;
+ keyRecord.softKey = softKey;
+
+ int size = mKeyRecords.size();
+ int pos = 0;
+ while (pos < size) {
+ if (mKeyRecords.get(pos).keyId >= keyId) break;
+ pos++;
+ }
+ mKeyRecords.add(pos, keyRecord);
+ }
+
+ public SoftKey getDefaultKey(int keyId) {
+ int size = mKeyRecords.size();
+ int pos = 0;
+ while (pos < size) {
+ KeyRecord keyRecord = mKeyRecords.get(pos);
+ if (keyRecord.keyId < keyId) {
+ pos++;
+ continue;
+ }
+ if (keyRecord.keyId == keyId) {
+ return keyRecord.softKey;
+ }
+ return null;
+ }
+ return null;
+ }
+}
+
+
+class SoftKeyType {
+ public static final int KEYTYPE_ID_NORMAL_KEY = 0;
+
+ public int mKeyTypeId;
+ public Drawable mKeyBg;
+ public Drawable mKeyHlBg;
+ public int mColor;
+ public int mColorHl;
+ public int mColorBalloon;
+
+ SoftKeyType(int id, Drawable bg, Drawable hlBg) {
+ mKeyTypeId = id;
+ mKeyBg = bg;
+ mKeyHlBg = hlBg;
+ }
+
+ public void setColors(int color, int colorHl, int colorBalloon) {
+ mColor = color;
+ mColorHl = colorHl;
+ mColorBalloon = colorBalloon;
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/SoftKey.java b/src/com/android/inputmethod/pinyin/SoftKey.java
new file mode 100644
index 0000000..67eaf29
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/SoftKey.java
@@ -0,0 +1,230 @@
+/*
+ * 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.graphics.drawable.Drawable;
+
+/**
+ * Class for soft keys which defined in the keyboard xml file. A soft key can be
+ * a basic key or a toggling key.
+ *
+ * @see com.android.inputmethod.pinyin.SoftKeyToggle
+ */
+public class SoftKey {
+ protected static final int KEYMASK_REPEAT = 0x10000000;
+ protected static final int KEYMASK_BALLOON = 0x20000000;
+
+ /**
+ * For a finger touch device, after user presses a key, there will be some
+ * consequent moving events because of the changing in touching pressure. If
+ * the moving distance in x is within this threshold, the moving events will
+ * be ignored.
+ */
+ public static final int MAX_MOVE_TOLERANCE_X = 0;
+
+ /**
+ * For a finger touch device, after user presses a key, there will be some
+ * consequent moving events because of the changing in touching pressure. If
+ * the moving distance in y is within this threshold, the moving events will
+ * be ignored.
+ */
+ public static final int MAX_MOVE_TOLERANCE_Y = 0;
+
+ /**
+ * Used to indicate the type and attributes of this key. the lowest 8 bits
+ * should be reserved for SoftkeyToggle.
+ */
+ protected int mKeyMask;
+
+ protected SoftKeyType mKeyType;
+
+ protected Drawable mKeyIcon;
+
+ protected Drawable mKeyIconPopup;
+
+ protected String mKeyLabel;
+
+ protected int mKeyCode;
+
+ /**
+ * If this value is not 0, this key can be used to popup a sub soft keyboard
+ * when user presses it for some time.
+ */
+ public int mPopupSkbId;
+
+ public float mLeftF;
+ public float mRightF;
+ public float mTopF;
+ public float mBottomF;
+ public int mLeft;
+ public int mRight;
+ public int mTop;
+ public int mBottom;
+
+ public void setKeyType(SoftKeyType keyType, Drawable keyIcon,
+ Drawable keyIconPopup) {
+ mKeyType = keyType;
+ mKeyIcon = keyIcon;
+ mKeyIconPopup = keyIconPopup;
+ }
+
+ // The caller guarantees that all parameters are in [0, 1]
+ public void setKeyDimensions(float left, float top, float right,
+ float bottom) {
+ mLeftF = left;
+ mTopF = top;
+ mRightF = right;
+ mBottomF = bottom;
+ }
+
+ public void setKeyAttribute(int keyCode, String label, boolean repeat,
+ boolean balloon) {
+ mKeyCode = keyCode;
+ mKeyLabel = label;
+
+ if (repeat) {
+ mKeyMask |= KEYMASK_REPEAT;
+ } else {
+ mKeyMask &= (~KEYMASK_REPEAT);
+ }
+
+ if (balloon) {
+ mKeyMask |= KEYMASK_BALLOON;
+ } else {
+ mKeyMask &= (~KEYMASK_BALLOON);
+ }
+ }
+
+ public void setPopupSkbId(int popupSkbId) {
+ mPopupSkbId = popupSkbId;
+ }
+
+ // Call after setKeyDimensions(). The caller guarantees that the
+ // keyboard with and height are valid.
+ public void setSkbCoreSize(int skbWidth, int skbHeight) {
+ mLeft = (int) (mLeftF * skbWidth);
+ mRight = (int) (mRightF * skbWidth);
+ mTop = (int) (mTopF * skbHeight);
+ mBottom = (int) (mBottomF * skbHeight);
+ }
+
+ public Drawable getKeyIcon() {
+ return mKeyIcon;
+ }
+
+ public Drawable getKeyIconPopup() {
+ if (null != mKeyIconPopup) {
+ return mKeyIconPopup;
+ }
+ return mKeyIcon;
+ }
+
+ public int getKeyCode() {
+ return mKeyCode;
+ }
+
+ public String getKeyLabel() {
+ return mKeyLabel;
+ }
+
+ public void changeCase(boolean upperCase) {
+ if (null != mKeyLabel) {
+ if (upperCase)
+ mKeyLabel = mKeyLabel.toUpperCase();
+ else
+ mKeyLabel = mKeyLabel.toLowerCase();
+ }
+ }
+
+ public Drawable getKeyBg() {
+ return mKeyType.mKeyBg;
+ }
+
+ public Drawable getKeyHlBg() {
+ return mKeyType.mKeyHlBg;
+ }
+
+ public int getColor() {
+ return mKeyType.mColor;
+ }
+
+ public int getColorHl() {
+ return mKeyType.mColorHl;
+ }
+
+ public int getColorBalloon() {
+ return mKeyType.mColorBalloon;
+ }
+
+ public boolean isKeyCodeKey() {
+ if (mKeyCode > 0) return true;
+ return false;
+ }
+
+ public boolean isUserDefKey() {
+ if (mKeyCode < 0) return true;
+ return false;
+ }
+
+ public boolean isUniStrKey() {
+ if (null != mKeyLabel && mKeyCode == 0) return true;
+ return false;
+ }
+
+ public boolean needBalloon() {
+ return (mKeyMask & KEYMASK_BALLOON) != 0;
+ }
+
+ public boolean repeatable() {
+ return (mKeyMask & KEYMASK_REPEAT) != 0;
+ }
+
+ public int getPopupResId() {
+ return mPopupSkbId;
+ }
+
+ public int width() {
+ return mRight - mLeft;
+ }
+
+ public int height() {
+ return mBottom - mTop;
+ }
+
+ public boolean moveWithinKey(int x, int y) {
+ if (mLeft - MAX_MOVE_TOLERANCE_X <= x
+ && mTop - MAX_MOVE_TOLERANCE_Y <= y
+ && mRight + MAX_MOVE_TOLERANCE_X > x
+ && mBottom + MAX_MOVE_TOLERANCE_Y > y) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ String str = "\n";
+ str += " keyCode: " + String.valueOf(mKeyCode) + "\n";
+ str += " keyMask: " + String.valueOf(mKeyMask) + "\n";
+ str += " keyLabel: " + (mKeyLabel == null ? "null" : mKeyLabel) + "\n";
+ str += " popupResId: " + String.valueOf(mPopupSkbId) + "\n";
+ str += " Position: " + String.valueOf(mLeftF) + ", "
+ + String.valueOf(mTopF) + ", " + String.valueOf(mRightF) + ", "
+ + String.valueOf(mBottomF) + "\n";
+ return str;
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/SoftKeyToggle.java b/src/com/android/inputmethod/pinyin/SoftKeyToggle.java
new file mode 100644
index 0000000..89ff2fe
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/SoftKeyToggle.java
@@ -0,0 +1,283 @@
+/*
+ * 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.graphics.drawable.Drawable;
+
+/**
+ * Class for soft keys which defined in the keyboard xml file. A soft key can be
+ * a basic key or a toggling key.
+ *
+ * @see com.android.inputmethod.pinyin.SoftKey
+ */
+public class SoftKeyToggle extends SoftKey {
+ /**
+ * The current state number is stored in the lowest 8 bits of mKeyMask, this
+ * mask is used to get the state number. If the current state is 0, the
+ * normal state is enabled; if the current state is more than 0, a toggle
+ * state in the toggle state chain will be enabled.
+ */
+ private static final int KEYMASK_TOGGLE_STATE = 0x000000ff;
+
+ private ToggleState mToggleState;
+
+ public int getToggleStateId() {
+ return (mKeyMask & KEYMASK_TOGGLE_STATE);
+ }
+
+ // The state id should be valid, and less than 255.
+ // If resetIfNotFound is true and there is no such toggle state with the
+ // given id, the key state will be reset.
+ // If the key state is newly changed (enabled to the given state, or
+ // reseted) and needs re-draw, return true.
+ public boolean enableToggleState(int stateId, boolean resetIfNotFound) {
+ int oldStateId = (mKeyMask & KEYMASK_TOGGLE_STATE);
+ if (oldStateId == stateId) return false;
+
+ mKeyMask &= (~KEYMASK_TOGGLE_STATE);
+ if (stateId > 0) {
+ mKeyMask |= (KEYMASK_TOGGLE_STATE & stateId);
+ if (getToggleState() == null) {
+ mKeyMask &= (~KEYMASK_TOGGLE_STATE);
+ if (!resetIfNotFound && oldStateId > 0) {
+ mKeyMask |= (KEYMASK_TOGGLE_STATE & oldStateId);
+ }
+ return resetIfNotFound;
+ } else {
+ return true;
+ }
+ } else {
+ return true;
+ }
+ }
+
+ // The state id should be valid, and less than 255.
+ // If resetIfNotFound is true and there is no such toggle state with the
+ // given id, the key state will be reset.
+ // If the key state is newly changed and needs re-draw, return true.
+ public boolean disableToggleState(int stateId, boolean resetIfNotFound) {
+ int oldStateId = (mKeyMask & KEYMASK_TOGGLE_STATE);
+ if (oldStateId == stateId) {
+ mKeyMask &= (~KEYMASK_TOGGLE_STATE);
+ return stateId != 0;
+ }
+
+ if (resetIfNotFound) {
+ mKeyMask &= (~KEYMASK_TOGGLE_STATE);
+ return oldStateId != 0;
+ }
+ return false;
+ }
+
+ // Clear any toggle state. If the key needs re-draw, return true.
+ public boolean disableAllToggleStates() {
+ int oldStateId = (mKeyMask & KEYMASK_TOGGLE_STATE);
+ mKeyMask &= (~KEYMASK_TOGGLE_STATE);
+ return oldStateId != 0;
+ }
+
+ @Override
+ public Drawable getKeyIcon() {
+ ToggleState state = getToggleState();
+ if (null != state) return state.mKeyIcon;
+ return super.getKeyIcon();
+ }
+
+ @Override
+ public Drawable getKeyIconPopup() {
+ ToggleState state = getToggleState();
+ if (null != state) {
+ if (null != state.mKeyIconPopup) {
+ return state.mKeyIconPopup;
+ } else {
+ return state.mKeyIcon;
+ }
+ }
+ return super.getKeyIconPopup();
+ }
+
+ @Override
+ public int getKeyCode() {
+ ToggleState state = getToggleState();
+ if (null != state) return state.mKeyCode;
+ return mKeyCode;
+ }
+
+ @Override
+ public String getKeyLabel() {
+ ToggleState state = getToggleState();
+ if (null != state) return state.mKeyLabel;
+ return mKeyLabel;
+ }
+
+ @Override
+ public Drawable getKeyBg() {
+ ToggleState state = getToggleState();
+ if (null != state && null != state.mKeyType) {
+ return state.mKeyType.mKeyBg;
+ }
+ return mKeyType.mKeyBg;
+ }
+
+ @Override
+ public Drawable getKeyHlBg() {
+ ToggleState state = getToggleState();
+ if (null != state && null != state.mKeyType) {
+ return state.mKeyType.mKeyHlBg;
+ }
+ return mKeyType.mKeyHlBg;
+ }
+
+ @Override
+ public int getColor() {
+ ToggleState state = getToggleState();
+ if (null != state && null != state.mKeyType) {
+ return state.mKeyType.mColor;
+ }
+ return mKeyType.mColor;
+ }
+
+ @Override
+ public int getColorHl() {
+ ToggleState state = getToggleState();
+ if (null != state && null != state.mKeyType) {
+ return state.mKeyType.mColorHl;
+ }
+ return mKeyType.mColorHl;
+ }
+
+ @Override
+ public int getColorBalloon() {
+ ToggleState state = getToggleState();
+ if (null != state && null != state.mKeyType) {
+ return state.mKeyType.mColorBalloon;
+ }
+ return mKeyType.mColorBalloon;
+ }
+
+ @Override
+ public boolean isKeyCodeKey() {
+ ToggleState state = getToggleState();
+ if (null != state) {
+ if (state.mKeyCode > 0) return true;
+ return false;
+ }
+ return super.isKeyCodeKey();
+ }
+
+ @Override
+ public boolean isUserDefKey() {
+ ToggleState state = getToggleState();
+ if (null != state) {
+ if (state.mKeyCode < 0) return true;
+ return false;
+ }
+ return super.isUserDefKey();
+ }
+
+ @Override
+ public boolean isUniStrKey() {
+ ToggleState state = getToggleState();
+ if (null != state) {
+ if (null != state.mKeyLabel && state.mKeyCode == 0) {
+ return true;
+ }
+ return false;
+ }
+ return super.isUniStrKey();
+ }
+
+ @Override
+ public boolean needBalloon() {
+ ToggleState state = getToggleState();
+ if (null != state) {
+ return (state.mIdAndFlags & KEYMASK_BALLOON) != 0;
+ }
+ return super.needBalloon();
+ }
+
+ @Override
+ public boolean repeatable() {
+ ToggleState state = getToggleState();
+ if (null != state) {
+ return (state.mIdAndFlags & KEYMASK_REPEAT) != 0;
+ }
+ return super.repeatable();
+ }
+
+ @Override
+ public void changeCase(boolean lowerCase) {
+ ToggleState state = getToggleState();
+ if (null != state && null != state.mKeyLabel) {
+ if (lowerCase)
+ state.mKeyLabel = state.mKeyLabel.toLowerCase();
+ else
+ state.mKeyLabel = state.mKeyLabel.toUpperCase();
+ }
+ }
+
+ public ToggleState createToggleState() {
+ return new ToggleState();
+ }
+
+ public boolean setToggleStates(ToggleState rootState) {
+ if (null == rootState) return false;
+ mToggleState = rootState;
+ return true;
+ }
+
+ private ToggleState getToggleState() {
+ int stateId = (mKeyMask & KEYMASK_TOGGLE_STATE);
+ if (0 == stateId) return null;
+
+ ToggleState state = mToggleState;
+ while ((null != state)
+ && (state.mIdAndFlags & KEYMASK_TOGGLE_STATE) != stateId) {
+ state = state.mNextState;
+ }
+ return state;
+ }
+
+ public class ToggleState {
+ // The id should be bigger than 0;
+ private int mIdAndFlags;
+ public SoftKeyType mKeyType;
+ public int mKeyCode;
+ public Drawable mKeyIcon;
+ public Drawable mKeyIconPopup;
+ public String mKeyLabel;
+ public ToggleState mNextState;
+
+ public void setStateId(int stateId) {
+ mIdAndFlags |= (stateId & KEYMASK_TOGGLE_STATE);
+ }
+
+ public void setStateFlags(boolean repeat, boolean balloon) {
+ if (repeat) {
+ mIdAndFlags |= KEYMASK_REPEAT;
+ } else {
+ mIdAndFlags &= (~KEYMASK_REPEAT);
+ }
+
+ if (balloon) {
+ mIdAndFlags |= KEYMASK_BALLOON;
+ } else {
+ mIdAndFlags &= (~KEYMASK_BALLOON);
+ }
+ }
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/SoftKeyboard.java b/src/com/android/inputmethod/pinyin/SoftKeyboard.java
new file mode 100644
index 0000000..b8cc504
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/SoftKeyboard.java
@@ -0,0 +1,520 @@
+/*
+ * 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 com.android.inputmethod.pinyin.InputModeSwitcher.ToggleStates;
+
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.KeyEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class used to represent a soft keyboard definition, including the height, the
+ * background image, the image for high light, the keys, etc.
+ */
+public class SoftKeyboard {
+ /** The XML resource id for this soft keyboard. */
+ private int mSkbXmlId;
+
+ /** Do we need to cache this soft keyboard? */
+ private boolean mCacheFlag;
+
+ /**
+ * After user switches to this soft keyboard, if this flag is true, this
+ * soft keyboard will be kept unless explicit switching operation is
+ * performed, otherwise IME will switch back to the previous keyboard layout
+ * whenever user clicks on any none-function key.
+ **/
+ private boolean mStickyFlag;
+
+ /**
+ * The cache id for this soft keyboard. It is used to identify it in the
+ * soft keyboard pool.
+ */
+ private int mCacheId;
+
+ /**
+ * Used to indicate whether this soft keyboard is newly loaded from an XML
+ * file or is just gotten from the soft keyboard pool.
+ */
+ private boolean mNewlyLoadedFlag = true;
+
+ /** The width of the soft keyboard. */
+ private int mSkbCoreWidth;
+
+ /** The height of the soft keyboard. */
+ private int mSkbCoreHeight;
+
+ /** The soft keyboard template for this soft keyboard. */
+ private SkbTemplate mSkbTemplate;
+
+ /** Used to indicate whether this soft keyboard is a QWERTY keyboard. */
+ private boolean mIsQwerty;
+
+ /**
+ * When {@link #mIsQwerty} is true, this member is Used to indicate that the
+ * soft keyboard should be displayed in uppercase.
+ */
+ private boolean mIsQwertyUpperCase;
+
+ /**
+ * The id of the rows which are enabled. Rows with id
+ * {@link KeyRow#ALWAYS_SHOW_ROW_ID} are always enabled.
+ */
+ private int mEnabledRowId;
+
+ /**
+ * Rows in this soft keyboard. Each row has a id. Only matched rows will be
+ * enabled.
+ */
+ private List<KeyRow> mKeyRows;
+
+ /**
+ * Background of the soft keyboard. If it is null, the one in the soft
+ * keyboard template will be used.
+ **/
+ public Drawable mSkbBg;
+
+ /**
+ * Background for key balloon. If it is null, the one in the soft keyboard
+ * template will be used.
+ **/
+ private Drawable mBalloonBg;
+
+ /**
+ * Background for popup mini soft keyboard. If it is null, the one in the
+ * soft keyboard template will be used.
+ **/
+ private Drawable mPopupBg;
+
+ /** The left and right margin of a key. */
+ private float mKeyXMargin = 0;
+
+ /** The top and bottom margin of a key. */
+ private float mKeyYMargin = 0;
+
+ private Rect mTmpRect = new Rect();
+
+ public SoftKeyboard(int skbXmlId, SkbTemplate skbTemplate, int skbWidth,
+ int skbHeight) {
+ mSkbXmlId = skbXmlId;
+ mSkbTemplate = skbTemplate;
+ mSkbCoreWidth = skbWidth;
+ mSkbCoreHeight = skbHeight;
+ }
+
+ public void setFlags(boolean cacheFlag, boolean stickyFlag,
+ boolean isQwerty, boolean isQwertyUpperCase) {
+ mCacheFlag = cacheFlag;
+ mStickyFlag = stickyFlag;
+ mIsQwerty = isQwerty;
+ mIsQwertyUpperCase = isQwertyUpperCase;
+ }
+
+ public boolean getCacheFlag() {
+ return mCacheFlag;
+ }
+
+ public void setCacheId(int cacheId) {
+ mCacheId = cacheId;
+ }
+
+ public boolean getStickyFlag() {
+ return mStickyFlag;
+ }
+
+ public void setSkbBackground(Drawable skbBg) {
+ mSkbBg = skbBg;
+ }
+
+ public void setPopupBackground(Drawable popupBg) {
+ mPopupBg = popupBg;
+ }
+
+ public void setKeyBalloonBackground(Drawable balloonBg) {
+ mBalloonBg = balloonBg;
+ }
+
+ public void setKeyMargins(float xMargin, float yMargin) {
+ mKeyXMargin = xMargin;
+ mKeyYMargin = yMargin;
+ }
+
+ public int getCacheId() {
+ return mCacheId;
+ }
+
+ public void reset() {
+ if (null != mKeyRows) mKeyRows.clear();
+ }
+
+ public void setNewlyLoadedFlag(boolean newlyLoadedFlag) {
+ mNewlyLoadedFlag = newlyLoadedFlag;
+ }
+
+ public boolean getNewlyLoadedFlag() {
+ return mNewlyLoadedFlag;
+ }
+
+ public void beginNewRow(int rowId, float yStartingPos) {
+ if (null == mKeyRows) mKeyRows = new ArrayList<KeyRow>();
+ KeyRow keyRow = new KeyRow();
+ keyRow.mRowId = rowId;
+ keyRow.mTopF = yStartingPos;
+ keyRow.mBottomF = yStartingPos;
+ keyRow.mSoftKeys = new ArrayList<SoftKey>();
+ mKeyRows.add(keyRow);
+ }
+
+ public boolean addSoftKey(SoftKey softKey) {
+ if (mKeyRows.size() == 0) return false;
+ KeyRow keyRow = mKeyRows.get(mKeyRows.size() - 1);
+ if (null == keyRow) return false;
+ List<SoftKey> softKeys = keyRow.mSoftKeys;
+
+ softKey.setSkbCoreSize(mSkbCoreWidth, mSkbCoreHeight);
+ softKeys.add(softKey);
+ if (softKey.mTopF < keyRow.mTopF) {
+ keyRow.mTopF = softKey.mTopF;
+ }
+ if (softKey.mBottomF > keyRow.mBottomF) {
+ keyRow.mBottomF = softKey.mBottomF;
+ }
+ return true;
+ }
+
+ public int getSkbXmlId() {
+ return mSkbXmlId;
+ }
+
+ // Set the size of the soft keyboard core. In other words, the background's
+ // padding are not counted.
+ public void setSkbCoreSize(int skbCoreWidth, int skbCoreHeight) {
+ if (null == mKeyRows
+ || (skbCoreWidth == mSkbCoreWidth && skbCoreHeight == mSkbCoreHeight)) {
+ return;
+ }
+ for (int row = 0; row < mKeyRows.size(); row++) {
+ KeyRow keyRow = mKeyRows.get(row);
+ keyRow.mBottom = (int) (skbCoreHeight * keyRow.mBottomF);
+ keyRow.mTop = (int) (skbCoreHeight * keyRow.mTopF);
+
+ List<SoftKey> softKeys = keyRow.mSoftKeys;
+ for (int i = 0; i < softKeys.size(); i++) {
+ SoftKey softKey = softKeys.get(i);
+ softKey.setSkbCoreSize(skbCoreWidth, skbCoreHeight);
+ }
+ }
+ mSkbCoreWidth = skbCoreWidth;
+ mSkbCoreHeight = skbCoreHeight;
+ }
+
+ public int getSkbCoreWidth() {
+ return mSkbCoreWidth;
+ }
+
+ public int getSkbCoreHeight() {
+ return mSkbCoreHeight;
+ }
+
+ public int getSkbTotalWidth() {
+ Rect padding = getPadding();
+ return mSkbCoreWidth + padding.left + padding.right;
+ }
+
+ public int getSkbTotalHeight() {
+ Rect padding = getPadding();
+ return mSkbCoreHeight + padding.top + padding.bottom;
+ }
+
+ public int getKeyXMargin() {
+ Environment env = Environment.getInstance();
+ return (int) (mKeyXMargin * mSkbCoreWidth * env.getKeyXMarginFactor());
+ }
+
+ public int getKeyYMargin() {
+ Environment env = Environment.getInstance();
+ return (int) (mKeyYMargin * mSkbCoreHeight * env.getKeyYMarginFactor());
+ }
+
+ public Drawable getSkbBackground() {
+ if (null != mSkbBg) return mSkbBg;
+ return mSkbTemplate.getSkbBackground();
+ }
+
+ public Drawable getBalloonBackground() {
+ if (null != mBalloonBg) return mBalloonBg;
+ return mSkbTemplate.getBalloonBackground();
+ }
+
+ public Drawable getPopupBackground() {
+ if (null != mPopupBg) return mPopupBg;
+ return mSkbTemplate.getPopupBackground();
+ }
+
+ public int getRowNum() {
+ if (null != mKeyRows) {
+ return mKeyRows.size();
+ }
+ return 0;
+ }
+
+ public KeyRow getKeyRowForDisplay(int row) {
+ if (null != mKeyRows && mKeyRows.size() > row) {
+ KeyRow keyRow = mKeyRows.get(row);
+ if (KeyRow.ALWAYS_SHOW_ROW_ID == keyRow.mRowId
+ || keyRow.mRowId == mEnabledRowId) {
+ return keyRow;
+ }
+ }
+ return null;
+ }
+
+ public SoftKey getKey(int row, int location) {
+ if (null != mKeyRows && mKeyRows.size() > row) {
+ List<SoftKey> softKeys = mKeyRows.get(row).mSoftKeys;
+ if (softKeys.size() > location) {
+ return softKeys.get(location);
+ }
+ }
+ return null;
+ }
+
+ public SoftKey mapToKey(int x, int y) {
+ if (null == mKeyRows) {
+ return null;
+ }
+ // If the position is inside the rectangle of a certain key, return that
+ // key.
+ int rowNum = mKeyRows.size();
+ for (int row = 0; row < rowNum; row++) {
+ KeyRow keyRow = mKeyRows.get(row);
+ if (KeyRow.ALWAYS_SHOW_ROW_ID != keyRow.mRowId
+ && keyRow.mRowId != mEnabledRowId) continue;
+ if (keyRow.mTop > y && keyRow.mBottom <= y) continue;
+
+ List<SoftKey> softKeys = keyRow.mSoftKeys;
+ int keyNum = softKeys.size();
+ for (int i = 0; i < keyNum; i++) {
+ SoftKey sKey = softKeys.get(i);
+ if (sKey.mLeft <= x && sKey.mTop <= y && sKey.mRight > x
+ && sKey.mBottom > y) {
+ return sKey;
+ }
+ }
+ }
+
+ // If the position is outside the rectangles of all keys, find the
+ // nearest one.
+ SoftKey nearestKey = null;
+ float nearestDis = Float.MAX_VALUE;
+ for (int row = 0; row < rowNum; row++) {
+ KeyRow keyRow = mKeyRows.get(row);
+ if (KeyRow.ALWAYS_SHOW_ROW_ID != keyRow.mRowId
+ && keyRow.mRowId != mEnabledRowId) continue;
+ if (keyRow.mTop > y && keyRow.mBottom <= y) continue;
+
+ List<SoftKey> softKeys = keyRow.mSoftKeys;
+ int keyNum = softKeys.size();
+ for (int i = 0; i < keyNum; i++) {
+ SoftKey sKey = softKeys.get(i);
+ int disx = (sKey.mLeft + sKey.mRight) / 2 - x;
+ int disy = (sKey.mTop + sKey.mBottom) / 2 - y;
+ float dis = disx * disx + disy * disy;
+ if (dis < nearestDis) {
+ nearestDis = dis;
+ nearestKey = sKey;
+ }
+ }
+ }
+ return nearestKey;
+ }
+
+ public void switchQwertyMode(int toggle_state_id, boolean upperCase) {
+ if (!mIsQwerty) return;
+
+ int rowNum = mKeyRows.size();
+ for (int row = 0; row < rowNum; row++) {
+ KeyRow keyRow = mKeyRows.get(row);
+ List<SoftKey> softKeys = keyRow.mSoftKeys;
+ int keyNum = softKeys.size();
+ for (int i = 0; i < keyNum; i++) {
+ SoftKey sKey = softKeys.get(i);
+ if (sKey instanceof SoftKeyToggle) {
+ ((SoftKeyToggle) sKey).enableToggleState(toggle_state_id,
+ true);
+ }
+ if (sKey.mKeyCode >= KeyEvent.KEYCODE_A
+ && sKey.mKeyCode <= KeyEvent.KEYCODE_Z) {
+ sKey.changeCase(upperCase);
+ }
+ }
+ }
+ }
+
+ public void enableToggleState(int toggleStateId, boolean resetIfNotFound) {
+ int rowNum = mKeyRows.size();
+ for (int row = 0; row < rowNum; row++) {
+ KeyRow keyRow = mKeyRows.get(row);
+ List<SoftKey> softKeys = keyRow.mSoftKeys;
+ int keyNum = softKeys.size();
+ for (int i = 0; i < keyNum; i++) {
+ SoftKey sKey = softKeys.get(i);
+ if (sKey instanceof SoftKeyToggle) {
+ ((SoftKeyToggle) sKey).enableToggleState(toggleStateId,
+ resetIfNotFound);
+ }
+ }
+ }
+ }
+
+ public void disableToggleState(int toggleStateId, boolean resetIfNotFound) {
+ int rowNum = mKeyRows.size();
+ for (int row = 0; row < rowNum; row++) {
+ KeyRow keyRow = mKeyRows.get(row);
+ List<SoftKey> softKeys = keyRow.mSoftKeys;
+ int keyNum = softKeys.size();
+ for (int i = 0; i < keyNum; i++) {
+ SoftKey sKey = softKeys.get(i);
+ if (sKey instanceof SoftKeyToggle) {
+ ((SoftKeyToggle) sKey).disableToggleState(toggleStateId,
+ resetIfNotFound);
+ }
+ }
+ }
+ }
+
+ public void enableToggleStates(ToggleStates toggleStates) {
+ if (null == toggleStates) return;
+
+ enableRow(toggleStates.mRowIdToEnable);
+
+ boolean isQwerty = toggleStates.mQwerty;
+ boolean isQwertyUpperCase = toggleStates.mQwertyUpperCase;
+ boolean needUpdateQwerty = (isQwerty && mIsQwerty && (mIsQwertyUpperCase != isQwertyUpperCase));
+ int states[] = toggleStates.mKeyStates;
+ int statesNum = toggleStates.mKeyStatesNum;
+
+ int rowNum = mKeyRows.size();
+ for (int row = 0; row < rowNum; row++) {
+ KeyRow keyRow = mKeyRows.get(row);
+ if (KeyRow.ALWAYS_SHOW_ROW_ID != keyRow.mRowId
+ && keyRow.mRowId != mEnabledRowId) {
+ continue;
+ }
+ List<SoftKey> softKeys = keyRow.mSoftKeys;
+ int keyNum = softKeys.size();
+ for (int keyPos = 0; keyPos < keyNum; keyPos++) {
+ SoftKey sKey = softKeys.get(keyPos);
+ if (sKey instanceof SoftKeyToggle) {
+ for (int statePos = 0; statePos < statesNum; statePos++) {
+ ((SoftKeyToggle) sKey).enableToggleState(
+ states[statePos], statePos == 0);
+ }
+ if (0 == statesNum) {
+ ((SoftKeyToggle) sKey).disableAllToggleStates();
+ }
+ }
+ if (needUpdateQwerty) {
+ if (sKey.mKeyCode >= KeyEvent.KEYCODE_A
+ && sKey.mKeyCode <= KeyEvent.KEYCODE_Z) {
+ sKey.changeCase(isQwertyUpperCase);
+ }
+ }
+ }
+ }
+ mIsQwertyUpperCase = isQwertyUpperCase;
+ }
+
+ private Rect getPadding() {
+ mTmpRect.set(0, 0, 0, 0);
+ Drawable skbBg = getSkbBackground();
+ if (null == skbBg) return mTmpRect;
+ skbBg.getPadding(mTmpRect);
+ return mTmpRect;
+ }
+
+ /**
+ * Enable a row with the give toggle Id. Rows with other toggle ids (except
+ * the id {@link KeyRow#ALWAYS_SHOW_ROW_ID}) will be disabled.
+ *
+ * @param rowId The row id to enable.
+ * @return True if the soft keyboard requires redrawing.
+ */
+ private boolean enableRow(int rowId) {
+ if (KeyRow.ALWAYS_SHOW_ROW_ID == rowId) return false;
+
+ boolean enabled = false;
+ int rowNum = mKeyRows.size();
+ for (int row = rowNum - 1; row >= 0; row--) {
+ if (mKeyRows.get(row).mRowId == rowId) {
+ enabled = true;
+ break;
+ }
+ }
+ if (enabled) {
+ mEnabledRowId = rowId;
+ }
+ return enabled;
+ }
+
+ @Override
+ public String toString() {
+ String str = "------------------SkbInfo----------------------\n";
+ String endStr = "-----------------------------------------------\n";
+ str += "Width: " + String.valueOf(mSkbCoreWidth) + "\n";
+ str += "Height: " + String.valueOf(mSkbCoreHeight) + "\n";
+ str += "KeyRowNum: " + mKeyRows == null ? "0" : String.valueOf(mKeyRows
+ .size())
+ + "\n";
+ if (null == mKeyRows) return str + endStr;
+ int rowNum = mKeyRows.size();
+ for (int row = 0; row < rowNum; row++) {
+ KeyRow keyRow = mKeyRows.get(row);
+ List<SoftKey> softKeys = keyRow.mSoftKeys;
+ int keyNum = softKeys.size();
+ for (int i = 0; i < softKeys.size(); i++) {
+ str += "-key " + String.valueOf(i) + ":"
+ + softKeys.get(i).toString();
+ }
+ }
+ return str + endStr;
+ }
+
+ public String toShortString() {
+ return super.toString();
+ }
+
+ class KeyRow {
+ static final int ALWAYS_SHOW_ROW_ID = -1;
+ static final int DEFAULT_ROW_ID = 0;
+
+ List<SoftKey> mSoftKeys;
+ /**
+ * If the row id is {@link #ALWAYS_SHOW_ROW_ID}, this row will always be
+ * enabled.
+ */
+ int mRowId;
+ float mTopF;
+ float mBottomF;
+ int mTop;
+ int mBottom;
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/SoftKeyboardView.java b/src/com/android/inputmethod/pinyin/SoftKeyboardView.java
new file mode 100644
index 0000000..5543f33
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/SoftKeyboardView.java
@@ -0,0 +1,478 @@
+/*
+ * 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 com.android.inputmethod.pinyin.SoftKeyboard.KeyRow;
+
+import java.util.List;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.drawable.Drawable;
+import android.os.Vibrator;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * Class used to show a soft keyboard.
+ *
+ * A soft keyboard view should not handle touch event itself, because we do bias
+ * correction, need a global strategy to map an event into a proper view to
+ * achieve better user experience.
+ */
+public class SoftKeyboardView extends View {
+ /**
+ * The definition of the soft keyboard for the current this soft keyboard
+ * view.
+ */
+ private SoftKeyboard mSoftKeyboard;
+
+ /**
+ * The popup balloon hint for key press/release.
+ */
+ private BalloonHint mBalloonPopup;
+
+ /**
+ * The on-key balloon hint for key press/release. If it is null, on-key
+ * highlight will be drawn on th soft keyboard view directly.
+ */
+ private BalloonHint mBalloonOnKey;
+
+ /** Used to play key sounds. */
+ private SoundManager mSoundManager;
+
+ /** The last key pressed. */
+ private SoftKey mSoftKeyDown;
+
+ /** Used to indicate whether the user is holding on a key. */
+ private boolean mKeyPressed = false;
+
+ /**
+ * The location offset of the view to the keyboard container.
+ */
+ private int mOffsetToSkbContainer[] = new int[2];
+
+ /**
+ * The location of the desired hint view to the keyboard container.
+ */
+ private int mHintLocationToSkbContainer[] = new int[2];
+
+ /**
+ * Text size for normal key.
+ */
+ private int mNormalKeyTextSize;
+
+ /**
+ * Text size for function key.
+ */
+ private int mFunctionKeyTextSize;
+
+ /**
+ * Long press timer used to response long-press.
+ */
+ private SkbContainer.LongPressTimer mLongPressTimer;
+
+ /**
+ * Repeated events for long press
+ */
+ private boolean mRepeatForLongPress = false;
+
+ /**
+ * If this parameter is true, the balloon will never be dismissed even if
+ * user moves a lot from the pressed point.
+ */
+ private boolean mMovingNeverHidePopupBalloon = false;
+
+ /** Vibration for key press. */
+ private Vibrator mVibrator;
+
+ /** Vibration pattern for key press. */
+ protected long[] mVibratePattern = new long[] {1, 20};
+
+ /**
+ * The dirty rectangle used to mark the area to re-draw during key press and
+ * release. Currently, whenever we can invalidate(Rect), view will call
+ * onDraw() and we MUST draw the whole view. This dirty information is for
+ * future use.
+ */
+ private Rect mDirtyRect = new Rect();
+
+ private Paint mPaint;
+ private FontMetricsInt mFmi;
+ private boolean mDimSkb;
+
+ public SoftKeyboardView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mSoundManager = SoundManager.getInstance(mContext);
+
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+ mFmi = mPaint.getFontMetricsInt();
+ }
+
+ public boolean setSoftKeyboard(SoftKeyboard softSkb) {
+ if (null == softSkb) {
+ return false;
+ }
+ mSoftKeyboard = softSkb;
+ Drawable bg = softSkb.getSkbBackground();
+ if (null != bg) setBackgroundDrawable(bg);
+ return true;
+ }
+
+ public SoftKeyboard getSoftKeyboard() {
+ return mSoftKeyboard;
+ }
+
+ public void resizeKeyboard(int skbWidth, int skbHeight) {
+ mSoftKeyboard.setSkbCoreSize(skbWidth, skbHeight);
+ }
+
+ public void setBalloonHint(BalloonHint balloonOnKey,
+ BalloonHint balloonPopup, boolean movingNeverHidePopup) {
+ mBalloonOnKey = balloonOnKey;
+ mBalloonPopup = balloonPopup;
+ mMovingNeverHidePopupBalloon = movingNeverHidePopup;
+ }
+
+ public void setOffsetToSkbContainer(int offsetToSkbContainer[]) {
+ mOffsetToSkbContainer[0] = offsetToSkbContainer[0];
+ mOffsetToSkbContainer[1] = offsetToSkbContainer[1];
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int measuredWidth = 0;
+ int measuredHeight = 0;
+ if (null != mSoftKeyboard) {
+ measuredWidth = mSoftKeyboard.getSkbCoreWidth();
+ measuredHeight = mSoftKeyboard.getSkbCoreHeight();
+ measuredWidth += mPaddingLeft + mPaddingRight;
+ measuredHeight += mPaddingTop + mPaddingBottom;
+ }
+ setMeasuredDimension(measuredWidth, measuredHeight);
+ }
+
+ private void showBalloon(BalloonHint balloon, int balloonLocationToSkb[],
+ boolean movePress) {
+ long delay = BalloonHint.TIME_DELAY_SHOW;
+ if (movePress) delay = 0;
+ if (balloon.needForceDismiss()) {
+ balloon.delayedDismiss(0);
+ }
+ if (!balloon.isShowing()) {
+ balloon.delayedShow(delay, balloonLocationToSkb);
+ } else {
+ balloon.delayedUpdate(delay, balloonLocationToSkb, balloon
+ .getWidth(), balloon.getHeight());
+ }
+ long b = System.currentTimeMillis();
+ }
+
+ public void resetKeyPress(long balloonDelay) {
+ if (!mKeyPressed) return;
+ mKeyPressed = false;
+ if (null != mBalloonOnKey) {
+ mBalloonOnKey.delayedDismiss(balloonDelay);
+ } else {
+ if (null != mSoftKeyDown) {
+ if (mDirtyRect.isEmpty()) {
+ mDirtyRect.set(mSoftKeyDown.mLeft, mSoftKeyDown.mTop,
+ mSoftKeyDown.mRight, mSoftKeyDown.mBottom);
+ }
+ invalidate(mDirtyRect);
+ } else {
+ invalidate();
+ }
+ }
+ mBalloonPopup.delayedDismiss(balloonDelay);
+ }
+
+ // If movePress is true, means that this function is called because user
+ // moves his finger to this button. If movePress is false, means that this
+ // function is called when user just presses this key.
+ public SoftKey onKeyPress(int x, int y,
+ SkbContainer.LongPressTimer longPressTimer, boolean movePress) {
+ mKeyPressed = false;
+ boolean moveWithinPreviousKey = false;
+ if (movePress) {
+ SoftKey newKey = mSoftKeyboard.mapToKey(x, y);
+ if (newKey == mSoftKeyDown) moveWithinPreviousKey = true;
+ mSoftKeyDown = newKey;
+ } else {
+ mSoftKeyDown = mSoftKeyboard.mapToKey(x, y);
+ }
+ if (moveWithinPreviousKey || null == mSoftKeyDown) return mSoftKeyDown;
+ mKeyPressed = true;
+
+ if (!movePress) {
+ tryPlayKeyDown();
+ tryVibrate();
+ }
+
+ mLongPressTimer = longPressTimer;
+
+ if (!movePress) {
+ if (mSoftKeyDown.getPopupResId() > 0 || mSoftKeyDown.repeatable()) {
+ mLongPressTimer.startTimer();
+ }
+ } else {
+ mLongPressTimer.removeTimer();
+ }
+
+ int desired_width;
+ int desired_height;
+ float textSize;
+ Environment env = Environment.getInstance();
+
+ if (null != mBalloonOnKey) {
+ Drawable keyHlBg = mSoftKeyDown.getKeyHlBg();
+ mBalloonOnKey.setBalloonBackground(keyHlBg);
+
+ // Prepare the on-key balloon
+ int keyXMargin = mSoftKeyboard.getKeyXMargin();
+ int keyYMargin = mSoftKeyboard.getKeyYMargin();
+ desired_width = mSoftKeyDown.width() - 2 * keyXMargin;
+ desired_height = mSoftKeyDown.height() - 2 * keyYMargin;
+ textSize = env
+ .getKeyTextSize(SoftKeyType.KEYTYPE_ID_NORMAL_KEY != mSoftKeyDown.mKeyType.mKeyTypeId);
+ Drawable icon = mSoftKeyDown.getKeyIcon();
+ if (null != icon) {
+ mBalloonOnKey.setBalloonConfig(icon, desired_width,
+ desired_height);
+ } else {
+ mBalloonOnKey.setBalloonConfig(mSoftKeyDown.getKeyLabel(),
+ textSize, true, mSoftKeyDown.getColorHl(),
+ desired_width, desired_height);
+ }
+
+ mHintLocationToSkbContainer[0] = mPaddingLeft + mSoftKeyDown.mLeft
+ - (mBalloonOnKey.getWidth() - mSoftKeyDown.width()) / 2;
+ mHintLocationToSkbContainer[0] += mOffsetToSkbContainer[0];
+ mHintLocationToSkbContainer[1] = mPaddingTop
+ + (mSoftKeyDown.mBottom - keyYMargin)
+ - mBalloonOnKey.getHeight();
+ mHintLocationToSkbContainer[1] += mOffsetToSkbContainer[1];
+ showBalloon(mBalloonOnKey, mHintLocationToSkbContainer, movePress);
+ } else {
+ mDirtyRect.union(mSoftKeyDown.mLeft, mSoftKeyDown.mTop,
+ mSoftKeyDown.mRight, mSoftKeyDown.mBottom);
+ invalidate(mDirtyRect);
+ }
+
+ // Prepare the popup balloon
+ if (mSoftKeyDown.needBalloon()) {
+ Drawable balloonBg = mSoftKeyboard.getBalloonBackground();
+ mBalloonPopup.setBalloonBackground(balloonBg);
+
+ desired_width = mSoftKeyDown.width() + env.getKeyBalloonWidthPlus();
+ desired_height = mSoftKeyDown.height()
+ + env.getKeyBalloonHeightPlus();
+ textSize = env
+ .getBalloonTextSize(SoftKeyType.KEYTYPE_ID_NORMAL_KEY != mSoftKeyDown.mKeyType.mKeyTypeId);
+ Drawable iconPopup = mSoftKeyDown.getKeyIconPopup();
+ if (null != iconPopup) {
+ mBalloonPopup.setBalloonConfig(iconPopup, desired_width,
+ desired_height);
+ } else {
+ mBalloonPopup.setBalloonConfig(mSoftKeyDown.getKeyLabel(),
+ textSize, mSoftKeyDown.needBalloon(), mSoftKeyDown
+ .getColorBalloon(), desired_width,
+ desired_height);
+ }
+
+ // The position to show.
+ mHintLocationToSkbContainer[0] = mPaddingLeft + mSoftKeyDown.mLeft
+ + -(mBalloonPopup.getWidth() - mSoftKeyDown.width()) / 2;
+ mHintLocationToSkbContainer[0] += mOffsetToSkbContainer[0];
+ mHintLocationToSkbContainer[1] = mPaddingTop + mSoftKeyDown.mTop
+ - mBalloonPopup.getHeight();
+ mHintLocationToSkbContainer[1] += mOffsetToSkbContainer[1];
+ showBalloon(mBalloonPopup, mHintLocationToSkbContainer, movePress);
+ } else {
+ mBalloonPopup.delayedDismiss(0);
+ }
+
+ if (mRepeatForLongPress) longPressTimer.startTimer();
+ return mSoftKeyDown;
+ }
+
+ public SoftKey onKeyRelease(int x, int y) {
+ mKeyPressed = false;
+ if (null == mSoftKeyDown) return null;
+
+ mLongPressTimer.removeTimer();
+
+ if (null != mBalloonOnKey) {
+ mBalloonOnKey.delayedDismiss(BalloonHint.TIME_DELAY_DISMISS);
+ } else {
+ mDirtyRect.union(mSoftKeyDown.mLeft, mSoftKeyDown.mTop,
+ mSoftKeyDown.mRight, mSoftKeyDown.mBottom);
+ invalidate(mDirtyRect);
+ }
+
+ if (mSoftKeyDown.needBalloon()) {
+ mBalloonPopup.delayedDismiss(BalloonHint.TIME_DELAY_DISMISS);
+ }
+
+ if (mSoftKeyDown.moveWithinKey(x - mPaddingLeft, y - mPaddingTop)) {
+ return mSoftKeyDown;
+ }
+ return null;
+ }
+
+ public SoftKey onKeyMove(int x, int y) {
+ if (null == mSoftKeyDown) return null;
+
+ if (mSoftKeyDown.moveWithinKey(x - mPaddingLeft, y - mPaddingTop)) {
+ return mSoftKeyDown;
+ }
+
+ // The current key needs to be updated.
+ mDirtyRect.union(mSoftKeyDown.mLeft, mSoftKeyDown.mTop,
+ mSoftKeyDown.mRight, mSoftKeyDown.mBottom);
+
+ if (mRepeatForLongPress) {
+ if (mMovingNeverHidePopupBalloon) {
+ return onKeyPress(x, y, mLongPressTimer, true);
+ }
+
+ if (null != mBalloonOnKey) {
+ mBalloonOnKey.delayedDismiss(0);
+ } else {
+ invalidate(mDirtyRect);
+ }
+
+ if (mSoftKeyDown.needBalloon()) {
+ mBalloonPopup.delayedDismiss(0);
+ }
+
+ if (null != mLongPressTimer) {
+ mLongPressTimer.removeTimer();
+ }
+ return onKeyPress(x, y, mLongPressTimer, true);
+ } else {
+ // When user moves between keys, repeated response is disabled.
+ return onKeyPress(x, y, mLongPressTimer, true);
+ }
+ }
+
+ private void tryVibrate() {
+ if (!Settings.getVibrate()) {
+ return;
+ }
+ if (mVibrator == null) {
+ mVibrator = new Vibrator();
+ }
+ mVibrator.vibrate(mVibratePattern, -1);
+ }
+
+ private void tryPlayKeyDown() {
+ if (Settings.getKeySound()) {
+ mSoundManager.playKeyDown();
+ }
+ }
+
+ public void dimSoftKeyboard(boolean dimSkb) {
+ mDimSkb = dimSkb;
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (null == mSoftKeyboard) return;
+
+ canvas.translate(mPaddingLeft, mPaddingTop);
+
+ Environment env = Environment.getInstance();
+ mNormalKeyTextSize = env.getKeyTextSize(false);
+ mFunctionKeyTextSize = env.getKeyTextSize(true);
+ // Draw the last soft keyboard
+ int rowNum = mSoftKeyboard.getRowNum();
+ int keyXMargin = mSoftKeyboard.getKeyXMargin();
+ int keyYMargin = mSoftKeyboard.getKeyYMargin();
+ for (int row = 0; row < rowNum; row++) {
+ KeyRow keyRow = mSoftKeyboard.getKeyRowForDisplay(row);
+ if (null == keyRow) continue;
+ List<SoftKey> softKeys = keyRow.mSoftKeys;
+ int keyNum = softKeys.size();
+ for (int i = 0; i < keyNum; i++) {
+ SoftKey softKey = softKeys.get(i);
+ if (SoftKeyType.KEYTYPE_ID_NORMAL_KEY == softKey.mKeyType.mKeyTypeId) {
+ mPaint.setTextSize(mNormalKeyTextSize);
+ } else {
+ mPaint.setTextSize(mFunctionKeyTextSize);
+ }
+ drawSoftKey(canvas, softKey, keyXMargin, keyYMargin);
+ }
+ }
+
+ if (mDimSkb) {
+ mPaint.setColor(0xa0000000);
+ canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
+ }
+
+ mDirtyRect.setEmpty();
+ }
+
+ private void drawSoftKey(Canvas canvas, SoftKey softKey, int keyXMargin,
+ int keyYMargin) {
+ Drawable bg;
+ int textColor;
+ if (mKeyPressed && softKey == mSoftKeyDown) {
+ bg = softKey.getKeyHlBg();
+ textColor = softKey.getColorHl();
+ } else {
+ bg = softKey.getKeyBg();
+ textColor = softKey.getColor();
+ }
+
+ if (null != bg) {
+ bg.setBounds(softKey.mLeft + keyXMargin, softKey.mTop + keyYMargin,
+ softKey.mRight - keyXMargin, softKey.mBottom - keyYMargin);
+ bg.draw(canvas);
+ }
+
+ String keyLabel = softKey.getKeyLabel();
+ Drawable keyIcon = softKey.getKeyIcon();
+ if (null != keyIcon) {
+ Drawable icon = keyIcon;
+ int marginLeft = (softKey.width() - icon.getIntrinsicWidth()) / 2;
+ int marginRight = softKey.width() - icon.getIntrinsicWidth()
+ - marginLeft;
+ int marginTop = (softKey.height() - icon.getIntrinsicHeight()) / 2;
+ int marginBottom = softKey.height() - icon.getIntrinsicHeight()
+ - marginTop;
+ icon.setBounds(softKey.mLeft + marginLeft,
+ softKey.mTop + marginTop, softKey.mRight - marginRight,
+ softKey.mBottom - marginBottom);
+ icon.draw(canvas);
+ } else if (null != keyLabel) {
+ mPaint.setColor(textColor);
+ float x = softKey.mLeft
+ + (softKey.width() - mPaint.measureText(keyLabel)) / 2.0f;
+ int fontHeight = mFmi.bottom - mFmi.top;
+ float marginY = (softKey.height() - fontHeight) / 2.0f;
+ float y = softKey.mTop + marginY - mFmi.top + mFmi.bottom / 1.5f;
+ canvas.drawText(keyLabel, x, y + 1, mPaint);
+ }
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/SoundManager.java b/src/com/android/inputmethod/pinyin/SoundManager.java
new file mode 100644
index 0000000..82be407
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/SoundManager.java
@@ -0,0 +1,63 @@
+/*
+ * 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.content.Context;
+import android.media.AudioManager;
+
+/**
+ * Class used to manage related sound resources.
+ */
+public class SoundManager {
+ private static SoundManager mInstance = null;
+ private Context mContext;
+ private AudioManager mAudioManager;
+ private final float FX_VOLUME = 1.0f;
+ private boolean mSilentMode;
+
+ private SoundManager(Context context) {
+ mContext = context;
+ updateRingerMode();
+ }
+
+ public void updateRingerMode() {
+ if (mAudioManager == null) {
+ mAudioManager = (AudioManager) mContext
+ .getSystemService(Context.AUDIO_SERVICE);
+ }
+ mSilentMode = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL);
+ }
+
+ public static SoundManager getInstance(Context context) {
+ if (null == mInstance) {
+ if (null != context) {
+ mInstance = new SoundManager(context);
+ }
+ }
+ return mInstance;
+ }
+
+ public void playKeyDown() {
+ if (mAudioManager == null) {
+ updateRingerMode();
+ }
+ if (!mSilentMode) {
+ int sound = AudioManager.FX_KEYPRESS_STANDARD;
+ mAudioManager.playSoundEffect(sound, FX_VOLUME);
+ }
+ }
+}
diff --git a/src/com/android/inputmethod/pinyin/XmlKeyboardLoader.java b/src/com/android/inputmethod/pinyin/XmlKeyboardLoader.java
new file mode 100644
index 0000000..fd192a3
--- /dev/null
+++ b/src/com/android/inputmethod/pinyin/XmlKeyboardLoader.java
@@ -0,0 +1,835 @@
+/*
+ * 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 com.android.inputmethod.pinyin.SoftKeyboard.KeyRow;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Drawable;
+
+import java.io.IOException;
+import java.util.regex.Pattern;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * Class used to load a soft keyboard or a soft keyboard template from xml
+ * files.
+ */
+public class XmlKeyboardLoader {
+ /**
+ * The tag used to define an xml-based soft keyboard template.
+ */
+ private static final String XMLTAG_SKB_TEMPLATE = "skb_template";
+
+ /**
+ * The tag used to indicate the soft key type which is defined inside the
+ * {@link #XMLTAG_SKB_TEMPLATE} element in the xml file. file.
+ */
+ private static final String XMLTAG_KEYTYPE = "key_type";
+
+ /**
+ * The tag used to define a default key icon for enter/delete/space keys. It
+ * is defined inside the {@link #XMLTAG_SKB_TEMPLATE} element in the xml
+ * file.
+ */
+ private static final String XMLTAG_KEYICON = "key_icon";
+
+ /**
+ * Attribute tag of the left and right margin for a key. A key's width
+ * should be larger than double of this value. Defined inside
+ * {@link #XMLTAG_SKB_TEMPLATE} and {@link #XMLTAG_KEYBOARD}.
+ */
+ private static final String XMLATTR_KEY_XMARGIN = "key_xmargin";
+
+ /**
+ * Attribute tag of the top and bottom margin for a key. A key's height
+ * should be larger than double of this value. Defined inside
+ * {@link #XMLTAG_SKB_TEMPLATE} and {@link #XMLTAG_KEYBOARD}.
+ */
+ private static final String XMLATTR_KEY_YMARGIN = "key_ymargin";
+
+ /**
+ * Attribute tag of the keyboard background image. Defined inside
+ * {@link #XMLTAG_SKB_TEMPLATE} and {@link #XMLTAG_KEYBOARD}.
+ */
+ private static final String XMLATTR_SKB_BG = "skb_bg";
+
+ /**
+ * Attribute tag of the balloon background image for key press. Defined
+ * inside {@link #XMLTAG_SKB_TEMPLATE} and {@link #XMLTAG_KEYBOARD}.
+ */
+ private static final String XMLATTR_BALLOON_BG = "balloon_bg";
+
+ /**
+ * Attribute tag of the popup balloon background image for key press or
+ * popup mini keyboard. Defined inside {@link #XMLTAG_SKB_TEMPLATE} and
+ * {@link #XMLTAG_KEYBOARD}.
+ */
+ private static final String XMLATTR_POPUP_BG = "popup_bg";
+
+ /**
+ * Attribute tag of the color to draw key label. Defined inside
+ * {@link #XMLTAG_SKB_TEMPLATE} and {@link #XMLTAG_KEYTYPE}.
+ */
+ private static final String XMLATTR_COLOR = "color";
+
+ /**
+ * Attribute tag of the color to draw key's highlighted label. Defined
+ * inside {@link #XMLTAG_SKB_TEMPLATE} and {@link #XMLTAG_KEYTYPE}.
+ */
+ private static final String XMLATTR_COLOR_HIGHLIGHT = "color_highlight";
+
+ /**
+ * Attribute tag of the color to draw key's label in the popup balloon.
+ * Defined inside {@link #XMLTAG_SKB_TEMPLATE} and {@link #XMLTAG_KEYTYPE}.
+ */
+ private static final String XMLATTR_COLOR_BALLOON = "color_balloon";
+
+ /**
+ * Attribute tag of the id of {@link #XMLTAG_KEYTYPE} and
+ * {@link #XMLTAG_KEY}. Key types and keys defined in a soft keyboard
+ * template should have id, because a soft keyboard needs the id to refer to
+ * these default definitions. If a key defined in {@link #XMLTAG_KEYBOARD}
+ * does not id, that means the key is newly defined; if it has id (and only
+ * has id), the id is used to find the default definition from the soft
+ * keyboard template.
+ */
+ private static final String XMLATTR_ID = "id";
+
+ /**
+ * Attribute tag of the key background for a specified key type. Defined
+ * inside {@link #XMLTAG_KEYTYPE}.
+ */
+ private static final String XMLATTR_KEYTYPE_BG = "bg";
+
+ /**
+ * Attribute tag of the key high-light background for a specified key type.
+ * Defined inside {@link #XMLTAG_KEYTYPE}.
+ */
+ private static final String XMLATTR_KEYTYPE_HLBG = "hlbg";
+
+ /**
+ * Attribute tag of the starting x-position of an element. It can be defined
+ * in {@link #XMLTAG_ROW} and {@link #XMLTAG_KEY} in {XMLTAG_SKB_TEMPLATE}.
+ * If not defined, 0 will be used. For a key defined in
+ * {@link #XMLTAG_KEYBOARD}, it always use its previous keys information to
+ * calculate its own position.
+ */
+ private static final String XMLATTR_START_POS_X = "start_pos_x";
+
+ /**
+ * Attribute tag of the starting y-position of an element. It can be defined
+ * in {@link #XMLTAG_ROW} and {@link #XMLTAG_KEY} in {XMLTAG_SKB_TEMPLATE}.
+ * If not defined, 0 will be used. For a key defined in
+ * {@link #XMLTAG_KEYBOARD}, it always use its previous keys information to
+ * calculate its own position.
+ */
+ private static final String XMLATTR_START_POS_Y = "start_pos_y";
+
+ /**
+ * Attribute tag of a row's id. Defined {@link #XMLTAG_ROW}. If not defined,
+ * -1 will be used. Rows with id -1 will be enabled always, rows with same
+ * row id will be enabled when the id is the same to the activated id of the
+ * soft keyboard.
+ */
+ private static final String XMLATTR_ROW_ID = "row_id";
+
+ /** The tag used to indicate the keyboard element in the xml file. */
+ private static final String XMLTAG_KEYBOARD = "keyboard";
+
+ /** The tag used to indicate the row element in the xml file. */
+ private static final String XMLTAG_ROW = "row";
+
+ /** The tag used to indicate key-array element in the xml file. */
+ private static final String XMLTAG_KEYS = "keys";
+
+ /**
+ * The tag used to indicate a key element in the xml file. If the element is
+ * defined in a soft keyboard template, it should have an id. If it is
+ * defined in a soft keyboard, id is not required.
+ */
+ private static final String XMLTAG_KEY = "key";
+
+ /** The tag used to indicate a key's toggle element in the xml file. */
+ private static final String XMLTAG_TOGGLE_STATE = "toggle_state";
+
+ /**
+ * Attribute tag of the toggle state id for toggle key. Defined inside
+ * {@link #XMLTAG_TOGGLE_STATE}
+ */
+ private static final String XMLATTR_TOGGLE_STATE_ID = "state_id";
+
+ /** Attribute tag of key template for the soft keyboard. */
+ private static final String XMLATTR_SKB_TEMPLATE = "skb_template";
+
+ /**
+ * Attribute tag used to indicate whether this soft keyboard needs to be
+ * cached in memory for future use. {@link #DEFAULT_SKB_CACHE_FLAG}
+ * specifies the default value.
+ */
+ private static final String XMLATTR_SKB_CACHE_FLAG = "skb_cache_flag";
+
+ /**
+ * Attribute tag used to indicate whether this soft keyboard is sticky. A
+ * sticky soft keyboard will keep the current layout unless user makes a
+ * switch explicitly. A none sticky soft keyboard will automatically goes
+ * back to the previous keyboard after click a none-function key.
+ * {@link #DEFAULT_SKB_STICKY_FLAG} specifies the default value.
+ */
+ private static final String XMLATTR_SKB_STICKY_FLAG = "skb_sticky_flag";
+
+ /** Attribute tag to indicate whether it is a QWERTY soft keyboard. */
+ private static final String XMLATTR_QWERTY = "qwerty";
+
+ /**
+ * When the soft keyboard is a QWERTY one, this attribute tag to get the
+ * information that whether it is defined in upper case.
+ */
+ private static final String XMLATTR_QWERTY_UPPERCASE = "qwerty_uppercase";
+
+ /** Attribute tag of key type. */
+ private static final String XMLATTR_KEY_TYPE = "key_type";
+
+ /** Attribute tag of key width. */
+ private static final String XMLATTR_KEY_WIDTH = "width";
+
+ /** Attribute tag of key height. */
+ private static final String XMLATTR_KEY_HEIGHT = "height";
+
+ /** Attribute tag of the key's repeating ability. */
+ private static final String XMLATTR_KEY_REPEAT = "repeat";
+
+ /** Attribute tag of the key's behavior for balloon. */
+ private static final String XMLATTR_KEY_BALLOON = "balloon";
+
+ /** Attribute tag of the key splitter in a key array. */
+ private static final String XMLATTR_KEY_SPLITTER = "splitter";
+
+ /** Attribute tag of the key labels in a key array. */
+ private static final String XMLATTR_KEY_LABELS = "labels";
+
+ /** Attribute tag of the key codes in a key array. */
+ private static final String XMLATTR_KEY_CODES = "codes";
+
+ /** Attribute tag of the key label in a key. */
+ private static final String XMLATTR_KEY_LABEL = "label";
+
+ /** Attribute tag of the key code in a key. */
+ private static final String XMLATTR_KEY_CODE = "code";
+
+ /** Attribute tag of the key icon in a key. */
+ private static final String XMLATTR_KEY_ICON = "icon";
+
+ /** Attribute tag of the key's popup icon in a key. */
+ private static final String XMLATTR_KEY_ICON_POPUP = "icon_popup";
+
+ /** The id for a mini popup soft keyboard. */
+ private static final String XMLATTR_KEY_POPUP_SKBID = "popup_skb";
+
+ private static boolean DEFAULT_SKB_CACHE_FLAG = true;
+
+ private static boolean DEFAULT_SKB_STICKY_FLAG = true;
+
+ /**
+ * The key type id for invalid key type. It is also used to generate next
+ * valid key type id by adding 1.
+ */
+ private static final int KEYTYPE_ID_LAST = -1;
+
+ private Context mContext;
+
+ private Resources mResources;
+
+ /** The event type in parsing the xml file. */
+ private int mXmlEventType;
+
+ /**
+ * The current soft keyboard template used by the current soft keyboard
+ * under loading.
+ **/
+ private SkbTemplate mSkbTemplate;
+
+ /** The x position for the next key. */
+ float mKeyXPos;
+
+ /** The y position for the next key. */
+ float mKeyYPos;
+
+ /** The width of the keyboard to load. */
+ int mSkbWidth;
+
+ /** The height of the keyboard to load. */
+ int mSkbHeight;
+
+ /** Key margin in x-way. */
+ float mKeyXMargin = 0;
+
+ /** Key margin in y-way. */
+ float mKeyYMargin = 0;
+
+ /**
+ * Used to indicate whether next event has been fetched during processing
+ * the the current event.
+ */
+ boolean mNextEventFetched = false;
+
+ String mAttrTmp;
+
+ class KeyCommonAttributes {
+ XmlResourceParser mXrp;
+ int keyType;
+ float keyWidth;
+ float keyHeight;
+ boolean repeat;
+ boolean balloon;
+
+ KeyCommonAttributes(XmlResourceParser xrp) {
+ mXrp = xrp;
+ balloon = true;
+ }
+
+ // Make sure the default object is not null.
+ boolean getAttributes(KeyCommonAttributes defAttr) {
+ keyType = getInteger(mXrp, XMLATTR_KEY_TYPE, defAttr.keyType);
+ keyWidth = getFloat(mXrp, XMLATTR_KEY_WIDTH, defAttr.keyWidth);
+ keyHeight = getFloat(mXrp, XMLATTR_KEY_HEIGHT, defAttr.keyHeight);
+ repeat = getBoolean(mXrp, XMLATTR_KEY_REPEAT, defAttr.repeat);
+ balloon = getBoolean(mXrp, XMLATTR_KEY_BALLOON, defAttr.balloon);
+ if (keyType < 0 || keyWidth <= 0 || keyHeight <= 0) {
+ return false;
+ }
+ return true;
+ }
+ }
+
+ public XmlKeyboardLoader(Context context) {
+ mContext = context;
+ mResources = mContext.getResources();
+ }
+
+ public SkbTemplate loadSkbTemplate(int resourceId) {
+ if (null == mContext || 0 == resourceId) {
+ return null;
+ }
+ Resources r = mResources;
+ XmlResourceParser xrp = r.getXml(resourceId);
+
+ KeyCommonAttributes attrDef = new KeyCommonAttributes(xrp);
+ KeyCommonAttributes attrKey = new KeyCommonAttributes(xrp);
+
+ mSkbTemplate = new SkbTemplate(resourceId);
+ int lastKeyTypeId = KEYTYPE_ID_LAST;
+ int globalColor = 0;
+ int globalColorHl = 0;
+ int globalColorBalloon = 0;
+ try {
+ mXmlEventType = xrp.next();
+ while (mXmlEventType != XmlResourceParser.END_DOCUMENT) {
+ mNextEventFetched = false;
+ if (mXmlEventType == XmlResourceParser.START_TAG) {
+ String attribute = xrp.getName();
+ if (XMLTAG_SKB_TEMPLATE.compareTo(attribute) == 0) {
+ Drawable skbBg = getDrawable(xrp, XMLATTR_SKB_BG, null);
+ Drawable balloonBg = getDrawable(xrp,
+ XMLATTR_BALLOON_BG, null);
+ Drawable popupBg = getDrawable(xrp, XMLATTR_POPUP_BG,
+ null);
+ if (null == skbBg || null == balloonBg
+ || null == popupBg) {
+ return null;
+ }
+ mSkbTemplate.setBackgrounds(skbBg, balloonBg, popupBg);
+
+ float xMargin = getFloat(xrp, XMLATTR_KEY_XMARGIN, 0);
+ float yMargin = getFloat(xrp, XMLATTR_KEY_YMARGIN, 0);
+ mSkbTemplate.setMargins(xMargin, yMargin);
+
+ // Get default global colors.
+ globalColor = getColor(xrp, XMLATTR_COLOR, 0);
+ globalColorHl = getColor(xrp, XMLATTR_COLOR_HIGHLIGHT,
+ 0xffffffff);
+ globalColorBalloon = getColor(xrp,
+ XMLATTR_COLOR_BALLOON, 0xffffffff);
+ } else if (XMLTAG_KEYTYPE.compareTo(attribute) == 0) {
+ int id = getInteger(xrp, XMLATTR_ID, KEYTYPE_ID_LAST);
+ Drawable bg = getDrawable(xrp, XMLATTR_KEYTYPE_BG, null);
+ Drawable hlBg = getDrawable(xrp, XMLATTR_KEYTYPE_HLBG,
+ null);
+ int color = getColor(xrp, XMLATTR_COLOR, globalColor);
+ int colorHl = getColor(xrp, XMLATTR_COLOR_HIGHLIGHT,
+ globalColorHl);
+ int colorBalloon = getColor(xrp, XMLATTR_COLOR_BALLOON,
+ globalColorBalloon);
+ if (id != lastKeyTypeId + 1) {
+ return null;
+ }
+ SoftKeyType keyType = mSkbTemplate.createKeyType(id,
+ bg, hlBg);
+ keyType.setColors(color, colorHl, colorBalloon);
+ if (!mSkbTemplate.addKeyType(keyType)) {
+ return null;
+ }
+ lastKeyTypeId = id;
+ } else if (XMLTAG_KEYICON.compareTo(attribute) == 0) {
+ int keyCode = getInteger(xrp, XMLATTR_KEY_CODE, 0);
+ Drawable icon = getDrawable(xrp, XMLATTR_KEY_ICON, null);
+ Drawable iconPopup = getDrawable(xrp,
+ XMLATTR_KEY_ICON_POPUP, null);
+ if (null != icon && null != iconPopup) {
+ mSkbTemplate.addDefaultKeyIcons(keyCode, icon,
+ iconPopup);
+ }
+ } else if (XMLTAG_KEY.compareTo(attribute) == 0) {
+ int keyId = this.getInteger(xrp, XMLATTR_ID, -1);
+ if (-1 == keyId) return null;
+
+ if (!attrKey.getAttributes(attrDef)) {
+ return null;
+ }
+
+ // Update the key position for the key.
+ mKeyXPos = getFloat(xrp, XMLATTR_START_POS_X, 0);
+ mKeyYPos = getFloat(xrp, XMLATTR_START_POS_Y, 0);
+
+ SoftKey softKey = getSoftKey(xrp, attrKey);
+ if (null == softKey) return null;
+ mSkbTemplate.addDefaultKey(keyId, softKey);
+ }
+ }
+ // Get the next tag.
+ if (!mNextEventFetched) mXmlEventType = xrp.next();
+ }
+ xrp.close();
+ return mSkbTemplate;
+ } catch (XmlPullParserException e) {
+ // Log.e(TAG, "Ill-formatted keyboard template resource file");
+ } catch (IOException e) {
+ // Log.e(TAG, "Unable to keyboard template resource file");
+ }
+ return null;
+ }
+
+ public SoftKeyboard loadKeyboard(int resourceId, int skbWidth, int skbHeight) {
+ if (null == mContext) return null;
+ Resources r = mResources;
+ SkbPool skbPool = SkbPool.getInstance();
+ XmlResourceParser xrp = mContext.getResources().getXml(resourceId);
+ mSkbTemplate = null;
+ SoftKeyboard softKeyboard = null;
+ Drawable skbBg;
+ Drawable popupBg;
+ Drawable balloonBg;
+ SoftKey softKey = null;
+
+ KeyCommonAttributes attrDef = new KeyCommonAttributes(xrp);
+ KeyCommonAttributes attrSkb = new KeyCommonAttributes(xrp);
+ KeyCommonAttributes attrRow = new KeyCommonAttributes(xrp);
+ KeyCommonAttributes attrKeys = new KeyCommonAttributes(xrp);
+ KeyCommonAttributes attrKey = new KeyCommonAttributes(xrp);
+
+ mKeyXPos = 0;
+ mKeyYPos = 0;
+ mSkbWidth = skbWidth;
+ mSkbHeight = skbHeight;
+
+ try {
+ mKeyXMargin = 0;
+ mKeyYMargin = 0;
+ mXmlEventType = xrp.next();
+ while (mXmlEventType != XmlResourceParser.END_DOCUMENT) {
+ mNextEventFetched = false;
+ if (mXmlEventType == XmlResourceParser.START_TAG) {
+ String attr = xrp.getName();
+ // 1. Is it the root element, "keyboard"?
+ if (XMLTAG_KEYBOARD.compareTo(attr) == 0) {
+ // 1.1 Get the keyboard template id.
+ int skbTemplateId = xrp.getAttributeResourceValue(null,
+ XMLATTR_SKB_TEMPLATE, 0);
+
+ // 1.2 Try to get the template from pool. If it is not
+ // in, the pool will try to load it.
+ mSkbTemplate = skbPool.getSkbTemplate(skbTemplateId,
+ mContext);
+
+ if (null == mSkbTemplate
+ || !attrSkb.getAttributes(attrDef)) {
+ return null;
+ }
+
+ boolean cacheFlag = getBoolean(xrp,
+ XMLATTR_SKB_CACHE_FLAG, DEFAULT_SKB_CACHE_FLAG);
+ boolean stickyFlag = getBoolean(xrp,
+ XMLATTR_SKB_STICKY_FLAG,
+ DEFAULT_SKB_STICKY_FLAG);
+ boolean isQwerty = getBoolean(xrp, XMLATTR_QWERTY,
+ false);
+ boolean isQwertyUpperCase = getBoolean(xrp,
+ XMLATTR_QWERTY_UPPERCASE, false);
+
+ softKeyboard = new SoftKeyboard(resourceId,
+ mSkbTemplate, mSkbWidth, mSkbHeight);
+ softKeyboard.setFlags(cacheFlag, stickyFlag, isQwerty,
+ isQwertyUpperCase);
+
+ mKeyXMargin = getFloat(xrp, XMLATTR_KEY_XMARGIN,
+ mSkbTemplate.getXMargin());
+ mKeyYMargin = getFloat(xrp, XMLATTR_KEY_YMARGIN,
+ mSkbTemplate.getYMargin());
+ skbBg = getDrawable(xrp, XMLATTR_SKB_BG, null);
+ popupBg = getDrawable(xrp, XMLATTR_POPUP_BG, null);
+ balloonBg = getDrawable(xrp, XMLATTR_BALLOON_BG, null);
+ if (null != skbBg) {
+ softKeyboard.setSkbBackground(skbBg);
+ }
+ if (null != popupBg) {
+ softKeyboard.setPopupBackground(popupBg);
+ }
+ if (null != balloonBg) {
+ softKeyboard.setKeyBalloonBackground(balloonBg);
+ }
+ softKeyboard.setKeyMargins(mKeyXMargin, mKeyYMargin);
+ } else if (XMLTAG_ROW.compareTo(attr) == 0) {
+ if (!attrRow.getAttributes(attrSkb)) {
+ return null;
+ }
+ // Get the starting positions for the row.
+ mKeyXPos = getFloat(xrp, XMLATTR_START_POS_X, 0);
+ mKeyYPos = getFloat(xrp, XMLATTR_START_POS_Y, mKeyYPos);
+ int rowId = getInteger(xrp, XMLATTR_ROW_ID,
+ KeyRow.ALWAYS_SHOW_ROW_ID);
+ softKeyboard.beginNewRow(rowId, mKeyYPos);
+ } else if (XMLTAG_KEYS.compareTo(attr) == 0) {
+ if (null == softKeyboard) return null;
+ if (!attrKeys.getAttributes(attrRow)) {
+ return null;
+ }
+
+ String splitter = xrp.getAttributeValue(null,
+ XMLATTR_KEY_SPLITTER);
+ splitter = Pattern.quote(splitter);
+ String labels = xrp.getAttributeValue(null,
+ XMLATTR_KEY_LABELS);
+ String codes = xrp.getAttributeValue(null,
+ XMLATTR_KEY_CODES);
+ if (null == splitter || null == labels) {
+ return null;
+ }
+ String labelArr[] = labels.split(splitter);
+ String codeArr[] = null;
+ if (null != codes) {
+ codeArr = codes.split(splitter);
+ if (labelArr.length != codeArr.length) {
+ return null;
+ }
+ }
+
+ for (int i = 0; i < labelArr.length; i++) {
+ softKey = new SoftKey();
+ int keyCode = 0;
+ if (null != codeArr) {
+ keyCode = Integer.valueOf(codeArr[i]);
+ }
+ softKey.setKeyAttribute(keyCode, labelArr[i],
+ attrKeys.repeat, attrKeys.balloon);
+
+ softKey.setKeyType(mSkbTemplate
+ .getKeyType(attrKeys.keyType), null, null);
+
+ float left, right, top, bottom;
+ left = mKeyXPos;
+
+ right = left + attrKeys.keyWidth;
+ top = mKeyYPos;
+ bottom = top + attrKeys.keyHeight;
+
+ if (right - left < 2 * mKeyXMargin) return null;
+ if (bottom - top < 2 * mKeyYMargin) return null;
+
+ softKey.setKeyDimensions(left, top, right, bottom);
+ softKeyboard.addSoftKey(softKey);
+ mKeyXPos = right;
+ if ((int) mKeyXPos * mSkbWidth > mSkbWidth) {
+ return null;
+ }
+ }
+ } else if (XMLTAG_KEY.compareTo(attr) == 0) {
+ if (null == softKeyboard) {
+ return null;
+ }
+ if (!attrKey.getAttributes(attrRow)) {
+ return null;
+ }
+
+ int keyId = this.getInteger(xrp, XMLATTR_ID, -1);
+ if (keyId >= 0) {
+ softKey = mSkbTemplate.getDefaultKey(keyId);
+ } else {
+ softKey = getSoftKey(xrp, attrKey);
+ }
+ if (null == softKey) return null;
+
+ // Update the position for next key.
+ mKeyXPos = softKey.mRightF;
+ if ((int) mKeyXPos * mSkbWidth > mSkbWidth) {
+ return null;
+ }
+ // If the current xml event type becomes a starting tag,
+ // it indicates that we have parsed too much to get
+ // toggling states, and we started a new row. In this
+ // case, the row starting position information should
+ // be updated.
+ if (mXmlEventType == XmlResourceParser.START_TAG) {
+ attr = xrp.getName();
+ if (XMLTAG_ROW.compareTo(attr) == 0) {
+ mKeyYPos += attrRow.keyHeight;
+ if ((int) mKeyYPos * mSkbHeight > mSkbHeight) {
+ return null;
+ }
+ }
+ }
+ softKeyboard.addSoftKey(softKey);
+ }
+ } else if (mXmlEventType == XmlResourceParser.END_TAG) {
+ String attr = xrp.getName();
+ if (XMLTAG_ROW.compareTo(attr) == 0) {
+ mKeyYPos += attrRow.keyHeight;
+ if ((int) mKeyYPos * mSkbHeight > mSkbHeight) {
+ return null;
+ }
+ }
+ }
+
+ // Get the next tag.
+ if (!mNextEventFetched) mXmlEventType = xrp.next();
+ }
+ xrp.close();
+ softKeyboard.setSkbCoreSize(mSkbWidth, mSkbHeight);
+ return softKeyboard;
+ } catch (XmlPullParserException e) {
+ // Log.e(TAG, "Ill-formatted keybaord resource file");
+ } catch (IOException e) {
+ // Log.e(TAG, "Unable to read keyboard resource file");
+ }
+ return null;
+ }
+
+ // Caller makes sure xrp and r are valid.
+ private SoftKey getSoftKey(XmlResourceParser xrp,
+ KeyCommonAttributes attrKey) throws XmlPullParserException,
+ IOException {
+ int keyCode = getInteger(xrp, XMLATTR_KEY_CODE, 0);
+ String keyLabel = getString(xrp, XMLATTR_KEY_LABEL, null);
+ Drawable keyIcon = getDrawable(xrp, XMLATTR_KEY_ICON, null);
+ Drawable keyIconPopup = getDrawable(xrp, XMLATTR_KEY_ICON_POPUP, null);
+ int popupSkbId = xrp.getAttributeResourceValue(null,
+ XMLATTR_KEY_POPUP_SKBID, 0);
+
+ if (null == keyLabel && null == keyIcon) {
+ keyIcon = mSkbTemplate.getDefaultKeyIcon(keyCode);
+ keyIconPopup = mSkbTemplate.getDefaultKeyIconPopup(keyCode);
+ if (null == keyIcon || null == keyIconPopup) return null;
+ }
+
+ // Dimension information must been initialized before
+ // getting toggle state, because mKeyYPos may be changed
+ // to next row when trying to get toggle state.
+ float left, right, top, bottom;
+ left = mKeyXPos;
+ right = left + attrKey.keyWidth;
+ top = mKeyYPos;
+ bottom = top + attrKey.keyHeight;
+
+ if (right - left < 2 * mKeyXMargin) return null;
+ if (bottom - top < 2 * mKeyYMargin) return null;
+
+ // Try to find if the next tag is
+ // {@link #XMLTAG_TOGGLE_STATE_OF_KEY}, if yes, try to
+ // create a toggle key.
+ boolean toggleKey = false;
+ mXmlEventType = xrp.next();
+ mNextEventFetched = true;
+
+ SoftKey softKey;
+ if (mXmlEventType == XmlResourceParser.START_TAG) {
+ mAttrTmp = xrp.getName();
+ if (mAttrTmp.compareTo(XMLTAG_TOGGLE_STATE) == 0) {
+ toggleKey = true;
+ }
+ }
+ if (toggleKey) {
+ softKey = new SoftKeyToggle();
+ if (!((SoftKeyToggle) softKey).setToggleStates(getToggleStates(
+ attrKey, (SoftKeyToggle) softKey, keyCode))) {
+ return null;
+ }
+ } else {
+ softKey = new SoftKey();
+ }
+
+ // Set the normal state
+ softKey.setKeyAttribute(keyCode, keyLabel, attrKey.repeat,
+ attrKey.balloon);
+ softKey.setPopupSkbId(popupSkbId);
+ softKey.setKeyType(mSkbTemplate.getKeyType(attrKey.keyType), keyIcon,
+ keyIconPopup);
+
+ softKey.setKeyDimensions(left, top, right, bottom);
+ return softKey;
+ }
+
+ private SoftKeyToggle.ToggleState getToggleStates(
+ KeyCommonAttributes attrKey, SoftKeyToggle softKey, int defKeyCode)
+ throws XmlPullParserException, IOException {
+ XmlResourceParser xrp = attrKey.mXrp;
+ int stateId = getInteger(xrp, XMLATTR_TOGGLE_STATE_ID, 0);
+ if (0 == stateId) return null;
+
+ String keyLabel = getString(xrp, XMLATTR_KEY_LABEL, null);
+ int keyTypeId = getInteger(xrp, XMLATTR_KEY_TYPE, KEYTYPE_ID_LAST);
+ int keyCode;
+ if (null == keyLabel) {
+ keyCode = getInteger(xrp, XMLATTR_KEY_CODE, defKeyCode);
+ } else {
+ keyCode = getInteger(xrp, XMLATTR_KEY_CODE, 0);
+ }
+ Drawable icon = getDrawable(xrp, XMLATTR_KEY_ICON, null);
+ Drawable iconPopup = getDrawable(xrp, XMLATTR_KEY_ICON_POPUP, null);
+ if (null == icon && null == keyLabel) {
+ return null;
+ }
+ SoftKeyToggle.ToggleState rootState = softKey.createToggleState();
+ rootState.setStateId(stateId);
+ rootState.mKeyType = null;
+ if (KEYTYPE_ID_LAST != keyTypeId) {
+ rootState.mKeyType = mSkbTemplate.getKeyType(keyTypeId);
+ }
+ rootState.mKeyCode = keyCode;
+ rootState.mKeyIcon = icon;
+ rootState.mKeyIconPopup = iconPopup;
+ rootState.mKeyLabel = keyLabel;
+
+ boolean repeat = getBoolean(xrp, XMLATTR_KEY_REPEAT, attrKey.repeat);
+ boolean balloon = getBoolean(xrp, XMLATTR_KEY_BALLOON, attrKey.balloon);
+ rootState.setStateFlags(repeat, balloon);
+
+ rootState.mNextState = null;
+
+ // If there is another toggle state.
+ mXmlEventType = xrp.next();
+ while (mXmlEventType != XmlResourceParser.START_TAG
+ && mXmlEventType != XmlResourceParser.END_DOCUMENT) {
+ mXmlEventType = xrp.next();
+ }
+ if (mXmlEventType == XmlResourceParser.START_TAG) {
+ String attr = xrp.getName();
+ if (attr.compareTo(XMLTAG_TOGGLE_STATE) == 0) {
+ SoftKeyToggle.ToggleState nextState = getToggleStates(attrKey,
+ softKey, defKeyCode);
+ if (null == nextState) return null;
+ rootState.mNextState = nextState;
+ }
+ }
+
+ return rootState;
+ }
+
+ private int getInteger(XmlResourceParser xrp, String name, int defValue) {
+ int resId = xrp.getAttributeResourceValue(null, name, 0);
+ String s;
+ if (resId == 0) {
+ s = xrp.getAttributeValue(null, name);
+ if (null == s) return defValue;
+ try {
+ int ret = Integer.valueOf(s);
+ return ret;
+ } catch (NumberFormatException e) {
+ return defValue;
+ }
+ } else {
+ return Integer.parseInt(mContext.getResources().getString(resId));
+ }
+ }
+
+ private int getColor(XmlResourceParser xrp, String name, int defValue) {
+ int resId = xrp.getAttributeResourceValue(null, name, 0);
+ String s;
+ if (resId == 0) {
+ s = xrp.getAttributeValue(null, name);
+ if (null == s) return defValue;
+ try {
+ int ret = Integer.valueOf(s);
+ return ret;
+ } catch (NumberFormatException e) {
+ return defValue;
+ }
+ } else {
+ return mContext.getResources().getColor(resId);
+ }
+ }
+
+ private String getString(XmlResourceParser xrp, String name, String defValue) {
+ int resId = xrp.getAttributeResourceValue(null, name, 0);
+ if (resId == 0) {
+ return xrp.getAttributeValue(null, name);
+ } else {
+ return mContext.getResources().getString(resId);
+ }
+ }
+
+ private float getFloat(XmlResourceParser xrp, String name, float defValue) {
+ int resId = xrp.getAttributeResourceValue(null, name, 0);
+ if (resId == 0) {
+ String s = xrp.getAttributeValue(null, name);
+ if (null == s) return defValue;
+ try {
+ float ret;
+ if (s.endsWith("%p")) {
+ ret = Float.parseFloat(s.substring(0, s.length() - 2)) / 100;
+ } else {
+ ret = Float.parseFloat(s);
+ }
+ return ret;
+ } catch (NumberFormatException e) {
+ return defValue;
+ }
+ } else {
+ return mContext.getResources().getDimension(resId);
+ }
+ }
+
+ private boolean getBoolean(XmlResourceParser xrp, String name,
+ boolean defValue) {
+ String s = xrp.getAttributeValue(null, name);
+ if (null == s) return defValue;
+ try {
+ boolean ret = Boolean.parseBoolean(s);
+ return ret;
+ } catch (NumberFormatException e) {
+ return defValue;
+ }
+ }
+
+ private Drawable getDrawable(XmlResourceParser xrp, String name,
+ Drawable defValue) {
+ int resId = xrp.getAttributeResourceValue(null, name, 0);
+ if (0 == resId) return defValue;
+ return mResources.getDrawable(resId);
+ }
+}