| /* |
| * 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.ui; |
| |
| import static android.view.accessibility.AccessibilityNodeInfo.ACTION_COLLAPSE; |
| import static android.view.accessibility.AccessibilityNodeInfo.ACTION_DISMISS; |
| |
| import android.content.Context; |
| import android.os.Bundle; |
| import android.util.AttributeSet; |
| import android.view.View; |
| import android.view.inputmethod.InputMethodManager; |
| |
| import androidx.annotation.Nullable; |
| |
| /** |
| * A transparent {@link View} that can take focus. It's used by {@link |
| * com.android.car.rotary.RotaryService} to support rotary controller navigation. Each {@link |
| * android.view.Window} must have at least one FocusParkingView. The {@link FocusParkingView} must |
| * be the first in Tab order, and outside of all {@link FocusArea}s. |
| * |
| * <p> |
| * Android doesn't clear focus automatically when focus is set in another window. If we try to clear |
| * focus in the previous window, Android will re-focus a view in that window, resulting in two |
| * windows being focused simultaneously. Adding this view to each window can fix this issue. This |
| * view is transparent and its default focus highlight is disabled, so it's invisible to the user no |
| * matter whether it's focused or not. It can take focus so that RotaryService can "park" the focus |
| * on it to remove the focus highlight. |
| * <p> |
| * If the focused view is scrolled off the screen, Android will refocus the first focusable view in |
| * the window. The FocusParkingView should be the first view so that it gets focus. The |
| * RotaryService detects this and moves focus to the scrolling container. |
| * <p> |
| * If there is only one focus area in the current window, rotating the controller within the focus |
| * area will cause RotaryService to move the focus around from the view on the right to the view on |
| * the left or vice versa. Adding this view to each window can fix this issue. When RotaryService |
| * finds out the focus target is a FocusParkingView, it will know a wrap-around is going to happen. |
| * Then it will avoid the wrap-around by not moving focus. |
| */ |
| public class FocusParkingView extends View { |
| |
| public FocusParkingView(Context context) { |
| super(context); |
| init(); |
| } |
| |
| public FocusParkingView(Context context, @Nullable AttributeSet attrs) { |
| super(context, attrs); |
| init(); |
| } |
| |
| public FocusParkingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| init(); |
| } |
| |
| public FocusParkingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, |
| int defStyleRes) { |
| super(context, attrs, defStyleAttr, defStyleRes); |
| init(); |
| } |
| |
| private void init() { |
| // This view is focusable, visible and enabled so it can take focus. |
| setFocusable(View.FOCUSABLE); |
| setVisibility(VISIBLE); |
| setEnabled(true); |
| |
| // This view is not clickable so it won't affect the app's behavior when the user clicks on |
| // it by accident. |
| setClickable(false); |
| |
| // This view is always transparent. |
| setAlpha(0f); |
| |
| // Prevent Android from drawing the default focus highlight for this view when it's focused. |
| setDefaultFocusHighlightEnabled(false); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| // This size of the view is always 1 x 1 pixel, no matter what value is set in the layout |
| // file (match_parent, wrap_content, 100dp, 0dp, etc). Small size is to ensure it has little |
| // impact on the layout, non-zero size is to ensure it can take focus. |
| setMeasuredDimension(1, 1); |
| } |
| |
| @Override |
| public void onWindowFocusChanged(boolean hasWindowFocus) { |
| if (!hasWindowFocus) { |
| // We need to clear the focus (by parking the focus on the FocusParkingView) once the |
| // current window goes to background. This can't be done by RotaryService because |
| // RotaryService sees the window as removed, thus can't perform any action (such as |
| // focus, clear focus) on the nodes in the window. So FocusParkingView has to grab the |
| // focus proactively. |
| requestFocus(); |
| } |
| super.onWindowFocusChanged(hasWindowFocus); |
| } |
| |
| @Override |
| public CharSequence getAccessibilityClassName() { |
| return FocusParkingView.class.getName(); |
| } |
| |
| @Override |
| public boolean performAccessibilityAction(int action, Bundle arguments) { |
| switch (action) { |
| case ACTION_DISMISS: |
| // Try to move focus to the default focus. |
| getRootView().restoreDefaultFocus(); |
| // The action failed if the FocusParkingView is still focused. |
| return !isFocused(); |
| case ACTION_COLLAPSE: |
| // Hide the IME. |
| InputMethodManager inputMethodManager = |
| getContext().getSystemService(InputMethodManager.class); |
| return inputMethodManager.hideSoftInputFromWindow(getWindowToken(), |
| /* flags= */ 0); |
| } |
| return super.performAccessibilityAction(action, arguments); |
| } |
| } |