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 package com.android.car.rotaryplayground; 17 18 import static java.lang.Math.min; 19 20 import android.content.Context; 21 import android.graphics.Canvas; 22 import android.graphics.Color; 23 import android.graphics.Paint; 24 import android.util.AttributeSet; 25 import android.view.KeyEvent; 26 import android.view.MotionEvent; 27 import android.view.View; 28 29 import androidx.annotation.NonNull; 30 import androidx.annotation.Nullable; 31 32 /** 33 * A {@link View} used to demonstrate direct manipulation mode. 34 * <p> 35 * This view draws nothing but a circle. It provides APIs to change the center and the radius of the 36 * circle. 37 */ 38 public class DirectManipulationView extends View { 39 40 /** 41 * How many pixels do we want to move the center of the circle horizontally from its initial 42 * position. 43 */ 44 private float mDeltaX; 45 /** 46 * How many pixels do we want to move the center of the circle vertically from its initial 47 * position. 48 */ 49 private float mDeltaY; 50 /** How many pixels do we want change the radius of the circle from its initial radius. */ 51 private float mDeltaRadius; 52 53 private Paint mPaint; 54 DirectManipulationView(Context context)55 public DirectManipulationView(Context context) { 56 super(context); 57 init(); 58 } 59 DirectManipulationView(Context context, @Nullable AttributeSet attrs)60 public DirectManipulationView(Context context, @Nullable AttributeSet attrs) { 61 super(context, attrs); 62 init(); 63 } 64 DirectManipulationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)65 public DirectManipulationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 66 super(context, attrs, defStyleAttr); 67 init(); 68 } 69 DirectManipulationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)70 public DirectManipulationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, 71 int defStyleRes) { 72 super(context, attrs, defStyleAttr, defStyleRes); 73 init(); 74 } 75 76 @Override onDraw(Canvas canvas)77 protected void onDraw(Canvas canvas) { 78 super.onDraw(canvas); 79 80 // Draw the circle. Initially the circle is in the center of the canvas, and its radius is 81 // min(getWidth(), getHeight()) / 4. We need to translate it and scale it. 82 canvas.drawCircle( 83 /* cx= */getWidth() / 2 + mDeltaX, 84 /* cy= */getHeight() / 2 + mDeltaY, 85 /* radius= */min(getWidth(), getHeight()) / 4 + mDeltaRadius, 86 mPaint); 87 88 } 89 90 /** 91 * Moves the center of the circle by {@code dx} horizontally and by {@code dy} vertically, then 92 * redraws it. 93 */ move(float dx, float dy)94 void move(float dx, float dy) { 95 mDeltaX += dx; 96 mDeltaY += dy; 97 invalidate(); 98 } 99 100 /** Changes the radius of the circle by {@code dr} then redraws it. */ resizeCircle(float dr)101 void resizeCircle(float dr) { 102 mDeltaRadius += dr; 103 invalidate(); 104 } 105 init()106 private void init() { 107 // The view must be focusable to enter direct manipulation mode. 108 setFocusable(View.FOCUSABLE); 109 110 // Set up paint with color and stroke styles. 111 mPaint = new Paint(); 112 mPaint.setColor(Color.GREEN); 113 mPaint.setAntiAlias(true); 114 mPaint.setStrokeWidth(5); 115 mPaint.setStyle(Paint.Style.FILL_AND_STROKE); 116 mPaint.setStrokeJoin(Paint.Join.ROUND); 117 mPaint.setStrokeCap(Paint.Cap.ROUND); 118 } 119 120 /** 121 * A {@link View.OnKeyListener} for handling Direct Manipulation rotary nudge behavior 122 * for a {@link DirectManipulationView}. 123 * <p> 124 * This handler expects that it is being used in Direct Manipulation mode, i.e. as a directional 125 * delegate through a {@link DirectManipulationHandler} which can invoke it at the 126 * appropriate times. 127 * <p> 128 * Moves the circle drawn in the {@link DirectManipulationView} in the relevant direction for 129 * following {@link KeyEvent}s: 130 * <ul> 131 * <li>{@link KeyEvent#KEYCODE_DPAD_UP} 132 * <li>{@link KeyEvent#KEYCODE_DPAD_DOWN} 133 * <li>{@link KeyEvent#KEYCODE_DPAD_LEFT} 134 * <li>{@link KeyEvent#KEYCODE_DPAD_RIGHT} 135 * </ul> 136 */ 137 static class NudgeHandler implements View.OnKeyListener { 138 139 /** How many pixels do we want to move the {@link DirectManipulationView} per nudge. */ 140 private static final float DIRECT_MANIPULATION_VIEW_PX_PER_NUDGE = 10f; 141 142 @Override onKey(View v, int keyCode, KeyEvent keyEvent)143 public boolean onKey(View v, int keyCode, KeyEvent keyEvent) { 144 if (keyEvent.getAction() != KeyEvent.ACTION_UP) { 145 return true; 146 } 147 148 if (v instanceof DirectManipulationView) { 149 DirectManipulationView dmv = (DirectManipulationView) v; 150 handleNudgeEvent(dmv, keyCode); 151 return true; 152 } 153 154 throw new UnsupportedOperationException("NudgeHandler shouldn't be registered " 155 + "as a listener on a view other than a DirectManipulationView."); 156 } 157 158 /** Moves the circle of the DirectManipulationView when the controller nudges. */ handleNudgeEvent(@onNull DirectManipulationView dmv, int keyCode)159 private void handleNudgeEvent(@NonNull DirectManipulationView dmv, int keyCode) { 160 switch (keyCode) { 161 case KeyEvent.KEYCODE_DPAD_UP: 162 dmv.move(0f, -DIRECT_MANIPULATION_VIEW_PX_PER_NUDGE); 163 return; 164 case KeyEvent.KEYCODE_DPAD_DOWN: 165 dmv.move(0f, DIRECT_MANIPULATION_VIEW_PX_PER_NUDGE); 166 return; 167 case KeyEvent.KEYCODE_DPAD_LEFT: 168 dmv.move(-DIRECT_MANIPULATION_VIEW_PX_PER_NUDGE, 0f); 169 return; 170 case KeyEvent.KEYCODE_DPAD_RIGHT: 171 dmv.move(DIRECT_MANIPULATION_VIEW_PX_PER_NUDGE, 0f); 172 return; 173 default: 174 throw new IllegalArgumentException("Invalid keycode: " + keyCode); 175 } 176 } 177 } 178 179 /** 180 * A {@link View.OnGenericMotionListener} for handling Direct Manipulation rotation events for 181 * a {@link DirectManipulationView}. It does so by increasing or decreasing the radius of 182 * the circle drawn depending on the direction of rotation. 183 */ 184 static class RotationHandler implements View.OnGenericMotionListener { 185 186 /** 187 * How many pixels do we want to change the radius of the circle in the 188 * {@link DirectManipulationView} for a rotation. 189 */ 190 private static final float DIRECT_MANIPULATION_VIEW_PX_PER_ROTATION = 10f; 191 192 @Override onGenericMotion(View v, MotionEvent event)193 public boolean onGenericMotion(View v, MotionEvent event) { 194 if (v instanceof DirectManipulationView) { 195 handleRotateEvent( 196 (DirectManipulationView) v, 197 event.getAxisValue(MotionEvent.AXIS_SCROLL)); 198 return true; 199 } 200 201 throw new UnsupportedOperationException("RotationHandler shouldn't be registered " 202 + "as a listener on a view other than a DirectManipulationView."); 203 } 204 205 /** Resizes the circle of the DirectManipulationView when the controller rotates. */ handleRotateEvent(@onNull DirectManipulationView dmv, float scroll)206 private void handleRotateEvent(@NonNull DirectManipulationView dmv, float scroll) { 207 dmv.resizeCircle(DIRECT_MANIPULATION_VIEW_PX_PER_ROTATION * scroll); 208 } 209 } 210 } 211