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