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