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 static com.android.car.ui.utils.RotaryConstants.I_FOCUS_AREA_CLASS_NAME;
20 
21 import android.content.Context;
22 import android.graphics.Canvas;
23 import android.graphics.Rect;
24 import android.os.Bundle;
25 import android.util.AttributeSet;
26 import android.view.FocusFinder;
27 import android.view.View;
28 import android.view.accessibility.AccessibilityNodeInfo;
29 import android.widget.LinearLayout;
30 
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.annotation.VisibleForTesting;
34 
35 /**
36  * A {@link LinearLayout} used as a navigation block for the rotary controller.
37  * <p>
38  * When creating a navigation block in the layout file, if you intend to use a LinearLayout as a
39  * container for that block, just use a FocusArea instead; otherwise use other {@link IFocusArea}
40  * implementations, such as {@link FrameFocusArea} which extends {@link android.widget.FrameLayout}.
41  * <p>
42  * DO NOT nest an IFocusArea inside another IFocusArea because it will result in undefined
43  * navigation behavior.
44  */
45 public class FocusArea extends LinearLayout implements IFocusArea {
46 
47     @NonNull
48     private final FocusAreaHelper mFocusAreaHelper;
49 
FocusArea(Context context)50     public FocusArea(Context context) {
51         this(context, null);
52     }
53 
FocusArea(Context context, @Nullable AttributeSet attrs)54     public FocusArea(Context context, @Nullable AttributeSet attrs) {
55         this(context, attrs, 0);
56     }
57 
FocusArea(Context context, @Nullable AttributeSet attrs, int defStyleAttr)58     public FocusArea(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
59         this(context, attrs, defStyleAttr, 0);
60     }
61 
FocusArea(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)62     public FocusArea(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
63             int defStyleRes) {
64         super(context, attrs, defStyleAttr, defStyleRes);
65         mFocusAreaHelper = new FocusAreaHelper(this, attrs);
66     }
67 
68     @Override
onFinishInflate()69     protected void onFinishInflate() {
70         super.onFinishInflate();
71         mFocusAreaHelper.onFinishInflate();
72     }
73 
74     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)75     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
76         super.onLayout(changed, left, top, right, bottom);
77         mFocusAreaHelper.onLayout();
78     }
79 
80     @Override
onAttachedToWindow()81     protected void onAttachedToWindow() {
82         super.onAttachedToWindow();
83         mFocusAreaHelper.onAttachedToWindow();
84     }
85 
86     @Override
onDetachedFromWindow()87     protected void onDetachedFromWindow() {
88         mFocusAreaHelper.onDetachedFromWindow();
89         super.onDetachedFromWindow();
90     }
91 
92     @Override
onWindowFocusChanged(boolean hasWindowFocus)93     public void onWindowFocusChanged(boolean hasWindowFocus) {
94         if (!mFocusAreaHelper.onWindowFocusChanged(hasWindowFocus)) {
95             super.onWindowFocusChanged(hasWindowFocus);
96         }
97     }
98 
99     @Override
performAccessibilityAction(int action, Bundle arguments)100     public boolean performAccessibilityAction(int action, Bundle arguments) {
101         if (mFocusAreaHelper.isFocusAreaAction(action)) {
102             return mFocusAreaHelper.performAccessibilityAction(action, arguments);
103         }
104         return super.performAccessibilityAction(action, arguments);
105     }
106 
107     @Override
onDraw(Canvas canvas)108     public void onDraw(Canvas canvas) {
109         super.onDraw(canvas);
110         mFocusAreaHelper.onDraw(canvas);
111     }
112 
113     @Override
draw(Canvas canvas)114     public void draw(Canvas canvas) {
115         super.draw(canvas);
116         mFocusAreaHelper.draw(canvas);
117     }
118 
119     @Override
getAccessibilityClassName()120     public CharSequence getAccessibilityClassName() {
121         return I_FOCUS_AREA_CLASS_NAME;
122     }
123 
124     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)125     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
126         super.onInitializeAccessibilityNodeInfo(info);
127         mFocusAreaHelper.onInitializeAccessibilityNodeInfo(info);
128     }
129 
130     @Override
onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)131     protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
132         if (isInTouchMode()) {
133             return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
134         }
135         return mFocusAreaHelper.onRequestFocusInDescendants();
136     }
137 
138     @Override
restoreDefaultFocus()139     public boolean restoreDefaultFocus() {
140         return mFocusAreaHelper.restoreDefaultFocus();
141     }
142 
143     /**
144      * @inheritDoc
145      * <p>
146      * When wrap-around is allowed, the search is restricted to descendants of this
147      * {@link FocusArea}.
148      */
149     @Override
focusSearch(View focused, int direction)150     public View focusSearch(View focused, int direction) {
151         if (mFocusAreaHelper.isWrapAround()) {
152             return FocusFinder.getInstance().findNextFocus(/* root= */ this, focused, direction);
153         }
154         return super.focusSearch(focused, direction);
155     }
156 
157     @VisibleForTesting
158     @Override
159     @NonNull
getHelper()160     public FocusAreaHelper getHelper() {
161         return mFocusAreaHelper;
162     }
163 
164     @Override
getDefaultFocusView()165     public View getDefaultFocusView() {
166         return mFocusAreaHelper.getDefaultFocusView();
167     }
168 
169     @Override
setDefaultFocus(@onNull View defaultFocus)170     public void setDefaultFocus(@NonNull View defaultFocus) {
171         mFocusAreaHelper.setDefaultFocus(defaultFocus);
172     }
173 
174     @Override
setHighlightPadding(int left, int top, int right, int bottom)175     public void setHighlightPadding(int left, int top, int right, int bottom) {
176         mFocusAreaHelper.setHighlightPadding(left, top, right, bottom);
177     }
178 
179     @Override
setBoundsOffset(int left, int top, int right, int bottom)180     public void setBoundsOffset(int left, int top, int right, int bottom) {
181         mFocusAreaHelper.setBoundsOffset(left, top, right, bottom);
182     }
183 
184     @Override
setWrapAround(boolean wrapAround)185     public void setWrapAround(boolean wrapAround) {
186         mFocusAreaHelper.setWrapAround(wrapAround);
187     }
188 
189     @Override
setNudgeShortcut(int direction, @Nullable View view)190     public void setNudgeShortcut(int direction, @Nullable View view) {
191         mFocusAreaHelper.setNudgeShortcut(direction, view);
192     }
193 
194     @Override
setNudgeTargetFocusArea(int direction, @Nullable IFocusArea target)195     public void setNudgeTargetFocusArea(int direction, @Nullable IFocusArea target) {
196         mFocusAreaHelper.setNudgeTargetFocusArea(direction, target);
197     }
198 
199     @Override
setDefaultFocusOverridesHistory(boolean override)200     public void setDefaultFocusOverridesHistory(boolean override) {
201         mFocusAreaHelper.setDefaultFocusOverridesHistory(override);
202     }
203 }
204