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.rotaryplayground; 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 mDirectionalDelegate} 41 * for handling nudge behavior and {@code mMotionDelegate} for rotation behavior. Generally 42 * it is expected that in Direct Manipulation mode, nudges are used for navigation and 43 * rotation is used for "manipulating" the value of the selected {@link View}. 44 * <p> 45 * To reduce boilerplate, this class provides "no op" nudge and rotation behavior if 46 * no {@link View.OnKeyListener} or {@link View.OnGenericMotionListener} are provided as 47 * delegates for tackling the relevant events. 48 * <p> 49 * Allows {@link View}s that are within a {@link ViewGroup} to provide a link to their 50 * ancestor {@link ViewGroup} from which Direct Manipulation mode was first enabled. That way 51 * when the user finally exits Direct Manipulation mode, both objects are restored to their 52 * original state. 53 */ 54 public class DirectManipulationHandler implements View.OnKeyListener, 55 View.OnGenericMotionListener { 56 57 /** 58 * Sets the provided {@link DirectManipulationHandler} to the key listener and motion 59 * listener of the provided view. 60 */ setDirectManipulationHandler(@ullable View view, DirectManipulationHandler handler)61 public static void setDirectManipulationHandler(@Nullable View view, 62 DirectManipulationHandler handler) { 63 if (view == null) { 64 return; 65 } 66 view.setOnKeyListener(handler); 67 view.setOnGenericMotionListener(handler); 68 } 69 70 /** 71 * A builder for {@link DirectManipulationHandler}. 72 */ 73 public static class Builder { 74 private final DirectManipulationState mDmState; 75 private View.OnKeyListener mNudgeDelegate; 76 private View.OnGenericMotionListener mRotationDelegate; 77 private View.OnKeyListener mBackDelegate; 78 Builder(DirectManipulationState dmState)79 public Builder(DirectManipulationState dmState) { 80 Preconditions.checkNotNull(dmState); 81 this.mDmState = dmState; 82 } 83 setNudgeHandler(View.OnKeyListener nudgeDelegate)84 public Builder setNudgeHandler(View.OnKeyListener nudgeDelegate) { 85 Preconditions.checkNotNull(nudgeDelegate); 86 this.mNudgeDelegate = nudgeDelegate; 87 return this; 88 } 89 setBackHandler(View.OnKeyListener backDelegate)90 public Builder setBackHandler(View.OnKeyListener backDelegate) { 91 Preconditions.checkNotNull(backDelegate); 92 this.mBackDelegate = backDelegate; 93 return this; 94 } 95 setRotationHandler(View.OnGenericMotionListener rotationDelegate)96 public Builder setRotationHandler(View.OnGenericMotionListener rotationDelegate) { 97 Preconditions.checkNotNull(rotationDelegate); 98 this.mRotationDelegate = rotationDelegate; 99 return this; 100 } 101 build()102 public DirectManipulationHandler build() { 103 if (mNudgeDelegate == null && mRotationDelegate == null) { 104 throw new IllegalStateException("Nudge and/or rotation delegate must be provided."); 105 } 106 return new DirectManipulationHandler(mDmState, mNudgeDelegate, mBackDelegate, 107 mRotationDelegate); 108 } 109 } 110 111 private final DirectManipulationState mDirectManipulationMode; 112 private final View.OnKeyListener mNudgeDelegate; 113 private final View.OnKeyListener mBackDelegate; 114 private final View.OnGenericMotionListener mRotationDelegate; 115 DirectManipulationHandler(DirectManipulationState dmState, @Nullable View.OnKeyListener nudgeDelegate, @Nullable View.OnKeyListener backDelegate, @Nullable View.OnGenericMotionListener rotationDelegate)116 private DirectManipulationHandler(DirectManipulationState dmState, 117 @Nullable View.OnKeyListener nudgeDelegate, 118 @Nullable View.OnKeyListener backDelegate, 119 @Nullable View.OnGenericMotionListener rotationDelegate) { 120 mDirectManipulationMode = dmState; 121 mNudgeDelegate = nudgeDelegate; 122 mBackDelegate = backDelegate; 123 mRotationDelegate = rotationDelegate; 124 } 125 126 @Override onKey(View view, int keyCode, KeyEvent keyEvent)127 public boolean onKey(View view, int keyCode, KeyEvent keyEvent) { 128 boolean isActionUp = keyEvent.getAction() == KeyEvent.ACTION_UP; 129 Log.d(L.TAG, "View: " + view + " is handling " + KeyEvent.keyCodeToString(keyCode) 130 + " and action " + keyEvent.getAction() 131 + " direct manipulation mode is " 132 + (mDirectManipulationMode.isActive() ? "active" : "inactive")); 133 134 switch (keyCode) { 135 case KeyEvent.KEYCODE_DPAD_CENTER: 136 // If not yet in Direct Manipulation mode, switch to that mode. 137 138 if (!mDirectManipulationMode.isActive() && isActionUp) { 139 mDirectManipulationMode.enable(view); 140 } 141 return true; 142 case KeyEvent.KEYCODE_BACK: 143 // If in Direct Manipulation mode, exit, and clean up state. 144 if (mDirectManipulationMode.isActive() && isActionUp) { 145 mDirectManipulationMode.disable(); 146 } 147 // If no delegate is present, silently consume the events. 148 if (mBackDelegate == null) { 149 return true; 150 } 151 152 return mBackDelegate.onKey(view, keyCode, keyEvent); 153 case KeyEvent.KEYCODE_DPAD_UP: 154 case KeyEvent.KEYCODE_DPAD_DOWN: 155 case KeyEvent.KEYCODE_DPAD_LEFT: 156 case KeyEvent.KEYCODE_DPAD_RIGHT: 157 // This handler is only responsible for nudging behavior during Direct Manipulation 158 // mode. When the mode is disabled, ignore events. 159 if (!mDirectManipulationMode.isActive()) { 160 return false; 161 } 162 // If no delegate is present, silently consume the events. 163 if (mNudgeDelegate == null) { 164 return true; 165 } 166 return mNudgeDelegate.onKey(view, keyCode, keyEvent); 167 default: 168 // Ignore all other key events. 169 return false; 170 } 171 } 172 173 @Override onGenericMotion(View v, MotionEvent event)174 public boolean onGenericMotion(View v, MotionEvent event) { 175 // This handler is only responsible for behavior during Direct Manipulation 176 // mode. When the mode is disabled, ignore events. 177 if (!mDirectManipulationMode.isActive()) { 178 return false; 179 } 180 // If no delegate is present, silently consume the events. 181 if (mRotationDelegate == null) { 182 return true; 183 } 184 return mRotationDelegate.onGenericMotion(v, event); 185 } 186 } 187