1/* 2 * Copyright (C) 2013 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.keyboard.emoji; 18 19import static com.android.inputmethod.latin.common.Constants.NOT_A_COORDINATE; 20 21import android.content.Context; 22import android.content.res.Resources; 23import android.content.res.TypedArray; 24import android.graphics.Color; 25import android.preference.PreferenceManager; 26import android.support.v4.view.ViewPager; 27import android.util.AttributeSet; 28import android.util.Pair; 29import android.util.TypedValue; 30import android.view.LayoutInflater; 31import android.view.MotionEvent; 32import android.view.View; 33import android.widget.ImageButton; 34import android.widget.ImageView; 35import android.widget.LinearLayout; 36import android.widget.TabHost; 37import android.widget.TabHost.OnTabChangeListener; 38import android.widget.TabWidget; 39import android.widget.TextView; 40 41import com.android.inputmethod.keyboard.Key; 42import com.android.inputmethod.keyboard.KeyboardActionListener; 43import com.android.inputmethod.keyboard.KeyboardLayoutSet; 44import com.android.inputmethod.keyboard.KeyboardView; 45import com.android.inputmethod.keyboard.internal.KeyDrawParams; 46import com.android.inputmethod.keyboard.internal.KeyVisualAttributes; 47import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; 48import com.android.inputmethod.latin.AudioAndHapticFeedbackManager; 49import com.android.inputmethod.latin.R; 50import com.android.inputmethod.latin.RichInputMethodSubtype; 51import com.android.inputmethod.latin.common.Constants; 52import com.android.inputmethod.latin.utils.ResourceUtils; 53 54/** 55 * View class to implement Emoji palettes. 56 * The Emoji keyboard consists of group of views layout/emoji_palettes_view. 57 * <ol> 58 * <li> Emoji category tabs. 59 * <li> Delete button. 60 * <li> Emoji keyboard pages that can be scrolled by swiping horizontally or by selecting a tab. 61 * <li> Back to main keyboard button and enter button. 62 * </ol> 63 * Because of the above reasons, this class doesn't extend {@link KeyboardView}. 64 */ 65public final class EmojiPalettesView extends LinearLayout implements OnTabChangeListener, 66 ViewPager.OnPageChangeListener, View.OnClickListener, View.OnTouchListener, 67 EmojiPageKeyboardView.OnKeyEventListener { 68 private final int mFunctionalKeyBackgroundId; 69 private final int mSpacebarBackgroundId; 70 private final boolean mCategoryIndicatorEnabled; 71 private final int mCategoryIndicatorDrawableResId; 72 private final int mCategoryIndicatorBackgroundResId; 73 private final int mCategoryPageIndicatorColor; 74 private final int mCategoryPageIndicatorBackground; 75 private EmojiPalettesAdapter mEmojiPalettesAdapter; 76 private final EmojiLayoutParams mEmojiLayoutParams; 77 private final DeleteKeyOnTouchListener mDeleteKeyOnTouchListener; 78 79 private ImageButton mDeleteKey; 80 private TextView mAlphabetKeyLeft; 81 private TextView mAlphabetKeyRight; 82 private View mSpacebar; 83 // TODO: Remove this workaround. 84 private View mSpacebarIcon; 85 private TabHost mTabHost; 86 private ViewPager mEmojiPager; 87 private int mCurrentPagerPosition = 0; 88 private EmojiCategoryPageIndicatorView mEmojiCategoryPageIndicatorView; 89 90 private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER; 91 92 private final EmojiCategory mEmojiCategory; 93 94 public EmojiPalettesView(final Context context, final AttributeSet attrs) { 95 this(context, attrs, R.attr.emojiPalettesViewStyle); 96 } 97 98 public EmojiPalettesView(final Context context, final AttributeSet attrs, final int defStyle) { 99 super(context, attrs, defStyle); 100 final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs, 101 R.styleable.KeyboardView, defStyle, R.style.KeyboardView); 102 final int keyBackgroundId = keyboardViewAttr.getResourceId( 103 R.styleable.KeyboardView_keyBackground, 0); 104 mFunctionalKeyBackgroundId = keyboardViewAttr.getResourceId( 105 R.styleable.KeyboardView_functionalKeyBackground, keyBackgroundId); 106 mSpacebarBackgroundId = keyboardViewAttr.getResourceId( 107 R.styleable.KeyboardView_spacebarBackground, keyBackgroundId); 108 keyboardViewAttr.recycle(); 109 final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder( 110 context, null /* editorInfo */); 111 final Resources res = context.getResources(); 112 mEmojiLayoutParams = new EmojiLayoutParams(res); 113 builder.setSubtype(RichInputMethodSubtype.getEmojiSubtype()); 114 builder.setKeyboardGeometry(ResourceUtils.getDefaultKeyboardWidth(res), 115 mEmojiLayoutParams.mEmojiKeyboardHeight); 116 final KeyboardLayoutSet layoutSet = builder.build(); 117 final TypedArray emojiPalettesViewAttr = context.obtainStyledAttributes(attrs, 118 R.styleable.EmojiPalettesView, defStyle, R.style.EmojiPalettesView); 119 mEmojiCategory = new EmojiCategory(PreferenceManager.getDefaultSharedPreferences(context), 120 res, layoutSet, emojiPalettesViewAttr); 121 mCategoryIndicatorEnabled = emojiPalettesViewAttr.getBoolean( 122 R.styleable.EmojiPalettesView_categoryIndicatorEnabled, false); 123 mCategoryIndicatorDrawableResId = emojiPalettesViewAttr.getResourceId( 124 R.styleable.EmojiPalettesView_categoryIndicatorDrawable, 0); 125 mCategoryIndicatorBackgroundResId = emojiPalettesViewAttr.getResourceId( 126 R.styleable.EmojiPalettesView_categoryIndicatorBackground, 0); 127 mCategoryPageIndicatorColor = emojiPalettesViewAttr.getColor( 128 R.styleable.EmojiPalettesView_categoryPageIndicatorColor, 0); 129 mCategoryPageIndicatorBackground = emojiPalettesViewAttr.getColor( 130 R.styleable.EmojiPalettesView_categoryPageIndicatorBackground, 0); 131 emojiPalettesViewAttr.recycle(); 132 mDeleteKeyOnTouchListener = new DeleteKeyOnTouchListener(); 133 } 134 135 @Override 136 protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { 137 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 138 final Resources res = getContext().getResources(); 139 // The main keyboard expands to the entire this {@link KeyboardView}. 140 final int width = ResourceUtils.getDefaultKeyboardWidth(res) 141 + getPaddingLeft() + getPaddingRight(); 142 final int height = ResourceUtils.getDefaultKeyboardHeight(res) 143 + res.getDimensionPixelSize(R.dimen.config_suggestions_strip_height) 144 + getPaddingTop() + getPaddingBottom(); 145 setMeasuredDimension(width, height); 146 } 147 148 private void addTab(final TabHost host, final int categoryId) { 149 final String tabId = EmojiCategory.getCategoryName(categoryId, 0 /* categoryPageId */); 150 final TabHost.TabSpec tspec = host.newTabSpec(tabId); 151 tspec.setContent(R.id.emoji_keyboard_dummy); 152 final ImageView iconView = (ImageView)LayoutInflater.from(getContext()).inflate( 153 R.layout.emoji_keyboard_tab_icon, null); 154 // TODO: Replace background color with its own setting rather than using the 155 // category page indicator background as a workaround. 156 iconView.setBackgroundColor(mCategoryPageIndicatorBackground); 157 iconView.setImageResource(mEmojiCategory.getCategoryTabIcon(categoryId)); 158 iconView.setContentDescription(mEmojiCategory.getAccessibilityDescription(categoryId)); 159 tspec.setIndicator(iconView); 160 host.addTab(tspec); 161 } 162 163 @Override 164 protected void onFinishInflate() { 165 mTabHost = (TabHost)findViewById(R.id.emoji_category_tabhost); 166 mTabHost.setup(); 167 for (final EmojiCategory.CategoryProperties properties 168 : mEmojiCategory.getShownCategories()) { 169 addTab(mTabHost, properties.mCategoryId); 170 } 171 mTabHost.setOnTabChangedListener(this); 172 final TabWidget tabWidget = mTabHost.getTabWidget(); 173 tabWidget.setStripEnabled(mCategoryIndicatorEnabled); 174 if (mCategoryIndicatorEnabled) { 175 // On TabWidget's strip, what looks like an indicator is actually a background. 176 // And what looks like a background are actually left and right drawables. 177 tabWidget.setBackgroundResource(mCategoryIndicatorDrawableResId); 178 tabWidget.setLeftStripDrawable(mCategoryIndicatorBackgroundResId); 179 tabWidget.setRightStripDrawable(mCategoryIndicatorBackgroundResId); 180 } 181 182 mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, this); 183 184 mEmojiPager = (ViewPager)findViewById(R.id.emoji_keyboard_pager); 185 mEmojiPager.setAdapter(mEmojiPalettesAdapter); 186 mEmojiPager.setOnPageChangeListener(this); 187 mEmojiPager.setOffscreenPageLimit(0); 188 mEmojiPager.setPersistentDrawingCache(PERSISTENT_NO_CACHE); 189 mEmojiLayoutParams.setPagerProperties(mEmojiPager); 190 191 mEmojiCategoryPageIndicatorView = 192 (EmojiCategoryPageIndicatorView)findViewById(R.id.emoji_category_page_id_view); 193 mEmojiCategoryPageIndicatorView.setColors( 194 mCategoryPageIndicatorColor, mCategoryPageIndicatorBackground); 195 mEmojiLayoutParams.setCategoryPageIdViewProperties(mEmojiCategoryPageIndicatorView); 196 197 setCurrentCategoryId(mEmojiCategory.getCurrentCategoryId(), true /* force */); 198 199 final LinearLayout actionBar = (LinearLayout)findViewById(R.id.emoji_action_bar); 200 mEmojiLayoutParams.setActionBarProperties(actionBar); 201 202 // deleteKey depends only on OnTouchListener. 203 mDeleteKey = (ImageButton)findViewById(R.id.emoji_keyboard_delete); 204 mDeleteKey.setBackgroundResource(mFunctionalKeyBackgroundId); 205 mDeleteKey.setTag(Constants.CODE_DELETE); 206 mDeleteKey.setOnTouchListener(mDeleteKeyOnTouchListener); 207 208 // {@link #mAlphabetKeyLeft}, {@link #mAlphabetKeyRight, and spaceKey depend on 209 // {@link View.OnClickListener} as well as {@link View.OnTouchListener}. 210 // {@link View.OnTouchListener} is used as the trigger of key-press, while 211 // {@link View.OnClickListener} is used as the trigger of key-release which does not occur 212 // if the event is canceled by moving off the finger from the view. 213 // The text on alphabet keys are set at 214 // {@link #startEmojiPalettes(String,int,float,Typeface)}. 215 mAlphabetKeyLeft = (TextView)findViewById(R.id.emoji_keyboard_alphabet_left); 216 mAlphabetKeyLeft.setBackgroundResource(mFunctionalKeyBackgroundId); 217 mAlphabetKeyLeft.setTag(Constants.CODE_ALPHA_FROM_EMOJI); 218 mAlphabetKeyLeft.setOnTouchListener(this); 219 mAlphabetKeyLeft.setOnClickListener(this); 220 mAlphabetKeyRight = (TextView)findViewById(R.id.emoji_keyboard_alphabet_right); 221 mAlphabetKeyRight.setBackgroundResource(mFunctionalKeyBackgroundId); 222 mAlphabetKeyRight.setTag(Constants.CODE_ALPHA_FROM_EMOJI); 223 mAlphabetKeyRight.setOnTouchListener(this); 224 mAlphabetKeyRight.setOnClickListener(this); 225 mSpacebar = findViewById(R.id.emoji_keyboard_space); 226 mSpacebar.setBackgroundResource(mSpacebarBackgroundId); 227 mSpacebar.setTag(Constants.CODE_SPACE); 228 mSpacebar.setOnTouchListener(this); 229 mSpacebar.setOnClickListener(this); 230 mEmojiLayoutParams.setKeyProperties(mSpacebar); 231 mSpacebarIcon = findViewById(R.id.emoji_keyboard_space_icon); 232 } 233 234 @Override 235 public boolean dispatchTouchEvent(final MotionEvent ev) { 236 // Add here to the stack trace to nail down the {@link IllegalArgumentException} exception 237 // in MotionEvent that sporadically happens. 238 // TODO: Remove this override method once the issue has been addressed. 239 return super.dispatchTouchEvent(ev); 240 } 241 242 @Override 243 public void onTabChanged(final String tabId) { 244 AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback( 245 Constants.CODE_UNSPECIFIED, this); 246 final int categoryId = mEmojiCategory.getCategoryId(tabId); 247 setCurrentCategoryId(categoryId, false /* force */); 248 updateEmojiCategoryPageIdView(); 249 } 250 251 @Override 252 public void onPageSelected(final int position) { 253 final Pair<Integer, Integer> newPos = 254 mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position); 255 setCurrentCategoryId(newPos.first /* categoryId */, false /* force */); 256 mEmojiCategory.setCurrentCategoryPageId(newPos.second /* categoryPageId */); 257 updateEmojiCategoryPageIdView(); 258 mCurrentPagerPosition = position; 259 } 260 261 @Override 262 public void onPageScrollStateChanged(final int state) { 263 // Ignore this message. Only want the actual page selected. 264 } 265 266 @Override 267 public void onPageScrolled(final int position, final float positionOffset, 268 final int positionOffsetPixels) { 269 mEmojiPalettesAdapter.onPageScrolled(); 270 final Pair<Integer, Integer> newPos = 271 mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position); 272 final int newCategoryId = newPos.first; 273 final int newCategorySize = mEmojiCategory.getCategoryPageSize(newCategoryId); 274 final int currentCategoryId = mEmojiCategory.getCurrentCategoryId(); 275 final int currentCategoryPageId = mEmojiCategory.getCurrentCategoryPageId(); 276 final int currentCategorySize = mEmojiCategory.getCurrentCategoryPageSize(); 277 if (newCategoryId == currentCategoryId) { 278 mEmojiCategoryPageIndicatorView.setCategoryPageId( 279 newCategorySize, newPos.second, positionOffset); 280 } else if (newCategoryId > currentCategoryId) { 281 mEmojiCategoryPageIndicatorView.setCategoryPageId( 282 currentCategorySize, currentCategoryPageId, positionOffset); 283 } else if (newCategoryId < currentCategoryId) { 284 mEmojiCategoryPageIndicatorView.setCategoryPageId( 285 currentCategorySize, currentCategoryPageId, positionOffset - 1); 286 } 287 } 288 289 /** 290 * Called from {@link EmojiPageKeyboardView} through {@link android.view.View.OnTouchListener} 291 * interface to handle touch events from View-based elements such as the space bar. 292 * Note that this method is used only for observing {@link MotionEvent#ACTION_DOWN} to trigger 293 * {@link KeyboardActionListener#onPressKey}. {@link KeyboardActionListener#onReleaseKey} will 294 * be covered by {@link #onClick} as long as the event is not canceled. 295 */ 296 @Override 297 public boolean onTouch(final View v, final MotionEvent event) { 298 if (event.getActionMasked() != MotionEvent.ACTION_DOWN) { 299 return false; 300 } 301 final Object tag = v.getTag(); 302 if (!(tag instanceof Integer)) { 303 return false; 304 } 305 final int code = (Integer) tag; 306 mKeyboardActionListener.onPressKey( 307 code, 0 /* repeatCount */, true /* isSinglePointer */); 308 // It's important to return false here. Otherwise, {@link #onClick} and touch-down visual 309 // feedback stop working. 310 return false; 311 } 312 313 /** 314 * Called from {@link EmojiPageKeyboardView} through {@link android.view.View.OnClickListener} 315 * interface to handle non-canceled touch-up events from View-based elements such as the space 316 * bar. 317 */ 318 @Override 319 public void onClick(View v) { 320 final Object tag = v.getTag(); 321 if (!(tag instanceof Integer)) { 322 return; 323 } 324 final int code = (Integer) tag; 325 mKeyboardActionListener.onCodeInput(code, NOT_A_COORDINATE, NOT_A_COORDINATE, 326 false /* isKeyRepeat */); 327 mKeyboardActionListener.onReleaseKey(code, false /* withSliding */); 328 } 329 330 /** 331 * Called from {@link EmojiPageKeyboardView} through 332 * {@link com.android.inputmethod.keyboard.emoji.EmojiPageKeyboardView.OnKeyEventListener} 333 * interface to handle touch events from non-View-based elements such as Emoji buttons. 334 */ 335 @Override 336 public void onPressKey(final Key key) { 337 final int code = key.getCode(); 338 mKeyboardActionListener.onPressKey(code, 0 /* repeatCount */, true /* isSinglePointer */); 339 } 340 341 /** 342 * Called from {@link EmojiPageKeyboardView} through 343 * {@link com.android.inputmethod.keyboard.emoji.EmojiPageKeyboardView.OnKeyEventListener} 344 * interface to handle touch events from non-View-based elements such as Emoji buttons. 345 */ 346 @Override 347 public void onReleaseKey(final Key key) { 348 mEmojiPalettesAdapter.addRecentKey(key); 349 mEmojiCategory.saveLastTypedCategoryPage(); 350 final int code = key.getCode(); 351 if (code == Constants.CODE_OUTPUT_TEXT) { 352 mKeyboardActionListener.onTextInput(key.getOutputText()); 353 } else { 354 mKeyboardActionListener.onCodeInput(code, NOT_A_COORDINATE, NOT_A_COORDINATE, 355 false /* isKeyRepeat */); 356 } 357 mKeyboardActionListener.onReleaseKey(code, false /* withSliding */); 358 } 359 360 public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) { 361 if (!enabled) return; 362 // TODO: Should use LAYER_TYPE_SOFTWARE when hardware acceleration is off? 363 setLayerType(LAYER_TYPE_HARDWARE, null); 364 } 365 366 private static void setupAlphabetKey(final TextView alphabetKey, final String label, 367 final KeyDrawParams params) { 368 alphabetKey.setText(label); 369 alphabetKey.setTextColor(params.mFunctionalTextColor); 370 alphabetKey.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mLabelSize); 371 alphabetKey.setTypeface(params.mTypeface); 372 } 373 374 public void startEmojiPalettes(final String switchToAlphaLabel, 375 final KeyVisualAttributes keyVisualAttr, 376 final KeyboardIconsSet iconSet) { 377 final int deleteIconResId = iconSet.getIconResourceId(KeyboardIconsSet.NAME_DELETE_KEY); 378 if (deleteIconResId != 0) { 379 mDeleteKey.setImageResource(deleteIconResId); 380 } 381 final int spacebarResId = iconSet.getIconResourceId(KeyboardIconsSet.NAME_SPACE_KEY); 382 if (spacebarResId != 0) { 383 // TODO: Remove this workaround to place the spacebar icon. 384 mSpacebarIcon.setBackgroundResource(spacebarResId); 385 } 386 final KeyDrawParams params = new KeyDrawParams(); 387 params.updateParams(mEmojiLayoutParams.getActionBarHeight(), keyVisualAttr); 388 setupAlphabetKey(mAlphabetKeyLeft, switchToAlphaLabel, params); 389 setupAlphabetKey(mAlphabetKeyRight, switchToAlphaLabel, params); 390 mEmojiPager.setAdapter(mEmojiPalettesAdapter); 391 mEmojiPager.setCurrentItem(mCurrentPagerPosition); 392 } 393 394 public void stopEmojiPalettes() { 395 mEmojiPalettesAdapter.releaseCurrentKey(true /* withKeyRegistering */); 396 mEmojiPalettesAdapter.flushPendingRecentKeys(); 397 mEmojiPager.setAdapter(null); 398 } 399 400 public void setKeyboardActionListener(final KeyboardActionListener listener) { 401 mKeyboardActionListener = listener; 402 mDeleteKeyOnTouchListener.setKeyboardActionListener(listener); 403 } 404 405 private void updateEmojiCategoryPageIdView() { 406 if (mEmojiCategoryPageIndicatorView == null) { 407 return; 408 } 409 mEmojiCategoryPageIndicatorView.setCategoryPageId( 410 mEmojiCategory.getCurrentCategoryPageSize(), 411 mEmojiCategory.getCurrentCategoryPageId(), 0.0f /* offset */); 412 } 413 414 private void setCurrentCategoryId(final int categoryId, final boolean force) { 415 final int oldCategoryId = mEmojiCategory.getCurrentCategoryId(); 416 if (oldCategoryId == categoryId && !force) { 417 return; 418 } 419 420 if (oldCategoryId == EmojiCategory.ID_RECENTS) { 421 // Needs to save pending updates for recent keys when we get out of the recents 422 // category because we don't want to move the recent emojis around while the user 423 // is in the recents category. 424 mEmojiPalettesAdapter.flushPendingRecentKeys(); 425 } 426 427 mEmojiCategory.setCurrentCategoryId(categoryId); 428 final int newTabId = mEmojiCategory.getTabIdFromCategoryId(categoryId); 429 final int newCategoryPageId = mEmojiCategory.getPageIdFromCategoryId(categoryId); 430 if (force || mEmojiCategory.getCategoryIdAndPageIdFromPagePosition( 431 mEmojiPager.getCurrentItem()).first != categoryId) { 432 mEmojiPager.setCurrentItem(newCategoryPageId, false /* smoothScroll */); 433 } 434 if (force || mTabHost.getCurrentTab() != newTabId) { 435 mTabHost.setCurrentTab(newTabId); 436 } 437 } 438 439 private static class DeleteKeyOnTouchListener implements OnTouchListener { 440 private KeyboardActionListener mKeyboardActionListener = 441 KeyboardActionListener.EMPTY_LISTENER; 442 443 public void setKeyboardActionListener(final KeyboardActionListener listener) { 444 mKeyboardActionListener = listener; 445 } 446 447 @Override 448 public boolean onTouch(final View v, final MotionEvent event) { 449 switch (event.getActionMasked()) { 450 case MotionEvent.ACTION_DOWN: 451 onTouchDown(v); 452 return true; 453 case MotionEvent.ACTION_MOVE: 454 final float x = event.getX(); 455 final float y = event.getY(); 456 if (x < 0.0f || v.getWidth() < x || y < 0.0f || v.getHeight() < y) { 457 // Stop generating key events once the finger moves away from the view area. 458 onTouchCanceled(v); 459 } 460 return true; 461 case MotionEvent.ACTION_CANCEL: 462 case MotionEvent.ACTION_UP: 463 onTouchUp(v); 464 return true; 465 } 466 return false; 467 } 468 469 private void onTouchDown(final View v) { 470 mKeyboardActionListener.onPressKey(Constants.CODE_DELETE, 471 0 /* repeatCount */, true /* isSinglePointer */); 472 v.setPressed(true /* pressed */); 473 } 474 475 private void onTouchUp(final View v) { 476 mKeyboardActionListener.onCodeInput(Constants.CODE_DELETE, 477 NOT_A_COORDINATE, NOT_A_COORDINATE, false /* isKeyRepeat */); 478 mKeyboardActionListener.onReleaseKey(Constants.CODE_DELETE, false /* withSliding */); 479 v.setPressed(false /* pressed */); 480 } 481 482 private void onTouchCanceled(final View v) { 483 v.setBackgroundColor(Color.TRANSPARENT); 484 } 485 } 486}