From 7bb95459b3d862103936c2cc9cd8f8dafbb7f6c6 Mon Sep 17 00:00:00 2001 From: Danny Epstein Date: Tue, 13 Oct 2020 14:03:54 -0700 Subject: Tag demo scrollable container Now that CarUiRecyclerViews no longer default to being considered scrollable containers, we need to explicitly tag the demo scrollable container in RotaryPlayground. Test: scroll in "scroll" tab of the demo app Bug: 160922045 Change-Id: I22c7d8a6e975d3a5718637d4d65e371254f2ac7c Merged-In: I22c7d8a6e975d3a5718637d4d65e371254f2ac7c --- RotaryPlayground/res/layout/rotary_scroll.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/RotaryPlayground/res/layout/rotary_scroll.xml b/RotaryPlayground/res/layout/rotary_scroll.xml index 4153ca1..843a380 100644 --- a/RotaryPlayground/res/layout/rotary_scroll.xml +++ b/RotaryPlayground/res/layout/rotary_scroll.xml @@ -26,11 +26,18 @@ android:layout_height="wrap_content" android:singleLine="true"/> + + Date: Tue, 10 Nov 2020 22:29:56 -0800 Subject: Update Rotary Playground for direct manipulation Bug: 169884295 Test: manual Change-Id: I2ef1589cf9ae1cc82480b69ad120e6c947df3746 --- .../DirectManipulationHandler.java | 70 ++++++++++++++++------ .../rotaryplayground/DirectManipulationState.java | 52 ++++++++-------- .../RotaryDirectManipulationWidgets.java | 52 +++++++--------- 3 files changed, 101 insertions(+), 73 deletions(-) diff --git a/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationHandler.java b/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationHandler.java index fc06754..29445c5 100644 --- a/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationHandler.java +++ b/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationHandler.java @@ -54,9 +54,18 @@ import androidx.core.util.Preconditions; public class DirectManipulationHandler implements View.OnKeyListener, View.OnGenericMotionListener { - private final DirectManipulationState mDirectManipulationMode; - private final View.OnKeyListener mNudgeDelegate; - private final View.OnGenericMotionListener mRotationDelegate; + /** + * Sets the provided {@link DirectManipulationHandler} to the key listener and motion + * listener of the provided view. + */ + public static void setDirectManipulationHandler(@Nullable View view, + DirectManipulationHandler handler) { + if (view == null) { + return; + } + view.setOnKeyListener(handler); + view.setOnGenericMotionListener(handler); + } /** * A builder for {@link DirectManipulationHandler}. @@ -65,45 +74,59 @@ public class DirectManipulationHandler implements View.OnKeyListener, private final DirectManipulationState mDmState; private View.OnKeyListener mNudgeDelegate; private View.OnGenericMotionListener mRotationDelegate; + private View.OnKeyListener mBackDelegate; public Builder(DirectManipulationState dmState) { Preconditions.checkNotNull(dmState); this.mDmState = dmState; } - public Builder setNudgeHandler(View.OnKeyListener directionalDelegate) { - Preconditions.checkNotNull(directionalDelegate); - this.mNudgeDelegate = directionalDelegate; + public Builder setNudgeHandler(View.OnKeyListener nudgeDelegate) { + Preconditions.checkNotNull(nudgeDelegate); + this.mNudgeDelegate = nudgeDelegate; + return this; + } + + public Builder setBackHandler(View.OnKeyListener backDelegate) { + Preconditions.checkNotNull(backDelegate); + this.mBackDelegate = backDelegate; return this; } - public Builder setRotationHandler(View.OnGenericMotionListener motionDelegate) { - Preconditions.checkNotNull(motionDelegate); - this.mRotationDelegate = motionDelegate; + public Builder setRotationHandler(View.OnGenericMotionListener rotationDelegate) { + Preconditions.checkNotNull(rotationDelegate); + this.mRotationDelegate = rotationDelegate; return this; } public DirectManipulationHandler build() { if (mNudgeDelegate == null && mRotationDelegate == null) { - throw new IllegalStateException("At least one delegate must be provided."); + throw new IllegalStateException("Nudge and/or rotation delegate must be provided."); } - return new DirectManipulationHandler(mDmState, mNudgeDelegate, mRotationDelegate); + return new DirectManipulationHandler(mDmState, mNudgeDelegate, mBackDelegate, + mRotationDelegate); } } + private final DirectManipulationState mDirectManipulationMode; + private final View.OnKeyListener mNudgeDelegate; + private final View.OnKeyListener mBackDelegate; + private final View.OnGenericMotionListener mRotationDelegate; + private DirectManipulationHandler(DirectManipulationState dmState, @Nullable View.OnKeyListener nudgeDelegate, + @Nullable View.OnKeyListener backDelegate, @Nullable View.OnGenericMotionListener rotationDelegate) { - Preconditions.checkNotNull(dmState); mDirectManipulationMode = dmState; mNudgeDelegate = nudgeDelegate; + mBackDelegate = backDelegate; mRotationDelegate = rotationDelegate; } @Override public boolean onKey(View view, int keyCode, KeyEvent keyEvent) { boolean isActionUp = keyEvent.getAction() == KeyEvent.ACTION_UP; - Log.d(L.TAG, "View: " + view + " is handling " + keyCode + Log.d(L.TAG, "View: " + view + " is handling " + KeyEvent.keyCodeToString(keyCode) + " and action " + keyEvent.getAction() + " direct manipulation mode is " + (mDirectManipulationMode.isActive() ? "active" : "inactive")); @@ -121,18 +144,29 @@ public class DirectManipulationHandler implements View.OnKeyListener, if (mDirectManipulationMode.isActive() && isActionUp) { mDirectManipulationMode.disable(); } - return true; - default: - // This handler is only responsible for behavior during Direct Manipulation + // If no delegate is present, silently consume the events. + if (mBackDelegate == null) { + return true; + } + + return mBackDelegate.onKey(view, keyCode, keyEvent); + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + // This handler is only responsible for nudging behavior during Direct Manipulation // mode. When the mode is disabled, ignore events. if (!mDirectManipulationMode.isActive()) { return false; } - // If no delegate present, silently consume the events. + // If no delegate is present, silently consume the events. if (mNudgeDelegate == null) { return true; } return mNudgeDelegate.onKey(view, keyCode, keyEvent); + default: + // Ignore all other key events. + return false; } } @@ -143,7 +177,7 @@ public class DirectManipulationHandler implements View.OnKeyListener, if (!mDirectManipulationMode.isActive()) { return false; } - // If no delegate present, silently consume the events. + // If no delegate is present, silently consume the events. if (mRotationDelegate == null) { return true; } diff --git a/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationState.java b/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationState.java index 05d236b..a802c71 100644 --- a/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationState.java +++ b/RotaryPlayground/src/com/android/car/rotaryplayground/DirectManipulationState.java @@ -16,7 +16,10 @@ package com.android.car.rotaryplayground; +import static android.view.ViewGroup.FOCUS_AFTER_DESCENDANTS; + import android.graphics.Color; +import android.graphics.drawable.Drawable; import android.view.View; import android.view.ViewGroup; @@ -34,19 +37,20 @@ import com.android.car.ui.utils.DirectManipulationHelper; */ public class DirectManipulationState { - /** Background color of a view when it's in direct manipulation mode. */ private static final int BACKGROUND_COLOR_IN_DIRECT_MANIPULATION_MODE = Color.BLUE; - /** Background color of a view when it's not in direct manipulation mode. */ - private static final int BACKGROUND_COLOR_NOT_IN_DIRECT_MANIPULATION_MODE = Color.TRANSPARENT; + /** Indicates that the descendant focusability has not been set. */ + private static final int UNKNOWN_DESCENDANT_FOCUSABILITY = -1; /** The view that is in direct manipulation mode, or null if none. */ - @Nullable private View mViewInDirectManipulationMode; - - private void setStartingView(@Nullable View view) { - mViewInDirectManipulationMode = view; - } + @Nullable + private View mViewInDirectManipulationMode; + /** The original background of the view in direct manipulation mode. */ + @Nullable + private Drawable mOriginalBackground; + /** The original descendant focusability value of the view in direct manipulation mode. */ + private int mOriginalDescendantFocusability = UNKNOWN_DESCENDANT_FOCUSABILITY; /** * Returns true if Direct Manipulation mode is active, false otherwise. @@ -62,19 +66,18 @@ public class DirectManipulationState { * We generally want to give some kind of visual indication that this change has happened. In * this example we change the background color of {@code view}. * - * @param view - the {@link View} from which we entered into Direct Manipulation mode. + * @param view the {@link View} from which we entered into Direct Manipulation mode */ public void enable(@NonNull View view) { - /* - * A more robust approach would be to fetch the current background color from - * the view object and store it back onto the View itself using the {@link - * View#setTag(int, java.lang.Object)} API. This could then be fetched back - * and used to restore the background color without needing to keep a constant - * reference to the color here which could fall out of sync with the xml files. - */ + mViewInDirectManipulationMode = view; + mOriginalBackground = view.getBackground(); + if (mViewInDirectManipulationMode instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) mViewInDirectManipulationMode; + mOriginalDescendantFocusability = viewGroup.getDescendantFocusability(); + viewGroup.setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + } view.setBackgroundColor(BACKGROUND_COLOR_IN_DIRECT_MANIPULATION_MODE); DirectManipulationHelper.enableDirectManipulationMode(view, /* enable= */ true); - setStartingView(view); } /** @@ -82,17 +85,18 @@ public class DirectManipulationState { * from which we entered into Direct Manipulation mode. */ public void disable() { - mViewInDirectManipulationMode.setBackgroundColor( - BACKGROUND_COLOR_NOT_IN_DIRECT_MANIPULATION_MODE); + mViewInDirectManipulationMode.setBackground(mOriginalBackground); DirectManipulationHelper.enableDirectManipulationMode( mViewInDirectManipulationMode, /* enable= */ false); - // For ViewGroup objects, restore descendant focusability to FOCUS_BLOCK_DESCENDANTS so - // during non-Direct Manipulation mode, aka, general rotary navigation, we don't go - // through the individual inner UI elements. - if (mViewInDirectManipulationMode instanceof ViewGroup) { + // For ViewGroup objects, restore descendant focusability to the previous value. + if (mViewInDirectManipulationMode instanceof ViewGroup + && mOriginalDescendantFocusability != UNKNOWN_DESCENDANT_FOCUSABILITY) { ViewGroup viewGroup = (ViewGroup) mViewInDirectManipulationMode; viewGroup.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); } - setStartingView(null); + + mViewInDirectManipulationMode = null; + mOriginalBackground = null; + mOriginalDescendantFocusability = UNKNOWN_DESCENDANT_FOCUSABILITY; } } diff --git a/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryDirectManipulationWidgets.java b/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryDirectManipulationWidgets.java index 9184597..68d7d0f 100644 --- a/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryDirectManipulationWidgets.java +++ b/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryDirectManipulationWidgets.java @@ -16,6 +16,8 @@ package com.android.car.rotaryplayground; +import static com.android.car.rotaryplayground.DirectManipulationHandler.setDirectManipulationHandler; + import android.os.Bundle; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -55,7 +57,7 @@ public class RotaryDirectManipulationWidgets extends Fragment { View view = inflater.inflate(R.layout.rotary_direct_manipulation, container, false); DirectManipulationView dmv = view.findViewById(R.id.direct_manipulation_view); - registerDirectManipulationHandler(dmv, + setDirectManipulationHandler(dmv, new DirectManipulationHandler.Builder(mDirectManipulationMode) .setNudgeHandler(new DirectManipulationView.NudgeHandler()) .setRotationHandler(new DirectManipulationView.RotationHandler()) @@ -63,7 +65,7 @@ public class RotaryDirectManipulationWidgets extends Fragment { TimePicker spinnerTimePicker = view.findViewById(R.id.spinner_time_picker); - registerDirectManipulationHandler(spinnerTimePicker, + setDirectManipulationHandler(spinnerTimePicker, new DirectManipulationHandler.Builder(mDirectManipulationMode) .setNudgeHandler(new TimePickerNudgeHandler()) .build()); @@ -71,11 +73,15 @@ public class RotaryDirectManipulationWidgets extends Fragment { DirectManipulationHandler numberPickerListener = new DirectManipulationHandler.Builder(mDirectManipulationMode) .setNudgeHandler(new NumberPickerNudgeHandler()) + .setBackHandler((v, keyCode, event) -> { + spinnerTimePicker.requestFocus(); + return true; + }) .setRotationHandler((v, motionEvent) -> { - float scroll = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL); View focusedView = v.findFocus(); if (focusedView instanceof NumberPicker) { NumberPicker numberPicker = (NumberPicker) focusedView; + float scroll = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL); numberPicker.setValue(numberPicker.getValue() + Math.round(scroll)); return true; } @@ -86,10 +92,10 @@ public class RotaryDirectManipulationWidgets extends Fragment { List numberPickers = new ArrayList<>(); getNumberPickerDescendants(numberPickers, spinnerTimePicker); for (int i = 0; i < numberPickers.size(); i++) { - registerDirectManipulationHandler(numberPickers.get(i), numberPickerListener); + setDirectManipulationHandler(numberPickers.get(i), numberPickerListener); } - registerDirectManipulationHandler(view.findViewById(R.id.clock_time_picker), + setDirectManipulationHandler(view.findViewById(R.id.clock_time_picker), new DirectManipulationHandler.Builder( mDirectManipulationMode) // TODO(pardis): fix the behavior here. It does not nudge as expected. @@ -103,13 +109,13 @@ public class RotaryDirectManipulationWidgets extends Fragment { }) .build()); - registerDirectManipulationHandler( + setDirectManipulationHandler( view.findViewById(R.id.seek_bar), new DirectManipulationHandler.Builder(mDirectManipulationMode) .setRotationHandler(new DelegateToA11yScrollRotationHandler()) .build()); - registerDirectManipulationHandler( + setDirectManipulationHandler( view.findViewById(R.id.radial_time_picker), new DirectManipulationHandler.Builder(mDirectManipulationMode) .setRotationHandler(new DelegateToA11yScrollRotationHandler()) @@ -128,23 +134,6 @@ public class RotaryDirectManipulationWidgets extends Fragment { super.onPause(); } - /** - * Register the given {@link DirectManipulationHandler} as both the - * {@link View.OnKeyListener} and {@link View.OnGenericMotionListener} for the given - * {@link View}. - *

- * Handles a {@link Nullable} {@link View} so that it can be used directly with the output of - * methods such as {@code findViewById}. - */ - private void registerDirectManipulationHandler(@Nullable View view, - DirectManipulationHandler handler) { - if (view == null) { - return; - } - view.setOnKeyListener(handler); - view.setOnGenericMotionListener(handler); - } - /** * A {@link View.OnGenericMotionListener} implementation that delegates handling the * {@link MotionEvent} to the {@link AccessibilityNodeInfo#ACTION_SCROLL_FORWARD} @@ -211,8 +200,7 @@ public class RotaryDirectManipulationWidgets extends Fragment { } @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - boolean isActionUp = event.getAction() == KeyEvent.ACTION_UP; + public boolean onKey(View view, int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_DOWN: @@ -220,10 +208,10 @@ public class RotaryDirectManipulationWidgets extends Fragment { return true; case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_RIGHT: - if (isActionUp) { + if (event.getAction() == KeyEvent.ACTION_UP) { int direction = KEYCODE_TO_DIRECTION_MAP.get(keyCode); - View nextView = v.focusSearch(direction); - if (areInTheSameTimePicker(v, nextView)) { + View nextView = view.focusSearch(direction); + if (areInTheSameTimePicker(view, nextView)) { nextView.requestFocus(direction); } } @@ -239,6 +227,9 @@ public class RotaryDirectManipulationWidgets extends Fragment { } TimePicker view1Ancestor = getTimePickerAncestor(view1); TimePicker view2Ancestor = getTimePickerAncestor(view2); + if (view1Ancestor == null || view2Ancestor == null) { + return false; + } return view1Ancestor == view2Ancestor; } @@ -291,7 +282,6 @@ public class RotaryDirectManipulationWidgets extends Fragment { if (!(view instanceof TimePicker)) { return false; } - boolean isActionUp = keyEvent.getAction() == KeyEvent.ACTION_UP; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_DOWN: @@ -302,7 +292,7 @@ public class RotaryDirectManipulationWidgets extends Fragment { return true; case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_RIGHT: - if (isActionUp) { + if (keyEvent.getAction() == KeyEvent.ACTION_UP) { TimePicker timePicker = (TimePicker) view; List numberPickers = new ArrayList<>(); getNumberPickerDescendants(numberPickers, timePicker); -- cgit v1.2.3 From c380a2e1378aa6421fc7b8c3ab566bb4dcde1357 Mon Sep 17 00:00:00 2001 From: Danny Epstein Date: Fri, 13 Nov 2020 16:36:37 -0800 Subject: Remove focus highlight on sample WebView Disable the default focus highlight on the WebView in the RotaryPlayground. Test: use rotary to focus a link in the sample WebView Bug: 173152508 Change-Id: I12adb7de0f144d4b82df10d2298b9b6cccef1e27 --- RotaryPlayground/res/layout/rotary_web_view.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RotaryPlayground/res/layout/rotary_web_view.xml b/RotaryPlayground/res/layout/rotary_web_view.xml index 7995dc6..6cdc5dd 100644 --- a/RotaryPlayground/res/layout/rotary_web_view.xml +++ b/RotaryPlayground/res/layout/rotary_web_view.xml @@ -51,7 +51,8 @@ android:id="@+id/web_view" android:layout_width="match_parent" android:layout_height="0dp" - android:layout_weight="1" /> + android:layout_weight="1" + android:defaultFocusHighlightEnabled="false"/>