diff options
author | Yabin Huang <yabinh@google.com> | 2021-04-09 15:52:49 -0700 |
---|---|---|
committer | Yabin Huang <yabinh@google.com> | 2021-04-16 01:13:53 +0000 |
commit | c1eaa136602fd93106c38912a79b5279a831741e (patch) | |
tree | 49889043e37b5badcd2ba8f388be671fa15fc980 /RotaryPlayground | |
parent | 20022aae9a24b50c391daf0cda2c7f6516d6c329 (diff) | |
download | tests-c1eaa136602fd93106c38912a79b5279a831741e.tar.gz |
Demo updating a view without losing focus
Add a FocusArea to the Cards tab to demonstrate how to update a
view properly.
Fixes: 185377477
Test: manual
Change-Id: I66038ef4677ee904246bf1dd16503fca4b27c9c1
Diffstat (limited to 'RotaryPlayground')
9 files changed, 353 insertions, 15 deletions
diff --git a/RotaryPlayground/res/drawable/button_background.xml b/RotaryPlayground/res/drawable/button_background.xml new file mode 100644 index 0000000..25566c5 --- /dev/null +++ b/RotaryPlayground/res/drawable/button_background.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" android:drawable="@drawable/ic_play_arrow_off" /> + <item android:drawable="@drawable/ic_play_arrow" /> +</selector> diff --git a/RotaryPlayground/res/drawable/custom_button_background.xml b/RotaryPlayground/res/drawable/custom_button_background.xml new file mode 100644 index 0000000..d4716fa --- /dev/null +++ b/RotaryPlayground/res/drawable/custom_button_background.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item app:state_rotary_enabled="true" android:drawable="@drawable/ic_play_arrow" /> + <item android:drawable="@drawable/ic_play_arrow_off" /> +</selector> diff --git a/RotaryPlayground/res/drawable/ic_play_arrow.xml b/RotaryPlayground/res/drawable/ic_play_arrow.xml new file mode 100644 index 0000000..5c0c252 --- /dev/null +++ b/RotaryPlayground/res/drawable/ic_play_arrow.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021, 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="56dp" + android:height="56dp" + android:viewportWidth="48" + android:viewportHeight="48"> + + <path + android:pathData="M-838-2232H562v3600H-838z" /> + <path + android:fillColor="#000000" + android:pathData="M16 10v28l22-14z" /> + <path + android:pathData="M0 0h48v48H0z" /> +</vector> diff --git a/RotaryPlayground/res/drawable/ic_play_arrow_off.xml b/RotaryPlayground/res/drawable/ic_play_arrow_off.xml new file mode 100644 index 0000000..8ad935f --- /dev/null +++ b/RotaryPlayground/res/drawable/ic_play_arrow_off.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021, 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="56dp" + android:height="56dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + <group + android:translateX="3.000000" + android:translateY="3.000000"> + <path + android:fillColor="#000000" + android:strokeWidth="1" + android:pathData="M16,9 L7.249,3.431 L14.048,10.242 L16,9 Z" /> + <path + android:fillColor="#000000" + android:strokeWidth="1" + android:pathData="M18,16.73 L1.27,0 L0,1.27 L5,6.27 L5,16 L10.946,12.216 L16.73,18 L18,16.73 Z" /> + </group> +</vector> diff --git a/RotaryPlayground/res/drawable/ic_stop.xml b/RotaryPlayground/res/drawable/ic_stop.xml new file mode 100644 index 0000000..fc42b32 --- /dev/null +++ b/RotaryPlayground/res/drawable/ic_stop.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021, 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="56dp" + android:height="56dp" + android:viewportWidth="48" + android:viewportHeight="48"> + + <path + android:pathData="M0 0h48v48H0z" /> + <path + android:fillColor="#000000" + android:pathData="M12 12h24v24H12z" /> +</vector> diff --git a/RotaryPlayground/res/layout/rotary_cards.xml b/RotaryPlayground/res/layout/rotary_cards.xml index e3d9e87..2ffdfdc 100644 --- a/RotaryPlayground/res/layout/rotary_cards.xml +++ b/RotaryPlayground/res/layout/rotary_cards.xml @@ -149,7 +149,8 @@ android:layout_width="@dimen/card_width" android:layout_height="match_parent" android:padding="@dimen/card_padding" - android:orientation="vertical"> + android:orientation="vertical" + app:defaultFocus="@+id/default_focus"> <TextView android:layout_height="@dimen/description_height" android:layout_width="match_parent" @@ -160,8 +161,8 @@ android:onClick="onRotaryButtonClick" android:tag="test_button" android:text="Button" /> - <!-- TODO(b/154180719): Make this button the default focus in this FocusArea --> <Button + android:id="@+id/default_focus" android:layout_width="match_parent" android:layout_height="50dp" android:onClick="onRotaryButtonClick" @@ -181,18 +182,9 @@ android:text="Button" /> </com.android.car.ui.FocusArea> - <!-- A FocusArea with buttons in a circle. The default focus should land on A. + <!-- A FocusArea with buttons in a circle. Rotating clockwise moves the focus from A -> B -> C -> D -> E -> F -> G -> H, - and reverse counterclockwise. - Adding app:defaultFocus to A to make it the default focus on this card - Adding android:nextFocusForward is necessary to ensure the expected focus - order, without it, the focus will move from - G -> H -> F -> A -> E -> B -> D -> C. - Lastly, android:nextFocusForward is not added to H -> A, to avoid linking - the nodes in a circle. app:wrapAround="true" should be used instead. - --> - <!-- TODO(agathaman): add app:wrapAround to this card when b/155698037 is fixed --> - <!-- TODO(agathaman): add app:defaultFocus to this card when b/155698037 is fixed --> + and reverse counterclockwise. --> <com.android.car.ui.FocusArea android:id="@+id/card_that_wraps_around" android:background="@color/card_background_color" @@ -200,7 +192,8 @@ android:layout_width="@dimen/card_width" android:layout_height="match_parent" android:padding="@dimen/card_padding" - android:orientation="vertical"> + android:orientation="vertical" + app:wrapAround="true"> <TextView android:layout_height="@dimen/description_height" android:layout_width="match_parent" @@ -305,6 +298,68 @@ /> </androidx.constraintlayout.widget.ConstraintLayout> </com.android.car.ui.FocusArea> + <!-- A FocusArea to demonstrate how to update a view properly. + Don't remove the focused view, otherwise Android framework will focus on another + view (the default focus view, or the first focusable view in the view tree), and + the user will see the focus highlight jump away unexpectedly. Some workarounds: + 1. Don't remove the view. You may achieve the desired behavior by updating the + source image of focused view. + 2. Delegate the view focus to its container, therefore removing the view won't + affect the focus + 3. Make another view request focus explicitly after Android framework adjusts the + focus. + Don't disable the focused view, otherwise Android framework will focus on another + view. A workaround is to use a custom state to replace android:state_disabled so + that it appears disabled but is not disabled actually. + --> + <com.android.car.ui.FocusArea + android:id="@+id/focus_area5" + android:background="@color/card_background_color" + android:layout_margin="16dp" + android:layout_width="@dimen/card_width" + android:layout_height="match_parent" + android:padding="@dimen/card_padding" + android:orientation="vertical"> + <Button + android:id="@+id/button_5a" + android:layout_width="match_parent" + android:layout_height="50dp" + android:text="focus jumps" /> + <Button + android:id="@+id/button_5b" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:background="@drawable/ic_play_arrow"/> + <FrameLayout + android:id="@+id/button_5c_container" + android:layout_width="match_parent" + android:layout_height="50dp" + android:focusable="true"> + <Button + android:id="@+id/button_5c" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:text="focus stays" /> + </FrameLayout> + <Button + android:id="@+id/button_5d" + android:layout_width="match_parent" + android:layout_height="50dp" + android:text="focus returns" /> + <Button + android:id="@+id/button_5e" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:background="@drawable/button_background"/> + <com.android.car.rotaryplayground.CustomButton + android:id="@+id/button_5f" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:background="@drawable/custom_button_background"/> + </com.android.car.ui.FocusArea> </LinearLayout> </HorizontalScrollView> -</LinearLayout>
\ No newline at end of file +</LinearLayout> diff --git a/RotaryPlayground/res/values/attrs.xml b/RotaryPlayground/res/values/attrs.xml new file mode 100644 index 0000000..6b5aa4f --- /dev/null +++ b/RotaryPlayground/res/values/attrs.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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 + --> +<resources> + <declare-styleable name="CustomButton"> + <attr name="state_rotary_enabled" format="boolean" /> + </declare-styleable> +</resources> diff --git a/RotaryPlayground/src/com/android/car/rotaryplayground/CustomButton.java b/RotaryPlayground/src/com/android/car/rotaryplayground/CustomButton.java new file mode 100644 index 0000000..924908f --- /dev/null +++ b/RotaryPlayground/src/com/android/car/rotaryplayground/CustomButton.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2021 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.util.AttributeSet; +import android.widget.Button; + +import androidx.annotation.Nullable; + +public class CustomButton extends Button { + + private static final int[] STATE_ROTARY_ENABLED = {R.attr.state_rotary_enabled}; + + private boolean mRotaryEnabled = true; + + public CustomButton(Context context) { + super(context); + } + + public CustomButton(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public CustomButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public CustomButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public void setRotaryEnabled(boolean enabled) { + mRotaryEnabled = enabled; + refreshDrawableState(); + } + + public boolean isRotaryEnabled() { + return mRotaryEnabled; + } + + @Override + protected int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + if (mRotaryEnabled) { + mergeDrawableStates(drawableState, STATE_ROTARY_ENABLED); + } + return drawableState; + } +} diff --git a/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryCards.java b/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryCards.java index 28f34a1..730c7c5 100644 --- a/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryCards.java +++ b/RotaryPlayground/src/com/android/car/rotaryplayground/RotaryCards.java @@ -16,13 +16,18 @@ package com.android.car.rotaryplayground; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; + import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import com.android.car.ui.FocusArea; + /** Fragment to demo a layout with cards that are FocusArea containers. */ public class RotaryCards extends Fragment { @@ -30,6 +35,62 @@ public class RotaryCards extends Fragment { public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.rotary_cards, container, false); + + // This button will be removed and immediately added back after click. So the focus + // highlight will jump to another view after rotary click. + FocusArea focusArea5 = view.findViewById(R.id.focus_area5); + Button button5a = view.findViewById(R.id.button_5a); + button5a.setOnClickListener(v -> { + int index = focusArea5.indexOfChild(button5a); + focusArea5.removeView(button5a); + focusArea5.addView(button5a, index); + }); + + // The background of this button will be changed after click. The focus highlight will stay + // on this Button after click. + Button button5b = view.findViewById(R.id.button_5b); + boolean[] stopped = {false}; + button5b.setOnClickListener(v -> { + stopped[0] = !stopped[0]; + Drawable drawable = view.getContext().getDrawable( + stopped[0] ? R.drawable.ic_stop : R.drawable.ic_play_arrow); + button5b.setBackground(drawable); + }); + + // This button will be removed and immediately added back after click. It's not focusable + // but its container is focusable. The focus highlight will stay on the container after + // click. + ViewGroup button5cContainer = view.findViewById(R.id.button_5c_container); + Button button5c = view.findViewById(R.id.button_5c); + button5c.setFocusable(false); + button5cContainer.setOnClickListener(v -> { + button5cContainer.removeView(button5c); + button5cContainer.addView(button5c, 0); + }); + + // This button will be removed then added back and request focus explicitly after click. + // So the focus highlight will jump to another view then jump back after rotary click. + Button button5d = view.findViewById(R.id.button_5d); + button5d.setOnClickListener(v -> { + boolean needRestoreFocus = button5d.isFocused(); + int index = focusArea5.indexOfChild(button5d); + focusArea5.removeView(button5d); + focusArea5.addView(button5d, index); + if (needRestoreFocus) { + button5d.requestFocus(); + } + }); + + // This button will be disabled after click. So the focus highlight will jump to another + // view after rotary click. + Button button5e = view.findViewById(R.id.button_5e); + button5e.setOnClickListener(v -> button5e.setEnabled(!button5e.isEnabled())); + + // This button will appear disabled but is not disabled after click. So the focus + // highlight will stay after rotary click. + CustomButton button5f = view.findViewById(R.id.button_5f); + button5f.setOnClickListener(v -> button5f.setRotaryEnabled(!button5f.isRotaryEnabled())); + return view; } } |