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 androidx.core.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 { showImportantNoticeContents()61 public void showImportantNoticeContents(); pickSuggestionManually(SuggestedWordInfo word)62 public void pickSuggestionManually(SuggestedWordInfo word); onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat)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 StripVisibilityGroup(final View suggestionStripView, final ViewGroup suggestionsStrip, final View importantNoticeStrip)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 setLayoutDirection(final boolean isRtlLanguage)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 showSuggestionsStrip()110 public void showSuggestionsStrip() { 111 mSuggestionsStrip.setVisibility(VISIBLE); 112 mImportantNoticeStrip.setVisibility(INVISIBLE); 113 } 114 showImportantNoticeStrip()115 public void showImportantNoticeStrip() { 116 mSuggestionsStrip.setVisibility(INVISIBLE); 117 mImportantNoticeStrip.setVisibility(VISIBLE); 118 } 119 isShowingImportantNoticeStrip()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 */ SuggestionStripView(final Context context, final AttributeSet attrs)130 public SuggestionStripView(final Context context, final AttributeSet attrs) { 131 this(context, attrs, R.attr.suggestionStripViewStyle); 132 } 133 SuggestionStripView(final Context context, final AttributeSet attrs, final int defStyle)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 */ setListener(final Listener listener, final View inputView)187 public void setListener(final Listener listener, final View inputView) { 188 mListener = listener; 189 mMainKeyboardView = (MainKeyboardView)inputView.findViewById(R.id.keyboard_view); 190 } 191 updateVisibility(final boolean shouldBeVisible, final boolean isFullscreenMode)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 setSuggestions(final SuggestedWords suggestedWords, final boolean isRtlLanguage)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 setMoreSuggestionsHeight(final int remainingHeight)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. maybeShowImportantNoticeTitle()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 clear()237 public void clear() { 238 mSuggestionsStrip.removeAllViews(); 239 removeAllDebugInfoViews(); 240 mStripVisibilityGroup.showSuggestionsStrip(); 241 dismissMoreSuggestionsPanel(); 242 } 243 removeAllDebugInfoViews()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 isShowingMoreSuggestionPanel()285 public boolean isShowingMoreSuggestionPanel() { 286 return mMoreSuggestionsView.isShowingInParent(); 287 } 288 dismissMoreSuggestionsPanel()289 public void dismissMoreSuggestionsPanel() { 290 mMoreSuggestionsView.dismissMoreKeysPanel(); 291 } 292 293 @Override onLongClick(final View view)294 public boolean onLongClick(final View view) { 295 AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback( 296 Constants.NOT_A_CODE, this); 297 return showMoreSuggestions(); 298 } 299 showMoreSuggestions()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 onInterceptTouchEvent(final MotionEvent me)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 dispatchPopulateAccessibilityEvent(final AccessibilityEvent event)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 onTouchEvent(final MotionEvent me)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 onClick(final View view)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 onDetachedFromWindow()475 protected void onDetachedFromWindow() { 476 super.onDetachedFromWindow(); 477 dismissMoreSuggestionsPanel(); 478 } 479 480 @Override onSizeChanged(final int w, final int h, final int oldw, final int oldh)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