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 android.content.Context;
20 import android.os.Handler;
21 import android.util.AttributeSet;
22 import android.view.GestureDetector;
23 import android.view.MotionEvent;
24 import android.view.accessibility.AccessibilityEvent;
25 
26 import com.android.inputmethod.accessibility.AccessibilityUtils;
27 import com.android.inputmethod.accessibility.KeyboardAccessibilityDelegate;
28 import com.android.inputmethod.keyboard.Key;
29 import com.android.inputmethod.keyboard.KeyDetector;
30 import com.android.inputmethod.keyboard.Keyboard;
31 import com.android.inputmethod.keyboard.KeyboardView;
32 import 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.
39 final 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 {
onPressKey(Key key)45         public void onPressKey(Key key);
onReleaseKey(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 
EmojiPageKeyboardView(final Context context, final AttributeSet attrs)61     public EmojiPageKeyboardView(final Context context, final AttributeSet attrs) {
62         this(context, attrs, R.attr.keyboardViewStyle);
63     }
64 
EmojiPageKeyboardView(final Context context, final AttributeSet attrs, final int defStyle)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 
setOnKeyEventListener(final OnKeyEventListener listener)73     public void setOnKeyEventListener(final OnKeyEventListener listener) {
74         mListener = listener;
75     }
76 
77     /**
78      * {@inheritDoc}
79      */
80     @Override
setKeyboard(final Keyboard keyboard)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
dispatchPopulateAccessibilityEvent(final AccessibilityEvent event)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
onHoverEvent(final MotionEvent event)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
onTouchEvent(final MotionEvent e)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 
getKey(final MotionEvent e)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 
callListenerOnReleaseKey(final Key releasedKey, final boolean withKeyRegistering)141     void callListenerOnReleaseKey(final Key releasedKey, final boolean withKeyRegistering) {
142         releasedKey.onReleased();
143         invalidateKey(releasedKey);
144         if (withKeyRegistering) {
145             mListener.onReleaseKey(releasedKey);
146         }
147     }
148 
callListenerOnPressKey(final Key pressedKey)149     void callListenerOnPressKey(final Key pressedKey) {
150         mPendingKeyDown = null;
151         pressedKey.onPressed();
152         invalidateKey(pressedKey);
153         mListener.onPressKey(pressedKey);
154     }
155 
releaseCurrentKey(final boolean withKeyRegistering)156     public void releaseCurrentKey(final boolean withKeyRegistering) {
157         mHandler.removeCallbacks(mPendingKeyDown);
158         mPendingKeyDown = null;
159         final Key currentKey = mCurrentKey;
160         if (currentKey == null) {
161             return;
162         }
163         callListenerOnReleaseKey(currentKey, withKeyRegistering);
164         mCurrentKey = null;
165     }
166 
167     @Override
onDown(final MotionEvent e)168     public boolean onDown(final MotionEvent e) {
169         final Key key = getKey(e);
170         releaseCurrentKey(false /* withKeyRegistering */);
171         mCurrentKey = key;
172         if (key == null) {
173             return false;
174         }
175         // Do not trigger key-down effect right now in case this is actually a fling action.
176         mPendingKeyDown = new Runnable() {
177             @Override
178             public void run() {
179                 callListenerOnPressKey(key);
180             }
181         };
182         mHandler.postDelayed(mPendingKeyDown, KEY_PRESS_DELAY_TIME);
183         return false;
184     }
185 
186     @Override
onShowPress(final MotionEvent e)187     public void onShowPress(final MotionEvent e) {
188         // User feedback is done at {@link #onDown(MotionEvent)}.
189     }
190 
191     @Override
onSingleTapUp(final MotionEvent e)192     public boolean onSingleTapUp(final MotionEvent e) {
193         final Key key = getKey(e);
194         final Runnable pendingKeyDown = mPendingKeyDown;
195         final Key currentKey = mCurrentKey;
196         releaseCurrentKey(false /* withKeyRegistering */);
197         if (key == null) {
198             return false;
199         }
200         if (key == currentKey && pendingKeyDown != null) {
201             pendingKeyDown.run();
202             // Trigger key-release event a little later so that a user can see visual feedback.
203             mHandler.postDelayed(new Runnable() {
204                 @Override
205                 public void run() {
206                     callListenerOnReleaseKey(key, true /* withRegistering */);
207                 }
208             }, KEY_RELEASE_DELAY_TIME);
209         } else {
210             callListenerOnReleaseKey(key, true /* withRegistering */);
211         }
212         return true;
213     }
214 
215     @Override
onScroll(final MotionEvent e1, final MotionEvent e2, final float distanceX, final float distanceY)216     public boolean onScroll(final MotionEvent e1, final MotionEvent e2, final float distanceX,
217            final float distanceY) {
218         releaseCurrentKey(false /* withKeyRegistering */);
219         return false;
220     }
221 
222     @Override
onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX, final float velocityY)223     public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX,
224             final float velocityY) {
225         releaseCurrentKey(false /* withKeyRegistering */);
226         return false;
227     }
228 
229     @Override
onLongPress(final MotionEvent e)230     public void onLongPress(final MotionEvent e) {
231         // Long press detection of {@link #mGestureDetector} is disabled and not used.
232     }
233 }
234