Home | History | Annotate | Download | only in suggestions
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.inputmethod.latin.suggestions;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.content.res.TypedArray;
     22 import android.graphics.Color;
     23 import android.graphics.drawable.Drawable;
     24 import android.support.v4.view.ViewCompat;
     25 import android.text.TextUtils;
     26 import android.util.AttributeSet;
     27 import android.util.TypedValue;
     28 import android.view.GestureDetector;
     29 import android.view.LayoutInflater;
     30 import android.view.MotionEvent;
     31 import android.view.View;
     32 import android.view.View.OnClickListener;
     33 import android.view.View.OnLongClickListener;
     34 import android.view.ViewGroup;
     35 import android.view.ViewParent;
     36 import android.view.accessibility.AccessibilityEvent;
     37 import android.widget.ImageButton;
     38 import android.widget.RelativeLayout;
     39 import android.widget.TextView;
     40 
     41 import com.android.inputmethod.accessibility.AccessibilityUtils;
     42 import com.android.inputmethod.keyboard.Keyboard;
     43 import com.android.inputmethod.keyboard.MainKeyboardView;
     44 import com.android.inputmethod.keyboard.MoreKeysPanel;
     45 import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
     46 import com.android.inputmethod.latin.R;
     47 import com.android.inputmethod.latin.SuggestedWords;
     48 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
     49 import com.android.inputmethod.latin.common.Constants;
     50 import com.android.inputmethod.latin.define.DebugFlags;
     51 import com.android.inputmethod.latin.settings.Settings;
     52 import com.android.inputmethod.latin.settings.SettingsValues;
     53 import com.android.inputmethod.latin.suggestions.MoreSuggestionsView.MoreSuggestionsListener;
     54 import com.android.inputmethod.latin.utils.ImportantNoticeUtils;
     55 
     56 import java.util.ArrayList;
     57 
     58 public final class SuggestionStripView extends RelativeLayout implements OnClickListener,
     59         OnLongClickListener {
     60     public interface Listener {
     61         public void showImportantNoticeContents();
     62         public void pickSuggestionManually(SuggestedWordInfo word);
     63         public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat);
     64     }
     65 
     66     static final boolean DBG = DebugFlags.DEBUG_ENABLED;
     67     private static final float DEBUG_INFO_TEXT_SIZE_IN_DIP = 6.0f;
     68 
     69     private final ViewGroup mSuggestionsStrip;
     70     private final ImageButton mVoiceKey;
     71     private final View mImportantNoticeStrip;
     72     MainKeyboardView mMainKeyboardView;
     73 
     74     private final View mMoreSuggestionsContainer;
     75     private final MoreSuggestionsView mMoreSuggestionsView;
     76     private final MoreSuggestions.Builder mMoreSuggestionsBuilder;
     77 
     78     private final ArrayList<TextView> mWordViews = new ArrayList<>();
     79     private final ArrayList<TextView> mDebugInfoViews = new ArrayList<>();
     80     private final ArrayList<View> mDividerViews = new ArrayList<>();
     81 
     82     Listener mListener;
     83     private SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance();
     84     private int mStartIndexOfMoreSuggestions;
     85 
     86     private final SuggestionStripLayoutHelper mLayoutHelper;
     87     private final StripVisibilityGroup mStripVisibilityGroup;
     88 
     89     private static class StripVisibilityGroup {
     90         private final View mSuggestionStripView;
     91         private final View mSuggestionsStrip;
     92         private final View mImportantNoticeStrip;
     93 
     94         public StripVisibilityGroup(final View suggestionStripView,
     95                 final ViewGroup suggestionsStrip, final View importantNoticeStrip) {
     96             mSuggestionStripView = suggestionStripView;
     97             mSuggestionsStrip = suggestionsStrip;
     98             mImportantNoticeStrip = importantNoticeStrip;
     99             showSuggestionsStrip();
    100         }
    101 
    102         public void setLayoutDirection(final boolean isRtlLanguage) {
    103             final int layoutDirection = isRtlLanguage ? ViewCompat.LAYOUT_DIRECTION_RTL
    104                     : ViewCompat.LAYOUT_DIRECTION_LTR;
    105             ViewCompat.setLayoutDirection(mSuggestionStripView, layoutDirection);
    106             ViewCompat.setLayoutDirection(mSuggestionsStrip, layoutDirection);
    107             ViewCompat.setLayoutDirection(mImportantNoticeStrip, layoutDirection);
    108         }
    109 
    110         public void showSuggestionsStrip() {
    111             mSuggestionsStrip.setVisibility(VISIBLE);
    112             mImportantNoticeStrip.setVisibility(INVISIBLE);
    113         }
    114 
    115         public void showImportantNoticeStrip() {
    116             mSuggestionsStrip.setVisibility(INVISIBLE);
    117             mImportantNoticeStrip.setVisibility(VISIBLE);
    118         }
    119 
    120         public boolean isShowingImportantNoticeStrip() {
    121             return mImportantNoticeStrip.getVisibility() == VISIBLE;
    122         }
    123     }
    124 
    125     /**
    126      * Construct a {@link SuggestionStripView} for showing suggestions to be picked by the user.
    127      * @param context
    128      * @param attrs
    129      */
    130     public SuggestionStripView(final Context context, final AttributeSet attrs) {
    131         this(context, attrs, R.attr.suggestionStripViewStyle);
    132     }
    133 
    134     public SuggestionStripView(final Context context, final AttributeSet attrs,
    135             final int defStyle) {
    136         super(context, attrs, defStyle);
    137 
    138         final LayoutInflater inflater = LayoutInflater.from(context);
    139         inflater.inflate(R.layout.suggestions_strip, this);
    140 
    141         mSuggestionsStrip = (ViewGroup)findViewById(R.id.suggestions_strip);
    142         mVoiceKey = (ImageButton)findViewById(R.id.suggestions_strip_voice_key);
    143         mImportantNoticeStrip = findViewById(R.id.important_notice_strip);
    144         mStripVisibilityGroup = new StripVisibilityGroup(this, mSuggestionsStrip,
    145                 mImportantNoticeStrip);
    146 
    147         for (int pos = 0; pos < SuggestedWords.MAX_SUGGESTIONS; pos++) {
    148             final TextView word = new TextView(context, null, R.attr.suggestionWordStyle);
    149             word.setContentDescription(getResources().getString(R.string.spoken_empty_suggestion));
    150             word.setOnClickListener(this);
    151             word.setOnLongClickListener(this);
    152             mWordViews.add(word);
    153             final View divider = inflater.inflate(R.layout.suggestion_divider, null);
    154             mDividerViews.add(divider);
    155             final TextView info = new TextView(context, null, R.attr.suggestionWordStyle);
    156             info.setTextColor(Color.WHITE);
    157             info.setTextSize(TypedValue.COMPLEX_UNIT_DIP, DEBUG_INFO_TEXT_SIZE_IN_DIP);
    158             mDebugInfoViews.add(info);
    159         }
    160 
    161         mLayoutHelper = new SuggestionStripLayoutHelper(
    162                 context, attrs, defStyle, mWordViews, mDividerViews, mDebugInfoViews);
    163 
    164         mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null);
    165         mMoreSuggestionsView = (MoreSuggestionsView)mMoreSuggestionsContainer
    166                 .findViewById(R.id.more_suggestions_view);
    167         mMoreSuggestionsBuilder = new MoreSuggestions.Builder(context, mMoreSuggestionsView);
    168 
    169         final Resources res = context.getResources();
    170         mMoreSuggestionsModalTolerance = res.getDimensionPixelOffset(
    171                 R.dimen.config_more_suggestions_modal_tolerance);
    172         mMoreSuggestionsSlidingDetector = new GestureDetector(
    173                 context, mMoreSuggestionsSlidingListener);
    174 
    175         final TypedArray keyboardAttr = context.obtainStyledAttributes(attrs,
    176                 R.styleable.Keyboard, defStyle, R.style.SuggestionStripView);
    177         final Drawable iconVoice = keyboardAttr.getDrawable(R.styleable.Keyboard_iconShortcutKey);
    178         keyboardAttr.recycle();
    179         mVoiceKey.setImageDrawable(iconVoice);
    180         mVoiceKey.setOnClickListener(this);
    181     }
    182 
    183     /**
    184      * A connection back to the input method.
    185      * @param listener
    186      */
    187     public void setListener(final Listener listener, final View inputView) {
    188         mListener = listener;
    189         mMainKeyboardView = (MainKeyboardView)inputView.findViewById(R.id.keyboard_view);
    190     }
    191 
    192     public void updateVisibility(final boolean shouldBeVisible, final boolean isFullscreenMode) {
    193         final int visibility = shouldBeVisible ? VISIBLE : (isFullscreenMode ? GONE : INVISIBLE);
    194         setVisibility(visibility);
    195         final SettingsValues currentSettingsValues = Settings.getInstance().getCurrent();
    196         mVoiceKey.setVisibility(currentSettingsValues.mShowsVoiceInputKey ? VISIBLE : INVISIBLE);
    197     }
    198 
    199     public void setSuggestions(final SuggestedWords suggestedWords, final boolean isRtlLanguage) {
    200         clear();
    201         mStripVisibilityGroup.setLayoutDirection(isRtlLanguage);
    202         mSuggestedWords = suggestedWords;
    203         mStartIndexOfMoreSuggestions = mLayoutHelper.layoutAndReturnStartIndexOfMoreSuggestions(
    204                 getContext(), mSuggestedWords, mSuggestionsStrip, this);
    205         mStripVisibilityGroup.showSuggestionsStrip();
    206     }
    207 
    208     public void setMoreSuggestionsHeight(final int remainingHeight) {
    209         mLayoutHelper.setMoreSuggestionsHeight(remainingHeight);
    210     }
    211 
    212     // This method checks if we should show the important notice (checks on permanent storage if
    213     // it has been shown once already or not, and if in the setup wizard). If applicable, it shows
    214     // the notice. In all cases, it returns true if it was shown, false otherwise.
    215     public boolean maybeShowImportantNoticeTitle() {
    216         final SettingsValues currentSettingsValues = Settings.getInstance().getCurrent();
    217         if (!ImportantNoticeUtils.shouldShowImportantNotice(getContext(), currentSettingsValues)) {
    218             return false;
    219         }
    220         if (getWidth() <= 0) {
    221             return false;
    222         }
    223         final String importantNoticeTitle = ImportantNoticeUtils.getSuggestContactsNoticeTitle(
    224                 getContext());
    225         if (TextUtils.isEmpty(importantNoticeTitle)) {
    226             return false;
    227         }
    228         if (isShowingMoreSuggestionPanel()) {
    229             dismissMoreSuggestionsPanel();
    230         }
    231         mLayoutHelper.layoutImportantNotice(mImportantNoticeStrip, importantNoticeTitle);
    232         mStripVisibilityGroup.showImportantNoticeStrip();
    233         mImportantNoticeStrip.setOnClickListener(this);
    234         return true;
    235     }
    236 
    237     public void clear() {
    238         mSuggestionsStrip.removeAllViews();
    239         removeAllDebugInfoViews();
    240         mStripVisibilityGroup.showSuggestionsStrip();
    241         dismissMoreSuggestionsPanel();
    242     }
    243 
    244     private void removeAllDebugInfoViews() {
    245         // The debug info views may be placed as children views of this {@link SuggestionStripView}.
    246         for (final View debugInfoView : mDebugInfoViews) {
    247             final ViewParent parent = debugInfoView.getParent();
    248             if (parent instanceof ViewGroup) {
    249                 ((ViewGroup)parent).removeView(debugInfoView);
    250             }
    251         }
    252     }
    253 
    254     private final MoreSuggestionsListener mMoreSuggestionsListener = new MoreSuggestionsListener() {
    255         @Override
    256         public void onSuggestionSelected(final SuggestedWordInfo wordInfo) {
    257             mListener.pickSuggestionManually(wordInfo);
    258             dismissMoreSuggestionsPanel();
    259         }
    260 
    261         @Override
    262         public void onCancelInput() {
    263             dismissMoreSuggestionsPanel();
    264         }
    265     };
    266 
    267     private final MoreKeysPanel.Controller mMoreSuggestionsController =
    268             new MoreKeysPanel.Controller() {
    269         @Override
    270         public void onDismissMoreKeysPanel() {
    271             mMainKeyboardView.onDismissMoreKeysPanel();
    272         }
    273 
    274         @Override
    275         public void onShowMoreKeysPanel(final MoreKeysPanel panel) {
    276             mMainKeyboardView.onShowMoreKeysPanel(panel);
    277         }
    278 
    279         @Override
    280         public void onCancelMoreKeysPanel() {
    281             dismissMoreSuggestionsPanel();
    282         }
    283     };
    284 
    285     public boolean isShowingMoreSuggestionPanel() {
    286         return mMoreSuggestionsView.isShowingInParent();
    287     }
    288 
    289     public void dismissMoreSuggestionsPanel() {
    290         mMoreSuggestionsView.dismissMoreKeysPanel();
    291     }
    292 
    293     @Override
    294     public boolean onLongClick(final View view) {
    295         AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(
    296                 Constants.NOT_A_CODE, this);
    297         return showMoreSuggestions();
    298     }
    299 
    300     boolean showMoreSuggestions() {
    301         final Keyboard parentKeyboard = mMainKeyboardView.getKeyboard();
    302         if (parentKeyboard == null) {
    303             return false;
    304         }
    305         final SuggestionStripLayoutHelper layoutHelper = mLayoutHelper;
    306         if (mSuggestedWords.size() <= mStartIndexOfMoreSuggestions) {
    307             return false;
    308         }
    309         final int stripWidth = getWidth();
    310         final View container = mMoreSuggestionsContainer;
    311         final int maxWidth = stripWidth - container.getPaddingLeft() - container.getPaddingRight();
    312         final MoreSuggestions.Builder builder = mMoreSuggestionsBuilder;
    313         builder.layout(mSuggestedWords, mStartIndexOfMoreSuggestions, maxWidth,
    314                 (int)(maxWidth * layoutHelper.mMinMoreSuggestionsWidth),
    315                 layoutHelper.getMaxMoreSuggestionsRow(), parentKeyboard);
    316         mMoreSuggestionsView.setKeyboard(builder.build());
    317         container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    318 
    319         final MoreKeysPanel moreKeysPanel = mMoreSuggestionsView;
    320         final int pointX = stripWidth / 2;
    321         final int pointY = -layoutHelper.mMoreSuggestionsBottomGap;
    322         moreKeysPanel.showMoreKeysPanel(this, mMoreSuggestionsController, pointX, pointY,
    323                 mMoreSuggestionsListener);
    324         mOriginX = mLastX;
    325         mOriginY = mLastY;
    326         for (int i = 0; i < mStartIndexOfMoreSuggestions; i++) {
    327             mWordViews.get(i).setPressed(false);
    328         }
    329         return true;
    330     }
    331 
    332     // Working variables for {@link onInterceptTouchEvent(MotionEvent)} and
    333     // {@link onTouchEvent(MotionEvent)}.
    334     private int mLastX;
    335     private int mLastY;
    336     private int mOriginX;
    337     private int mOriginY;
    338     private final int mMoreSuggestionsModalTolerance;
    339     private boolean mNeedsToTransformTouchEventToHoverEvent;
    340     private boolean mIsDispatchingHoverEventToMoreSuggestions;
    341     private final GestureDetector mMoreSuggestionsSlidingDetector;
    342     private final GestureDetector.OnGestureListener mMoreSuggestionsSlidingListener =
    343             new GestureDetector.SimpleOnGestureListener() {
    344         @Override
    345         public boolean onScroll(MotionEvent down, MotionEvent me, float deltaX, float deltaY) {
    346             final float dy = me.getY() - down.getY();
    347             if (deltaY > 0 && dy < 0) {
    348                 return showMoreSuggestions();
    349             }
    350             return false;
    351         }
    352     };
    353 
    354     @Override
    355     public boolean onInterceptTouchEvent(final MotionEvent me) {
    356         if (mStripVisibilityGroup.isShowingImportantNoticeStrip()) {
    357             return false;
    358         }
    359         // Detecting sliding up finger to show {@link MoreSuggestionsView}.
    360         if (!mMoreSuggestionsView.isShowingInParent()) {
    361             mLastX = (int)me.getX();
    362             mLastY = (int)me.getY();
    363             return mMoreSuggestionsSlidingDetector.onTouchEvent(me);
    364         }
    365         if (mMoreSuggestionsView.isInModalMode()) {
    366             return false;
    367         }
    368 
    369         final int action = me.getAction();
    370         final int index = me.getActionIndex();
    371         final int x = (int)me.getX(index);
    372         final int y = (int)me.getY(index);
    373         if (Math.abs(x - mOriginX) >= mMoreSuggestionsModalTolerance
    374                 || mOriginY - y >= mMoreSuggestionsModalTolerance) {
    375             // Decided to be in the sliding suggestion mode only when the touch point has been moved
    376             // upward. Further {@link MotionEvent}s will be delivered to
    377             // {@link #onTouchEvent(MotionEvent)}.
    378             mNeedsToTransformTouchEventToHoverEvent =
    379                     AccessibilityUtils.getInstance().isTouchExplorationEnabled();
    380             mIsDispatchingHoverEventToMoreSuggestions = false;
    381             return true;
    382         }
    383 
    384         if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
    385             // Decided to be in the modal input mode.
    386             mMoreSuggestionsView.setModalMode();
    387         }
    388         return false;
    389     }
    390 
    391     @Override
    392     public boolean dispatchPopulateAccessibilityEvent(final AccessibilityEvent event) {
    393         // Don't populate accessibility event with suggested words and voice key.
    394         return true;
    395     }
    396 
    397     @Override
    398     public boolean onTouchEvent(final MotionEvent me) {
    399         if (!mMoreSuggestionsView.isShowingInParent()) {
    400             // Ignore any touch event while more suggestions panel hasn't been shown.
    401             // Detecting sliding up is done at {@link #onInterceptTouchEvent}.
    402             return true;
    403         }
    404         // In the sliding input mode. {@link MotionEvent} should be forwarded to
    405         // {@link MoreSuggestionsView}.
    406         final int index = me.getActionIndex();
    407         final int x = mMoreSuggestionsView.translateX((int)me.getX(index));
    408         final int y = mMoreSuggestionsView.translateY((int)me.getY(index));
    409         me.setLocation(x, y);
    410         if (!mNeedsToTransformTouchEventToHoverEvent) {
    411             mMoreSuggestionsView.onTouchEvent(me);
    412             return true;
    413         }
    414         // In sliding suggestion mode with accessibility mode on, a touch event should be
    415         // transformed to a hover event.
    416         final int width = mMoreSuggestionsView.getWidth();
    417         final int height = mMoreSuggestionsView.getHeight();
    418         final boolean onMoreSuggestions = (x >= 0 && x < width && y >= 0 && y < height);
    419         if (!onMoreSuggestions && !mIsDispatchingHoverEventToMoreSuggestions) {
    420             // Just drop this touch event because dispatching hover event isn't started yet and
    421             // the touch event isn't on {@link MoreSuggestionsView}.
    422             return true;
    423         }
    424         final int hoverAction;
    425         if (onMoreSuggestions && !mIsDispatchingHoverEventToMoreSuggestions) {
    426             // Transform this touch event to a hover enter event and start dispatching a hover
    427             // event to {@link MoreSuggestionsView}.
    428             mIsDispatchingHoverEventToMoreSuggestions = true;
    429             hoverAction = MotionEvent.ACTION_HOVER_ENTER;
    430         } else if (me.getActionMasked() == MotionEvent.ACTION_UP) {
    431             // Transform this touch event to a hover exit event and stop dispatching a hover event
    432             // after this.
    433             mIsDispatchingHoverEventToMoreSuggestions = false;
    434             mNeedsToTransformTouchEventToHoverEvent = false;
    435             hoverAction = MotionEvent.ACTION_HOVER_EXIT;
    436         } else {
    437             // Transform this touch event to a hover move event.
    438             hoverAction = MotionEvent.ACTION_HOVER_MOVE;
    439         }
    440         me.setAction(hoverAction);
    441         mMoreSuggestionsView.onHoverEvent(me);
    442         return true;
    443     }
    444 
    445     @Override
    446     public void onClick(final View view) {
    447         AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(
    448                 Constants.CODE_UNSPECIFIED, this);
    449         if (view == mImportantNoticeStrip) {
    450             mListener.showImportantNoticeContents();
    451             return;
    452         }
    453         if (view == mVoiceKey) {
    454             mListener.onCodeInput(Constants.CODE_SHORTCUT,
    455                     Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE,
    456                     false /* isKeyRepeat */);
    457             return;
    458         }
    459 
    460         final Object tag = view.getTag();
    461         // {@link Integer} tag is set at
    462         // {@link SuggestionStripLayoutHelper#setupWordViewsTextAndColor(SuggestedWords,int)} and
    463         // {@link SuggestionStripLayoutHelper#layoutPunctuationSuggestions(SuggestedWords,ViewGroup}
    464         if (tag instanceof Integer) {
    465             final int index = (Integer) tag;
    466             if (index >= mSuggestedWords.size()) {
    467                 return;
    468             }
    469             final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index);
    470             mListener.pickSuggestionManually(wordInfo);
    471         }
    472     }
    473 
    474     @Override
    475     protected void onDetachedFromWindow() {
    476         super.onDetachedFromWindow();
    477         dismissMoreSuggestionsPanel();
    478     }
    479 
    480     @Override
    481     protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
    482         // Called by the framework when the size is known. Show the important notice if applicable.
    483         // This may be overriden by showing suggestions later, if applicable.
    484         if (oldw <= 0 && w > 0) {
    485             maybeShowImportantNoticeTitle();
    486         }
    487     }
    488 }
    489