最近一段时间,公司要开发安卓输入法,看了几款开源的例子,其中对 PinyinIME 整篇阅读,并进行了相应的注释,现在把注释的代码发上来,和大家分享学习。
下载改过包名并进行过注释的PinyinIME: http://download.csdn.net/detail/keanbin/6656347
下载没有改过包名的PinyinIME(也进行了代码注释): http://download.csdn.net/detail/keanbin/6656395
下载原装的PinyinIME: http://download.csdn.net/detail/keanbin/6656443
1、BalloonHint.java :
/*
* 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.keanbin.pinyinime;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.Rect;
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. 气泡View
*/
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;
}
/**
* 延时显示气泡
*
* @param delay
* 延时的时间
* @param locationInParent
* 气泡显示的位置,相对于父视图
*/
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);
}
}
/**
* 延时更新气泡
*
* @param delay
* 延时的时间
* @param locationInParent
* 气泡显示的位置,相对于父视图
*/
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);
}
}
/**
* 气泡延时消失
*
* @param delay
*/
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.
// 调用update()去更新位置和大小,系统会先移动对话框到新的位置,然后再去更新大小,所以如果需要更新大小,那么我们就需要先强制去销毁它,再去显示。
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;
}
}
/**
* 气泡View
*
* @author keanbin
*
*/
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. mIcon 如果不为空,mLabel就被忽略。
*/
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 = getPaddingLeft() + getPaddingRight();
int measuredHeight = getPaddingTop() + getPaddingBottom();
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;
}
// TODO
// measuredWidth不是包含getPaddingLeft()和getPaddingRight()吗?怎么屏幕宽度还需要再减去它们?
int maxWidth = Environment.getInstance().getScreenWidth()
- getPaddingLeft() - getPaddingRight();
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 = getPaddingLeft();
x += (width - labelMeasuredWidth - getPaddingLeft() - getPaddingRight()) / 2.0f;
String labelToDraw = mLabel;
if (x < getPaddingLeft()) {
// 区域不够显示,显示短语+省略号
x = getPaddingLeft();
labelToDraw = getLimitedLabelForDrawing(mLabel, width
- getPaddingLeft() - getPaddingRight());
}
int fontHeight = mFmi.bottom - mFmi.top;
float marginY = (height - fontHeight) / 2.0f;
float y = marginY - mFmi.top;
canvas.drawText(labelToDraw, x, y, mPaintLabel);
}
}
/**
* 显示的文本过长,截取适合的短语+省略号
*
* @param rawLabel
* @param widthToDraw
* @return
*/
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);
}
}
}
2、CandidatesContainer.java
/*
* 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.keanbin.pinyinime;
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.Animation.AnimationListener;
import android.view.animation.AnimationSet;
import android.view.animation.TranslateAnimation;
import android.widget.ImageButton;
import android.widget.RelativeLayout;
import android.widget.ViewFlipper;
import com.keanbin.pinyinime.PinyinIME.DecodingInfo;
/**
* 集装箱中的箭头更新监听器
*
* @ClassName ArrowUpdater
* @author keanbin
*/
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>
*/
/**
* 候选词集装箱
*
* @ClassName CandidatesContainer
* @author keanbin
*/
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.
* 显示或者关闭一个候选词View的动画时间
*/
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.
* ViewFlipper页面管理,它包含两个视图,正常只显示其中一个,当切换候选词页的时候,就启动另一个视图装载接着要显示的候选词切入进来。
*/
private ViewFlipper mFlipper;
/**
* The x offset of the flipper in this container. ViewFlipper 在集装箱的偏移位置。
*/
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);
}
}
/**
* 显示候选词
*
* @param decInfo
* @param enableActiveHighlight
*/
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();
}
/**
* 获取当前的页码
*
* @return
*/
public int getCurrentPage() {
return mCurrentPage;
}
/**
* 设置候选词是否高亮
*
* @param enableActiveHighlight
*/
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) {
// 设置候选词所在的 ViewFlipper 在集装箱中的偏移位置
xOffsetForFlipper = mLeftArrowBtn.getMeasuredWidth();
}
}
/**
* 高亮位置向上一个候选词移动或者移动到上一页的最后一个候选词的位置。
*
* @return
*/
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);
}
}
/**
* 高亮位置向下一个候选词移动或者移动到下一页的第一个候选词的位置。
*
* @return
*/
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);
}
}
/**
* 到上一页候选词
*
* @param animLeftRight
* 高亮位置是否到本页最后一个候选词位置
* @param enableActiveHighlight
* @return
*/
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;
}
/**
* 到下一页候选词
*
* @param animLeftRight
* 高亮位置是否到本页第一个候选词位置
* @param enableActiveHighlight
* @return
*/
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;
}
/**
* 获取活动(高亮)的候选词在所有候选词中的位置
*
* @return
*/
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);
}
}
/**
* 设置箭头图标是否有效,和图标的透明度。
*
* @param arrowBtn
* @param enabled
*/
private void enableArrow(ImageButton arrowBtn, boolean enabled) {
arrowBtn.setEnabled(enabled);
if (enabled)
arrowBtn.setAlpha(ARROW_ALPHA_ENABLED);
else
arrowBtn.setAlpha(ARROW_ALPHA_DISABLED);
}
/**
* 设置箭头图标是否显示
*
* @param arrowBtn
* @param show
*/
private void showArrow(ImageButton arrowBtn, boolean show) {
if (show)
arrowBtn.setVisibility(View.VISIBLE);
else
arrowBtn.setVisibility(View.INVISIBLE);
}
/**
* view的触摸事件监听器
*
* @param v
* @param event
*/
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) {
// TODO 触摸事件的坐标点是以哪里为原点?
// 调整触摸事件坐标点的坐标
event.offsetLocation(-xOffsetForFlipper, 0);
// 调用候选词视图触摸事件处理函数
CandidateView cv = (CandidateView) mFlipper.getCurrentView();
cv.onTouchEventReal(event);
return true;
}
/**
* 创建动画,并设置给ViewFlipper mFlipper。
*
* @param animLeftRight
* @param forward
*/
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;
}
}
// 设置动画监听器,当动画结束的时候,调用onAnimationEnd()。
mInAnimInUse.setAnimationListener(this);
mFlipper.setInAnimation(mInAnimInUse);
mFlipper.setOutAnimation(mOutAnimInUse);
}
/**
* 创建移动动画
*
* @param xFrom
* @param xTo
* @param yFrom
* @param yTo
* @param alphaFrom
* @param alphaTo
* @param duration
* @return
*/
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;
}
/**
* 开始动画,mFlipper显示下一个。
*/
private void startAnimation() {
mFlipper.showNext();
}
/**
* 停止动画,mFlipper停止切换Flipping。
*/
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) {
}
}
3、CandidateView.java:
/*
* 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.keanbin.pinyinime;
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.Paint.FontMetricsInt;
import android.graphics.RectF;
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;
import com.keanbin.pinyinime.PinyinIME.DecodingInfo;
/**
* View to show candidate list. There two candidate view instances which are
* used to show animation when user navigates between pages.
*/
/**
* 候选词视图
*
* @ClassName CandidateView
* @author keanbin
*/
public class CandidateView extends View {
/**
* The minimum width to show a item. 一个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. 箭头更新接口。在onDraw()中,当mUpdateArrowStatusWhenDraw为true,
* 该接口的updateArrowStatus()方法被调用。因为箭头是放在候选词集装箱中的,不是放在候选词视图中。
*/
private ArrowUpdater mArrowUpdater;
/**
* If true, update the arrow status when drawing candidates.
* 在onDraw()的时候是否更新箭头
*/
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.
* 推荐候选词的颜色,推荐的候选词是来自APP的。
*/
private int mRecommendedCandidateColor;
/**
* Color to draw the normal(not highlighted) candidates, it can be one of
* {@link #mImeCandidateColor} or {@link #mRecommendedCandidateColor}.
* 候选词的颜色,它可以是 mImeCandidateColor 和 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.
* 推荐候选词的文本大小,推荐的候选词是来自APP的。
*/
private int mRecommendedCandidateTextSize;
/**
* The current text size to draw candidates. It can be one of
* {@link #mImeCandidateTextSize} or {@link #mRecommendedCandidateTextSize}.
* 候选词的文本大小,它可以是 mImeCandidateTextSize 和 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 = getMeasuredWidth();
int mOldHeight = getMeasuredHeight();
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
if (mOldWidth != getMeasuredWidth()
|| mOldHeight != getMeasuredHeight()) {
onSizeChanged();
}
}
/**
* 初始化。
*
* @param arrowUpdater
* @param balloonHint
* @param gestureDetector
* @param cvListener
*/
public void initialize(ArrowUpdater arrowUpdater, BalloonHint balloonHint,
GestureDetector gestureDetector, CandidateViewListener cvListener) {
mArrowUpdater = arrowUpdater;
mBalloonHint = balloonHint;
mGestureDetector = gestureDetector;
mCvListener = cvListener;
}
/**
* 根据候选词的来源设置候选词使用的颜色和文本大小,并计算省略号的宽度。
*
* @param decInfo
*/
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();
}
/**
* 获取活动(高亮)的候选词在页面的位置。
*
* @return
*/
public int getActiveCandiatePosInPage() {
return mActiveCandInPage;
}
/**
* 获取活动(高亮)的候选词在所有候选词中的位置
*
* @return
*/
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.
*/
/**
* 显示指定页的候选词
*
* @param pageNo
* @param activeCandInPage
* @param enableActiveHighlight
*/
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();
}
/**
* 设置是否高亮候选词
*
* @param enableActiveHighlight
*/
public void enableActiveHighlight(boolean enableActiveHighlight) {
if (enableActiveHighlight == mEnableActiveHighlight)
return;
mEnableActiveHighlight = enableActiveHighlight;
invalidate();
}
/**
* 高亮位置向下一个候选词移动。
*
* @return
*/
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;
}
/**
* 高亮位置向上一个候选词移动。
*
* @return
*/
public boolean activeCurseBackward() {
if (mActiveCandInPage > 0) {
showPage(mPageNo, mActiveCandInPage - 1, true);
return true;
}
return false;
}
/**
* 计算候选词区域的宽度和高度、候选词文本大小、附注文本大小、省略号宽度。当尺寸发生改变时调用。在onMeasure()中调用。
*/
private void onSizeChanged() {
// 计算候选词区域的宽度和高度
mContentWidth = getMeasuredWidth() - getPaddingLeft()
- getPaddingRight();
mContentHeight = (int) ((getMeasuredHeight() - getPaddingTop() - getPaddingBottom()) * 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;
}
/**
* 对还没有分页的候选词进行分页,计算指定页的候选词左右的额外间隔。
*
* @param pageNo
* @return
*/
private boolean calculatePage(int pageNo) {
if (pageNo == mPageNoCalculated)
return true;
// 计算候选词区域宽度和高度
mContentWidth = getMeasuredWidth() - getPaddingLeft()
- getPaddingRight();
mContentHeight = (int) ((getMeasuredHeight() - getPaddingTop() - getPaddingBottom()) * 0.95f);
if (mContentWidth <= 0 || mContentHeight <= 0)
return false;
// 候选词列表的size,即候选词的数量。
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) {
// pageNo是最后一页之前的页码,不包括最后一页
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) {
// pageNo是最后一页或者往后的一页,这里应该就是对候选词进行分页的地方,保证每页候选词都能正常显示。
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 = getPaddingLeft();
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, getPaddingTop() + 1, xPos
+ itemTotalWidth, getHeight() - getPaddingBottom() - 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;
}
}
/**
* 截取要显示的候选词短语+省略号
*
* @param rawCandidate
* @param widthToDraw
* @return
*/
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);
}
/**
* 画分隔符
*
* @param canvas
* @param xPos
* @return
*/
private float drawVerticalSeparator(Canvas canvas, float xPos) {
mSeparatorDrawable.setBounds((int) xPos, getPaddingTop(), (int) xPos
+ mSeparatorDrawable.getIntrinsicWidth(), getMeasuredHeight()
- getPaddingBottom());
mSeparatorDrawable.draw(canvas);
return mSeparatorDrawable.getIntrinsicWidth();
}
/**
* 返回坐标点所在或者离的最近的候选词区域在mCandRects的索引
*
* @param x
* @param y
* @return
*/
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);
}
/**
* 候选词视图触摸事件处理。在候选词集装箱 CandidatesContainer 中的触摸事件处理函数中调用。
*
* @param event
* @return
*/
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;
}
/**
* 显示气泡
*
* @param candPos
* @param delayedShow
*/
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);
}
}
/**
* 按下某个候选词的定时器。主要是刷新页面,显示按下的候选词为高亮状态。
*
* @ClassName PressTimer
* @author keanbin
*/
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;
}
}
}
4、CandidateViewListener.java:
/*
* 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.keanbin.pinyinime;
/**
* Interface to notify the input method when the user clicks a candidate or
* makes a direction-gesture on candidate view.
*/
/**
* 候选词视图监听器接口
*
* @ClassName CandidateViewListener
* @author keanbin
*/
public interface CandidateViewListener {
/**
* 选择了候选词的处理函数
*
* @param choiceId
*/
public void onClickChoice(int choiceId);
/**
* 向左滑动的手势处理函数
*/
public void onToLeftGesture();
/**
* 向右滑动的手势处理函数
*/
public void onToRightGesture();
/**
* 向上滑动的手势处理函数
*/
public void onToTopGesture();
/**
* 向下滑动的手势处理函数
*/
public void onToBottomGesture();
}
5、ComposingView.java:
/*
* 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.keanbin.pinyinime;
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.)
*/
/**
* 拼音字符串View,用于显示输入的拼音。
*
* @ClassName ComposingView
* @author keanbin
*/
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();
}
/**
* 重置拼音字符串View状态
*/
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.
*/
/**
* 设置 解码操作对象,然后刷新View。
*
* @param decInfo
* @param imeStatus
*/
public void setDecodingInfo(PinyinIME.DecodingInfo decInfo,
PinyinIME.ImeState imeStatus) {
mDecInfo = decInfo;
if (PinyinIME.ImeState.STATE_INPUT ==