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 android.content.Context; 20import android.os.Handler; 21import android.util.AttributeSet; 22import android.view.GestureDetector; 23import android.view.MotionEvent; 24import android.view.accessibility.AccessibilityEvent; 25 26import com.android.inputmethod.accessibility.AccessibilityUtils; 27import com.android.inputmethod.accessibility.KeyboardAccessibilityDelegate; 28import com.android.inputmethod.keyboard.Key; 29import com.android.inputmethod.keyboard.KeyDetector; 30import com.android.inputmethod.keyboard.Keyboard; 31import com.android.inputmethod.keyboard.KeyboardView; 32import com.android.inputmethod.latin.R; 33 34/** 35 * This is an extended {@link KeyboardView} class that hosts an emoji page keyboard. 36 * Multi-touch unsupported. No gesture support. 37 */ 38// TODO: Implement key popup preview. 39final class EmojiPageKeyboardView extends KeyboardView implements 40 GestureDetector.OnGestureListener { 41 private static final long KEY_PRESS_DELAY_TIME = 250; // msec 42 private static final long KEY_RELEASE_DELAY_TIME = 30; // msec 43 44 public interface OnKeyEventListener { 45 public void onPressKey(Key key); 46 public void onReleaseKey(Key key); 47 } 48 49 private static final OnKeyEventListener EMPTY_LISTENER = new OnKeyEventListener() { 50 @Override 51 public void onPressKey(final Key key) {} 52 @Override 53 public void onReleaseKey(final Key key) {} 54 }; 55 56 private OnKeyEventListener mListener = EMPTY_LISTENER; 57 private final KeyDetector mKeyDetector = new KeyDetector(); 58 private final GestureDetector mGestureDetector; 59 private KeyboardAccessibilityDelegate<EmojiPageKeyboardView> mAccessibilityDelegate; 60 61 public EmojiPageKeyboardView(final Context context, final AttributeSet attrs) { 62 this(context, attrs, R.attr.keyboardViewStyle); 63 } 64 65 public EmojiPageKeyboardView(final Context context, final AttributeSet attrs, 66 final int defStyle) { 67 super(context, attrs, defStyle); 68 mGestureDetector = new GestureDetector(context, this); 69 mGestureDetector.setIsLongpressEnabled(false /* isLongpressEnabled */); 70 mHandler = new Handler(); 71 } 72 73 public void setOnKeyEventListener(final OnKeyEventListener listener) { 74 mListener = listener; 75 } 76 77 /** 78 * {@inheritDoc} 79 */ 80 @Override 81 public void setKeyboard(final Keyboard keyboard) { 82 super.setKeyboard(keyboard); 83 mKeyDetector.setKeyboard(keyboard, 0 /* correctionX */, 0 /* correctionY */); 84 if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) { 85 if (mAccessibilityDelegate == null) { 86 mAccessibilityDelegate = new KeyboardAccessibilityDelegate<>(this, mKeyDetector); 87 } 88 mAccessibilityDelegate.setKeyboard(keyboard); 89 } else { 90 mAccessibilityDelegate = null; 91 } 92 } 93 94 @Override 95 public boolean dispatchPopulateAccessibilityEvent(final AccessibilityEvent event) { 96 // Don't populate accessibility event with all Emoji keys. 97 return true; 98 } 99 100 /** 101 * {@inheritDoc} 102 */ 103 @Override 104 public boolean onHoverEvent(final MotionEvent event) { 105 final KeyboardAccessibilityDelegate<EmojiPageKeyboardView> accessibilityDelegate = 106 mAccessibilityDelegate; 107 if (accessibilityDelegate != null 108 && AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { 109 return accessibilityDelegate.onHoverEvent(event); 110 } 111 return super.onHoverEvent(event); 112 } 113 114 /** 115 * {@inheritDoc} 116 */ 117 @Override 118 public boolean onTouchEvent(final MotionEvent e) { 119 if (mGestureDetector.onTouchEvent(e)) { 120 return true; 121 } 122 final Key key = getKey(e); 123 if (key != null && key != mCurrentKey) { 124 releaseCurrentKey(false /* withKeyRegistering */); 125 } 126 return true; 127 } 128 129 // {@link GestureEnabler#OnGestureListener} methods. 130 private Key mCurrentKey; 131 private Runnable mPendingKeyDown; 132 private final Handler mHandler; 133 134 private Key getKey(final MotionEvent e) { 135 final int index = e.getActionIndex(); 136 final int x = (int)e.getX(index); 137 final int y = (int)e.getY(index); 138 return mKeyDetector.detectHitKey(x, y); 139 } 140 141 public void releaseCurrentKey(final boolean withKeyRegistering) { 142 mHandler.removeCallbacks(mPendingKeyDown); 143 mPendingKeyDown = null; 144 final Key currentKey = mCurrentKey; 145 if (currentKey == null) { 146 return; 147 } 148 currentKey.onReleased(); 149 invalidateKey(currentKey); 150 if (withKeyRegistering) { 151 mListener.onReleaseKey(currentKey); 152 } 153 mCurrentKey = null; 154 } 155 156 @Override 157 public boolean onDown(final MotionEvent e) { 158 final Key key = getKey(e); 159 releaseCurrentKey(false /* withKeyRegistering */); 160 mCurrentKey = key; 161 if (key == null) { 162 return false; 163 } 164 // Do not trigger key-down effect right now in case this is actually a fling action. 165 mPendingKeyDown = new Runnable() { 166 @Override 167 public void run() { 168 mPendingKeyDown = null; 169 key.onPressed(); 170 invalidateKey(key); 171 mListener.onPressKey(key); 172 } 173 }; 174 mHandler.postDelayed(mPendingKeyDown, KEY_PRESS_DELAY_TIME); 175 return false; 176 } 177 178 @Override 179 public void onShowPress(final MotionEvent e) { 180 // User feedback is done at {@link #onDown(MotionEvent)}. 181 } 182 183 @Override 184 public boolean onSingleTapUp(final MotionEvent e) { 185 final Key key = getKey(e); 186 final Runnable pendingKeyDown = mPendingKeyDown; 187 final Key currentKey = mCurrentKey; 188 releaseCurrentKey(false /* withKeyRegistering */); 189 if (key == null) { 190 return false; 191 } 192 if (key == currentKey && pendingKeyDown != null) { 193 pendingKeyDown.run(); 194 // Trigger key-release event a little later so that a user can see visual feedback. 195 mHandler.postDelayed(new Runnable() { 196 @Override 197 public void run() { 198 key.onReleased(); 199 invalidateKey(key); 200 mListener.onReleaseKey(key); 201 } 202 }, KEY_RELEASE_DELAY_TIME); 203 } else { 204 key.onReleased(); 205 invalidateKey(key); 206 mListener.onReleaseKey(key); 207 } 208 return true; 209 } 210 211 @Override 212 public boolean onScroll(final MotionEvent e1, final MotionEvent e2, final float distanceX, 213 final float distanceY) { 214 releaseCurrentKey(false /* withKeyRegistering */); 215 return false; 216 } 217 218 @Override 219 public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX, 220 final float velocityY) { 221 releaseCurrentKey(false /* withKeyRegistering */); 222 return false; 223 } 224 225 @Override 226 public void onLongPress(final MotionEvent e) { 227 // Long press detection of {@link #mGestureDetector} is disabled and not used. 228 } 229} 230