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 
17 package com.android.car.ui;
18 
19 import android.content.Context;
20 import android.graphics.Canvas;
21 import android.graphics.drawable.Drawable;
22 import android.util.AttributeSet;
23 import android.view.KeyEvent;
24 import android.view.View;
25 import android.widget.LinearLayout;
26 
27 import androidx.annotation.Nullable;
28 
29 /**
30  * A {@link LinearLayout} used as a navigation block for the rotary controller.
31  * <p>
32  * The {@link com.android.car.rotary.RotaryService} looks for instances of {@link FocusArea} in the
33  * view hierarchy when handling rotate and nudge actions. When receiving a rotation event ({@link
34  * android.car.input.RotaryEvent}), RotaryService will move the focus to another {@link View} that
35  * can take focus within the same FocusArea. When receiving a nudge event ({@link
36  * KeyEvent#KEYCODE_SYSTEM_NAVIGATION_UP}, {@link KeyEvent#KEYCODE_SYSTEM_NAVIGATION_DOWN}, {@link
37  * KeyEvent#KEYCODE_SYSTEM_NAVIGATION_LEFT}, or {@link KeyEvent#KEYCODE_SYSTEM_NAVIGATION_RIGHT}),
38  * RotaryService will move the focus to another view that can take focus in another (typically
39  * adjacent) FocusArea.
40  * <p>
41  * If enabled, FocusArea can draw highlights when one of its descendants has focus.
42  * <p>
43  * When creating a navigation block in the layout file, if you intend to use a LinearLayout as a
44  * container for that block, just use a FocusArea instead; otherwise wrap the block in a FocusArea.
45  * <p>
46  * DO NOT nest a FocusArea inside another FocusArea because it will result in undefined navigation
47  * behavior.
48  */
49 public class FocusArea extends LinearLayout {
50 
51     /** Whether the FocusArea's descendant has focus (the FocusArea itself is not focusable). */
52     private boolean mHasFocus;
53 
54     /**
55      * Whether to draw {@link #mForegroundHighlight} when one of the FocusArea's descendants has
56      * focus.
57      */
58     private boolean mEnableForegroundHighlight;
59 
60     /**
61      * Whether to draw {@link #mBackgroundHighlight} when one of the FocusArea's descendants has
62      * focus.
63      */
64     private boolean mEnableBackgroundHighlight;
65 
66     /**
67      * Highlight (typically outline of the FocusArea) drawn on top of the FocusArea and its
68      * descendants.
69      */
70     private Drawable mForegroundHighlight;
71 
72     /**
73      * Highlight (typically a solid or gradient shape) drawn on top of the FocusArea but behind its
74      * descendants.
75      */
76     private Drawable mBackgroundHighlight;
77 
78     /** The padding (in pixels) of the FocusArea highlight. */
79     private int mPaddingLeft;
80     private int mPaddingRight;
81     private int mPaddingTop;
82     private int mPaddingBottom;
83 
FocusArea(Context context)84     public FocusArea(Context context) {
85         super(context);
86         init();
87     }
88 
FocusArea(Context context, @Nullable AttributeSet attrs)89     public FocusArea(Context context, @Nullable AttributeSet attrs) {
90         super(context, attrs);
91         init();
92     }
93 
FocusArea(Context context, @Nullable AttributeSet attrs, int defStyleAttr)94     public FocusArea(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
95         super(context, attrs, defStyleAttr);
96         init();
97     }
98 
FocusArea(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)99     public FocusArea(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
100             int defStyleRes) {
101         super(context, attrs, defStyleAttr, defStyleRes);
102         init();
103     }
104 
init()105     private void init() {
106         mEnableForegroundHighlight = getContext().getResources().getBoolean(
107                 R.bool.car_ui_enable_focus_area_foreground_highlight);
108         mEnableBackgroundHighlight = getContext().getResources().getBoolean(
109                 R.bool.car_ui_enable_focus_area_background_highlight);
110         mForegroundHighlight = getContext().getResources().getDrawable(
111                 R.drawable.car_ui_focus_area_foreground_highlight, getContext().getTheme());
112         mBackgroundHighlight = getContext().getResources().getDrawable(
113                 R.drawable.car_ui_focus_area_background_highlight, getContext().getTheme());
114 
115         // Ensure that an AccessibilityNodeInfo is created for this view.
116         setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
117 
118         // By default all ViewGroup subclasses do not call their draw() and onDraw() methods. We
119         // should enable it since we override these methods.
120         setWillNotDraw(false);
121 
122         // Update highlight of the FocusArea when the focus of its descendants has changed.
123         getViewTreeObserver().addOnGlobalFocusChangeListener(
124                 (oldFocus, newFocus) -> {
125                     boolean hasFocus = hasFocus();
126                     if (mHasFocus != hasFocus) {
127                         mHasFocus = hasFocus;
128                         invalidate();
129                     }
130                 });
131     }
132 
133     @Override
onDraw(Canvas canvas)134     public void onDraw(Canvas canvas) {
135         super.onDraw(canvas);
136 
137         // Draw highlight on top of this FocusArea (including its background and content) but
138         // behind its children.
139         if (mEnableBackgroundHighlight && mHasFocus) {
140             mBackgroundHighlight.setBounds(
141                     mPaddingLeft + getScrollX(),
142                     mPaddingTop + getScrollY(),
143                     getScrollX() + getWidth() - mPaddingRight,
144                     getScrollY() + getHeight() - mPaddingBottom);
145             mBackgroundHighlight.draw(canvas);
146         }
147     }
148 
149     @Override
draw(Canvas canvas)150     public void draw(Canvas canvas) {
151         super.draw(canvas);
152 
153         // Draw highlight on top of this FocusArea (including its background and content) and its
154         // children (including background, content, focus highlight, etc).
155         if (mEnableForegroundHighlight && mHasFocus) {
156             mForegroundHighlight.setBounds(
157                     mPaddingLeft + getScrollX(),
158                     mPaddingTop + getScrollY(),
159                     getScrollX() + getWidth() - mPaddingRight,
160                     getScrollY() + getHeight() - mPaddingBottom);
161             mForegroundHighlight.draw(canvas);
162         }
163     }
164 
165     @Override
getAccessibilityClassName()166     public CharSequence getAccessibilityClassName() {
167         return FocusArea.class.getName();
168     }
169 
170     /** Sets the padding (in pixels) of the FocusArea highlight. */
setHighlightPadding(int left, int top, int right, int bottom)171     public void setHighlightPadding(int left, int top, int right, int bottom) {
172         if (mPaddingLeft == left && mPaddingTop == top && mPaddingRight == right
173                 && mPaddingBottom == bottom) {
174             return;
175         }
176         mPaddingLeft = left;
177         mPaddingTop = top;
178         mPaddingRight = right;
179         mPaddingBottom = bottom;
180         invalidate();
181     }
182 }
183