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 private final DirectManipulationState mDirectManipulationMode; 58 private final View.OnKeyListener mNudgeDelegate; 59 private final View.OnGenericMotionListener mRotationDelegate; 60 61 /** 62 * A builder for {@link DirectManipulationHandler}. 63 */ 64 public static class Builder { 65 private final DirectManipulationState mDmState; 66 private View.OnKeyListener mNudgeDelegate; 67 private View.OnGenericMotionListener mRotationDelegate; 68 Builder(DirectManipulationState dmState)69 public Builder(DirectManipulationState dmState) { 70 Preconditions.checkNotNull(dmState); 71 this.mDmState = dmState; 72 } 73 setNudgeHandler(View.OnKeyListener directionalDelegate)74 public Builder setNudgeHandler(View.OnKeyListener directionalDelegate) { 75 Preconditions.checkNotNull(directionalDelegate); 76 this.mNudgeDelegate = directionalDelegate; 77 return this; 78 } 79 setRotationHandler(View.OnGenericMotionListener motionDelegate)80 public Builder setRotationHandler(View.OnGenericMotionListener motionDelegate) { 81 Preconditions.checkNotNull(motionDelegate); 82 this.mRotationDelegate = motionDelegate; 83 return this; 84 } 85 build()86 public DirectManipulationHandler build() { 87 if (mNudgeDelegate == null && mRotationDelegate == null) { 88 throw new IllegalStateException("At least one delegate must be provided."); 89 } 90 return new DirectManipulationHandler(mDmState, mNudgeDelegate, mRotationDelegate); 91 } 92 } 93 DirectManipulationHandler(DirectManipulationState dmState, @Nullable View.OnKeyListener nudgeDelegate, @Nullable View.OnGenericMotionListener rotationDelegate)94 private DirectManipulationHandler(DirectManipulationState dmState, 95 @Nullable View.OnKeyListener nudgeDelegate, 96 @Nullable View.OnGenericMotionListener rotationDelegate) { 97 Preconditions.checkNotNull(dmState); 98 mDirectManipulationMode = dmState; 99 mNudgeDelegate = nudgeDelegate; 100 mRotationDelegate = rotationDelegate; 101 } 102 103 @Override onKey(View view, int keyCode, KeyEvent keyEvent)104 public boolean onKey(View view, int keyCode, KeyEvent keyEvent) { 105 boolean isActionUp = keyEvent.getAction() == KeyEvent.ACTION_UP; 106 Log.d(L.TAG, "View: " + view + " is handling " + keyCode 107 + " and action " + keyEvent.getAction() 108 + " direct manipulation mode is " 109 + (mDirectManipulationMode.isActive() ? "active" : "inactive")); 110 111 switch (keyCode) { 112 case KeyEvent.KEYCODE_DPAD_CENTER: 113 // If not yet in Direct Manipulation mode, switch to that mode. 114 115 if (!mDirectManipulationMode.isActive() && isActionUp) { 116 mDirectManipulationMode.enable(view); 117 } 118 return true; 119 case KeyEvent.KEYCODE_BACK: 120 // If in Direct Manipulation mode, exit, and clean up state. 121 if (mDirectManipulationMode.isActive() && isActionUp) { 122 mDirectManipulationMode.disable(); 123 } 124 return true; 125 default: 126 // This handler is only responsible for behavior during Direct Manipulation 127 // mode. When the mode is disabled, ignore events. 128 if (!mDirectManipulationMode.isActive()) { 129 return false; 130 } 131 // If no delegate present, silently consume the events. 132 if (mNudgeDelegate == null) { 133 return true; 134 } 135 return mNudgeDelegate.onKey(view, keyCode, keyEvent); 136 } 137 } 138 139 @Override onGenericMotion(View v, MotionEvent event)140 public boolean onGenericMotion(View v, MotionEvent event) { 141 // This handler is only responsible for behavior during Direct Manipulation 142 // mode. When the mode is disabled, ignore events. 143 if (!mDirectManipulationMode.isActive()) { 144 return false; 145 } 146 // If no delegate present, silently consume the events. 147 if (mRotationDelegate == null) { 148 return true; 149 } 150 return mRotationDelegate.onGenericMotion(v, event); 151 } 152 } 153