1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 package androidx.leanback.widget;
15 
16 import android.content.Context;
17 import android.graphics.Rect;
18 import android.util.AttributeSet;
19 import android.view.KeyEvent;
20 import android.view.View;
21 import android.widget.FrameLayout;
22 
23 import org.jspecify.annotations.NonNull;
24 import org.jspecify.annotations.Nullable;
25 
26 /**
27  * A ViewGroup for managing focus behavior between overlapping views.
28  */
29 public class BrowseFrameLayout extends FrameLayout {
30 
31     /**
32      * Interface for selecting a focused view in a BrowseFrameLayout when the system focus finder
33      * couldn't find a view to focus.
34      */
35     public interface OnFocusSearchListener {
36         /**
37          * Returns the view where focus should be requested given the current focused view and
38          * the direction of focus search.
39          */
onFocusSearch(@ullable View focused, int direction)40         @Nullable View onFocusSearch(@Nullable View focused, int direction);
41     }
42 
43     /**
44      * Interface for managing child focus in a BrowseFrameLayout.
45      */
46     public interface OnChildFocusListener {
47         /**
48          * See {@link android.view.ViewGroup#onRequestFocusInDescendants(
49          * int, android.graphics.Rect)}.
50          * @return True if handled by listener, otherwise returns {@link
51          * android.view.ViewGroup#onRequestFocusInDescendants(int, android.graphics.Rect)}.
52          */
onRequestFocusInDescendants(int direction, @Nullable Rect previouslyFocusedRect)53         boolean onRequestFocusInDescendants(int direction,
54                 @Nullable Rect previouslyFocusedRect);
55         /**
56          * See {@link android.view.ViewGroup#requestChildFocus(
57          * android.view.View, android.view.View)}.
58          */
onRequestChildFocus(@ullable View child, @Nullable View focused)59         void onRequestChildFocus(@Nullable View child, @Nullable View focused);
60     }
61 
BrowseFrameLayout(@onNull Context context)62     public BrowseFrameLayout(@NonNull Context context) {
63         this(context, null, 0);
64     }
65 
BrowseFrameLayout(@onNull Context context, @Nullable AttributeSet attrs)66     public BrowseFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
67         this(context, attrs, 0);
68     }
69 
BrowseFrameLayout(@onNull Context context, @Nullable AttributeSet attrs, int defStyle)70     public BrowseFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
71         super(context, attrs, defStyle);
72     }
73 
74     private OnFocusSearchListener mListener;
75     private OnChildFocusListener mOnChildFocusListener;
76     private OnKeyListener mOnDispatchKeyListener;
77 
78     /**
79      * Sets a {@link OnFocusSearchListener}.
80      */
setOnFocusSearchListener(@ullable OnFocusSearchListener listener)81     public void setOnFocusSearchListener(@Nullable OnFocusSearchListener listener) {
82         mListener = listener;
83     }
84 
85     /**
86      * Returns the {@link OnFocusSearchListener}.
87      */
getOnFocusSearchListener()88     public @Nullable OnFocusSearchListener getOnFocusSearchListener() {
89         return mListener;
90     }
91 
92     /**
93      * Sets a {@link OnChildFocusListener}.
94      */
setOnChildFocusListener(@ullable OnChildFocusListener listener)95     public void setOnChildFocusListener(@Nullable OnChildFocusListener listener) {
96         mOnChildFocusListener = listener;
97     }
98 
99     /**
100      * Returns the {@link OnChildFocusListener}.
101      */
getOnChildFocusListener()102     public @Nullable OnChildFocusListener getOnChildFocusListener() {
103         return mOnChildFocusListener;
104     }
105 
106     @Override
onRequestFocusInDescendants(int direction, @Nullable Rect previouslyFocusedRect)107     protected boolean onRequestFocusInDescendants(int direction,
108             @Nullable Rect previouslyFocusedRect) {
109         if (mOnChildFocusListener != null) {
110             if (mOnChildFocusListener.onRequestFocusInDescendants(direction,
111                     previouslyFocusedRect)) {
112                 return true;
113             }
114         }
115         return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
116     }
117 
118     @Override
focusSearch(@ullable View focused, int direction)119     public View focusSearch(@Nullable View focused, int direction) {
120         if (mListener != null) {
121             View view = mListener.onFocusSearch(focused, direction);
122             if (view != null) {
123                 return view;
124             }
125         }
126         return super.focusSearch(focused, direction);
127     }
128 
129     @Override
requestChildFocus(View child, View focused)130     public void requestChildFocus(View child, View focused) {
131         if (mOnChildFocusListener != null) {
132             mOnChildFocusListener.onRequestChildFocus(child, focused);
133         }
134         super.requestChildFocus(child, focused);
135     }
136 
137     @Override
dispatchKeyEvent(KeyEvent event)138     public boolean dispatchKeyEvent(KeyEvent event) {
139         boolean consumed = super.dispatchKeyEvent(event);
140         if (mOnDispatchKeyListener != null) {
141             if (!consumed) {
142                 return mOnDispatchKeyListener.onKey(getRootView(), event.getKeyCode(), event);
143             }
144         }
145         return consumed;
146     }
147 
148     /**
149      * Sets the {@link android.view.View.OnKeyListener} on this view. This listener would fire
150      * only for unhandled {@link KeyEvent}s. We need to provide an external key listener to handle
151      * back button clicks when we are in full screen video mode because
152      * {@link View#setOnKeyListener(OnKeyListener)} doesn't fire as the focus is not on this view.
153      */
setOnDispatchKeyListener(@ullable OnKeyListener listener)154     public void setOnDispatchKeyListener(@Nullable OnKeyListener listener) {
155         this.mOnDispatchKeyListener = listener;
156     }
157 }
158