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