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