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 17 package com.android.inputmethod.keyboard; 18 19 import static com.android.inputmethod.latin.Constants.NOT_A_COORDINATE; 20 21 import android.content.Context; 22 import android.content.SharedPreferences; 23 import android.content.res.ColorStateList; 24 import android.content.res.Resources; 25 import android.content.res.TypedArray; 26 import android.graphics.Rect; 27 import android.os.Build; 28 import android.preference.PreferenceManager; 29 import android.support.v4.view.PagerAdapter; 30 import android.support.v4.view.ViewPager; 31 import android.text.format.DateUtils; 32 import android.util.AttributeSet; 33 import android.util.Log; 34 import android.util.Pair; 35 import android.util.SparseArray; 36 import android.view.LayoutInflater; 37 import android.view.MotionEvent; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.widget.ImageView; 41 import android.widget.LinearLayout; 42 import android.widget.TabHost; 43 import android.widget.TabHost.OnTabChangeListener; 44 import android.widget.TextView; 45 46 import com.android.inputmethod.keyboard.internal.DynamicGridKeyboard; 47 import com.android.inputmethod.keyboard.internal.ScrollKeyboardView; 48 import com.android.inputmethod.keyboard.internal.ScrollViewWithNotifier; 49 import com.android.inputmethod.latin.Constants; 50 import com.android.inputmethod.latin.R; 51 import com.android.inputmethod.latin.SubtypeSwitcher; 52 import com.android.inputmethod.latin.settings.Settings; 53 import com.android.inputmethod.latin.utils.CollectionUtils; 54 import com.android.inputmethod.latin.utils.ResourceUtils; 55 56 import java.util.ArrayList; 57 import java.util.Arrays; 58 import java.util.Comparator; 59 import java.util.HashMap; 60 import java.util.concurrent.ConcurrentHashMap; 61 62 /** 63 * View class to implement Emoji palettes. 64 * The Emoji keyboard consists of group of views {@link R.layout#emoji_palettes_view}. 65 * <ol> 66 * <li> Emoji category tabs. 67 * <li> Delete button. 68 * <li> Emoji keyboard pages that can be scrolled by swiping horizontally or by selecting a tab. 69 * <li> Back to main keyboard button and enter button. 70 * </ol> 71 * Because of the above reasons, this class doesn't extend {@link KeyboardView}. 72 */ 73 public final class EmojiPalettesView extends LinearLayout implements OnTabChangeListener, 74 ViewPager.OnPageChangeListener, View.OnClickListener, 75 ScrollKeyboardView.OnKeyClickListener { 76 private static final String TAG = EmojiPalettesView.class.getSimpleName(); 77 private static final boolean DEBUG_PAGER = false; 78 private final int mKeyBackgroundId; 79 private final int mEmojiFunctionalKeyBackgroundId; 80 private final KeyboardLayoutSet mLayoutSet; 81 private final ColorStateList mTabLabelColor; 82 private final DeleteKeyOnTouchListener mDeleteKeyOnTouchListener; 83 private EmojiPalettesAdapter mEmojiPalettesAdapter; 84 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 static final int CATEGORY_ID_UNSPECIFIED = -1; 93 public static final int CATEGORY_ID_RECENTS = 0; 94 public static final int CATEGORY_ID_PEOPLE = 1; 95 public static final int CATEGORY_ID_OBJECTS = 2; 96 public static final int CATEGORY_ID_NATURE = 3; 97 public static final int CATEGORY_ID_PLACES = 4; 98 public static final int CATEGORY_ID_SYMBOLS = 5; 99 public static final int CATEGORY_ID_EMOTICONS = 6; 100 101 private static class CategoryProperties { 102 public int mCategoryId; 103 public int mPageCount; 104 public CategoryProperties(final int categoryId, final int pageCount) { 105 mCategoryId = categoryId; 106 mPageCount = pageCount; 107 } 108 } 109 110 private static class EmojiCategory { 111 private static final String[] sCategoryName = { 112 "recents", 113 "people", 114 "objects", 115 "nature", 116 "places", 117 "symbols", 118 "emoticons" }; 119 private static final int[] sCategoryIcon = new int[] { 120 R.drawable.ic_emoji_recent_light, 121 R.drawable.ic_emoji_people_light, 122 R.drawable.ic_emoji_objects_light, 123 R.drawable.ic_emoji_nature_light, 124 R.drawable.ic_emoji_places_light, 125 R.drawable.ic_emoji_symbols_light, 126 0 }; 127 private static final String[] sCategoryLabel = 128 { null, null, null, null, null, null, ":-)" }; 129 private static final int[] sCategoryElementId = { 130 KeyboardId.ELEMENT_EMOJI_RECENTS, 131 KeyboardId.ELEMENT_EMOJI_CATEGORY1, 132 KeyboardId.ELEMENT_EMOJI_CATEGORY2, 133 KeyboardId.ELEMENT_EMOJI_CATEGORY3, 134 KeyboardId.ELEMENT_EMOJI_CATEGORY4, 135 KeyboardId.ELEMENT_EMOJI_CATEGORY5, 136 KeyboardId.ELEMENT_EMOJI_CATEGORY6 }; 137 private final SharedPreferences mPrefs; 138 private final int mMaxPageKeyCount; 139 private final KeyboardLayoutSet mLayoutSet; 140 private final HashMap<String, Integer> mCategoryNameToIdMap = CollectionUtils.newHashMap(); 141 private final ArrayList<CategoryProperties> mShownCategories = 142 CollectionUtils.newArrayList(); 143 private final ConcurrentHashMap<Long, DynamicGridKeyboard> 144 mCategoryKeyboardMap = new ConcurrentHashMap<Long, DynamicGridKeyboard>(); 145 146 private int mCurrentCategoryId = CATEGORY_ID_UNSPECIFIED; 147 private int mCurrentCategoryPageId = 0; 148 149 public EmojiCategory(final SharedPreferences prefs, final Resources res, 150 final KeyboardLayoutSet layoutSet) { 151 mPrefs = prefs; 152 mMaxPageKeyCount = res.getInteger(R.integer.emoji_keyboard_max_key_count); 153 mLayoutSet = layoutSet; 154 for (int i = 0; i < sCategoryName.length; ++i) { 155 mCategoryNameToIdMap.put(sCategoryName[i], i); 156 } 157 addShownCategoryId(CATEGORY_ID_RECENTS); 158 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2 159 || android.os.Build.VERSION.CODENAME.equalsIgnoreCase("KeyLimePie") 160 || android.os.Build.VERSION.CODENAME.equalsIgnoreCase("KitKat")) { 161 addShownCategoryId(CATEGORY_ID_PEOPLE); 162 addShownCategoryId(CATEGORY_ID_OBJECTS); 163 addShownCategoryId(CATEGORY_ID_NATURE); 164 addShownCategoryId(CATEGORY_ID_PLACES); 165 mCurrentCategoryId = 166 Settings.readLastShownEmojiCategoryId(mPrefs, CATEGORY_ID_PEOPLE); 167 } else { 168 mCurrentCategoryId = 169 Settings.readLastShownEmojiCategoryId(mPrefs, CATEGORY_ID_SYMBOLS); 170 } 171 addShownCategoryId(CATEGORY_ID_SYMBOLS); 172 addShownCategoryId(CATEGORY_ID_EMOTICONS); 173 getKeyboard(CATEGORY_ID_RECENTS, 0 /* cagetoryPageId */) 174 .loadRecentKeys(mCategoryKeyboardMap.values()); 175 } 176 177 private void addShownCategoryId(int categoryId) { 178 // Load a keyboard of categoryId 179 getKeyboard(categoryId, 0 /* cagetoryPageId */); 180 final CategoryProperties properties = 181 new CategoryProperties(categoryId, getCategoryPageCount(categoryId)); 182 mShownCategories.add(properties); 183 } 184 185 public String getCategoryName(int categoryId, int categoryPageId) { 186 return sCategoryName[categoryId] + "-" + categoryPageId; 187 } 188 189 public int getCategoryId(String name) { 190 final String[] strings = name.split("-"); 191 return mCategoryNameToIdMap.get(strings[0]); 192 } 193 194 public int getCategoryIcon(int categoryId) { 195 return sCategoryIcon[categoryId]; 196 } 197 198 public String getCategoryLabel(int categoryId) { 199 return sCategoryLabel[categoryId]; 200 } 201 202 public ArrayList<CategoryProperties> getShownCategories() { 203 return mShownCategories; 204 } 205 206 public int getCurrentCategoryId() { 207 return mCurrentCategoryId; 208 } 209 210 public int getCurrentCategoryPageSize() { 211 return getCategoryPageSize(mCurrentCategoryId); 212 } 213 214 public int getCategoryPageSize(int categoryId) { 215 for (final CategoryProperties prop : mShownCategories) { 216 if (prop.mCategoryId == categoryId) { 217 return prop.mPageCount; 218 } 219 } 220 Log.w(TAG, "Invalid category id: " + categoryId); 221 // Should not reach here. 222 return 0; 223 } 224 225 public void setCurrentCategoryId(int categoryId) { 226 mCurrentCategoryId = categoryId; 227 Settings.writeLastShownEmojiCategoryId(mPrefs, categoryId); 228 } 229 230 public void setCurrentCategoryPageId(int id) { 231 mCurrentCategoryPageId = id; 232 } 233 234 public int getCurrentCategoryPageId() { 235 return mCurrentCategoryPageId; 236 } 237 238 public void saveLastTypedCategoryPage() { 239 Settings.writeLastTypedEmojiCategoryPageId( 240 mPrefs, mCurrentCategoryId, mCurrentCategoryPageId); 241 } 242 243 public boolean isInRecentTab() { 244 return mCurrentCategoryId == CATEGORY_ID_RECENTS; 245 } 246 247 public int getTabIdFromCategoryId(int categoryId) { 248 for (int i = 0; i < mShownCategories.size(); ++i) { 249 if (mShownCategories.get(i).mCategoryId == categoryId) { 250 return i; 251 } 252 } 253 Log.w(TAG, "categoryId not found: " + categoryId); 254 return 0; 255 } 256 257 // Returns the view pager's page position for the categoryId 258 public int getPageIdFromCategoryId(int categoryId) { 259 final int lastSavedCategoryPageId = 260 Settings.readLastTypedEmojiCategoryPageId(mPrefs, categoryId); 261 int sum = 0; 262 for (int i = 0; i < mShownCategories.size(); ++i) { 263 final CategoryProperties props = mShownCategories.get(i); 264 if (props.mCategoryId == categoryId) { 265 return sum + lastSavedCategoryPageId; 266 } 267 sum += props.mPageCount; 268 } 269 Log.w(TAG, "categoryId not found: " + categoryId); 270 return 0; 271 } 272 273 public int getRecentTabId() { 274 return getTabIdFromCategoryId(CATEGORY_ID_RECENTS); 275 } 276 277 private int getCategoryPageCount(int categoryId) { 278 final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]); 279 return (keyboard.getKeys().length - 1) / mMaxPageKeyCount + 1; 280 } 281 282 // Returns a pair of the category id and the category page id from the view pager's page 283 // position. The category page id is numbered in each category. And the view page position 284 // is the position of the current shown page in the view pager which contains all pages of 285 // all categories. 286 public Pair<Integer, Integer> getCategoryIdAndPageIdFromPagePosition(int position) { 287 int sum = 0; 288 for (CategoryProperties properties : mShownCategories) { 289 final int temp = sum; 290 sum += properties.mPageCount; 291 if (sum > position) { 292 return new Pair<Integer, Integer>(properties.mCategoryId, position - temp); 293 } 294 } 295 return null; 296 } 297 298 // Returns a keyboard from the view pager's page position. 299 public DynamicGridKeyboard getKeyboardFromPagePosition(int position) { 300 final Pair<Integer, Integer> categoryAndId = 301 getCategoryIdAndPageIdFromPagePosition(position); 302 if (categoryAndId != null) { 303 return getKeyboard(categoryAndId.first, categoryAndId.second); 304 } 305 return null; 306 } 307 308 public DynamicGridKeyboard getKeyboard(int categoryId, int id) { 309 synchronized(mCategoryKeyboardMap) { 310 final long key = (((long) categoryId) << Constants.MAX_INT_BIT_COUNT) | id; 311 final DynamicGridKeyboard kbd; 312 if (!mCategoryKeyboardMap.containsKey(key)) { 313 if (categoryId != CATEGORY_ID_RECENTS) { 314 final Keyboard keyboard = 315 mLayoutSet.getKeyboard(sCategoryElementId[categoryId]); 316 final Key[][] sortedKeys = sortKeys(keyboard.getKeys(), mMaxPageKeyCount); 317 for (int i = 0; i < sortedKeys.length; ++i) { 318 final DynamicGridKeyboard tempKbd = new DynamicGridKeyboard(mPrefs, 319 mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), 320 mMaxPageKeyCount, categoryId, i /* categoryPageId */); 321 for (Key emojiKey : sortedKeys[i]) { 322 if (emojiKey == null) { 323 break; 324 } 325 tempKbd.addKeyLast(emojiKey); 326 } 327 mCategoryKeyboardMap.put((((long) categoryId) 328 << Constants.MAX_INT_BIT_COUNT) | i, tempKbd); 329 } 330 kbd = mCategoryKeyboardMap.get(key); 331 } else { 332 kbd = new DynamicGridKeyboard(mPrefs, 333 mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), 334 mMaxPageKeyCount, categoryId, 0 /* categoryPageId */); 335 mCategoryKeyboardMap.put(key, kbd); 336 } 337 } else { 338 kbd = mCategoryKeyboardMap.get(key); 339 } 340 return kbd; 341 } 342 } 343 344 public int getTotalPageCountOfAllCategories() { 345 int sum = 0; 346 for (CategoryProperties properties : mShownCategories) { 347 sum += properties.mPageCount; 348 } 349 return sum; 350 } 351 352 private Key[][] sortKeys(Key[] inKeys, int maxPageCount) { 353 Key[] keys = Arrays.copyOf(inKeys, inKeys.length); 354 Arrays.sort(keys, 0, keys.length, new Comparator<Key>() { 355 @Override 356 public int compare(Key lhs, Key rhs) { 357 final Rect lHitBox = lhs.getHitBox(); 358 final Rect rHitBox = rhs.getHitBox(); 359 if (lHitBox.top < rHitBox.top) { 360 return -1; 361 } else if (lHitBox.top > rHitBox.top) { 362 return 1; 363 } 364 if (lHitBox.left < rHitBox.left) { 365 return -1; 366 } else if (lHitBox.left > rHitBox.left) { 367 return 1; 368 } 369 if (lhs.getCode() == rhs.getCode()) { 370 return 0; 371 } 372 return lhs.getCode() < rhs.getCode() ? -1 : 1; 373 } 374 }); 375 final int pageCount = (keys.length - 1) / maxPageCount + 1; 376 final Key[][] retval = new Key[pageCount][maxPageCount]; 377 for (int i = 0; i < keys.length; ++i) { 378 retval[i / maxPageCount][i % maxPageCount] = keys[i]; 379 } 380 return retval; 381 } 382 } 383 384 private final EmojiCategory mEmojiCategory; 385 386 public EmojiPalettesView(final Context context, final AttributeSet attrs) { 387 this(context, attrs, R.attr.emojiPalettesViewStyle); 388 } 389 390 public EmojiPalettesView(final Context context, final AttributeSet attrs, final int defStyle) { 391 super(context, attrs, defStyle); 392 final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs, 393 R.styleable.KeyboardView, defStyle, R.style.KeyboardView); 394 mKeyBackgroundId = keyboardViewAttr.getResourceId( 395 R.styleable.KeyboardView_keyBackground, 0); 396 mEmojiFunctionalKeyBackgroundId = keyboardViewAttr.getResourceId( 397 R.styleable.KeyboardView_keyBackgroundEmojiFunctional, 0); 398 keyboardViewAttr.recycle(); 399 final TypedArray emojiPalettesViewAttr = context.obtainStyledAttributes(attrs, 400 R.styleable.EmojiPalettesView, defStyle, R.style.EmojiPalettesView); 401 mTabLabelColor = emojiPalettesViewAttr.getColorStateList( 402 R.styleable.EmojiPalettesView_emojiTabLabelColor); 403 emojiPalettesViewAttr.recycle(); 404 final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder( 405 context, null /* editorInfo */); 406 final Resources res = context.getResources(); 407 final EmojiLayoutParams emojiLp = new EmojiLayoutParams(res); 408 builder.setSubtype(SubtypeSwitcher.getInstance().getEmojiSubtype()); 409 builder.setKeyboardGeometry(ResourceUtils.getDefaultKeyboardWidth(res), 410 emojiLp.mEmojiKeyboardHeight); 411 builder.setOptions(false, false, false /* lanuageSwitchKeyEnabled */); 412 mLayoutSet = builder.build(); 413 mEmojiCategory = new EmojiCategory(PreferenceManager.getDefaultSharedPreferences(context), 414 context.getResources(), builder.build()); 415 mDeleteKeyOnTouchListener = new DeleteKeyOnTouchListener(context); 416 } 417 418 @Override 419 protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { 420 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 421 final Resources res = getContext().getResources(); 422 // The main keyboard expands to the entire this {@link KeyboardView}. 423 final int width = ResourceUtils.getDefaultKeyboardWidth(res) 424 + getPaddingLeft() + getPaddingRight(); 425 final int height = ResourceUtils.getDefaultKeyboardHeight(res) 426 + res.getDimensionPixelSize(R.dimen.suggestions_strip_height) 427 + getPaddingTop() + getPaddingBottom(); 428 setMeasuredDimension(width, height); 429 } 430 431 private void addTab(final TabHost host, final int categoryId) { 432 final String tabId = mEmojiCategory.getCategoryName(categoryId, 0 /* categoryPageId */); 433 final TabHost.TabSpec tspec = host.newTabSpec(tabId); 434 tspec.setContent(R.id.emoji_keyboard_dummy); 435 if (mEmojiCategory.getCategoryIcon(categoryId) != 0) { 436 final ImageView iconView = (ImageView)LayoutInflater.from(getContext()).inflate( 437 R.layout.emoji_keyboard_tab_icon, null); 438 iconView.setImageResource(mEmojiCategory.getCategoryIcon(categoryId)); 439 tspec.setIndicator(iconView); 440 } 441 if (mEmojiCategory.getCategoryLabel(categoryId) != null) { 442 final TextView textView = (TextView)LayoutInflater.from(getContext()).inflate( 443 R.layout.emoji_keyboard_tab_label, null); 444 textView.setText(mEmojiCategory.getCategoryLabel(categoryId)); 445 textView.setTextColor(mTabLabelColor); 446 tspec.setIndicator(textView); 447 } 448 host.addTab(tspec); 449 } 450 451 @Override 452 protected void onFinishInflate() { 453 mTabHost = (TabHost)findViewById(R.id.emoji_category_tabhost); 454 mTabHost.setup(); 455 for (final CategoryProperties properties : mEmojiCategory.getShownCategories()) { 456 addTab(mTabHost, properties.mCategoryId); 457 } 458 mTabHost.setOnTabChangedListener(this); 459 mTabHost.getTabWidget().setStripEnabled(true); 460 461 mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, mLayoutSet, this); 462 463 mEmojiPager = (ViewPager)findViewById(R.id.emoji_keyboard_pager); 464 mEmojiPager.setAdapter(mEmojiPalettesAdapter); 465 mEmojiPager.setOnPageChangeListener(this); 466 mEmojiPager.setOffscreenPageLimit(0); 467 mEmojiPager.setPersistentDrawingCache(ViewPager.PERSISTENT_NO_CACHE); 468 final Resources res = getResources(); 469 final EmojiLayoutParams emojiLp = new EmojiLayoutParams(res); 470 emojiLp.setPagerProperties(mEmojiPager); 471 472 mEmojiCategoryPageIndicatorView = 473 (EmojiCategoryPageIndicatorView)findViewById(R.id.emoji_category_page_id_view); 474 emojiLp.setCategoryPageIdViewProperties(mEmojiCategoryPageIndicatorView); 475 476 setCurrentCategoryId(mEmojiCategory.getCurrentCategoryId(), true /* force */); 477 478 final LinearLayout actionBar = (LinearLayout)findViewById(R.id.emoji_action_bar); 479 emojiLp.setActionBarProperties(actionBar); 480 481 final ImageView deleteKey = (ImageView)findViewById(R.id.emoji_keyboard_delete); 482 deleteKey.setTag(Constants.CODE_DELETE); 483 deleteKey.setOnTouchListener(mDeleteKeyOnTouchListener); 484 final ImageView alphabetKey = (ImageView)findViewById(R.id.emoji_keyboard_alphabet); 485 alphabetKey.setBackgroundResource(mEmojiFunctionalKeyBackgroundId); 486 alphabetKey.setTag(Constants.CODE_SWITCH_ALPHA_SYMBOL); 487 alphabetKey.setOnClickListener(this); 488 final ImageView spaceKey = (ImageView)findViewById(R.id.emoji_keyboard_space); 489 spaceKey.setBackgroundResource(mKeyBackgroundId); 490 spaceKey.setTag(Constants.CODE_SPACE); 491 spaceKey.setOnClickListener(this); 492 emojiLp.setKeyProperties(spaceKey); 493 final ImageView alphabetKey2 = (ImageView)findViewById(R.id.emoji_keyboard_alphabet2); 494 alphabetKey2.setBackgroundResource(mEmojiFunctionalKeyBackgroundId); 495 alphabetKey2.setTag(Constants.CODE_SWITCH_ALPHA_SYMBOL); 496 alphabetKey2.setOnClickListener(this); 497 } 498 499 @Override 500 public void onTabChanged(final String tabId) { 501 final int categoryId = mEmojiCategory.getCategoryId(tabId); 502 setCurrentCategoryId(categoryId, false /* force */); 503 updateEmojiCategoryPageIdView(); 504 } 505 506 507 @Override 508 public void onPageSelected(final int position) { 509 final Pair<Integer, Integer> newPos = 510 mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position); 511 setCurrentCategoryId(newPos.first /* categoryId */, false /* force */); 512 mEmojiCategory.setCurrentCategoryPageId(newPos.second /* categoryPageId */); 513 updateEmojiCategoryPageIdView(); 514 mCurrentPagerPosition = position; 515 } 516 517 @Override 518 public void onPageScrollStateChanged(final int state) { 519 // Ignore this message. Only want the actual page selected. 520 } 521 522 @Override 523 public void onPageScrolled(final int position, final float positionOffset, 524 final int positionOffsetPixels) { 525 final Pair<Integer, Integer> newPos = 526 mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position); 527 final int newCategoryId = newPos.first; 528 final int newCategorySize = mEmojiCategory.getCategoryPageSize(newCategoryId); 529 final int currentCategoryId = mEmojiCategory.getCurrentCategoryId(); 530 final int currentCategoryPageId = mEmojiCategory.getCurrentCategoryPageId(); 531 final int currentCategorySize = mEmojiCategory.getCurrentCategoryPageSize(); 532 if (newCategoryId == currentCategoryId) { 533 mEmojiCategoryPageIndicatorView.setCategoryPageId( 534 newCategorySize, newPos.second, positionOffset); 535 } else if (newCategoryId > currentCategoryId) { 536 mEmojiCategoryPageIndicatorView.setCategoryPageId( 537 currentCategorySize, currentCategoryPageId, positionOffset); 538 } else if (newCategoryId < currentCategoryId) { 539 mEmojiCategoryPageIndicatorView.setCategoryPageId( 540 currentCategorySize, currentCategoryPageId, positionOffset - 1); 541 } 542 } 543 544 @Override 545 public void onClick(final View v) { 546 if (v.getTag() instanceof Integer) { 547 final int code = (Integer)v.getTag(); 548 registerCode(code); 549 return; 550 } 551 } 552 553 private void registerCode(final int code) { 554 mKeyboardActionListener.onPressKey(code, 0 /* repeatCount */, true /* isSinglePointer */); 555 mKeyboardActionListener.onCodeInput(code, NOT_A_COORDINATE, NOT_A_COORDINATE); 556 mKeyboardActionListener.onReleaseKey(code, false /* withSliding */); 557 } 558 559 @Override 560 public void onKeyClick(final Key key) { 561 mEmojiPalettesAdapter.addRecentKey(key); 562 mEmojiCategory.saveLastTypedCategoryPage(); 563 final int code = key.getCode(); 564 if (code == Constants.CODE_OUTPUT_TEXT) { 565 mKeyboardActionListener.onTextInput(key.getOutputText()); 566 return; 567 } 568 registerCode(code); 569 } 570 571 public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) { 572 // TODO: 573 } 574 575 public void startEmojiPalettes() { 576 if (DEBUG_PAGER) { 577 Log.d(TAG, "allocate emoji palettes memory " + mCurrentPagerPosition); 578 } 579 mEmojiPager.setAdapter(mEmojiPalettesAdapter); 580 mEmojiPager.setCurrentItem(mCurrentPagerPosition); 581 } 582 583 public void stopEmojiPalettes() { 584 if (DEBUG_PAGER) { 585 Log.d(TAG, "deallocate emoji palettes memory"); 586 } 587 mEmojiPalettesAdapter.flushPendingRecentKeys(); 588 mEmojiPager.setAdapter(null); 589 } 590 591 public void setKeyboardActionListener(final KeyboardActionListener listener) { 592 mKeyboardActionListener = listener; 593 mDeleteKeyOnTouchListener.setKeyboardActionListener(mKeyboardActionListener); 594 } 595 596 private void updateEmojiCategoryPageIdView() { 597 if (mEmojiCategoryPageIndicatorView == null) { 598 return; 599 } 600 mEmojiCategoryPageIndicatorView.setCategoryPageId( 601 mEmojiCategory.getCurrentCategoryPageSize(), 602 mEmojiCategory.getCurrentCategoryPageId(), 0.0f /* offset */); 603 } 604 605 private void setCurrentCategoryId(final int categoryId, final boolean force) { 606 final int oldCategoryId = mEmojiCategory.getCurrentCategoryId(); 607 if (oldCategoryId == categoryId && !force) { 608 return; 609 } 610 611 if (oldCategoryId == CATEGORY_ID_RECENTS) { 612 // Needs to save pending updates for recent keys when we get out of the recents 613 // category because we don't want to move the recent emojis around while the user 614 // is in the recents category. 615 mEmojiPalettesAdapter.flushPendingRecentKeys(); 616 } 617 618 mEmojiCategory.setCurrentCategoryId(categoryId); 619 final int newTabId = mEmojiCategory.getTabIdFromCategoryId(categoryId); 620 final int newCategoryPageId = mEmojiCategory.getPageIdFromCategoryId(categoryId); 621 if (force || mEmojiCategory.getCategoryIdAndPageIdFromPagePosition( 622 mEmojiPager.getCurrentItem()).first != categoryId) { 623 mEmojiPager.setCurrentItem(newCategoryPageId, false /* smoothScroll */); 624 } 625 if (force || mTabHost.getCurrentTab() != newTabId) { 626 mTabHost.setCurrentTab(newTabId); 627 } 628 } 629 630 private static class EmojiPalettesAdapter extends PagerAdapter { 631 private final ScrollKeyboardView.OnKeyClickListener mListener; 632 private final DynamicGridKeyboard mRecentsKeyboard; 633 private final SparseArray<ScrollKeyboardView> mActiveKeyboardViews = 634 CollectionUtils.newSparseArray(); 635 private final EmojiCategory mEmojiCategory; 636 private int mActivePosition = 0; 637 638 public EmojiPalettesAdapter(final EmojiCategory emojiCategory, 639 final KeyboardLayoutSet layoutSet, 640 final ScrollKeyboardView.OnKeyClickListener listener) { 641 mEmojiCategory = emojiCategory; 642 mListener = listener; 643 mRecentsKeyboard = mEmojiCategory.getKeyboard(CATEGORY_ID_RECENTS, 0); 644 } 645 646 public void flushPendingRecentKeys() { 647 mRecentsKeyboard.flushPendingRecentKeys(); 648 final KeyboardView recentKeyboardView = 649 mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId()); 650 if (recentKeyboardView != null) { 651 recentKeyboardView.invalidateAllKeys(); 652 } 653 } 654 655 public void addRecentKey(final Key key) { 656 if (mEmojiCategory.isInRecentTab()) { 657 mRecentsKeyboard.addPendingKey(key); 658 return; 659 } 660 mRecentsKeyboard.addKeyFirst(key); 661 final KeyboardView recentKeyboardView = 662 mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId()); 663 if (recentKeyboardView != null) { 664 recentKeyboardView.invalidateAllKeys(); 665 } 666 } 667 668 @Override 669 public int getCount() { 670 return mEmojiCategory.getTotalPageCountOfAllCategories(); 671 } 672 673 @Override 674 public void setPrimaryItem(final View container, final int position, final Object object) { 675 if (mActivePosition == position) { 676 return; 677 } 678 final ScrollKeyboardView oldKeyboardView = mActiveKeyboardViews.get(mActivePosition); 679 if (oldKeyboardView != null) { 680 oldKeyboardView.releaseCurrentKey(); 681 oldKeyboardView.deallocateMemory(); 682 } 683 mActivePosition = position; 684 } 685 686 @Override 687 public Object instantiateItem(final ViewGroup container, final int position) { 688 if (DEBUG_PAGER) { 689 Log.d(TAG, "instantiate item: " + position); 690 } 691 final ScrollKeyboardView oldKeyboardView = mActiveKeyboardViews.get(position); 692 if (oldKeyboardView != null) { 693 oldKeyboardView.deallocateMemory(); 694 // This may be redundant but wanted to be safer.. 695 mActiveKeyboardViews.remove(position); 696 } 697 final Keyboard keyboard = 698 mEmojiCategory.getKeyboardFromPagePosition(position); 699 final LayoutInflater inflater = LayoutInflater.from(container.getContext()); 700 final View view = inflater.inflate( 701 R.layout.emoji_keyboard_page, container, false /* attachToRoot */); 702 final ScrollKeyboardView keyboardView = (ScrollKeyboardView)view.findViewById( 703 R.id.emoji_keyboard_page); 704 keyboardView.setKeyboard(keyboard); 705 keyboardView.setOnKeyClickListener(mListener); 706 final ScrollViewWithNotifier scrollView = (ScrollViewWithNotifier)view.findViewById( 707 R.id.emoji_keyboard_scroller); 708 keyboardView.setScrollView(scrollView); 709 container.addView(view); 710 mActiveKeyboardViews.put(position, keyboardView); 711 return view; 712 } 713 714 @Override 715 public boolean isViewFromObject(final View view, final Object object) { 716 return view == object; 717 } 718 719 @Override 720 public void destroyItem(final ViewGroup container, final int position, 721 final Object object) { 722 if (DEBUG_PAGER) { 723 Log.d(TAG, "destroy item: " + position + ", " + object.getClass().getSimpleName()); 724 } 725 final ScrollKeyboardView keyboardView = mActiveKeyboardViews.get(position); 726 if (keyboardView != null) { 727 keyboardView.deallocateMemory(); 728 mActiveKeyboardViews.remove(position); 729 } 730 if (object instanceof View) { 731 container.removeView((View)object); 732 } else { 733 Log.w(TAG, "Warning!!! Emoji palette may be leaking. " + object); 734 } 735 } 736 } 737 738 // TODO: Do the same things done in PointerTracker 739 private static class DeleteKeyOnTouchListener implements OnTouchListener { 740 private static final long MAX_REPEAT_COUNT_TIME = 30 * DateUtils.SECOND_IN_MILLIS; 741 private final int mDeleteKeyPressedBackgroundColor; 742 private final long mKeyRepeatStartTimeout; 743 private final long mKeyRepeatInterval; 744 745 public DeleteKeyOnTouchListener(Context context) { 746 final Resources res = context.getResources(); 747 mDeleteKeyPressedBackgroundColor = 748 res.getColor(R.color.emoji_key_pressed_background_color); 749 mKeyRepeatStartTimeout = res.getInteger(R.integer.config_key_repeat_start_timeout); 750 mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval); 751 } 752 753 private KeyboardActionListener mKeyboardActionListener = 754 KeyboardActionListener.EMPTY_LISTENER; 755 private DummyRepeatKeyRepeatTimer mTimer; 756 757 private synchronized void startRepeat() { 758 if (mTimer != null) { 759 abortRepeat(); 760 } 761 mTimer = new DummyRepeatKeyRepeatTimer(); 762 mTimer.start(); 763 } 764 765 private synchronized void abortRepeat() { 766 mTimer.abort(); 767 mTimer = null; 768 } 769 770 // TODO: Remove 771 // This function is mimicking the repeat code in PointerTracker. 772 // Specifically referring to PointerTracker#startRepeatKey and PointerTracker#onKeyRepeat. 773 private class DummyRepeatKeyRepeatTimer extends Thread { 774 public boolean mAborted = false; 775 776 @Override 777 public void run() { 778 int repeatCount = 1; 779 int timeCount = 0; 780 while (timeCount < MAX_REPEAT_COUNT_TIME && !mAborted) { 781 if (timeCount > mKeyRepeatStartTimeout) { 782 pressDelete(repeatCount); 783 } 784 timeCount += mKeyRepeatInterval; 785 ++repeatCount; 786 try { 787 Thread.sleep(mKeyRepeatInterval); 788 } catch (InterruptedException e) { 789 } 790 } 791 } 792 793 public void abort() { 794 mAborted = true; 795 } 796 } 797 798 public void pressDelete(int repeatCount) { 799 mKeyboardActionListener.onPressKey( 800 Constants.CODE_DELETE, repeatCount, true /* isSinglePointer */); 801 mKeyboardActionListener.onCodeInput( 802 Constants.CODE_DELETE, NOT_A_COORDINATE, NOT_A_COORDINATE); 803 mKeyboardActionListener.onReleaseKey( 804 Constants.CODE_DELETE, false /* withSliding */); 805 } 806 807 public void setKeyboardActionListener(KeyboardActionListener listener) { 808 mKeyboardActionListener = listener; 809 } 810 811 @Override 812 public boolean onTouch(View v, MotionEvent event) { 813 switch(event.getAction()) { 814 case MotionEvent.ACTION_DOWN: 815 v.setBackgroundColor(mDeleteKeyPressedBackgroundColor); 816 pressDelete(0 /* repeatCount */); 817 startRepeat(); 818 return true; 819 case MotionEvent.ACTION_UP: 820 v.setBackgroundColor(0); 821 abortRepeat(); 822 return true; 823 } 824 return false; 825 } 826 } 827 } 828