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