| /* |
| * 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 com.android.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 android.support.v4.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 com.android.inputmethod.accessibility.AccessibilityUtils; |
| import com.android.inputmethod.keyboard.Keyboard; |
| import com.android.inputmethod.keyboard.MainKeyboardView; |
| import com.android.inputmethod.keyboard.MoreKeysPanel; |
| import com.android.inputmethod.latin.AudioAndHapticFeedbackManager; |
| import com.android.inputmethod.latin.R; |
| import com.android.inputmethod.latin.SuggestedWords; |
| import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; |
| import com.android.inputmethod.latin.common.Constants; |
| import com.android.inputmethod.latin.define.DebugFlags; |
| import com.android.inputmethod.latin.settings.Settings; |
| import com.android.inputmethod.latin.settings.SettingsValues; |
| import com.android.inputmethod.latin.suggestions.MoreSuggestionsView.MoreSuggestionsListener; |
| import com.android.inputmethod.latin.utils.ImportantNoticeUtils; |
| |
| import java.util.ArrayList; |
| |
| public final class SuggestionStripView extends RelativeLayout implements OnClickListener, |
| OnLongClickListener { |
| public interface Listener { |
| public void addWordToUserDictionary(String word); |
| 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 ViewGroup mAddToDictionaryStrip; |
| 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 mAddToDictionaryStrip; |
| private final View mImportantNoticeStrip; |
| |
| public StripVisibilityGroup(final View suggestionStripView, |
| final ViewGroup suggestionsStrip, final ViewGroup addToDictionaryStrip, |
| final View importantNoticeStrip) { |
| mSuggestionStripView = suggestionStripView; |
| mSuggestionsStrip = suggestionsStrip; |
| mAddToDictionaryStrip = addToDictionaryStrip; |
| 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(mAddToDictionaryStrip, layoutDirection); |
| ViewCompat.setLayoutDirection(mImportantNoticeStrip, layoutDirection); |
| } |
| |
| public void showSuggestionsStrip() { |
| mSuggestionsStrip.setVisibility(VISIBLE); |
| mAddToDictionaryStrip.setVisibility(INVISIBLE); |
| mImportantNoticeStrip.setVisibility(INVISIBLE); |
| } |
| |
| public void showAddToDictionaryStrip() { |
| mSuggestionsStrip.setVisibility(INVISIBLE); |
| mAddToDictionaryStrip.setVisibility(VISIBLE); |
| mImportantNoticeStrip.setVisibility(INVISIBLE); |
| } |
| |
| public void showImportantNoticeStrip() { |
| mSuggestionsStrip.setVisibility(INVISIBLE); |
| mAddToDictionaryStrip.setVisibility(INVISIBLE); |
| mImportantNoticeStrip.setVisibility(VISIBLE); |
| } |
| |
| public boolean isShowingImportantNoticeStrip() { |
| return mImportantNoticeStrip.getVisibility() == VISIBLE; |
| } |
| |
| public boolean isShowingAddToDictionaryStrip() { |
| return mAddToDictionaryStrip.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); |
| mAddToDictionaryStrip = (ViewGroup)findViewById(R.id.add_to_dictionary_strip); |
| mImportantNoticeStrip = findViewById(R.id.important_notice_strip); |
| mStripVisibilityGroup = new StripVisibilityGroup(this, mSuggestionsStrip, |
| mAddToDictionaryStrip, mImportantNoticeStrip); |
| |
| for (int pos = 0; pos < SuggestedWords.MAX_SUGGESTIONS; pos++) { |
| final TextView word = new TextView(context, null, R.attr.suggestionWordStyle); |
| 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( |
| mSuggestedWords, mSuggestionsStrip, this); |
| mStripVisibilityGroup.showSuggestionsStrip(); |
| } |
| |
| public void setMoreSuggestionsHeight(final int remainingHeight) { |
| mLayoutHelper.setMoreSuggestionsHeight(remainingHeight); |
| } |
| |
| public boolean isShowingAddToDictionaryHint() { |
| return mStripVisibilityGroup.isShowingAddToDictionaryStrip(); |
| } |
| |
| public void showAddToDictionaryHint(final String word, final boolean shouldShowWordToSave) { |
| mLayoutHelper.layoutAddToDictionaryHint(word, mAddToDictionaryStrip, shouldShowWordToSave); |
| // {@link TextView#setTag()} is used to hold the word to be added to dictionary. The word |
| // will be extracted at {@link #onClick(View)}. |
| mAddToDictionaryStrip.setTag(word); |
| mAddToDictionaryStrip.setOnClickListener(this); |
| mStripVisibilityGroup.showAddToDictionaryStrip(); |
| } |
| |
| public boolean dismissAddToDictionaryHint() { |
| if (isShowingAddToDictionaryHint()) { |
| clear(); |
| return true; |
| } |
| return false; |
| } |
| |
| // 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() { |
| if (!ImportantNoticeUtils.shouldShowImportantNotice(getContext())) { |
| return false; |
| } |
| if (getWidth() <= 0) { |
| return false; |
| } |
| final String importantNoticeTitle = ImportantNoticeUtils.getNextImportantNoticeTitle( |
| 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) { |
| 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 String} tag is set at {@link #suggestAddingToDictionary(String,CharSequence)}. |
| if (tag instanceof String) { |
| final String wordToSave = (String)tag; |
| mListener.addWordToUserDictionary(wordToSave); |
| clear(); |
| return; |
| } |
| |
| // {@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(); |
| } |
| } |
| } |