/* * 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(); } } } } } }