diff options
author | Yabin Huang <yabinh@google.com> | 2020-05-12 23:55:03 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2020-05-12 23:55:03 +0000 |
commit | a4aa5d5675762a5c8480a30a494eeab646b69a69 (patch) | |
tree | 11b8493afcac5898dd83f7f37035a3f7749f0395 | |
parent | 778d489585053d9219466ce1371915e18a0b7069 (diff) | |
parent | b5b8a5d6a783e12ca0ee06e7770b94dae44a1d9a (diff) | |
download | tests-a4aa5d5675762a5c8480a30a494eeab646b69a69.tar.gz |
Add a custom DirectManipulationView am: b5b8a5d6a7
Change-Id: I2c27037b69e2e24852f34c95bf2e526b3dc18b0e
3 files changed, 259 insertions, 14 deletions
diff --git a/RotaryPlayground/res/layout/rotary_direct_manipulation.xml b/RotaryPlayground/res/layout/rotary_direct_manipulation.xml index 914a6e3..6203cdd 100644 --- a/RotaryPlayground/res/layout/rotary_direct_manipulation.xml +++ b/RotaryPlayground/res/layout/rotary_direct_manipulation.xml @@ -21,25 +21,38 @@ <!-- Split the screen in half horizontally. --> <!-- Two time pickers formatted differently. --> - <com.android.car.ui.FocusArea + <LinearLayout android:layout_width="0dp" android:layout_height="match_parent" android:orientation="vertical" android:layout_weight="1"> - <TimePicker + <!-- Put each TimePicker into a separate FocusArea. A TimePicker has several focusable views + and it's difficult to move to another TimePicker via rotation. Let's wrap each TimePicker + with a FocusArea so that we can use nudge to move to another TimePicker. --> + <com.android.car.ui.FocusArea + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + <TimePicker + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:focusable="true" + android:timePickerMode="spinner"> + </TimePicker> + </com.android.car.ui.FocusArea> + <com.android.car.ui.FocusArea android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:timePickerMode="spinner"> - </TimePicker> - <TimePicker - android:layout_width="match_parent" android:layout_height="0dp" - android:layout_weight="1" - android:timePickerMode="clock"> - </TimePicker> - </com.android.car.ui.FocusArea> + android:layout_weight="1"> + <TimePicker + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:focusable="true" + android:timePickerMode="clock"> + </TimePicker> + </com.android.car.ui.FocusArea> + </LinearLayout> - <!-- A seek bar and a radial time picker. --> + <!-- A seek bar, a radial time picker, and a custom DirectManipulationView. --> <com.android.car.ui.FocusArea android:layout_width="0dp" android:layout_height="match_parent" @@ -52,7 +65,14 @@ <RadialTimePickerView android:layout_width="match_parent" android:layout_height="0dp" - android:layout_weight="1"> + android:layout_weight="1" + android:focusable="true"> </RadialTimePickerView> + <com.android.car.rotaryplayground.DirectManipulationView + android:id="@+id/direct_manipulation_view" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1"> + </com.android.car.rotaryplayground.DirectManipulationView> </com.android.car.ui.FocusArea> </LinearLayout> diff --git a/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationView.java b/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationView.java new file mode 100644 index 0000000..6c37556 --- /dev/null +++ b/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationView.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.car.rotaryplayground; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.Nullable; + +import static java.lang.Math.min; + +/** + * A {@link View} used to demonstrate direct manipulation mode. + * <p> + * This view draws nothing but a circle. It provides APIs to change the center and the radius of the + * circle. + */ +public class DirectManipulationView extends View { + + /** + * How many pixels do we want to move the center of the circle horizontally from its initial + * position. + */ + private float mDeltaX; + /** + * How many pixels do we want to move the center of the circle vertically from its initial + * position. + */ + private float mDeltaY; + /** How many pixels do we want change the radius of the circle from its initial radius. */ + private float mDeltaRadius; + + private Paint mPaint; + + public DirectManipulationView(Context context) { + super(context); + init(); + } + + public DirectManipulationView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(); + } + + public DirectManipulationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public DirectManipulationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Draw the circle. Initially the circle is in the center of the canvas, and its radius is + // min(getWidth(), getHeight()) / 4. We need to translate it and scale it. + canvas.drawCircle( + /* cx= */getWidth() / 2 + mDeltaX, + /* cy= */getHeight() / 2 + mDeltaY, + /* radius= */min(getWidth(), getHeight()) / 4 + mDeltaRadius, + mPaint); + + } + + /** + * Moves the center of the circle by {@code dx} horizontally and by {@code dy} vertically, then + * redraws it. + */ + void move(float dx, float dy) { + mDeltaX += dx; + mDeltaY += dy; + invalidate(); + } + + /** Changes the radius of the circle by {@code dr} then redraws it. */ + void zoom(float dr) { + mDeltaRadius += dr; + invalidate(); + } + + private void init() { + // The view must be focusable to enter direct manipulation mode. + setFocusable(View.FOCUSABLE); + + // Set up paint with color and stroke styles. + mPaint = new Paint(); + mPaint.setColor(Color.GREEN); + mPaint.setAntiAlias(true); + mPaint.setStrokeWidth(5); + mPaint.setStyle(Paint.Style.FILL_AND_STROKE); + mPaint.setStrokeJoin(Paint.Join.ROUND); + mPaint.setStrokeCap(Paint.Cap.ROUND); + } +} diff --git a/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryDirectManipulationWidgets.java b/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryDirectManipulationWidgets.java index 34f9f60..fb02105 100644 --- a/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryDirectManipulationWidgets.java +++ b/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryDirectManipulationWidgets.java @@ -16,14 +16,20 @@ package com.android.car.rotaryplayground; +import android.graphics.Color; import android.os.Bundle; +import android.view.KeyEvent; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import com.android.car.ui.utils.DirectManipulationHelper; + /** * Fragment that demos rotary interactions directly manipulating the state of UI widgets such as a * {@link android.widget.SeekBar}, {@link android.widget.DatePicker}, and @@ -32,9 +38,112 @@ import androidx.fragment.app.Fragment; public class RotaryDirectManipulationWidgets extends Fragment { // TODO(agathaman): refactor a common class that takes in a fragment xml id and inflates it, to // share between this and RotaryCards. + + /** How many pixels do we want to move the {@link DirectManipulationView} for nudge. */ + private static final float DIRECT_MANIPULATION_VIEW_PX_PER_NUDGE = 10f; + + /** How many pixels do we want to zoom the {@link DirectManipulationView} for a rotation. */ + private static final float DIRECT_MANIPULATION_VIEW_PX_PER_ROTATION = 10f; + + /** Background color of {@link DirectManipulationView} when it's in direct manipulation mode. */ + private static final int BACKGROUND_COLOR_IN_DIRECT_MANIPULATION_MODE = Color.BLUE; + + /** + * Background color of {@link DirectManipulationView} when it's not in direct manipulation + * mode. + */ + private static final int BACKGROUND_COLOR_NOT_IN_DIRECT_MANIPULATION_MODE = Color.TRANSPARENT; + + /** Whether any view in this Fragment is in direct manipulation mode. */ + private boolean mInDirectManipulationMode; + @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.rotary_direct_manipulation, container, false); + View view = inflater.inflate(R.layout.rotary_direct_manipulation, container, false); + DirectManipulationView directManipulationView = + view.findViewById(R.id.direct_manipulation_view); + initDirectManipulationView(directManipulationView); + return view; + } + + /** + * Initializes the {@link DirectManipulationView} so that it can enter/exit direct manipulation + * mode and interact with the rotary controller directly. In direct manipulation mode, the + * circle of the DirectManipulationView can move when the controller nudges, and the circle of + * the DirectManipulationView can zoom when the controller rotates. + */ + private void initDirectManipulationView(@NonNull DirectManipulationView dmv) { + dmv.setOnKeyListener((view, keyCode, keyEvent) -> { + boolean isActionUp = keyEvent.getAction() == KeyEvent.ACTION_UP; + switch (keyCode) { + // Always consume KEYCODE_DPAD_CENTER and KEYCODE_BACK event. + case KeyEvent.KEYCODE_DPAD_CENTER: + if (!mInDirectManipulationMode && isActionUp) { + mInDirectManipulationMode = true; + dmv.setBackgroundColor(BACKGROUND_COLOR_IN_DIRECT_MANIPULATION_MODE); + dmv.invalidate(); + DirectManipulationHelper.enableDirectManipulationMode(dmv, true); + } + return true; + case KeyEvent.KEYCODE_BACK: + if (mInDirectManipulationMode && isActionUp) { + mInDirectManipulationMode = false; + dmv.setBackgroundColor(BACKGROUND_COLOR_NOT_IN_DIRECT_MANIPULATION_MODE); + dmv.invalidate(); + DirectManipulationHelper.enableDirectManipulationMode(dmv, false); + } + return true; + // Consume controller nudge event (KEYCODE_DPAD_UP, KEYCODE_DPAD_DOWN, + // KEYCODE_DPAD_LEFT, or KEYCODE_DPAD_RIGHT) only when in direct manipulation mode. + // When handling nudge event, move the circle of the DirectManipulationView. + case KeyEvent.KEYCODE_DPAD_UP: + if (!mInDirectManipulationMode) { + return false; + } + if (isActionUp) { + dmv.move(0f, -DIRECT_MANIPULATION_VIEW_PX_PER_NUDGE); + } + return true; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!mInDirectManipulationMode) { + return false; + } + if (isActionUp) { + dmv.move(0f, DIRECT_MANIPULATION_VIEW_PX_PER_NUDGE); + } + return true; + case KeyEvent.KEYCODE_DPAD_LEFT: + if (!mInDirectManipulationMode) { + return false; + } + if (isActionUp) { + dmv.move(-DIRECT_MANIPULATION_VIEW_PX_PER_NUDGE, 0f); + } + return true; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (!mInDirectManipulationMode) { + return false; + } + if (isActionUp) { + dmv.move(DIRECT_MANIPULATION_VIEW_PX_PER_NUDGE, 0f); + } + return true; + // Don't consume other key events. + default: + return false; + } + }); + + // When in direct manipulation mode, zoom the circle of the DirectManipulationView on + // controller rotate event. + dmv.setOnGenericMotionListener(((view, motionEvent) -> { + if (!mInDirectManipulationMode) { + return false; + } + float scroll = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL); + dmv.zoom(DIRECT_MANIPULATION_VIEW_PX_PER_ROTATION * scroll); + return true; + })); } } |