blob: 5bf1fb91bc1805adb097013533999bebf53009f3 [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_FOCUS;
import static com.android.car.ui.utils.RotaryConstants.ACTION_NUDGE_SHORTCUT;
import static com.android.car.ui.utils.RotaryConstants.ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA;
import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_BOTTOM_BOUND_OFFSET;
import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_LEFT_BOUND_OFFSET;
import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_RIGHT_BOUND_OFFSET;
import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_TOP_BOUND_OFFSET;
import static com.android.car.ui.utils.RotaryConstants.NUDGE_DIRECTION;
import static com.android.car.ui.utils.ViewUtils.NO_FOCUS;
import static com.android.car.ui.utils.ViewUtils.REGULAR_FOCUS;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.car.ui.utils.CarUiUtils;
import com.android.car.ui.utils.ViewUtils;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A {@link LinearLayout} used as a navigation block for the rotary controller.
* <p>
* The {@link com.android.car.rotary.RotaryService} looks for instances of {@link FocusArea} in the
* view hierarchy when handling rotate and nudge actions. When receiving a rotation event ({@link
* android.car.input.RotaryEvent}), RotaryService will move the focus to another {@link View} that
* can take focus within the same FocusArea. When receiving a nudge event ({@link
* KeyEvent#KEYCODE_SYSTEM_NAVIGATION_UP}, {@link KeyEvent#KEYCODE_SYSTEM_NAVIGATION_DOWN}, {@link
* KeyEvent#KEYCODE_SYSTEM_NAVIGATION_LEFT}, or {@link KeyEvent#KEYCODE_SYSTEM_NAVIGATION_RIGHT}),
* RotaryService will move the focus to another view that can take focus in another (typically
* adjacent) FocusArea.
* <p>
* If enabled, FocusArea can draw highlights when one of its descendants has focus and it's not in
* touch mode.
* <p>
* When creating a navigation block in the layout file, if you intend to use a LinearLayout as a
* container for that block, just use a FocusArea instead; otherwise wrap the block in a FocusArea.
* <p>
* DO NOT nest a FocusArea inside another FocusArea because it will result in undefined navigation
* behavior.
*/
public class FocusArea extends LinearLayout {
private static final String TAG = "FocusArea";
private static final int INVALID_DIMEN = -1;
private static final int INVALID_DIRECTION = -1;
private static final List<Integer> NUDGE_DIRECTIONS =
Arrays.asList(FOCUS_LEFT, FOCUS_RIGHT, FOCUS_UP, FOCUS_DOWN);
/** Whether the FocusArea's descendant has focus (the FocusArea itself is not focusable). */
private boolean mHasFocus;
/**
* Whether to draw {@link #mForegroundHighlight} when one of the FocusArea's descendants has
* focus and it's not in touch mode.
*/
private boolean mEnableForegroundHighlight;
/**
* Whether to draw {@link #mBackgroundHighlight} when one of the FocusArea's descendants has
* focus and it's not in touch mode.
*/
private boolean mEnableBackgroundHighlight;
/**
* Highlight (typically outline of the FocusArea) drawn on top of the FocusArea and its
* descendants.
*/
private Drawable mForegroundHighlight;
/**
* Highlight (typically a solid or gradient shape) drawn on top of the FocusArea but behind its
* descendants.
*/
private Drawable mBackgroundHighlight;
/** The padding (in pixels) of the FocusArea highlight. */
private int mPaddingLeft;
private int mPaddingRight;
private int mPaddingTop;
private int mPaddingBottom;
/** The offset (in pixels) of the FocusArea's bounds. */
private int mLeftOffset;
private int mRightOffset;
private int mTopOffset;
private int mBottomOffset;
/** Whether the layout direction is {@link View#LAYOUT_DIRECTION_RTL}. */
private boolean mRtl;
/** The ID of the view specified in {@code app:defaultFocus}. */
private int mDefaultFocusId;
/** The view specified in {@code app:defaultFocus}. */
@Nullable
private View mDefaultFocusView;
/**
* Whether to focus on the {@code app:defaultFocus} view when nudging to the FocusArea, even if
* there was another view in the FocusArea focused before.
*/
private boolean mDefaultFocusOverridesHistory;
/** The ID of the view specified in {@code app:nudgeShortcut}. */
private int mNudgeShortcutId;
/** The view specified in {@code app:nudgeShortcut}. */
@Nullable
private View mNudgeShortcutView;
/** The direction specified in {@code app:nudgeShortcutDirection}. */
private int mNudgeShortcutDirection;
/**
* Map of nudge target FocusArea IDs specified in {@code app:nudgeLeft}, {@code app:nudgRight},
* {@code app:nudgeUp}, or {@code app:nudgeDown}.
*/
private Map<Integer, Integer> mSpecifiedNudgeIdMap;
/** Map of specified nudge target FocusAreas. */
private Map<Integer, FocusArea> mSpecifiedNudgeFocusAreaMap;
/**
* Cache of focus history and nudge history of the rotary controller.
* <p>
* For focus history, the previously focused view and a timestamp will be saved when the
* focused view has changed.
* <p>
* For nudge history, the target FocusArea, direction, and a timestamp will be saved when the
* focus has moved from another FocusArea to this FocusArea. There are 2 cases:
* <ul>
* <li>The focus is moved to another FocusArea because this FocusArea has called {@link
* #nudgeToAnotherFocusArea}. In this case, the target FocusArea and direction are
* trivial to this FocusArea.
* <li>The focus is moved to this FocusArea because RotaryService has performed {@link
* AccessibilityNodeInfo#ACTION_FOCUS} on this FocusArea. In this case, this FocusArea
* can get the source FocusArea through the {@link
* android.view.ViewTreeObserver.OnGlobalFocusChangeListener} registered, and can get
* the direction when handling the action. Since the listener is triggered before
* {@link #requestFocus} returns (which is called when handling the action), the
* source FocusArea is revealed earlier than the direction, so the nudge history should
* be saved when the direction is revealed.
* </ul>
*/
private RotaryCache mRotaryCache;
/** Whether to clear focus area history when the user rotates the rotary controller. */
private boolean mClearFocusAreaHistoryWhenRotating;
/** The FocusArea that had focus before this FocusArea, if any. */
private FocusArea mPreviousFocusArea;
/** The focused view in this FocusArea, if any. */
private View mFocusedView;
public FocusArea(Context context) {
super(context);
init(context, null);
}
public FocusArea(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public FocusArea(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
public FocusArea(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
private void init(Context context, @Nullable AttributeSet attrs) {
Resources resources = getContext().getResources();
mEnableForegroundHighlight = resources.getBoolean(
R.bool.car_ui_enable_focus_area_foreground_highlight);
mEnableBackgroundHighlight = resources.getBoolean(
R.bool.car_ui_enable_focus_area_background_highlight);
mForegroundHighlight = resources.getDrawable(
R.drawable.car_ui_focus_area_foreground_highlight, getContext().getTheme());
mBackgroundHighlight = resources.getDrawable(
R.drawable.car_ui_focus_area_background_highlight, getContext().getTheme());
mDefaultFocusOverridesHistory = resources.getBoolean(
R.bool.car_ui_focus_area_default_focus_overrides_history);
mClearFocusAreaHistoryWhenRotating = resources.getBoolean(
R.bool.car_ui_clear_focus_area_history_when_rotating);
@RotaryCache.CacheType
int focusHistoryCacheType = resources.getInteger(R.integer.car_ui_focus_history_cache_type);
int focusHistoryExpirationPeriodMs =
resources.getInteger(R.integer.car_ui_focus_history_expiration_period_ms);
@RotaryCache.CacheType
int focusAreaHistoryCacheType = resources.getInteger(
R.integer.car_ui_focus_area_history_cache_type);
int focusAreaHistoryExpirationPeriodMs =
resources.getInteger(R.integer.car_ui_focus_area_history_expiration_period_ms);
mRotaryCache = new RotaryCache(focusHistoryCacheType, focusHistoryExpirationPeriodMs,
focusAreaHistoryCacheType, focusAreaHistoryExpirationPeriodMs);
// Ensure that an AccessibilityNodeInfo is created for this view.
setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
// By default all ViewGroup subclasses do not call their draw() and onDraw() methods. We
// should enable it since we override these methods.
setWillNotDraw(false);
registerFocusChangeListener();
initAttrs(context, attrs);
}
private void registerFocusChangeListener() {
getViewTreeObserver().addOnGlobalFocusChangeListener(
(oldFocus, newFocus) -> {
boolean hasFocus = hasFocus();
saveFocusHistory(hasFocus);
maybeUpdatePreviousFocusArea(hasFocus, oldFocus);
maybeClearFocusAreaHistory(hasFocus, oldFocus);
maybeUpdateFocusAreaHighlight(hasFocus);
mHasFocus = hasFocus;
});
}
private void saveFocusHistory(boolean hasFocus) {
if (!hasFocus) {
mRotaryCache.saveFocusedView(mFocusedView, SystemClock.uptimeMillis());
mFocusedView = null;
return;
}
View v = getFocusedChild();
while (v != null) {
if (v.isFocused()) {
break;
}
v = v instanceof ViewGroup ? ((ViewGroup) v).getFocusedChild() : null;
}
mFocusedView = v;
}
/**
* Updates {@link #mPreviousFocusArea} when the focus has moved from another FocusArea to this
* FocusArea, and sets it to {@code null} in any other cases.
*/
private void maybeUpdatePreviousFocusArea(boolean hasFocus, View oldFocus) {
if (mHasFocus || !hasFocus || oldFocus == null || oldFocus instanceof FocusParkingView) {
mPreviousFocusArea = null;
return;
}
mPreviousFocusArea = ViewUtils.getAncestorFocusArea(oldFocus);
if (mPreviousFocusArea == null) {
Log.w(TAG, "No parent FocusArea for " + oldFocus);
}
}
/**
* Clears FocusArea nudge history when the user rotates the controller to move focus within this
* FocusArea.
*/
private void maybeClearFocusAreaHistory(boolean hasFocus, View oldFocus) {
if (!mClearFocusAreaHistoryWhenRotating) {
return;
}
if (!hasFocus || oldFocus == null) {
return;
}
FocusArea oldFocusArea = ViewUtils.getAncestorFocusArea(oldFocus);
if (oldFocusArea != this) {
return;
}
mRotaryCache.clearFocusAreaHistory();
}
/** Updates highlight of the FocusArea if this FocusArea has gained or lost focus. */
private void maybeUpdateFocusAreaHighlight(boolean hasFocus) {
if (!mEnableBackgroundHighlight && !mEnableForegroundHighlight) {
return;
}
if (mHasFocus != hasFocus) {
invalidate();
}
}
private void initAttrs(Context context, @Nullable AttributeSet attrs) {
if (attrs == null) {
return;
}
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FocusArea);
try {
mDefaultFocusId = a.getResourceId(R.styleable.FocusArea_defaultFocus, View.NO_ID);
// Initialize the highlight padding. The padding, for example, left padding, is set in
// the following order:
// 1. if highlightPaddingStart (or highlightPaddingEnd in RTL layout) specified, use it
// 2. otherwise, if highlightPaddingHorizontal is specified, use it
// 3. otherwise use 0
int paddingStart = a.getDimensionPixelSize(
R.styleable.FocusArea_highlightPaddingStart, INVALID_DIMEN);
if (paddingStart == INVALID_DIMEN) {
paddingStart = a.getDimensionPixelSize(
R.styleable.FocusArea_highlightPaddingHorizontal, 0);
}
int paddingEnd = a.getDimensionPixelSize(
R.styleable.FocusArea_highlightPaddingEnd, INVALID_DIMEN);
if (paddingEnd == INVALID_DIMEN) {
paddingEnd = a.getDimensionPixelSize(
R.styleable.FocusArea_highlightPaddingHorizontal, 0);
}
mRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
mPaddingLeft = mRtl ? paddingEnd : paddingStart;
mPaddingRight = mRtl ? paddingStart : paddingEnd;
mPaddingTop = a.getDimensionPixelSize(
R.styleable.FocusArea_highlightPaddingTop, INVALID_DIMEN);
if (mPaddingTop == INVALID_DIMEN) {
mPaddingTop = a.getDimensionPixelSize(
R.styleable.FocusArea_highlightPaddingVertical, 0);
}
mPaddingBottom = a.getDimensionPixelSize(
R.styleable.FocusArea_highlightPaddingBottom, INVALID_DIMEN);
if (mPaddingBottom == INVALID_DIMEN) {
mPaddingBottom = a.getDimensionPixelSize(
R.styleable.FocusArea_highlightPaddingVertical, 0);
}
// Initialize the offset of the FocusArea's bounds. The offset, for example, left
// offset, is set in the following order:
// 1. if startBoundOffset (or endBoundOffset in RTL layout) specified, use it
// 2. otherwise, if horizontalBoundOffset is specified, use it
// 3. otherwise use mPaddingLeft
int startOffset = a.getDimensionPixelSize(
R.styleable.FocusArea_startBoundOffset, INVALID_DIMEN);
if (startOffset == INVALID_DIMEN) {
startOffset = a.getDimensionPixelSize(
R.styleable.FocusArea_horizontalBoundOffset, paddingStart);
}
int endOffset = a.getDimensionPixelSize(
R.styleable.FocusArea_endBoundOffset, INVALID_DIMEN);
if (endOffset == INVALID_DIMEN) {
endOffset = a.getDimensionPixelSize(
R.styleable.FocusArea_horizontalBoundOffset, paddingEnd);
}
mLeftOffset = mRtl ? endOffset : startOffset;
mRightOffset = mRtl ? startOffset : endOffset;
mTopOffset = a.getDimensionPixelSize(
R.styleable.FocusArea_topBoundOffset, INVALID_DIMEN);
if (mTopOffset == INVALID_DIMEN) {
mTopOffset = a.getDimensionPixelSize(
R.styleable.FocusArea_verticalBoundOffset, mPaddingTop);
}
mBottomOffset = a.getDimensionPixelSize(
R.styleable.FocusArea_bottomBoundOffset, INVALID_DIMEN);
if (mBottomOffset == INVALID_DIMEN) {
mBottomOffset = a.getDimensionPixelSize(
R.styleable.FocusArea_verticalBoundOffset, mPaddingBottom);
}
mNudgeShortcutId = a.getResourceId(R.styleable.FocusArea_nudgeShortcut, View.NO_ID);
mNudgeShortcutDirection = a.getInt(
R.styleable.FocusArea_nudgeShortcutDirection, INVALID_DIRECTION);
if ((mNudgeShortcutId == View.NO_ID) ^ (mNudgeShortcutDirection == INVALID_DIRECTION)) {
throw new IllegalStateException("nudgeShortcut and nudgeShortcutDirection must "
+ "be specified together");
}
mSpecifiedNudgeIdMap = new HashMap<>();
mSpecifiedNudgeIdMap.put(FOCUS_LEFT,
a.getResourceId(R.styleable.FocusArea_nudgeLeft, View.NO_ID));
mSpecifiedNudgeIdMap.put(FOCUS_RIGHT,
a.getResourceId(R.styleable.FocusArea_nudgeRight, View.NO_ID));
mSpecifiedNudgeIdMap.put(FOCUS_UP,
a.getResourceId(R.styleable.FocusArea_nudgeUp, View.NO_ID));
mSpecifiedNudgeIdMap.put(FOCUS_DOWN,
a.getResourceId(R.styleable.FocusArea_nudgeDown, View.NO_ID));
} finally {
a.recycle();
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (mDefaultFocusId != View.NO_ID) {
mDefaultFocusView = CarUiUtils.requireViewByRefId(this, mDefaultFocusId);
}
if (mNudgeShortcutId != View.NO_ID) {
mNudgeShortcutView = CarUiUtils.requireViewByRefId(this, mNudgeShortcutId);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
boolean rtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
if (mRtl != rtl) {
mRtl = rtl;
int temp = mPaddingLeft;
mPaddingLeft = mPaddingRight;
mPaddingRight = temp;
temp = mLeftOffset;
mLeftOffset = mRightOffset;
mRightOffset = temp;
}
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
// To ensure the focus is initialized properly in rotary mode when there is a window focus
// change, this FocusArea will grab the focus from the currently focused view if one of this
// FocusArea's descendants is a better focus candidate than the currently focused view.
if (hasWindowFocus && !isInTouchMode()) {
maybeAdjustFocus();
}
super.onWindowFocusChanged(hasWindowFocus);
}
/**
* Focuses on another view in this FocusArea if the view is a better focus candidate than the
* currently focused view.
*/
private boolean maybeAdjustFocus() {
View root = getRootView();
View focus = root.findFocus();
return ViewUtils.adjustFocus(root, focus);
}
@Override
public boolean performAccessibilityAction(int action, Bundle arguments) {
switch (action) {
case ACTION_FOCUS:
// Repurpose ACTION_FOCUS to focus on its descendant. We can do this because
// FocusArea is not focusable and it didn't consume ACTION_FOCUS previously.
boolean success = focusOnDescendant();
if (success && mPreviousFocusArea != null) {
int direction = getNudgeDirection(arguments);
if (direction != INVALID_DIRECTION) {
saveFocusAreaHistory(direction, mPreviousFocusArea, this,
SystemClock.uptimeMillis());
}
}
return success;
case ACTION_NUDGE_SHORTCUT:
return nudgeToShortcutView(arguments);
case ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA:
return nudgeToAnotherFocusArea(arguments);
default:
return super.performAccessibilityAction(action, arguments);
}
}
private boolean focusOnDescendant() {
if (mDefaultFocusOverridesHistory) {
// Check mDefaultFocus before last focused view.
if (focusDefaultFocusView() || focusOnLastFocusedView()) {
return true;
}
} else {
// Check last focused view before mDefaultFocus.
if (focusOnLastFocusedView() || focusDefaultFocusView()) {
return true;
}
}
return focusOnFirstFocusableView();
}
private boolean focusDefaultFocusView() {
return ViewUtils.adjustFocus(this, /* currentLevel= */ REGULAR_FOCUS);
}
/**
* Gets the {@code app:defaultFocus} view.
*
* @hidden
*/
public View getDefaultFocusView() {
return mDefaultFocusView;
}
private boolean focusOnLastFocusedView() {
View lastFocusedView = mRotaryCache.getFocusedView(SystemClock.uptimeMillis());
return ViewUtils.requestFocus(lastFocusedView);
}
private boolean focusOnFirstFocusableView() {
return ViewUtils.adjustFocus(this, /* currentLevel= */ NO_FOCUS);
}
private boolean nudgeToShortcutView(Bundle arguments) {
if (mNudgeShortcutDirection == INVALID_DIRECTION) {
// No nudge shortcut configured for this FocusArea.
return false;
}
if (arguments == null
|| arguments.getInt(NUDGE_DIRECTION, INVALID_DIRECTION)
!= mNudgeShortcutDirection) {
// The user is not nudging in the nudge shortcut direction.
return false;
}
if (mNudgeShortcutView.isFocused()) {
// The nudge shortcut view is already focused; return false so that the user can
// nudge to another FocusArea.
return false;
}
return ViewUtils.requestFocus(mNudgeShortcutView);
}
private boolean nudgeToAnotherFocusArea(Bundle arguments) {
int direction = getNudgeDirection(arguments);
long elapsedRealtime = SystemClock.uptimeMillis();
// Try to nudge to specified FocusArea, if any.
FocusArea targetFocusArea = getSpecifiedFocusArea(direction);
boolean success = targetFocusArea != null && targetFocusArea.focusOnDescendant();
// If failed, try to nudge to cached FocusArea, if any.
if (!success) {
targetFocusArea = mRotaryCache.getCachedFocusArea(direction, elapsedRealtime);
success = targetFocusArea != null && targetFocusArea.focusOnDescendant();
}
if (success) {
saveFocusAreaHistory(direction, this, targetFocusArea, elapsedRealtime);
}
return success;
}
private static int getNudgeDirection(Bundle arguments) {
return arguments == null
? INVALID_DIRECTION
: arguments.getInt(NUDGE_DIRECTION, INVALID_DIRECTION);
}
/** Saves bidirectional FocusArea nudge history. */
private void saveFocusAreaHistory(int direction, @NonNull FocusArea sourceFocusArea,
@NonNull FocusArea targetFocusArea, long elapsedRealtime) {
sourceFocusArea.mRotaryCache.saveFocusArea(direction, targetFocusArea, elapsedRealtime);
int oppositeDirection = getOppositeDirection(direction);
targetFocusArea.mRotaryCache.saveFocusArea(oppositeDirection, sourceFocusArea,
elapsedRealtime);
}
/** Returns the direction opposite the given {@code direction} */
@VisibleForTesting
private static int getOppositeDirection(int direction) {
switch (direction) {
case View.FOCUS_LEFT:
return View.FOCUS_RIGHT;
case View.FOCUS_RIGHT:
return View.FOCUS_LEFT;
case View.FOCUS_UP:
return View.FOCUS_DOWN;
case View.FOCUS_DOWN:
return View.FOCUS_UP;
}
throw new IllegalArgumentException("direction must be "
+ "FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, or FOCUS_RIGHT.");
}
@Nullable
private FocusArea getSpecifiedFocusArea(int direction) {
maybeInitializeSpecifiedFocusAreas();
return mSpecifiedNudgeFocusAreaMap.get(direction);
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw highlight on top of this FocusArea (including its background and content) but
// behind its children.
if (mEnableBackgroundHighlight && mHasFocus && !isInTouchMode()) {
mBackgroundHighlight.setBounds(
mPaddingLeft + getScrollX(),
mPaddingTop + getScrollY(),
getScrollX() + getWidth() - mPaddingRight,
getScrollY() + getHeight() - mPaddingBottom);
mBackgroundHighlight.draw(canvas);
}
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
// Draw highlight on top of this FocusArea (including its background and content) and its
// children (including background, content, focus highlight, etc).
if (mEnableForegroundHighlight && mHasFocus && !isInTouchMode()) {
mForegroundHighlight.setBounds(
mPaddingLeft + getScrollX(),
mPaddingTop + getScrollY(),
getScrollX() + getWidth() - mPaddingRight,
getScrollY() + getHeight() - mPaddingBottom);
mForegroundHighlight.draw(canvas);
}
}
@Override
public CharSequence getAccessibilityClassName() {
return FocusArea.class.getName();
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
Bundle bundle = info.getExtras();
bundle.putInt(FOCUS_AREA_LEFT_BOUND_OFFSET, mLeftOffset);
bundle.putInt(FOCUS_AREA_RIGHT_BOUND_OFFSET, mRightOffset);
bundle.putInt(FOCUS_AREA_TOP_BOUND_OFFSET, mTopOffset);
bundle.putInt(FOCUS_AREA_BOTTOM_BOUND_OFFSET, mBottomOffset);
}
@Override
protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
return maybeAdjustFocus();
}
@Override
public boolean restoreDefaultFocus() {
return maybeAdjustFocus();
}
private void maybeInitializeSpecifiedFocusAreas() {
if (mSpecifiedNudgeFocusAreaMap != null) {
return;
}
View root = getRootView();
mSpecifiedNudgeFocusAreaMap = new HashMap<>();
for (Integer direction : NUDGE_DIRECTIONS) {
int id = mSpecifiedNudgeIdMap.get(direction);
mSpecifiedNudgeFocusAreaMap.put(direction, root.findViewById(id));
}
}
/**
* Sets the padding (in pixels) of the FocusArea highlight.
* <p>
* It doesn't affect other values, such as the paddings on its child views.
*/
public void setHighlightPadding(int left, int top, int right, int bottom) {
if (mPaddingLeft == left && mPaddingTop == top && mPaddingRight == right
&& mPaddingBottom == bottom) {
return;
}
mPaddingLeft = left;
mPaddingTop = top;
mPaddingRight = right;
mPaddingBottom = bottom;
invalidate();
}
/**
* Sets the offset (in pixels) of the FocusArea's bounds.
* <p>
* It only affects the perceived bounds for the purposes of finding the nudge target. It doesn't
* affect the FocusArea's view bounds or highlight bounds. The offset should only be used when
* FocusAreas are overlapping and nudge interaction is ambiguous.
*/
public void setBoundsOffset(int left, int top, int right, int bottom) {
mLeftOffset = left;
mTopOffset = top;
mRightOffset = right;
mBottomOffset = bottom;
}
@VisibleForTesting
void enableForegroundHighlight() {
mEnableForegroundHighlight = true;
}
@VisibleForTesting
void setDefaultFocusOverridesHistory(boolean override) {
mDefaultFocusOverridesHistory = override;
}
@VisibleForTesting
void setRotaryCache(@NonNull RotaryCache rotaryCache) {
mRotaryCache = rotaryCache;
}
@VisibleForTesting
void setClearFocusAreaHistoryWhenRotating(boolean clear) {
mClearFocusAreaHistoryWhenRotating = clear;
}
}