blob: 2167d2386b6d08c7e5bfdc1dba05a28e622ed163 [file] [log] [blame]
/*
* 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);
}
}