blob: 22948602ae64c6e7a03c51f4ec3732e7c6724e5e [file] [log] [blame]
/*
* 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();
}
}
}
}
}
}