• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.car.settings.common.rotary;
18 
19 import android.util.Log;
20 import android.view.KeyEvent;
21 import android.view.MotionEvent;
22 import android.view.View;
23 import android.view.ViewGroup;
24 
25 import androidx.annotation.Nullable;
26 import androidx.core.util.Preconditions;
27 
28 /**
29  * A {@link View.OnKeyListener} and {@link View.OnGenericMotionListener} that adds a
30  * "Direct Manipulation" mode to any {@link View} that uses it.
31  * <p>
32  * Direct Manipulation mode in the Rotary context is a mode in which the user can use the
33  * Rotary controls to manipulate and change the UI elements they are interacting with rather
34  * than navigate through the entire UI.
35  * <p>
36  * Treats {@link KeyEvent#KEYCODE_DPAD_CENTER} as the signal to enter Direct Manipulation
37  * mode, and {@link KeyEvent#KEYCODE_BACK} as the signal to exit, and keeps track of which
38  * mode the {@link View} using it is currently in.
39  * <p>
40  * When in Direct Manipulation mode, it delegates to {@code mNudgeDelegate}
41  * for handling nudge behavior, {@code mBackDelegate} for back behavior, and
42  * {@code mRotationDelegate} for rotation behavior. Generally it is expected that in Direct
43  * Manipulation mode, nudges are used for navigation and rotation is used for "manipulating" the
44  * value of the selected {@link View}.
45  * <p>
46  * To reduce boilerplate, this class provides "no op" nudge and rotation behavior if
47  * no {@link View.OnKeyListener} or {@link View.OnGenericMotionListener} are provided as
48  * delegates for tackling the relevant events.
49  * <p>
50  * Allows {@link View}s that are within a {@link ViewGroup} to provide a link to their
51  * ancestor {@link ViewGroup} from which Direct Manipulation mode was first enabled. That way
52  * when the user finally exits Direct Manipulation mode, both objects are restored to their
53  * original state.
54  */
55 public class DirectManipulationHandler implements View.OnKeyListener,
56         View.OnGenericMotionListener {
57     private static final String TAG = DirectManipulationHandler.class.getSimpleName();
58 
59     /**
60      * Sets the provided {@link DirectManipulationHandler} to the key listener and motion
61      * listener of the provided view.
62      */
setDirectManipulationHandler(@ullable View view, @Nullable DirectManipulationHandler handler)63     public static void setDirectManipulationHandler(@Nullable View view,
64             @Nullable DirectManipulationHandler handler) {
65         if (view == null) {
66             return;
67         }
68         view.setOnKeyListener(handler);
69         view.setOnGenericMotionListener(handler);
70     }
71 
72     /** A builder for {@link DirectManipulationHandler}. */
73     public static class Builder {
74         private final DirectManipulationState mDmState;
75         private View.OnKeyListener mNudgeDelegate;
76         private EventListener mCenterButtonDelegate;
77         private EventListener mBackDelegate;
78         private View.OnGenericMotionListener mRotationDelegate;
79 
Builder(DirectManipulationState dmState)80         public Builder(DirectManipulationState dmState) {
81             Preconditions.checkNotNull(dmState);
82             mDmState = dmState;
83         }
84 
85         /** Sets a nudge handler. */
setNudgeHandler(View.OnKeyListener nudgeDelegate)86         public Builder setNudgeHandler(View.OnKeyListener nudgeDelegate) {
87             Preconditions.checkNotNull(nudgeDelegate);
88             mNudgeDelegate = nudgeDelegate;
89             return this;
90         }
91 
92         /** Sets an enter handler. */
setCenterButtonHandler(EventListener centerButtonDelegate)93         public Builder setCenterButtonHandler(EventListener centerButtonDelegate) {
94             Preconditions.checkNotNull(centerButtonDelegate);
95             mCenterButtonDelegate = centerButtonDelegate;
96             return this;
97         }
98 
99         /** Sets a back handler. */
setBackHandler(EventListener backDelegate)100         public Builder setBackHandler(EventListener backDelegate) {
101             Preconditions.checkNotNull(backDelegate);
102             mBackDelegate = backDelegate;
103             return this;
104         }
105 
106         /** Sets a rotation handler. */
setRotationHandler(View.OnGenericMotionListener rotationDelegate)107         public Builder setRotationHandler(View.OnGenericMotionListener rotationDelegate) {
108             Preconditions.checkNotNull(rotationDelegate);
109             mRotationDelegate = rotationDelegate;
110             return this;
111         }
112 
113         /** Constructs a {@link DirectManipulationHandler}. */
build()114         public DirectManipulationHandler build() {
115             if (mNudgeDelegate == null && mRotationDelegate == null) {
116                 throw new IllegalStateException("Nudge and/or rotation delegate must be provided.");
117             }
118             return new DirectManipulationHandler(mDmState, mNudgeDelegate, mCenterButtonDelegate,
119                     mBackDelegate, mRotationDelegate);
120         }
121     }
122 
123     private final DirectManipulationState mDirectManipulationMode;
124     private final View.OnKeyListener mNudgeDelegate;
125     private final EventListener mCenterButtonDelegate;
126     private final EventListener mBackDelegate;
127     private final View.OnGenericMotionListener mRotationDelegate;
128 
DirectManipulationHandler(DirectManipulationState dmState, @Nullable View.OnKeyListener nudgeDelegate, @Nullable EventListener centerButtonDelegate, @Nullable EventListener backDelegate, @Nullable View.OnGenericMotionListener rotationDelegate)129     private DirectManipulationHandler(DirectManipulationState dmState,
130             @Nullable View.OnKeyListener nudgeDelegate,
131             @Nullable EventListener centerButtonDelegate,
132             @Nullable EventListener backDelegate,
133             @Nullable View.OnGenericMotionListener rotationDelegate) {
134         mDirectManipulationMode = dmState;
135         mNudgeDelegate = nudgeDelegate;
136         mCenterButtonDelegate = centerButtonDelegate;
137         mBackDelegate = backDelegate;
138         mRotationDelegate = rotationDelegate;
139     }
140 
141     @Override
onKey(View view, int keyCode, KeyEvent keyEvent)142     public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
143         boolean isActionUp = keyEvent.getAction() == KeyEvent.ACTION_UP;
144         Log.d(TAG, "View: " + view + " is handling " + KeyEvent.keyCodeToString(keyCode)
145                 + " and action " + KeyEvent.actionToString(keyEvent.getAction())
146                 + " direct manipulation mode is "
147                 + (mDirectManipulationMode.isActive() ? "active" : "inactive"));
148 
149         boolean inDirectManipulationMode = mDirectManipulationMode.isActive();
150         switch (keyCode) {
151             case KeyEvent.KEYCODE_DPAD_CENTER:
152                 // If not yet in Direct Manipulation mode, switch to that mode. If in Direct
153                 // Manipulation mode, exit, and clean up state.
154                 if (isActionUp) {
155                     if (inDirectManipulationMode) {
156                         mDirectManipulationMode.disable();
157                     } else {
158                         mDirectManipulationMode.enable(view);
159                     }
160                 }
161 
162                 if (mCenterButtonDelegate == null) {
163                     return true;
164                 }
165 
166                 return mCenterButtonDelegate.onEvent(inDirectManipulationMode);
167             case KeyEvent.KEYCODE_BACK:
168                 // If in Direct Manipulation mode, exit, and clean up state.
169                 if (inDirectManipulationMode && isActionUp) {
170                     mDirectManipulationMode.disable();
171                 }
172                 // If no delegate is present, silently consume the events.
173                 if (mBackDelegate == null) {
174                     return true;
175                 }
176 
177                 return mBackDelegate.onEvent(inDirectManipulationMode);
178             case KeyEvent.KEYCODE_DPAD_UP:
179             case KeyEvent.KEYCODE_DPAD_DOWN:
180             case KeyEvent.KEYCODE_DPAD_LEFT:
181             case KeyEvent.KEYCODE_DPAD_RIGHT:
182                 // This handler is only responsible for nudging behavior during Direct Manipulation
183                 // mode. When the mode is disabled, ignore events.
184                 if (!inDirectManipulationMode) {
185                     return false;
186                 }
187                 // If no delegate is present, silently consume the events.
188                 if (mNudgeDelegate == null) {
189                     return true;
190                 }
191                 return mNudgeDelegate.onKey(view, keyCode, keyEvent);
192             default:
193                 // Ignore all other key events.
194                 return false;
195         }
196     }
197 
198     @Override
onGenericMotion(View v, MotionEvent event)199     public boolean onGenericMotion(View v, MotionEvent event) {
200         // This handler is only responsible for behavior during Direct Manipulation
201         // mode. When the mode is disabled, ignore events.
202         if (!mDirectManipulationMode.isActive()) {
203             return false;
204         }
205 
206         // If no delegate is present, silently consume the events.
207         if (mRotationDelegate == null) {
208             return true;
209         }
210 
211         return mRotationDelegate.onGenericMotion(v, event);
212     }
213 
214     /** A custom event listener. */
215     public interface EventListener {
216         /**
217          * Handles an event.
218          *
219          * @param inDirectManipulationMode specifies whether we were in direct manipulation mode
220          *                                 before the event is handled
221          */
onEvent(boolean inDirectManipulationMode)222         boolean onEvent(boolean inDirectManipulationMode);
223     }
224 }
225