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.ui; 17 18 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_DISMISS; 19 20 import android.content.Context; 21 import android.os.Bundle; 22 import android.util.AttributeSet; 23 import android.view.View; 24 25 import androidx.annotation.Nullable; 26 27 /** 28 * A transparent {@link View} that can take focus. It's used by {@link 29 * com.android.car.rotary.RotaryService} to support rotary controller navigation. Each {@link 30 * android.view.Window} must have at least one FocusParkingView. The {@link FocusParkingView} must 31 * be the first in Tab order, and outside of all {@link FocusArea}s. 32 * 33 * <p> 34 * Android doesn't clear focus automatically when focus is set in another window. If we try to clear 35 * focus in the previous window, Android will re-focus a view in that window, resulting in two 36 * windows being focused simultaneously. Adding this view to each window can fix this issue. This 37 * view is transparent and its default focus highlight is disabled, so it's invisible to the user no 38 * matter whether it's focused or not. It can take focus so that RotaryService can "park" the focus 39 * on it to remove the focus highlight. 40 * <p> 41 * If the focused view is scrolled off the screen, Android will refocus the first focusable view in 42 * the window. The FocusParkingView should be the first view so that it gets focus. The 43 * RotaryService detects this and moves focus to the scrolling container. 44 * <p> 45 * If there is only one focus area in the current window, rotating the controller within the focus 46 * area will cause RotaryService to move the focus around from the view on the right to the view on 47 * the left or vice versa. Adding this view to each window can fix this issue. When RotaryService 48 * finds out the focus target is a FocusParkingView, it will know a wrap-around is going to happen. 49 * Then it will avoid the wrap-around by not moving focus. 50 */ 51 public class FocusParkingView extends View { 52 FocusParkingView(Context context)53 public FocusParkingView(Context context) { 54 super(context); 55 init(); 56 } 57 FocusParkingView(Context context, @Nullable AttributeSet attrs)58 public FocusParkingView(Context context, @Nullable AttributeSet attrs) { 59 super(context, attrs); 60 init(); 61 } 62 FocusParkingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)63 public FocusParkingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 64 super(context, attrs, defStyleAttr); 65 init(); 66 } 67 FocusParkingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)68 public FocusParkingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, 69 int defStyleRes) { 70 super(context, attrs, defStyleAttr, defStyleRes); 71 init(); 72 } 73 init()74 private void init() { 75 // This view is focusable, visible and enabled so it can take focus. 76 setFocusable(View.FOCUSABLE); 77 setVisibility(VISIBLE); 78 setEnabled(true); 79 80 // This view is not clickable so it won't affect the app's behavior when the user clicks on 81 // it by accident. 82 setClickable(false); 83 84 // This view is always transparent. 85 setAlpha(0f); 86 87 // Prevent Android from drawing the default focus highlight for this view when it's focused. 88 setDefaultFocusHighlightEnabled(false); 89 } 90 91 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)92 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 93 // This size of the view is always 1 x 1 pixel, no matter what value is set in the layout 94 // file (match_parent, wrap_content, 100dp, 0dp, etc). Small size is to ensure it has little 95 // impact on the layout, non-zero size is to ensure it can take focus. 96 setMeasuredDimension(1, 1); 97 } 98 99 @Override onWindowFocusChanged(boolean hasWindowFocus)100 public void onWindowFocusChanged(boolean hasWindowFocus) { 101 if (!hasWindowFocus) { 102 // We need to clear the focus (by parking the focus on the FocusParkingView) once the 103 // current window goes to background. This can't be done by RotaryService because 104 // RotaryService sees the window as removed, thus can't perform any action (such as 105 // focus, clear focus) on the nodes in the window. So FocusParkingView has to grab the 106 // focus proactively. 107 requestFocus(); 108 } 109 super.onWindowFocusChanged(hasWindowFocus); 110 } 111 112 @Override getAccessibilityClassName()113 public CharSequence getAccessibilityClassName() { 114 return FocusParkingView.class.getName(); 115 } 116 117 @Override performAccessibilityAction(int action, Bundle arguments)118 public boolean performAccessibilityAction(int action, Bundle arguments) { 119 if (action == ACTION_DISMISS) { 120 // Try to move focus to the default focus. 121 getRootView().restoreDefaultFocus(); 122 // The action failed if the FocusParkingView is still focused. 123 return !isFocused(); 124 } 125 return super.performAccessibilityAction(action, arguments); 126 } 127 } 128