Java tutorial
/* * * * * ** Copyright 2015, The LimeIME Open Source Project * ** * ** Project Url: http://github.com/lime-ime/limeime/ * ** http://android.toload.net/ * ** * ** This program is free software: you can redistribute it and/or modify * ** it under the terms of the GNU General Public License as published by * ** the Free Software Foundation, either version 3 of the License, or * ** (at your option) any later version. * * * ** This program is distributed in the hope that it will be useful, * ** but WITHOUT ANY WARRANTY; without even the implied warranty of * ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * ** GNU General Public License for more details. * * * ** You should have received a copy of the GNU General Public License * ** along with this program. If not, see <http://www.gnu.org/licenses/>. * * * */ package net.toload.main.hd.candidate; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Message; import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.Display; import android.view.GestureDetector; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.WindowManager; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.PopupWindow; import android.widget.ScrollView; import android.widget.TextView; import net.toload.main.hd.LIMEService; import net.toload.main.hd.R; import net.toload.main.hd.data.Mapping; import net.toload.main.hd.global.LIMEPreferenceManager; import java.lang.ref.WeakReference; import java.util.LinkedList; import java.util.List; public class CandidateView extends View implements View.OnClickListener { private static final boolean DEBUG = false; private static final String TAG = "CandidateView"; protected static final int OUT_OF_BOUNDS = -1; protected LIMEService mService; protected List<Mapping> mSuggestions; protected CandidateView mCandidateView; private TextView embeddedComposing; protected int mSelectedIndex; protected int mTouchX = OUT_OF_BOUNDS; //private boolean mTypedWordValid; private boolean mShowNumber; //Jeremy '11,5,25 for showing physical keyboard number or not. protected Rect mBgPadding; private static final int MAX_SUGGESTIONS = 500; private static final int SCROLL_PIXELS = 20; // Add by Jeremy '10, 3, 29. // Suggestions size. Set to MAX_GUGGESTIONS if larger then it. protected int mCount = 0; //Composing view private TextView mComposingTextView; private static TextView mComposingPopupTextView; private static PopupWindow mComposingTextPopup; //private String mComposingText = ""; protected int[] mWordWidth = new int[MAX_SUGGESTIONS]; protected int[] mWordX = new int[MAX_SUGGESTIONS]; protected static int X_GAP = 12; private static final List<Mapping> EMPTY_LIST = new LinkedList<>(); protected int mHeight; private int configHeight; private int currentX; protected int mColorBackground; protected int mColorNormalText; protected int mColorNormalTextHighlight; protected int mColorInvertedTextTransparent; protected int mColorComposingText; protected int mColorComposingBackground; protected int mColorComposingCodeHighlight; protected int mColorComposingCode; protected int mColorSpacer; protected Drawable mDrawableSuggestHighlight; protected Drawable mDrawableVoiceInput; protected Drawable mDrawableExpandButton; protected Drawable mDrawableCloseButton; protected int mColorSelKey; protected int mColorSelKeyShifted; protected int mVerticalPadding; protected int mExpandButtonWidth; protected Paint mCandidatePaint; protected Paint mSelKeyPaint; private boolean mScrolled; protected int mTargetScrollX; private String mDisplaySelkey = "1234567890"; protected int mTotalWidth; private boolean goLeft = false; private boolean goRight = false; private boolean hasSlide = false; //private int bgcolor = 0; private View mCandidatePopupContainer; private PopupWindow mCandidatePopupWindow; protected int mScreenWidth; protected int mScreenHeight; protected GestureDetector mGestureDetector; protected final Context mContext; protected LIMEPreferenceManager mLIMEPref; private CandidateExpandedView mPopupCandidateView; private int mCloseButtonHeight; private ScrollView mPopupScrollView; private boolean candidateExpanded = false; private boolean waitingForMoreRecords = false; private boolean mTransparentCandidateView = false; //private Rect padding = null; /** * Construct a CandidateView for showing suggested words for completion. */ public CandidateView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.LIMECandidateView); } public CandidateView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mContext = context; mCandidateView = this; embeddedComposing = null; // Jeremy '15,6,4 for embedded composing view in candidateView when floating candidateView (not fixed) mLIMEPref = new LIMEPreferenceManager(context); //Jeremy '16,7,24 get themed objects TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.LIMECandidateView, defStyle, R.style.LIMECandidateView); int n = a.getIndexCount(); for (int i = 0; i < n; i++) { int attr = a.getIndex(i); switch (attr) { case R.styleable.LIMECandidateView_suggestHighlight: mDrawableSuggestHighlight = a.getDrawable(attr); break; case R.styleable.LIMECandidateView_voiceInputIcon: mDrawableVoiceInput = a.getDrawable(attr); break; case R.styleable.LIMECandidateView_ExpandButtonIcon: mDrawableExpandButton = a.getDrawable(attr); break; case R.styleable.LIMECandidateView_closeButtonIcon: mDrawableCloseButton = a.getDrawable(attr); break; case R.styleable.LIMECandidateView_candidateBackground: mColorBackground = a.getColor(attr, ContextCompat.getColor(context, R.color.third_background_light)); break; case R.styleable.LIMECandidateView_composingTextColor: mColorComposingText = a.getColor(attr, ContextCompat.getColor(context, R.color.second_foreground_light)); break; case R.styleable.LIMECandidateView_composingBackgroundColor: mColorComposingBackground = a.getColor(attr, ContextCompat.getColor(context, R.color.composing_background_light)); break; case R.styleable.LIMECandidateView_candidateNormalTextColor: mColorNormalText = a.getColor(attr, ContextCompat.getColor(context, R.color.foreground_light)); break; case R.styleable.LIMECandidateView_candidateNormalTextHighlightColor: mColorNormalTextHighlight = a.getColor(attr, ContextCompat.getColor(context, R.color.foreground_light)); break; case R.styleable.LIMECandidateView_composingCodeColor: mColorComposingCode = a.getColor(attr, ContextCompat.getColor(context, R.color.color_common_green_hl)); break; case R.styleable.LIMECandidateView_composingCodeHighlightColor: mColorComposingCodeHighlight = a.getColor(attr, ContextCompat.getColor(context, R.color.third_background_light)); break; case R.styleable.LIMECandidateView_spacerColor: mColorSpacer = a.getColor(attr, ContextCompat.getColor(context, R.color.candidate_spacer)); break; case R.styleable.LIMECandidateView_selKeyColor: mColorSelKey = a.getColor(attr, ContextCompat.getColor(context, R.color.candidate_selection_keys)); break; case R.styleable.LIMECandidateView_selKeyShiftedColor: mColorSelKeyShifted = a.getColor(attr, ContextCompat.getColor(context, R.color.color_common_green_hl)); break; } } a.recycle(); final Resources r = context.getResources(); Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); Point screenSize = new Point(); display.getSize(screenSize); mScreenWidth = screenSize.x; mScreenHeight = screenSize.y; mVerticalPadding = (int) (r.getDimensionPixelSize(R.dimen.candidate_vertical_padding) * mLIMEPref.getFontSize()); configHeight = (int) (r.getDimensionPixelSize(R.dimen.candidate_stripe_height) * mLIMEPref.getFontSize()); mHeight = configHeight + mVerticalPadding; mExpandButtonWidth = r.getDimensionPixelSize(R.dimen.candidate_expand_button_width);// *mLIMEPref.getFontSize()); mCandidatePaint = new Paint(); mCandidatePaint.setColor(mColorNormalText); mCandidatePaint.setAntiAlias(true); mCandidatePaint.setTextSize(r.getDimensionPixelSize(R.dimen.candidate_font_size) * mLIMEPref.getFontSize()); mCandidatePaint.setStrokeWidth(0); mSelKeyPaint = new Paint(); mSelKeyPaint.setColor(mColorSelKey); mSelKeyPaint.setAntiAlias(true); mSelKeyPaint .setTextSize(r.getDimensionPixelSize(R.dimen.candidate_number_font_size) * mLIMEPref.getFontSize()); mSelKeyPaint.setStyle(Paint.Style.FILL_AND_STROKE); //final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); //Jeremy '12,4,23 add mContext parameter. The constructor without context is deprecated mGestureDetector = new GestureDetector(mContext, new GestureDetector.SimpleOnGestureListener() { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (DEBUG) Log.i(TAG, "onScroll(): distanceX = " + distanceX + "; distanceY = " + distanceY); //Jeremy '12,4,8 filter out small scroll which is actually candidate selection. if (Math.abs(distanceX) < mHeight / 5 && Math.abs(distanceY) < mHeight / 5) return true; mScrolled = true; // Update full candidate list before scroll checkHasMoreRecords(); int sx = getScrollX(); sx += distanceX; if (sx < 0) { sx = 0; } if (sx + getWidth() > mTotalWidth) { sx -= distanceX; } if (mLIMEPref.getParameterBoolean("candidate_switch", false)) { hasSlide = true; mTargetScrollX = sx; scrollTo(sx, getScrollY()); currentX = getScrollX(); //Jeremy '12,7,6 set currentX to the left edge of current scrollview after scrolled } else { hasSlide = false; if (distanceX < 0) { goLeft = true; goRight = false; } else if (distanceX > 0) { goLeft = false; goRight = true; } else { mTargetScrollX = sx; } } return true; } }); } /* * New embedded composing view inside candidate container for floating candidate mode. Jeremy '15,6,14 * (android 5.1 does not allow popup composing go over candidate area). */ public void setEmbeddedComposingView(TextView composingView) { if (DEBUG) Log.i(TAG, "setEmbeddedComposingView()"); embeddedComposing = composingView; } private UIHandler mHandler = new UIHandler(this); private static class UIHandler extends Handler { private final WeakReference<CandidateView> mCandidateViewWeakReference; private static final int MSG_UPDATE_UI = 1; private static final int MSG_UPDATE_COMPOSING = 2; private static final int MSG_HIDE_COMPOSING = 3; private static final int MSG_SHOW_CANDIDATE_POPUP = 4; private static final int MSG_HIDE_CANDIDATE_POPUP = 5; private static final int MSG_SET_COMPOSING = 6; public UIHandler(CandidateView candiInstance) { mCandidateViewWeakReference = new WeakReference<>(candiInstance); } @Override public void handleMessage(Message msg) { if (DEBUG) Log.i(TAG, "UIHandler.handlMessage(): message:" + msg.what); CandidateView mCandiInstance = mCandidateViewWeakReference.get(); if (mCandiInstance == null) return; switch (msg.what) { case MSG_UPDATE_UI: mCandiInstance.doUpdateUI(); break; case MSG_UPDATE_COMPOSING: mCandiInstance.doUpdateComposing(); break; case MSG_HIDE_COMPOSING: { mCandiInstance.doHideComposing(); break; } case MSG_SHOW_CANDIDATE_POPUP: { mCandiInstance.doUpdateCandidatePopup(); break; } case MSG_HIDE_CANDIDATE_POPUP: { mCandiInstance.doHideCandidatePopup(); break; } case MSG_SET_COMPOSING: { String composingText = (String) msg.obj; if (DEBUG) Log.i(TAG, "UIHandler.handleMessage(): composingText" + composingText); mCandiInstance.doSetComposing(composingText); break; } } } public void updateUI(int delay) { sendMessageDelayed(obtainMessage(MSG_UPDATE_UI, 0, 0, null), delay); } public void setComposing(String text, int delay) { sendMessageDelayed(obtainMessage(MSG_SET_COMPOSING, 0, 0, text), delay); } public void updateComposing(int delay) { sendMessageDelayed(obtainMessage(MSG_UPDATE_COMPOSING, 0, 0, null), delay); } public void dismissComposing(int delay) { sendMessageDelayed(obtainMessage(MSG_HIDE_COMPOSING, 0, 0, null), delay); } public void showCandidatePopup(int delay) { sendMessageDelayed(obtainMessage(MSG_SHOW_CANDIDATE_POPUP, 0, 0, null), delay); } public void dismissCandidatePopup(int delay) { sendMessageDelayed(obtainMessage(MSG_HIDE_CANDIDATE_POPUP, 0, 0, null), delay); } } public void doUpdateUI() { if (DEBUG) Log.i(TAG, "doUpdateUI()"); if ((mSuggestions == null || mSuggestions.isEmpty()) && (mCandidatePopupWindow != null && mCandidatePopupWindow.isShowing())) { doHideCandidatePopup(); return; } if (mCandidatePopupWindow != null && mCandidatePopupWindow.isShowing()) { doUpdateCandidatePopup(); } else { if (!waitingForMoreRecords) { // New suggestion list, reset scroll to (0,0); scrollTo(0, 0); mTargetScrollX = 0; } resetWidth(); // update layout width of this view invalidate(); // caused onDraw and update mTotoalX } waitingForMoreRecords = false; } protected void updateFontSize() { Resources r = mContext.getResources(); float scaling = mLIMEPref.getFontSize(); mVerticalPadding = (int) (r.getDimensionPixelSize(R.dimen.candidate_vertical_padding) * scaling); mCandidatePaint.setTextSize(r.getDimensionPixelSize(R.dimen.candidate_font_size) * scaling); mSelKeyPaint.setTextSize(r.getDimensionPixelSize(R.dimen.candidate_number_font_size) * scaling); configHeight = (int) (r.getDimensionPixelSize(R.dimen.candidate_stripe_height) * scaling); if (DEBUG) Log.i(TAG, "updateFontSize(), scaling=" + scaling + ", mVerticalPadding=" + mVerticalPadding); } private void doHideCandidatePopup() { if (DEBUG) Log.i(TAG, "doHideCandidatePopup()"); if (mCandidatePopupWindow != null && mCandidatePopupWindow.isShowing()) { mCandidatePopupWindow.dismiss(); //resetWidth(); } candidateExpanded = false; doUpdateUI(); } /* * Contains requestLayout() which can only call from UI thread */ private void resetWidth() { if (DEBUG) Log.i(TAG, "resetWidth() mHeight:" + mHeight); int candiWidth = mScreenWidth; if (mTotalWidth > mScreenWidth || isEmpty()) candiWidth -= mExpandButtonWidth; if (DEBUG) Log.i(TAG, "resetWidth() candiWidth:" + candiWidth); this.setLayoutParams(new LinearLayout.LayoutParams(candiWidth, mHeight)); requestLayout(); } public void doUpdateCandidatePopup() { if (DEBUG) Log.i(TAG, "doUpdateCandidatePopup(), mHeight:" + mHeight); //Jeremy '11,8.27 do vibrate and sound on candidateview expand button pressed. if (!candidateExpanded) mService.doVibrateSound(0); candidateExpanded = true; requestLayout(); checkHasMoreRecords(); if (mCandidatePopupWindow == null) { mCandidatePopupWindow = new PopupWindow(mContext); LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mCandidatePopupContainer = inflater.inflate(R.layout.candidatepopup, (ViewGroup) this.getRootView(), false); mCandidatePopupContainer.setBackgroundColor(mColorBackground); mCandidatePopupWindow.setContentView(mCandidatePopupContainer); ImageButton btnClose = (ImageButton) mCandidatePopupContainer.findViewById(R.id.closeButton); if (btnClose != null) { btnClose.setOnClickListener(this); btnClose.setImageDrawable(mDrawableCloseButton); btnClose.setBackgroundColor(mColorBackground); btnClose.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); mCloseButtonHeight = btnClose.getMeasuredHeight(); } mPopupScrollView = (ScrollView) mCandidatePopupContainer.findViewById(R.id.sv); CandidateExpandedView popupCandidate = (CandidateExpandedView) mCandidatePopupContainer .findViewById(R.id.candidatePopup); popupCandidate.setParentCandidateView(this); popupCandidate.setParentScrollView(mPopupScrollView); popupCandidate.setService(mService); mPopupCandidateView = popupCandidate; } if (mSuggestions.size() == 0) return; mCandidatePopupWindow.setContentView(mCandidatePopupContainer); int[] offsetOnScreen = new int[2]; this.getLocationOnScreen(offsetOnScreen); mPopupCandidateView.setSuggestions(mSuggestions); mPopupCandidateView.prepareLayout(); mPopupCandidateView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); int popHeight = mScreenHeight - offsetOnScreen[1]; if (mPopupCandidateView.getMeasuredHeight() + mCloseButtonHeight < popHeight) popHeight = mPopupCandidateView.getMeasuredHeight() + mCloseButtonHeight; if (!hasRoomForExpanding()) { popHeight = 3 * (configHeight + mVerticalPadding) + mCloseButtonHeight; if (DEBUG) Log.i(TAG, "doUpdateCandidatePopup(), " + "no enough room for expanded view, expand self first. newHeight:" + popHeight); if (mPopupCandidateView.getMeasuredHeight() + mCloseButtonHeight < popHeight) popHeight = mPopupCandidateView.getMeasuredHeight() + mCloseButtonHeight; this.setLayoutParams(new LinearLayout.LayoutParams(mScreenWidth - mExpandButtonWidth, popHeight)); } if (DEBUG) Log.i(TAG, "doUpdateCandidatePopup(), mHeight=" + mHeight + ", getHeight() = " + getHeight() + ", mPopupCandidateView.getHeight() = " + mPopupCandidateView.getHeight() + ", mPopupScrollView.getHeight() = " + mPopupScrollView.getHeight() + ", offsetOnScreen[1] = " + offsetOnScreen[1] + ", popHeight = " + popHeight + ", CandidateExpandedView.measureHeight = " + mPopupCandidateView.getMeasuredHeight() + ", btnClose.getMeasuredHeight() = " + mCloseButtonHeight); if (mCandidatePopupWindow.isShowing()) { if (DEBUG) Log.i(TAG, "doUpdateCandidatePopup(),mCandidatePopup.isShowing "); mCandidatePopupWindow.update(mScreenWidth, popHeight); } else { mCandidatePopupWindow.setWidth(mScreenWidth); mCandidatePopupWindow.setHeight(popHeight); mCandidatePopupWindow.showAsDropDown(this, 0, -getHeight()); mPopupScrollView.scrollTo(0, 0); } //Jeremy '12,5,31 do update layoutparams after popupWindow update or creation. mPopupCandidateView.setLayoutParams( new ScrollView.LayoutParams(ScrollView.LayoutParams.MATCH_PARENT, popHeight - mCloseButtonHeight)); mPopupScrollView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, popHeight - mCloseButtonHeight)); } public void setComposingText(String composingText) { if (DEBUG) Log.i(TAG, "setComposingText():composingText:" + composingText); if (!composingText.trim().equals("")) { mHandler.setComposing(composingText, 0); showComposing(); } else { hideComposing(); } } public void doHideComposing() { if (DEBUG) Log.i(TAG, "doHideComposing()"); if (mComposingTextView == null) return; if (embeddedComposing != null || // for embedded composing in floating candidateView (mComposingTextPopup != null // for fixed candidate View && (mComposingTextPopup.isShowing()) || mComposingTextView.getVisibility() == VISIBLE)) { mComposingTextView.setVisibility(INVISIBLE); } } /** * Jeremy '12,6,2 separated from doupdateComposing */ public void doSetComposing(String composingText) { if (DEBUG) Log.i(TAG, "doSetComposing():" + composingText + "; this.isShown()" + this.isShown() + "(mComposingTextView == null):" + (mComposingTextView == null) + ";(embeddedComposing == null):" + (embeddedComposing != null)); // Initialize mComposingTextView as embedding composing or popup window for fixed candidate mode. Jeremy '15,6,4 if (embeddedComposing != null) { if (mComposingTextView != embeddedComposing) { mComposingTextView = embeddedComposing; mComposingTextView.setBackgroundColor(mColorComposingBackground); mComposingTextView.setTextColor(mColorComposingText); } } else { if (mComposingPopupTextView == null) { LayoutInflater inflater = (LayoutInflater) mContext .getSystemService(Context.LAYOUT_INFLATER_SERVICE); mComposingPopupTextView = (TextView) inflater.inflate(R.layout.composingtext, (ViewGroup) getRootView(), false); if (mComposingTextPopup == null) { mComposingTextPopup = new PopupWindow(mContext); mComposingTextPopup.setTouchable(false); } //mComposingTextPopup.setWindowLayoutMode(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); //Deprecated in API 23. Jeremy '16,7,16 mComposingTextPopup.setWidth(LayoutParams.WRAP_CONTENT); mComposingTextPopup.setHeight(LayoutParams.WRAP_CONTENT); mComposingTextPopup.setContentView(mComposingPopupTextView); mComposingTextPopup.setBackgroundDrawable(null); } if (mComposingTextView != mComposingPopupTextView) { mComposingTextView = mComposingPopupTextView; mComposingTextView.setBackgroundColor(mColorComposingBackground); mComposingTextView.setTextColor(mColorComposingText); } } if (composingText != null) { mComposingTextView.setText(composingText); //The textsize got is coverted into PX already. Thus force setup the setTextSize in unit of PX. float scaledTextSize = mContext.getResources().getDimensionPixelSize(R.dimen.composing_text_size) * mLIMEPref.getFontSize(); mComposingTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, scaledTextSize); } else return; mComposingTextView.invalidate(); //Jeremy '12,6,2 invalidate and measure so as to get correct height and width later. mComposingTextView.setVisibility(VISIBLE); //Jeremy '15,6, 4 bypass updating popup when composing view is embedded in candidate container if (embeddedComposing == null) doUpdateComposing(); } /** * Update composing to correct location with a delay after setComposing. */ public void doUpdateComposing() { if (DEBUG) Log.i(TAG, "doUpdateComposing(): this.isShown()" + this.isShown() + "; embeddedComposing is null:" + (embeddedComposing == null)); if (embeddedComposing != null) return; //Jeremy '15,6, 4 bypass updating popup when composing view is embedded in candidate container //mComposingTextView.measure( //MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), //MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); //final int popupWidth = mComposingTextView.getMeasuredWidth(); //Jeremy '12,6,2 use getWidth and getHeight instead //final int popupHeight = mComposingTextView.getMeasuredHeight(); // getMeasuredWidth cannot get correct width of textVIEW in android 6 Jeremy '16,7,16 String composingText = String.valueOf(mComposingTextView.getText()); if (composingText == null) return; // avoid measureText on null object. Jeremy '16/7/26 Paint paint = mComposingTextView.getPaint(); Paint.FontMetrics metrics = paint.getFontMetrics(); final int popupWidth = (int) paint.measureText(composingText); final int popupHeight = (int) (metrics.bottom - metrics.top); int[] offsetInWindow = new int[2]; this.getLocationInWindow(offsetInWindow); int mPopupComposingY = offsetInWindow[1]; int mPopupComposingX = 0; mPopupComposingY -= popupHeight; if (DEBUG) Log.i(TAG, "doUpdateComposing():mPopupComposingX:" + mPopupComposingX + ". mPopupComposingY:" + mPopupComposingY + ". popupWidth = " + popupWidth + ". popupHeight = " + popupHeight + ". mComposingTextPopup.isShowing()=" + mComposingTextPopup.isShowing()); try { if (mComposingTextPopup.isShowing()) { mComposingTextPopup.update(mPopupComposingX, mPopupComposingY, popupWidth, popupHeight); } else { mComposingTextPopup.setWidth(popupWidth); mComposingTextPopup.setHeight(popupHeight); mComposingTextPopup.showAtLocation(this, Gravity.NO_GRAVITY, mPopupComposingX, mPopupComposingY); } } catch (Exception e) { // ignore candidate construct error e.printStackTrace(); } } public void showComposing() { if (DEBUG) Log.i(TAG, "showComposing()"); //jeremy '12,6,3 moved the creation of mComposingTextPopup and mComposingTextView from doUpdateComposing //Jeremy '12,4,8 to avoid fc when hard keyboard is engaged and candidateview is not shown if (!this.isShown()) return; mHandler.updateComposing(50); } public void hideComposing() { if (DEBUG) Log.i(TAG, "hideComposing()"); mHandler.dismissComposing(100); //Jeremy '12,6,3 the same delay as showComposing to avoid showed after hided } public void showCandidatePopup() { if (DEBUG) Log.i(TAG, "showCandidatePopup()"); mHandler.showCandidatePopup(0); } public void hideCandidatePopup() { if (DEBUG) Log.i(TAG, "hideCandidatePopup()"); mHandler.dismissCandidatePopup(0); } public boolean isCandidateExpanded() { return candidateExpanded; } private boolean mHasRoomForExpanding = true; public boolean hasRoomForExpanding() { if (!mCandidatePopupWindow.isShowing()) { int[] offsetOnScreen = new int[2]; this.getLocationOnScreen(offsetOnScreen); mHasRoomForExpanding = (mScreenHeight - offsetOnScreen[1]) > 2 * mHeight; } return mHasRoomForExpanding; } public void setTransparentCandidateView(boolean transparent) { mTransparentCandidateView = transparent; } /** * A connection back to the service to communicate with the text field */ public void setService(LIMEService listener) { mService = listener; } @Override public int computeHorizontalScrollRange() { return mTotalWidth; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (DEBUG) Log.i(TAG, "onMeasure()"); int measuredWidth = resolveSize(mTotalWidth, widthMeasureSpec); final int desiredHeight = mHeight; // Maximum possible width and desired height setMeasuredDimension(measuredWidth, resolveSize(desiredHeight, heightMeasureSpec)); } /** * If the canvas is null, then only touch calculations are performed to pick the target * candidate. */ @Override protected synchronized void onDraw(Canvas canvas) { doDraw(canvas); } private void prepareLayout() { doDraw(null); } private void doDraw(Canvas canvas) { if (mSuggestions == null) return; if (DEBUG) Log.i(TAG, "CandidateView:doDraw():Suggestion mCount:" + mCount + " mSuggestions.size:" + mSuggestions.size()); mTotalWidth = 0; updateFontSize(); if (mBgPadding == null) { mBgPadding = new Rect(0, 0, 0, 0); if (getBackground() != null) { getBackground().getPadding(mBgPadding); } } final int height = mHeight; final Rect bgPadding = mBgPadding; final Paint candidatePaint = mCandidatePaint; final Paint candidateEmojiPaint = mCandidatePaint; candidateEmojiPaint.setTextSize((float) (candidateEmojiPaint.getTextSize() * 0.9)); final Paint selKeyPaint = mSelKeyPaint; final int touchX = mTouchX; final int scrollX = getScrollX(); final boolean scrolled = mScrolled; final int textBaseLine = (int) (((height - mCandidatePaint.getTextSize()) / 2) - mCandidatePaint.ascent()); // Modified by jeremy '10, 3, 29. Update mselectedindex if touched and build wordX[i] and wordwidth[i] int x = 0; final int count = mCount; //Cache count here '11,8,18 for (int i = 0; i < count; i++) { if (count != mCount || mSuggestions == null || count != mSuggestions.size() || mSuggestions.size() == 0 || i >= mSuggestions.size()) return; // mSuggestion is updated, force abort String suggestion = mSuggestions.get(i).getWord(); if (i == 0 && mSuggestions.size() > 1 && mSuggestions.get(1).isRuntimeBuiltPhraseRecord() && suggestion.length() > 8) { suggestion = suggestion.substring(0, 2) + ".."; } float base = (suggestion == null) ? 0 : candidatePaint.measureText(""); float textWidth = (suggestion == null) ? 0 : candidatePaint.measureText(suggestion); if (textWidth < base) { textWidth = base; } final int wordWidth = (int) textWidth + X_GAP * 2; mWordX[i] = x; mWordWidth[i] = wordWidth; if (touchX + scrollX >= x && touchX + scrollX < x + wordWidth && !scrolled) { mSelectedIndex = i; } x += wordWidth; } mTotalWidth = x; if (DEBUG) Log.i(TAG, "CandidateView:doDraw():mTotalWidth :" + mTotalWidth + " this.getWidth():" + this.getWidth()); //Jeremy '11,8,11. If the candidate list is within 1 page and has more records, get full records first. if (mTotalWidth < this.getWidth()) checkHasMoreRecords(); // Paint all the suggestions and lines. if (canvas != null) { // Moved from above by jeremy '10 3, 29. Paint mSelectedindex in highlight here if (count > 0 && mSelectedIndex >= 0) { // candidatePaint.setColor(mColorComposingCode); // canvas.drawRect(mWordX[mSelectedIndex],bgPadding.top, mWordWidth[mSelectedIndex] , height, candidatePaint); canvas.translate(mWordX[mSelectedIndex], 0); mDrawableSuggestHighlight.setBounds(0, bgPadding.top, mWordWidth[mSelectedIndex], height); mDrawableSuggestHighlight.draw(canvas); canvas.translate(-mWordX[mSelectedIndex], 0); } if (mTransparentCandidateView) { canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); Paint backgroundPaint = new Paint(); backgroundPaint.setColor(ContextCompat.getColor(mContext, R.color.third_background_light)); backgroundPaint.setAlpha(33); backgroundPaint.setStyle(Paint.Style.FILL); canvas.drawRect(0.5f, bgPadding.top, mScreenWidth, height, backgroundPaint); } for (int i = 0; i < count; i++) { if (count != mCount || mSuggestions == null || count != mSuggestions.size() || mSuggestions.size() == 0 || i >= mSuggestions.size()) break; boolean isEmoji = mSuggestions.get(i).isEmojiRecord(); String suggestion = mSuggestions.get(i).getWord(); if (i == 0 && mSuggestions.size() > 1 && mSuggestions.get(1).isRuntimeBuiltPhraseRecord() && suggestion.length() > 8) { suggestion = suggestion.substring(0, 2) + ".."; } int c = i + 1; switch (mSuggestions.get(i).getRecordType()) { case Mapping.RECORD_COMPOSING_CODE: if (mSelectedIndex == 0) { if (mTransparentCandidateView) { candidatePaint.setColor(mColorInvertedTextTransparent); } else { candidatePaint.setColor(mColorComposingCodeHighlight); } } else candidatePaint.setColor(mColorComposingCode); break; case Mapping.RECORD_CHINESE_PUNCTUATION_SYMBOL: case Mapping.RECORD_RELATED_PHRASE: selKeyPaint.setColor(mColorSelKeyShifted); if (i == mSelectedIndex) candidatePaint.setColor(mColorNormalTextHighlight); else candidatePaint.setColor(mColorNormalText); break; case Mapping.RECORD_EXACT_MATCH_TO_CODE: case Mapping.RECORD_PARTIAL_MATCH_TO_CODE: case Mapping.RECORD_RUNTIME_BUILT_PHRASE: case Mapping.RECORD_ENGLISH_SUGGESTION: default: selKeyPaint.setColor(mColorSelKey); if (i == mSelectedIndex) candidatePaint.setColor(mColorNormalTextHighlight); else candidatePaint.setColor(mColorNormalText); break; } if (isEmoji) { canvas.drawText(suggestion, mWordX[i] + X_GAP, Math.round(textBaseLine * 0.95), candidateEmojiPaint); } else { canvas.drawText(suggestion, mWordX[i] + X_GAP, textBaseLine, candidatePaint); } if (mShowNumber) { //Jeremy '11,6,17 changed from <=10 to mDisplaySelkey length. The length maybe 11 or 12 if shifted with space. if (c <= mDisplaySelkey.length()) { //Jeremy '11,6,11 Drawing text using relative font dimensions. canvas.drawText(mDisplaySelkey.substring(c - 1, c), mWordX[i] + mWordWidth[i] - height * 0.3f, height * 0.4f, selKeyPaint); } } //Draw spacer candidatePaint.setColor(mColorSpacer); canvas.drawLine(mWordX[i] + mWordWidth[i] + 0.5f, bgPadding.top + (mVerticalPadding / 2), mWordX[i] + mWordWidth[i] + 0.5f, height - (mVerticalPadding / 2), candidatePaint); candidatePaint.setFakeBoldText(false); } if (mTargetScrollX != getScrollX()) { if (DEBUG) Log.i(TAG, "CandidateView:doDraw():mTargetScrollX :" + mTargetScrollX + " getScrollX():" + getScrollX()); scrollToTarget(); } } } private boolean checkHasMoreRecords() { if (DEBUG) Log.i(TAG, "checkHasMoreRecords(), waitingForMoreRecords = " + waitingForMoreRecords); if (waitingForMoreRecords) return false; //Jeremy '12,7,6 avoid repeated calls of requestFullrecords(). if (mSuggestions != null && mSuggestions.size() > 0 && mSuggestions.get(mSuggestions.size() - 1).getCode() != null && mSuggestions.get(mSuggestions.size() - 1).isHasMoreRecordsMarkRecord()) { //getCode().equals("has_more_records")) { waitingForMoreRecords = true; Thread updatingThread = new Thread() { public void run() { mService.requestFullRecords(mSuggestions.get(0).isRelatedPhraseRecord()); } }; updatingThread.start(); return true; } return false; } private void scrollToTarget() { int sx = getScrollX(); if (mTargetScrollX > sx) { sx += SCROLL_PIXELS; if (sx >= mTargetScrollX) { sx = mTargetScrollX; requestLayout(); } } else { sx -= SCROLL_PIXELS; if (sx <= mTargetScrollX) { sx = mTargetScrollX; requestLayout(); } } scrollTo(sx, getScrollY()); invalidate(); } public void setSuggestions(List<Mapping> suggestions, boolean showNumber, String displaySelkey) { mDisplaySelkey = displaySelkey; setSuggestions(suggestions, showNumber); } public synchronized void setSuggestions(List<Mapping> suggestions, boolean showNumber) { //clear(); //Jeremy '11,8,14 moved from clear(); if (DEBUG) Log.i(TAG, "setSuggestions()"); Resources res = mContext.getResources(); configHeight = (int) (res.getDimensionPixelSize(R.dimen.candidate_stripe_height) * mLIMEPref.getFontSize()); mVerticalPadding = (int) (res.getDimensionPixelSize(R.dimen.candidate_vertical_padding) * mLIMEPref.getFontSize()); mHeight = configHeight + mVerticalPadding; currentX = 0; mTouchX = OUT_OF_BOUNDS; mCount = 0; mSelectedIndex = -1; if (mLIMEPref.getDisablePhysicalSelKeyOption()) { showNumber = true; } mShowNumber = showNumber; if (mShowNumber) X_GAP = (int) (res.getDimensionPixelSize(R.dimen.candidate_font_size) * 0.35f);//13; else X_GAP = (int) (res.getDimensionPixelSize(R.dimen.candidate_font_size) * 0.25f); if (suggestions != null) { mSuggestions = new LinkedList<>(suggestions); if (mSuggestions.size() > 0) { // Add by Jeremy '10, 3, 29 mCount = mSuggestions.size(); if (mCount > MAX_SUGGESTIONS) mCount = MAX_SUGGESTIONS; if (DEBUG) Log.i(TAG, "setSuggestions():mSuggestions.size():" + mSuggestions.size() + " mCount=" + mCount); if (mCount > 1 && mSuggestions.get(1).isExactMatchToCodeRecord()) { mSelectedIndex = 1; } else if (mCount > 0 && (mSuggestions.get(0).isComposingCodeRecord() || mSuggestions.get(0).isRuntimeBuiltPhraseRecord())) { /* int seloption = mLIMEPref.getSelkeyOption(); if(seloption > 0 && suggestions.size() > seloption){ mSelectedIndex = seloption; }else{*/ mSelectedIndex = 0; //} } else { // no default selection for related phrase, chinese punctuation symbols1 and English suggestions Jeremy '15,6,4 mSelectedIndex = -1; } } else { if (DEBUG) Log.i(TAG, "setSuggestions():mSuggestions=null"); } } else { mSuggestions = new LinkedList<>(); hideCandidatePopup(); } prepareLayout(); mHandler.updateUI(0); } public void clear() { if (DEBUG) Log.i(TAG, "clear()"); //mHeight =0; //Jeremy '12,5,6 hide candidate bar when candidateview is fixed. if (mSuggestions != null) mSuggestions.clear(); mCount = 0; // Jeremy 11,8,14 close all popup on clear setComposingText(""); mTargetScrollX = 0; mTotalWidth = 0; hideComposing(); prepareLayout(); mHandler.updateUI(0); Resources r = mContext.getResources(); configHeight = (int) (r.getDimensionPixelSize(R.dimen.candidate_stripe_height) * mLIMEPref.getFontSize()); mVerticalPadding = (int) (r.getDimensionPixelSize(R.dimen.candidate_vertical_padding) * mLIMEPref.getFontSize()); configHeight = (int) (r.getDimensionPixelSize(R.dimen.candidate_stripe_height) * mLIMEPref.getFontSize()); mHeight = configHeight + mVerticalPadding; } public boolean isEmpty() { return mCount == 0; } public void startVoiceInput() { if (mService != null) mService.startVoiceInput(); } //Jeremy '12,5,6 hide candidate bar when candidateView is fixed. public void forceHide() { if (DEBUG) Log.i(TAG, "forceHide()"); mHeight = 0; //clear(); //resetWidth();// will cause wrong thread exception. clear() will call updateUI() and will do resetWidth mSuggestions = EMPTY_LIST; // Jeremy 11,8,14 close all popup on clear setComposingText(""); mTargetScrollX = 0; mTotalWidth = 0; mHandler.dismissComposing(0); mHandler.updateUI(0); } @Override public boolean onTouchEvent(@NonNull MotionEvent me) { if (DEBUG) Log.i(TAG, "OnTouchEvent() action = " + me.getAction()); if (mGestureDetector != null && mGestureDetector.onTouchEvent(me)) { if (DEBUG) Log.i(TAG, "OnTouchEvent() event processed by mGestureDetector"); return true; } int action = me.getAction(); int x = (int) me.getX(); int y = (int) me.getY(); mTouchX = x; switch (action) { case MotionEvent.ACTION_DOWN: mScrolled = false; invalidate(); break; case MotionEvent.ACTION_MOVE: if (y <= 0) { // Fling up!? if (mSelectedIndex >= 0) { takeSelectedSuggestion(true); mSelectedIndex = -1; } } invalidate(); break; case MotionEvent.ACTION_UP: if (DEBUG) Log.i(TAG, "OnTouchEvent():MotionEvent.ACTION_UP, mScrolled=" + mScrolled + "; mSelectedIndex = " + mSelectedIndex); if (!mScrolled) { if (mSelectedIndex >= 0) { takeSelectedSuggestion(true); } } mSelectedIndex = -1; removeHighlight(); requestLayout(); if (!hasSlide) { if (goLeft) { scrollPrev(); } if (goRight) { scrollNext(); } } performClick(); break; } return true; } @Override public boolean performClick() { // Calls the super implementation, which generates an AccessibilityEvent // and calls the onClick() listener on the view, if any super.performClick(); return true; } public void scrollPrev() { int i = 0; //final int mCount = mSuggestions.size(); int firstItem = 0; // Actually just before the first item, if at the boundary while (i < mCount) { if (mWordX[i] < currentX && mWordX[i] + mWordWidth[i] >= currentX - 1) { firstItem = i; break; } i++; } int leftEdge = mWordX[firstItem] + mWordWidth[firstItem] - getWidth(); if (leftEdge < 0) { leftEdge = 0; currentX = leftEdge; } else { currentX = leftEdge; } updateScrollPosition(leftEdge); } public void scrollNext() { if (DEBUG) Log.i(TAG, "scrollNext(), currentX = " + currentX + ", mSelectedIndex = " + mSelectedIndex); checkHasMoreRecords(); //Jeremy '12,7,6 check if has more records before scroll int i = 0; int targetX = currentX; //final int mCount = mSuggestions.size(); int rightEdge = currentX + getWidth(); while (i < mCount) { if (mWordX[i] <= rightEdge && mWordX[i] + mWordWidth[i] >= rightEdge) { targetX = Math.min(mWordX[i], mTotalWidth - getWidth()); currentX = mWordX[i]; break; } i++; } if (DEBUG) Log.i(TAG, "scrollNext(), new currentX = " + currentX + ", new mSelectedIndex = " + mSelectedIndex); updateScrollPosition(targetX); } private void updateScrollPosition(int targetX) { if (targetX != mTouchX) { mTargetScrollX = targetX; requestLayout(); invalidate(); mScrolled = true; } } //Add by Jeremy '10, 3, 29 for DPAD (physical keyboard) selection. public void selectNext() { if (DEBUG) Log.i(TAG, "selectNext(), currentX = " + currentX + ", mSelectedIndex = " + mSelectedIndex); if (mSuggestions == null) return; if (mCandidatePopupWindow != null && mCandidatePopupWindow.isShowing()) { mPopupCandidateView.selectNext(); } else { if (mSelectedIndex < mCount - 1) { mSelectedIndex++; if (mWordX[mSelectedIndex] + mWordWidth[mSelectedIndex] > currentX + getWidth()) scrollNext(); //Jeremy '12,7,6 if the selected index is not in current visible area, set the selected index to the fist item visible int rightEdge = currentX + getWidth(); if (mWordX[mSelectedIndex] < currentX || mWordX[mSelectedIndex] + mWordWidth[mSelectedIndex] > rightEdge) { for (int i = 0; i < mCount - 1; i++) if (mWordX[i] >= currentX) { mSelectedIndex = i; break; } } } invalidate(); } } public void selectPrev() { if (DEBUG) Log.i(TAG, "selectPrev(), currentX = " + currentX + ", mSelectedIndex = " + mSelectedIndex); if (mSuggestions == null) return; if (mCandidatePopupWindow != null && mCandidatePopupWindow.isShowing()) { mPopupCandidateView.selectPrev(); } else { if (mSelectedIndex > 0) { mSelectedIndex--; if (mWordX[mSelectedIndex] < currentX) scrollPrev(); } //Jeremy '12,7,6 if the selected index is not in current visible area, set the selected index to the last item visible int rightEdge = currentX + getWidth(); if (mSelectedIndex == -1 || mWordX[mSelectedIndex] < currentX || mWordX[mSelectedIndex] + mWordWidth[mSelectedIndex] > rightEdge) { for (int i = mCount - 2; i < mCount - 1; i--) if (mWordX[i] + mWordWidth[i] <= rightEdge) { mSelectedIndex = i; break; } } invalidate(); } } //Jeremy '11,8,28 public void selectNextRow() { if (mSuggestions == null) return; if (mCandidatePopupWindow != null && mCandidatePopupWindow.isShowing()) mPopupCandidateView.selectNextRow(); else if (mScreenWidth < mTotalWidth) showCandidatePopup(); } public void selectPrevRow() { if (mSuggestions == null) return; if (mCandidatePopupWindow != null && mCandidatePopupWindow.isShowing()) mPopupCandidateView.selectPrevRow(); } public boolean takeSuggstionAtIndex(int index) { if (DEBUG) { Log.i(TAG, "takeSuggestion():mSelectedIndex:" + mSelectedIndex); } if (mSuggestions != null && index >= 0 && index <= mSuggestions.size()) { mService.pickCandidateManually(index); return true; // Selection picked } else return false; } public boolean takeSelectedSuggestion() { return this.takeSelectedSuggestion(false); } public boolean takeSelectedSuggestion(boolean vibrateSound) { if (DEBUG) { Log.i(TAG, "takeSelectedSuggestion():mSelectedIndex:" + mSelectedIndex); } //Jeremy '11,9,1 do vibrate and sound on suggestion picked from candidateview if (vibrateSound) mService.doVibrateSound(0); hideComposing(); //Jeremy '12,5,6 if (mCandidatePopupWindow != null && mCandidatePopupWindow.isShowing()) { hideCandidatePopup(); return takeSuggstionAtIndex(mPopupCandidateView.mSelectedIndex); } else return takeSuggstionAtIndex(mSelectedIndex); } /** * For flick through from keyboard, call this method with the x coordinate of the flick * gesture. */ /*public void takeSuggestionAt(float x) { mTouchX = (int) x; // To detect candidate onDraw(null); takeSelectedSuggestion(); invalidate(); }*/ private void removeHighlight() { mTouchX = OUT_OF_BOUNDS; invalidate(); } @Override public void onDetachedFromWindow() { if (DEBUG) Log.i(TAG, "onDetachedFromWindow() "); super.onDetachedFromWindow(); hideComposing(); hideCandidatePopup(); } @Override public void onClick(View v) { //Jeremy '11,8.27 do vibrate and sound on candidateexpandedview close button pressed. mService.doVibrateSound(0); hideCandidatePopup(); } }