Home | History | Annotate | Download | only in emoji
      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.emoji;
     18 
     19 import static com.android.inputmethod.latin.Constants.NOT_A_COORDINATE;
     20 
     21 import android.content.Context;
     22 import android.content.res.Resources;
     23 import android.content.res.TypedArray;
     24 import android.graphics.Color;
     25 import android.os.CountDownTimer;
     26 import android.preference.PreferenceManager;
     27 import android.support.v4.view.ViewPager;
     28 import android.util.AttributeSet;
     29 import android.util.Pair;
     30 import android.util.TypedValue;
     31 import android.view.LayoutInflater;
     32 import android.view.MotionEvent;
     33 import android.view.View;
     34 import android.widget.ImageButton;
     35 import android.widget.ImageView;
     36 import android.widget.LinearLayout;
     37 import android.widget.TabHost;
     38 import android.widget.TabHost.OnTabChangeListener;
     39 import android.widget.TabWidget;
     40 import android.widget.TextView;
     41 
     42 import com.android.inputmethod.keyboard.Key;
     43 import com.android.inputmethod.keyboard.KeyboardActionListener;
     44 import com.android.inputmethod.keyboard.KeyboardLayoutSet;
     45 import com.android.inputmethod.keyboard.KeyboardView;
     46 import com.android.inputmethod.keyboard.internal.KeyDrawParams;
     47 import com.android.inputmethod.keyboard.internal.KeyVisualAttributes;
     48 import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
     49 import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
     50 import com.android.inputmethod.latin.Constants;
     51 import com.android.inputmethod.latin.R;
     52 import com.android.inputmethod.latin.SubtypeSwitcher;
     53 import com.android.inputmethod.latin.utils.ResourceUtils;
     54 
     55 import java.util.concurrent.TimeUnit;
     56 
     57 /**
     58  * View class to implement Emoji palettes.
     59  * The Emoji keyboard consists of group of views layout/emoji_palettes_view.
     60  * <ol>
     61  * <li> Emoji category tabs.
     62  * <li> Delete button.
     63  * <li> Emoji keyboard pages that can be scrolled by swiping horizontally or by selecting a tab.
     64  * <li> Back to main keyboard button and enter button.
     65  * </ol>
     66  * Because of the above reasons, this class doesn't extend {@link KeyboardView}.
     67  */
     68 public final class EmojiPalettesView extends LinearLayout implements OnTabChangeListener,
     69         ViewPager.OnPageChangeListener, View.OnClickListener, View.OnTouchListener,
     70         EmojiPageKeyboardView.OnKeyEventListener {
     71     private final int mFunctionalKeyBackgroundId;
     72     private final int mSpacebarBackgroundId;
     73     private final boolean mCategoryIndicatorEnabled;
     74     private final int mCategoryIndicatorDrawableResId;
     75     private final int mCategoryIndicatorBackgroundResId;
     76     private final int mCategoryPageIndicatorColor;
     77     private final int mCategoryPageIndicatorBackground;
     78     private final DeleteKeyOnTouchListener mDeleteKeyOnTouchListener;
     79     private EmojiPalettesAdapter mEmojiPalettesAdapter;
     80     private final EmojiLayoutParams mEmojiLayoutParams;
     81 
     82     private ImageButton mDeleteKey;
     83     private TextView mAlphabetKeyLeft;
     84     private TextView mAlphabetKeyRight;
     85     private View mSpacebar;
     86     // TODO: Remove this workaround.
     87     private View mSpacebarIcon;
     88     private TabHost mTabHost;
     89     private ViewPager mEmojiPager;
     90     private int mCurrentPagerPosition = 0;
     91     private EmojiCategoryPageIndicatorView mEmojiCategoryPageIndicatorView;
     92 
     93     private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
     94 
     95     private final EmojiCategory mEmojiCategory;
     96 
     97     public EmojiPalettesView(final Context context, final AttributeSet attrs) {
     98         this(context, attrs, R.attr.emojiPalettesViewStyle);
     99     }
    100 
    101     public EmojiPalettesView(final Context context, final AttributeSet attrs, final int defStyle) {
    102         super(context, attrs, defStyle);
    103         final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
    104                 R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
    105         final int keyBackgroundId = keyboardViewAttr.getResourceId(
    106                 R.styleable.KeyboardView_keyBackground, 0);
    107         mFunctionalKeyBackgroundId = keyboardViewAttr.getResourceId(
    108                 R.styleable.KeyboardView_functionalKeyBackground, keyBackgroundId);
    109         mSpacebarBackgroundId = keyboardViewAttr.getResourceId(
    110                 R.styleable.KeyboardView_spacebarBackground, keyBackgroundId);
    111         keyboardViewAttr.recycle();
    112         final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
    113                 context, null /* editorInfo */);
    114         final Resources res = context.getResources();
    115         mEmojiLayoutParams = new EmojiLayoutParams(res);
    116         builder.setSubtype(SubtypeSwitcher.getInstance().getEmojiSubtype());
    117         builder.setKeyboardGeometry(ResourceUtils.getDefaultKeyboardWidth(res),
    118                 mEmojiLayoutParams.mEmojiKeyboardHeight);
    119         final KeyboardLayoutSet layoutSet = builder.build();
    120         final TypedArray emojiPalettesViewAttr = context.obtainStyledAttributes(attrs,
    121                 R.styleable.EmojiPalettesView, defStyle, R.style.EmojiPalettesView);
    122         mEmojiCategory = new EmojiCategory(PreferenceManager.getDefaultSharedPreferences(context),
    123                 res, layoutSet, emojiPalettesViewAttr);
    124         mCategoryIndicatorEnabled = emojiPalettesViewAttr.getBoolean(
    125                 R.styleable.EmojiPalettesView_categoryIndicatorEnabled, false);
    126         mCategoryIndicatorDrawableResId = emojiPalettesViewAttr.getResourceId(
    127                 R.styleable.EmojiPalettesView_categoryIndicatorDrawable, 0);
    128         mCategoryIndicatorBackgroundResId = emojiPalettesViewAttr.getResourceId(
    129                 R.styleable.EmojiPalettesView_categoryIndicatorBackground, 0);
    130         mCategoryPageIndicatorColor = emojiPalettesViewAttr.getColor(
    131                 R.styleable.EmojiPalettesView_categoryPageIndicatorColor, 0);
    132         mCategoryPageIndicatorBackground = emojiPalettesViewAttr.getColor(
    133                 R.styleable.EmojiPalettesView_categoryPageIndicatorBackground, 0);
    134         emojiPalettesViewAttr.recycle();
    135         mDeleteKeyOnTouchListener = new DeleteKeyOnTouchListener(context);
    136     }
    137 
    138     @Override
    139     protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
    140         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    141         final Resources res = getContext().getResources();
    142         // The main keyboard expands to the entire this {@link KeyboardView}.
    143         final int width = ResourceUtils.getDefaultKeyboardWidth(res)
    144                 + getPaddingLeft() + getPaddingRight();
    145         final int height = ResourceUtils.getDefaultKeyboardHeight(res)
    146                 + res.getDimensionPixelSize(R.dimen.config_suggestions_strip_height)
    147                 + getPaddingTop() + getPaddingBottom();
    148         setMeasuredDimension(width, height);
    149     }
    150 
    151     private void addTab(final TabHost host, final int categoryId) {
    152         final String tabId = mEmojiCategory.getCategoryName(categoryId, 0 /* categoryPageId */);
    153         final TabHost.TabSpec tspec = host.newTabSpec(tabId);
    154         tspec.setContent(R.id.emoji_keyboard_dummy);
    155         final ImageView iconView = (ImageView)LayoutInflater.from(getContext()).inflate(
    156                 R.layout.emoji_keyboard_tab_icon, null);
    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, final KeyboardIconsSet iconSet) {
    376         final int deleteIconResId = iconSet.getIconResourceId(KeyboardIconsSet.NAME_DELETE_KEY);
    377         if (deleteIconResId != 0) {
    378             mDeleteKey.setImageResource(deleteIconResId);
    379         }
    380         final int spacebarResId = iconSet.getIconResourceId(KeyboardIconsSet.NAME_SPACE_KEY);
    381         if (spacebarResId != 0) {
    382             // TODO: Remove this workaround to place the spacebar icon.
    383             mSpacebarIcon.setBackgroundResource(spacebarResId);
    384         }
    385         final KeyDrawParams params = new KeyDrawParams();
    386         params.updateParams(mEmojiLayoutParams.getActionBarHeight(), keyVisualAttr);
    387         setupAlphabetKey(mAlphabetKeyLeft, switchToAlphaLabel, params);
    388         setupAlphabetKey(mAlphabetKeyRight, switchToAlphaLabel, params);
    389         mEmojiPager.setAdapter(mEmojiPalettesAdapter);
    390         mEmojiPager.setCurrentItem(mCurrentPagerPosition);
    391     }
    392 
    393     public void stopEmojiPalettes() {
    394         mEmojiPalettesAdapter.releaseCurrentKey(true /* withKeyRegistering */);
    395         mEmojiPalettesAdapter.flushPendingRecentKeys();
    396         mEmojiPager.setAdapter(null);
    397     }
    398 
    399     public void setKeyboardActionListener(final KeyboardActionListener listener) {
    400         mKeyboardActionListener = listener;
    401         mDeleteKeyOnTouchListener.setKeyboardActionListener(mKeyboardActionListener);
    402     }
    403 
    404     private void updateEmojiCategoryPageIdView() {
    405         if (mEmojiCategoryPageIndicatorView == null) {
    406             return;
    407         }
    408         mEmojiCategoryPageIndicatorView.setCategoryPageId(
    409                 mEmojiCategory.getCurrentCategoryPageSize(),
    410                 mEmojiCategory.getCurrentCategoryPageId(), 0.0f /* offset */);
    411     }
    412 
    413     private void setCurrentCategoryId(final int categoryId, final boolean force) {
    414         final int oldCategoryId = mEmojiCategory.getCurrentCategoryId();
    415         if (oldCategoryId == categoryId && !force) {
    416             return;
    417         }
    418 
    419         if (oldCategoryId == EmojiCategory.ID_RECENTS) {
    420             // Needs to save pending updates for recent keys when we get out of the recents
    421             // category because we don't want to move the recent emojis around while the user
    422             // is in the recents category.
    423             mEmojiPalettesAdapter.flushPendingRecentKeys();
    424         }
    425 
    426         mEmojiCategory.setCurrentCategoryId(categoryId);
    427         final int newTabId = mEmojiCategory.getTabIdFromCategoryId(categoryId);
    428         final int newCategoryPageId = mEmojiCategory.getPageIdFromCategoryId(categoryId);
    429         if (force || mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(
    430                 mEmojiPager.getCurrentItem()).first != categoryId) {
    431             mEmojiPager.setCurrentItem(newCategoryPageId, false /* smoothScroll */);
    432         }
    433         if (force || mTabHost.getCurrentTab() != newTabId) {
    434             mTabHost.setCurrentTab(newTabId);
    435         }
    436     }
    437 
    438     private static class DeleteKeyOnTouchListener implements OnTouchListener {
    439         static final long MAX_REPEAT_COUNT_TIME = TimeUnit.SECONDS.toMillis(30);
    440         final long mKeyRepeatStartTimeout;
    441         final long mKeyRepeatInterval;
    442 
    443         public DeleteKeyOnTouchListener(Context context) {
    444             final Resources res = context.getResources();
    445             mKeyRepeatStartTimeout = res.getInteger(R.integer.config_key_repeat_start_timeout);
    446             mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval);
    447             mTimer = new CountDownTimer(MAX_REPEAT_COUNT_TIME, mKeyRepeatInterval) {
    448                 @Override
    449                 public void onTick(long millisUntilFinished) {
    450                     final long elapsed = MAX_REPEAT_COUNT_TIME - millisUntilFinished;
    451                     if (elapsed < mKeyRepeatStartTimeout) {
    452                         return;
    453                     }
    454                     onKeyRepeat();
    455                 }
    456                 @Override
    457                 public void onFinish() {
    458                     onKeyRepeat();
    459                 }
    460             };
    461         }
    462 
    463         /** Key-repeat state. */
    464         private static final int KEY_REPEAT_STATE_INITIALIZED = 0;
    465         // The key is touched but auto key-repeat is not started yet.
    466         private static final int KEY_REPEAT_STATE_KEY_DOWN = 1;
    467         // At least one key-repeat event has already been triggered and the key is not released.
    468         private static final int KEY_REPEAT_STATE_KEY_REPEAT = 2;
    469 
    470         private KeyboardActionListener mKeyboardActionListener =
    471                 KeyboardActionListener.EMPTY_LISTENER;
    472 
    473         // TODO: Do the same things done in PointerTracker
    474         private final CountDownTimer mTimer;
    475         private int mState = KEY_REPEAT_STATE_INITIALIZED;
    476         private int mRepeatCount = 0;
    477 
    478         public void setKeyboardActionListener(final KeyboardActionListener listener) {
    479             mKeyboardActionListener = listener;
    480         }
    481 
    482         @Override
    483         public boolean onTouch(final View v, final MotionEvent event) {
    484             switch (event.getActionMasked()) {
    485             case MotionEvent.ACTION_DOWN:
    486                 onTouchDown(v);
    487                 return true;
    488             case MotionEvent.ACTION_MOVE:
    489                 final float x = event.getX();
    490                 final float y = event.getY();
    491                 if (x < 0.0f || v.getWidth() < x || y < 0.0f || v.getHeight() < y) {
    492                     // Stop generating key events once the finger moves away from the view area.
    493                     onTouchCanceled(v);
    494                 }
    495                 return true;
    496             case MotionEvent.ACTION_CANCEL:
    497             case MotionEvent.ACTION_UP:
    498                 onTouchUp(v);
    499                 return true;
    500             }
    501             return false;
    502         }
    503 
    504         private void handleKeyDown() {
    505             mKeyboardActionListener.onPressKey(
    506                     Constants.CODE_DELETE, mRepeatCount, true /* isSinglePointer */);
    507         }
    508 
    509         private void handleKeyUp() {
    510             mKeyboardActionListener.onCodeInput(Constants.CODE_DELETE,
    511                     NOT_A_COORDINATE, NOT_A_COORDINATE, false /* isKeyRepeat */);
    512             mKeyboardActionListener.onReleaseKey(
    513                     Constants.CODE_DELETE, false /* withSliding */);
    514             ++mRepeatCount;
    515         }
    516 
    517         private void onTouchDown(final View v) {
    518             mTimer.cancel();
    519             mRepeatCount = 0;
    520             handleKeyDown();
    521             v.setPressed(true /* pressed */);
    522             mState = KEY_REPEAT_STATE_KEY_DOWN;
    523             mTimer.start();
    524         }
    525 
    526         private void onTouchUp(final View v) {
    527             mTimer.cancel();
    528             if (mState == KEY_REPEAT_STATE_KEY_DOWN) {
    529                 handleKeyUp();
    530             }
    531             v.setPressed(false /* pressed */);
    532             mState = KEY_REPEAT_STATE_INITIALIZED;
    533         }
    534 
    535         private void onTouchCanceled(final View v) {
    536             mTimer.cancel();
    537             v.setBackgroundColor(Color.TRANSPARENT);
    538             mState = KEY_REPEAT_STATE_INITIALIZED;
    539         }
    540 
    541         // Called by {@link #mTimer} in the UI thread as an auto key-repeat signal.
    542         void onKeyRepeat() {
    543             switch (mState) {
    544             case KEY_REPEAT_STATE_INITIALIZED:
    545                 // Basically this should not happen.
    546                 break;
    547             case KEY_REPEAT_STATE_KEY_DOWN:
    548                 // Do not call {@link #handleKeyDown} here because it has already been called
    549                 // in {@link #onTouchDown}.
    550                 handleKeyUp();
    551                 mState = KEY_REPEAT_STATE_KEY_REPEAT;
    552                 break;
    553             case KEY_REPEAT_STATE_KEY_REPEAT:
    554                 handleKeyDown();
    555                 handleKeyUp();
    556                 break;
    557             }
    558         }
    559     }
    560 }
    561