1 /*
2  * Copyright (C) 2011 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.latin;
18 
19 import android.content.Context;
20 import android.graphics.Rect;
21 import android.util.AttributeSet;
22 import android.view.MotionEvent;
23 import android.view.View;
24 import android.widget.FrameLayout;
25 
26 import com.android.inputmethod.accessibility.AccessibilityUtils;
27 import com.android.inputmethod.keyboard.MainKeyboardView;
28 import com.android.inputmethod.latin.suggestions.MoreSuggestionsView;
29 import com.android.inputmethod.latin.suggestions.SuggestionStripView;
30 
31 public final class InputView extends FrameLayout {
32     private final Rect mInputViewRect = new Rect();
33     private MainKeyboardView mMainKeyboardView;
34     private KeyboardTopPaddingForwarder mKeyboardTopPaddingForwarder;
35     private MoreSuggestionsViewCanceler mMoreSuggestionsViewCanceler;
36     private MotionEventForwarder<?, ?> mActiveForwarder;
37 
InputView(final Context context, final AttributeSet attrs)38     public InputView(final Context context, final AttributeSet attrs) {
39         super(context, attrs, 0);
40     }
41 
42     @Override
onFinishInflate()43     protected void onFinishInflate() {
44         final SuggestionStripView suggestionStripView =
45                 (SuggestionStripView)findViewById(R.id.suggestion_strip_view);
46         mMainKeyboardView = (MainKeyboardView)findViewById(R.id.keyboard_view);
47         mKeyboardTopPaddingForwarder = new KeyboardTopPaddingForwarder(
48                 mMainKeyboardView, suggestionStripView);
49         mMoreSuggestionsViewCanceler = new MoreSuggestionsViewCanceler(
50                 mMainKeyboardView, suggestionStripView);
51     }
52 
setKeyboardTopPadding(final int keyboardTopPadding)53     public void setKeyboardTopPadding(final int keyboardTopPadding) {
54         mKeyboardTopPaddingForwarder.setKeyboardTopPadding(keyboardTopPadding);
55     }
56 
57     @Override
dispatchHoverEvent(final MotionEvent event)58     protected boolean dispatchHoverEvent(final MotionEvent event) {
59         if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()
60                 && mMainKeyboardView.isShowingMoreKeysPanel()) {
61             // With accessibility mode on, discard hover events while a more keys keyboard is shown.
62             // The {@link MoreKeysKeyboard} receives hover events directly from the platform.
63             return true;
64         }
65         return super.dispatchHoverEvent(event);
66     }
67 
68     @Override
onInterceptTouchEvent(final MotionEvent me)69     public boolean onInterceptTouchEvent(final MotionEvent me) {
70         final Rect rect = mInputViewRect;
71         getGlobalVisibleRect(rect);
72         final int index = me.getActionIndex();
73         final int x = (int)me.getX(index) + rect.left;
74         final int y = (int)me.getY(index) + rect.top;
75 
76         // The touch events that hit the top padding of keyboard should be forwarded to
77         // {@link SuggestionStripView}.
78         if (mKeyboardTopPaddingForwarder.onInterceptTouchEvent(x, y, me)) {
79             mActiveForwarder = mKeyboardTopPaddingForwarder;
80             return true;
81         }
82 
83         // To cancel {@link MoreSuggestionsView}, we should intercept a touch event to
84         // {@link MainKeyboardView} and dismiss the {@link MoreSuggestionsView}.
85         if (mMoreSuggestionsViewCanceler.onInterceptTouchEvent(x, y, me)) {
86             mActiveForwarder = mMoreSuggestionsViewCanceler;
87             return true;
88         }
89 
90         mActiveForwarder = null;
91         return false;
92     }
93 
94     @Override
onTouchEvent(final MotionEvent me)95     public boolean onTouchEvent(final MotionEvent me) {
96         if (mActiveForwarder == null) {
97             return super.onTouchEvent(me);
98         }
99 
100         final Rect rect = mInputViewRect;
101         getGlobalVisibleRect(rect);
102         final int index = me.getActionIndex();
103         final int x = (int)me.getX(index) + rect.left;
104         final int y = (int)me.getY(index) + rect.top;
105         return mActiveForwarder.onTouchEvent(x, y, me);
106     }
107 
108     /**
109      * This class forwards series of {@link MotionEvent}s from <code>SenderView</code> to
110      * <code>ReceiverView</code>.
111      *
112      * @param <SenderView> a {@link View} that may send a {@link MotionEvent} to <ReceiverView>.
113      * @param <ReceiverView> a {@link View} that receives forwarded {@link MotionEvent} from
114      *     <SenderView>.
115      */
116     private static abstract class
117             MotionEventForwarder<SenderView extends View, ReceiverView extends View> {
118         protected final SenderView mSenderView;
119         protected final ReceiverView mReceiverView;
120 
121         protected final Rect mEventSendingRect = new Rect();
122         protected final Rect mEventReceivingRect = new Rect();
123 
MotionEventForwarder(final SenderView senderView, final ReceiverView receiverView)124         public MotionEventForwarder(final SenderView senderView, final ReceiverView receiverView) {
125             mSenderView = senderView;
126             mReceiverView = receiverView;
127         }
128 
129         // Return true if a touch event of global coordinate x, y needs to be forwarded.
needsToForward(final int x, final int y)130         protected abstract boolean needsToForward(final int x, final int y);
131 
132         // Translate global x-coordinate to <code>ReceiverView</code> local coordinate.
translateX(final int x)133         protected int translateX(final int x) {
134             return x - mEventReceivingRect.left;
135         }
136 
137         // Translate global y-coordinate to <code>ReceiverView</code> local coordinate.
translateY(final int y)138         protected int translateY(final int y) {
139             return y - mEventReceivingRect.top;
140         }
141 
142         /**
143          * Callback when a {@link MotionEvent} is forwarded.
144          * @param me the motion event to be forwarded.
145          */
onForwardingEvent(final MotionEvent me)146         protected void onForwardingEvent(final MotionEvent me) {}
147 
148         // Returns true if a {@link MotionEvent} is needed to be forwarded to
149         // <code>ReceiverView</code>. Otherwise returns false.
onInterceptTouchEvent(final int x, final int y, final MotionEvent me)150         public boolean onInterceptTouchEvent(final int x, final int y, final MotionEvent me) {
151             // Forwards a {link MotionEvent} only if both <code>SenderView</code> and
152             // <code>ReceiverView</code> are visible.
153             if (mSenderView.getVisibility() != View.VISIBLE ||
154                     mReceiverView.getVisibility() != View.VISIBLE) {
155                 return false;
156             }
157             mSenderView.getGlobalVisibleRect(mEventSendingRect);
158             if (!mEventSendingRect.contains(x, y)) {
159                 return false;
160             }
161 
162             if (me.getActionMasked() == MotionEvent.ACTION_DOWN) {
163                 // If the down event happens in the forwarding area, successive
164                 // {@link MotionEvent}s should be forwarded to <code>ReceiverView</code>.
165                 if (needsToForward(x, y)) {
166                     return true;
167                 }
168             }
169 
170             return false;
171         }
172 
173         // Returns true if a {@link MotionEvent} is forwarded to <code>ReceiverView</code>.
174         // Otherwise returns false.
onTouchEvent(final int x, final int y, final MotionEvent me)175         public boolean onTouchEvent(final int x, final int y, final MotionEvent me) {
176             mReceiverView.getGlobalVisibleRect(mEventReceivingRect);
177             // Translate global coordinates to <code>ReceiverView</code> local coordinates.
178             me.setLocation(translateX(x), translateY(y));
179             mReceiverView.dispatchTouchEvent(me);
180             onForwardingEvent(me);
181             return true;
182         }
183     }
184 
185     /**
186      * This class forwards {@link MotionEvent}s happened in the top padding of
187      * {@link MainKeyboardView} to {@link SuggestionStripView}.
188      */
189     private static class KeyboardTopPaddingForwarder
190             extends MotionEventForwarder<MainKeyboardView, SuggestionStripView> {
191         private int mKeyboardTopPadding;
192 
KeyboardTopPaddingForwarder(final MainKeyboardView mainKeyboardView, final SuggestionStripView suggestionStripView)193         public KeyboardTopPaddingForwarder(final MainKeyboardView mainKeyboardView,
194                 final SuggestionStripView suggestionStripView) {
195             super(mainKeyboardView, suggestionStripView);
196         }
197 
setKeyboardTopPadding(final int keyboardTopPadding)198         public void setKeyboardTopPadding(final int keyboardTopPadding) {
199             mKeyboardTopPadding = keyboardTopPadding;
200         }
201 
isInKeyboardTopPadding(final int y)202         private boolean isInKeyboardTopPadding(final int y) {
203             return y < mEventSendingRect.top + mKeyboardTopPadding;
204         }
205 
206         @Override
needsToForward(final int x, final int y)207         protected boolean needsToForward(final int x, final int y) {
208             // Forwarding an event only when {@link MainKeyboardView} is visible.
209             // Because the visibility of {@link MainKeyboardView} is controlled by its parent
210             // view in {@link KeyboardSwitcher#setMainKeyboardFrame()}, we should check the
211             // visibility of the parent view.
212             final View mainKeyboardFrame = (View)mSenderView.getParent();
213             return mainKeyboardFrame.getVisibility() == View.VISIBLE && isInKeyboardTopPadding(y);
214         }
215 
216         @Override
translateY(final int y)217         protected int translateY(final int y) {
218             final int translatedY = super.translateY(y);
219             if (isInKeyboardTopPadding(y)) {
220                 // The forwarded event should have coordinates that are inside of the target.
221                 return Math.min(translatedY, mEventReceivingRect.height() - 1);
222             }
223             return translatedY;
224         }
225     }
226 
227     /**
228      * This class forwards {@link MotionEvent}s happened in the {@link MainKeyboardView} to
229      * {@link SuggestionStripView} when the {@link MoreSuggestionsView} is showing.
230      * {@link SuggestionStripView} dismisses {@link MoreSuggestionsView} when it receives any event
231      * outside of it.
232      */
233     private static class MoreSuggestionsViewCanceler
234             extends MotionEventForwarder<MainKeyboardView, SuggestionStripView> {
MoreSuggestionsViewCanceler(final MainKeyboardView mainKeyboardView, final SuggestionStripView suggestionStripView)235         public MoreSuggestionsViewCanceler(final MainKeyboardView mainKeyboardView,
236                 final SuggestionStripView suggestionStripView) {
237             super(mainKeyboardView, suggestionStripView);
238         }
239 
240         @Override
needsToForward(final int x, final int y)241         protected boolean needsToForward(final int x, final int y) {
242             return mReceiverView.isShowingMoreSuggestionPanel() && mEventSendingRect.contains(x, y);
243         }
244 
245         @Override
onForwardingEvent(final MotionEvent me)246         protected void onForwardingEvent(final MotionEvent me) {
247             if (me.getActionMasked() == MotionEvent.ACTION_DOWN) {
248                 mReceiverView.dismissMoreSuggestionsPanel();
249             }
250         }
251     }
252 }
253