diff options
Diffstat (limited to 'java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripView.java')
-rw-r--r-- | java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripView.java | 491 |
1 files changed, 491 insertions, 0 deletions
diff --git a/java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripView.java new file mode 100644 index 000000000..9e75a8f8d --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripView.java @@ -0,0 +1,491 @@ +/* + * Copyright (C) 2011 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 org.kelar.inputmethod.latin.suggestions; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import androidx.core.view.ViewCompat; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.GestureDetector; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.widget.ImageButton; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import org.kelar.inputmethod.accessibility.AccessibilityUtils; +import org.kelar.inputmethod.keyboard.Keyboard; +import org.kelar.inputmethod.keyboard.MainKeyboardView; +import org.kelar.inputmethod.keyboard.MoreKeysPanel; +import org.kelar.inputmethod.latin.AudioAndHapticFeedbackManager; +import org.kelar.inputmethod.latin.R; +import org.kelar.inputmethod.latin.SuggestedWords; +import org.kelar.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import org.kelar.inputmethod.latin.common.Constants; +import org.kelar.inputmethod.latin.define.DebugFlags; +import org.kelar.inputmethod.latin.settings.Settings; +import org.kelar.inputmethod.latin.settings.SettingsValues; +import org.kelar.inputmethod.latin.suggestions.MoreSuggestionsView.MoreSuggestionsListener; +import org.kelar.inputmethod.latin.utils.ImportantNoticeUtils; + +import java.util.ArrayList; + +public final class SuggestionStripView extends RelativeLayout implements OnClickListener, + OnLongClickListener { + public interface Listener { + public void showImportantNoticeContents(); + public void pickSuggestionManually(SuggestedWordInfo word); + public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat); + } + + static final boolean DBG = DebugFlags.DEBUG_ENABLED; + private static final float DEBUG_INFO_TEXT_SIZE_IN_DIP = 6.0f; + + private final ViewGroup mSuggestionsStrip; + private final ImageButton mVoiceKey; + private final View mImportantNoticeStrip; + MainKeyboardView mMainKeyboardView; + + private final View mMoreSuggestionsContainer; + private final MoreSuggestionsView mMoreSuggestionsView; + private final MoreSuggestions.Builder mMoreSuggestionsBuilder; + + private final ArrayList<TextView> mWordViews = new ArrayList<>(); + private final ArrayList<TextView> mDebugInfoViews = new ArrayList<>(); + private final ArrayList<View> mDividerViews = new ArrayList<>(); + + Listener mListener; + private SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance(); + private int mStartIndexOfMoreSuggestions; + + private final SuggestionStripLayoutHelper mLayoutHelper; + private final StripVisibilityGroup mStripVisibilityGroup; + + private static class StripVisibilityGroup { + private final View mSuggestionStripView; + private final View mSuggestionsStrip; + private final View mImportantNoticeStrip; + + public StripVisibilityGroup(final View suggestionStripView, + final ViewGroup suggestionsStrip, final View importantNoticeStrip) { + mSuggestionStripView = suggestionStripView; + mSuggestionsStrip = suggestionsStrip; + mImportantNoticeStrip = importantNoticeStrip; + showSuggestionsStrip(); + } + + public void setLayoutDirection(final boolean isRtlLanguage) { + final int layoutDirection = isRtlLanguage ? ViewCompat.LAYOUT_DIRECTION_RTL + : ViewCompat.LAYOUT_DIRECTION_LTR; + ViewCompat.setLayoutDirection(mSuggestionStripView, layoutDirection); + ViewCompat.setLayoutDirection(mSuggestionsStrip, layoutDirection); + ViewCompat.setLayoutDirection(mImportantNoticeStrip, layoutDirection); + } + + public void showSuggestionsStrip() { + mSuggestionsStrip.setVisibility(VISIBLE); + mImportantNoticeStrip.setVisibility(INVISIBLE); + } + + public void showImportantNoticeStrip() { + mSuggestionsStrip.setVisibility(INVISIBLE); + mImportantNoticeStrip.setVisibility(VISIBLE); + } + + public boolean isShowingImportantNoticeStrip() { + return mImportantNoticeStrip.getVisibility() == VISIBLE; + } + } + + /** + * Construct a {@link SuggestionStripView} for showing suggestions to be picked by the user. + * @param context + * @param attrs + */ + public SuggestionStripView(final Context context, final AttributeSet attrs) { + this(context, attrs, R.attr.suggestionStripViewStyle); + } + + public SuggestionStripView(final Context context, final AttributeSet attrs, + final int defStyle) { + super(context, attrs, defStyle); + + final LayoutInflater inflater = LayoutInflater.from(context); + inflater.inflate(R.layout.suggestions_strip, this); + + mSuggestionsStrip = (ViewGroup)findViewById(R.id.suggestions_strip); + mVoiceKey = (ImageButton)findViewById(R.id.suggestions_strip_voice_key); + mImportantNoticeStrip = findViewById(R.id.important_notice_strip); + mStripVisibilityGroup = new StripVisibilityGroup(this, mSuggestionsStrip, + mImportantNoticeStrip); + + for (int pos = 0; pos < SuggestedWords.MAX_SUGGESTIONS; pos++) { + final TextView word = new TextView(context, null, R.attr.suggestionWordStyle); + word.setContentDescription(getResources().getString(R.string.spoken_empty_suggestion)); + word.setOnClickListener(this); + word.setOnLongClickListener(this); + mWordViews.add(word); + final View divider = inflater.inflate(R.layout.suggestion_divider, null); + mDividerViews.add(divider); + final TextView info = new TextView(context, null, R.attr.suggestionWordStyle); + info.setTextColor(Color.WHITE); + info.setTextSize(TypedValue.COMPLEX_UNIT_DIP, DEBUG_INFO_TEXT_SIZE_IN_DIP); + mDebugInfoViews.add(info); + } + + mLayoutHelper = new SuggestionStripLayoutHelper( + context, attrs, defStyle, mWordViews, mDividerViews, mDebugInfoViews); + + mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null); + mMoreSuggestionsView = (MoreSuggestionsView)mMoreSuggestionsContainer + .findViewById(R.id.more_suggestions_view); + mMoreSuggestionsBuilder = new MoreSuggestions.Builder(context, mMoreSuggestionsView); + + final Resources res = context.getResources(); + mMoreSuggestionsModalTolerance = res.getDimensionPixelOffset( + R.dimen.config_more_suggestions_modal_tolerance); + mMoreSuggestionsSlidingDetector = new GestureDetector( + context, mMoreSuggestionsSlidingListener); + + final TypedArray keyboardAttr = context.obtainStyledAttributes(attrs, + R.styleable.Keyboard, defStyle, R.style.SuggestionStripView); + final Drawable iconVoice = keyboardAttr.getDrawable(R.styleable.Keyboard_iconShortcutKey); + keyboardAttr.recycle(); + mVoiceKey.setImageDrawable(iconVoice); + mVoiceKey.setOnClickListener(this); + } + + /** + * A connection back to the input method. + * @param listener + */ + public void setListener(final Listener listener, final View inputView) { + mListener = listener; + mMainKeyboardView = (MainKeyboardView)inputView.findViewById(R.id.keyboard_view); + } + + public void updateVisibility(final boolean shouldBeVisible, final boolean isFullscreenMode) { + final int visibility = shouldBeVisible ? VISIBLE : (isFullscreenMode ? GONE : INVISIBLE); + setVisibility(visibility); + final SettingsValues currentSettingsValues = Settings.getInstance().getCurrent(); + mVoiceKey.setVisibility(currentSettingsValues.mShowsVoiceInputKey ? VISIBLE : INVISIBLE); + } + + public void setSuggestions(final SuggestedWords suggestedWords, final boolean isRtlLanguage) { + clear(); + mStripVisibilityGroup.setLayoutDirection(isRtlLanguage); + mSuggestedWords = suggestedWords; + mStartIndexOfMoreSuggestions = mLayoutHelper.layoutAndReturnStartIndexOfMoreSuggestions( + getContext(), mSuggestedWords, mSuggestionsStrip, this); + mStripVisibilityGroup.showSuggestionsStrip(); + } + + public void setMoreSuggestionsHeight(final int remainingHeight) { + mLayoutHelper.setMoreSuggestionsHeight(remainingHeight); + } + + // This method checks if we should show the important notice (checks on permanent storage if + // it has been shown once already or not, and if in the setup wizard). If applicable, it shows + // the notice. In all cases, it returns true if it was shown, false otherwise. + public boolean maybeShowImportantNoticeTitle() { + final SettingsValues currentSettingsValues = Settings.getInstance().getCurrent(); + if (!ImportantNoticeUtils.shouldShowImportantNotice(getContext(), currentSettingsValues)) { + return false; + } + if (getWidth() <= 0) { + return false; + } + final String importantNoticeTitle = ImportantNoticeUtils.getSuggestContactsNoticeTitle( + getContext()); + if (TextUtils.isEmpty(importantNoticeTitle)) { + return false; + } + if (isShowingMoreSuggestionPanel()) { + dismissMoreSuggestionsPanel(); + } + mLayoutHelper.layoutImportantNotice(mImportantNoticeStrip, importantNoticeTitle); + mStripVisibilityGroup.showImportantNoticeStrip(); + mImportantNoticeStrip.setOnClickListener(this); + return true; + } + + public void clear() { + mSuggestionsStrip.removeAllViews(); + removeAllDebugInfoViews(); + mStripVisibilityGroup.showSuggestionsStrip(); + dismissMoreSuggestionsPanel(); + } + + private void removeAllDebugInfoViews() { + // The debug info views may be placed as children views of this {@link SuggestionStripView}. + for (final View debugInfoView : mDebugInfoViews) { + final ViewParent parent = debugInfoView.getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup)parent).removeView(debugInfoView); + } + } + } + + private final MoreSuggestionsListener mMoreSuggestionsListener = new MoreSuggestionsListener() { + @Override + public void onSuggestionSelected(final SuggestedWordInfo wordInfo) { + mListener.pickSuggestionManually(wordInfo); + dismissMoreSuggestionsPanel(); + } + + @Override + public void onCancelInput() { + dismissMoreSuggestionsPanel(); + } + }; + + private final MoreKeysPanel.Controller mMoreSuggestionsController = + new MoreKeysPanel.Controller() { + @Override + public void onDismissMoreKeysPanel() { + mMainKeyboardView.onDismissMoreKeysPanel(); + } + + @Override + public void onShowMoreKeysPanel(final MoreKeysPanel panel) { + mMainKeyboardView.onShowMoreKeysPanel(panel); + } + + @Override + public void onCancelMoreKeysPanel() { + dismissMoreSuggestionsPanel(); + } + }; + + public boolean isShowingMoreSuggestionPanel() { + return mMoreSuggestionsView.isShowingInParent(); + } + + public void dismissMoreSuggestionsPanel() { + mMoreSuggestionsView.dismissMoreKeysPanel(); + } + + @Override + public boolean onLongClick(final View view) { + AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback( + Constants.NOT_A_CODE, this); + return showMoreSuggestions(); + } + + boolean showMoreSuggestions() { + final Keyboard parentKeyboard = mMainKeyboardView.getKeyboard(); + if (parentKeyboard == null) { + return false; + } + final SuggestionStripLayoutHelper layoutHelper = mLayoutHelper; + if (mSuggestedWords.size() <= mStartIndexOfMoreSuggestions) { + return false; + } + final int stripWidth = getWidth(); + final View container = mMoreSuggestionsContainer; + final int maxWidth = stripWidth - container.getPaddingLeft() - container.getPaddingRight(); + final MoreSuggestions.Builder builder = mMoreSuggestionsBuilder; + builder.layout(mSuggestedWords, mStartIndexOfMoreSuggestions, maxWidth, + (int)(maxWidth * layoutHelper.mMinMoreSuggestionsWidth), + layoutHelper.getMaxMoreSuggestionsRow(), parentKeyboard); + mMoreSuggestionsView.setKeyboard(builder.build()); + container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + + final MoreKeysPanel moreKeysPanel = mMoreSuggestionsView; + final int pointX = stripWidth / 2; + final int pointY = -layoutHelper.mMoreSuggestionsBottomGap; + moreKeysPanel.showMoreKeysPanel(this, mMoreSuggestionsController, pointX, pointY, + mMoreSuggestionsListener); + mOriginX = mLastX; + mOriginY = mLastY; + for (int i = 0; i < mStartIndexOfMoreSuggestions; i++) { + mWordViews.get(i).setPressed(false); + } + return true; + } + + // Working variables for {@link onInterceptTouchEvent(MotionEvent)} and + // {@link onTouchEvent(MotionEvent)}. + private int mLastX; + private int mLastY; + private int mOriginX; + private int mOriginY; + private final int mMoreSuggestionsModalTolerance; + private boolean mNeedsToTransformTouchEventToHoverEvent; + private boolean mIsDispatchingHoverEventToMoreSuggestions; + private final GestureDetector mMoreSuggestionsSlidingDetector; + private final GestureDetector.OnGestureListener mMoreSuggestionsSlidingListener = + new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onScroll(MotionEvent down, MotionEvent me, float deltaX, float deltaY) { + if (down == null) { + return false; + } + final float dy = me.getY() - down.getY(); + if (deltaY > 0 && dy < 0) { + return showMoreSuggestions(); + } + return false; + } + }; + + @Override + public boolean onInterceptTouchEvent(final MotionEvent me) { + if (mStripVisibilityGroup.isShowingImportantNoticeStrip()) { + return false; + } + // Detecting sliding up finger to show {@link MoreSuggestionsView}. + if (!mMoreSuggestionsView.isShowingInParent()) { + mLastX = (int)me.getX(); + mLastY = (int)me.getY(); + return mMoreSuggestionsSlidingDetector.onTouchEvent(me); + } + if (mMoreSuggestionsView.isInModalMode()) { + return false; + } + + final int action = me.getAction(); + final int index = me.getActionIndex(); + final int x = (int)me.getX(index); + final int y = (int)me.getY(index); + if (Math.abs(x - mOriginX) >= mMoreSuggestionsModalTolerance + || mOriginY - y >= mMoreSuggestionsModalTolerance) { + // Decided to be in the sliding suggestion mode only when the touch point has been moved + // upward. Further {@link MotionEvent}s will be delivered to + // {@link #onTouchEvent(MotionEvent)}. + mNeedsToTransformTouchEventToHoverEvent = + AccessibilityUtils.getInstance().isTouchExplorationEnabled(); + mIsDispatchingHoverEventToMoreSuggestions = false; + return true; + } + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { + // Decided to be in the modal input mode. + mMoreSuggestionsView.setModalMode(); + } + return false; + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(final AccessibilityEvent event) { + // Don't populate accessibility event with suggested words and voice key. + return true; + } + + @Override + public boolean onTouchEvent(final MotionEvent me) { + if (!mMoreSuggestionsView.isShowingInParent()) { + // Ignore any touch event while more suggestions panel hasn't been shown. + // Detecting sliding up is done at {@link #onInterceptTouchEvent}. + return true; + } + // In the sliding input mode. {@link MotionEvent} should be forwarded to + // {@link MoreSuggestionsView}. + final int index = me.getActionIndex(); + final int x = mMoreSuggestionsView.translateX((int)me.getX(index)); + final int y = mMoreSuggestionsView.translateY((int)me.getY(index)); + me.setLocation(x, y); + if (!mNeedsToTransformTouchEventToHoverEvent) { + mMoreSuggestionsView.onTouchEvent(me); + return true; + } + // In sliding suggestion mode with accessibility mode on, a touch event should be + // transformed to a hover event. + final int width = mMoreSuggestionsView.getWidth(); + final int height = mMoreSuggestionsView.getHeight(); + final boolean onMoreSuggestions = (x >= 0 && x < width && y >= 0 && y < height); + if (!onMoreSuggestions && !mIsDispatchingHoverEventToMoreSuggestions) { + // Just drop this touch event because dispatching hover event isn't started yet and + // the touch event isn't on {@link MoreSuggestionsView}. + return true; + } + final int hoverAction; + if (onMoreSuggestions && !mIsDispatchingHoverEventToMoreSuggestions) { + // Transform this touch event to a hover enter event and start dispatching a hover + // event to {@link MoreSuggestionsView}. + mIsDispatchingHoverEventToMoreSuggestions = true; + hoverAction = MotionEvent.ACTION_HOVER_ENTER; + } else if (me.getActionMasked() == MotionEvent.ACTION_UP) { + // Transform this touch event to a hover exit event and stop dispatching a hover event + // after this. + mIsDispatchingHoverEventToMoreSuggestions = false; + mNeedsToTransformTouchEventToHoverEvent = false; + hoverAction = MotionEvent.ACTION_HOVER_EXIT; + } else { + // Transform this touch event to a hover move event. + hoverAction = MotionEvent.ACTION_HOVER_MOVE; + } + me.setAction(hoverAction); + mMoreSuggestionsView.onHoverEvent(me); + return true; + } + + @Override + public void onClick(final View view) { + AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback( + Constants.CODE_UNSPECIFIED, this); + if (view == mImportantNoticeStrip) { + mListener.showImportantNoticeContents(); + return; + } + if (view == mVoiceKey) { + mListener.onCodeInput(Constants.CODE_SHORTCUT, + Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, + false /* isKeyRepeat */); + return; + } + + final Object tag = view.getTag(); + // {@link Integer} tag is set at + // {@link SuggestionStripLayoutHelper#setupWordViewsTextAndColor(SuggestedWords,int)} and + // {@link SuggestionStripLayoutHelper#layoutPunctuationSuggestions(SuggestedWords,ViewGroup} + if (tag instanceof Integer) { + final int index = (Integer) tag; + if (index >= mSuggestedWords.size()) { + return; + } + final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index); + mListener.pickSuggestionManually(wordInfo); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + dismissMoreSuggestionsPanel(); + } + + @Override + protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) { + // Called by the framework when the size is known. Show the important notice if applicable. + // This may be overriden by showing suggestions later, if applicable. + if (oldw <= 0 && w > 0) { + maybeShowImportantNoticeTitle(); + } + } +} |