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.Constants; 47 import com.android.inputmethod.latin.R; 48 import com.android.inputmethod.latin.SuggestedWords; 49 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 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 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 mStartIndexOfMoreSuggestions; 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 mStartIndexOfMoreSuggestions = mLayoutHelper.layoutAndReturnStartIndexOfMoreSuggestions( 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 (mSuggestedWords.size() <= mStartIndexOfMoreSuggestions) { 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, mStartIndexOfMoreSuggestions, 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 < mStartIndexOfMoreSuggestions; 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 = 412 AccessibilityUtils.getInstance().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