• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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.tv.menu;
18 
19 import android.content.Context;
20 import android.graphics.Rect;
21 import android.util.AttributeSet;
22 import android.util.Log;
23 import android.view.LayoutInflater;
24 import android.view.View;
25 import android.view.ViewParent;
26 import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
27 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
28 import android.view.accessibility.AccessibilityEvent;
29 import android.view.accessibility.AccessibilityManager;
30 import android.widget.FrameLayout;
31 import com.android.tv.menu.Menu.MenuShowReason;
32 import java.util.ArrayList;
33 import java.util.List;
34 
35 /** A view that represents TV main menu. */
36 public class MenuView extends FrameLayout implements IMenuView {
37     static final String TAG = MenuView.class.getSimpleName();
38     static final boolean DEBUG = false;
39 
40     private final LayoutInflater mLayoutInflater;
41     private final List<MenuRow> mMenuRows = new ArrayList<>();
42     private final List<MenuRowView> mMenuRowViews = new ArrayList<>();
43 
44     @MenuShowReason private int mShowReason = Menu.REASON_NONE;
45 
46     private final MenuLayoutManager mLayoutManager;
47 
MenuView(Context context)48     public MenuView(Context context) {
49         this(context, null, 0);
50     }
51 
MenuView(Context context, AttributeSet attrs)52     public MenuView(Context context, AttributeSet attrs) {
53         this(context, attrs, 0);
54     }
55 
MenuView(Context context, AttributeSet attrs, int defStyle)56     public MenuView(Context context, AttributeSet attrs, int defStyle) {
57         super(context, attrs, defStyle);
58         mLayoutInflater = LayoutInflater.from(context);
59         // Set hardware layer type for smooth animation of lots of views.
60         setLayerType(LAYER_TYPE_HARDWARE, null);
61         getViewTreeObserver()
62                 .addOnGlobalFocusChangeListener(
63                         new OnGlobalFocusChangeListener() {
64                             @Override
65                             public void onGlobalFocusChanged(View oldFocus, View newFocus) {
66                                 MenuRowView newParent = getParentMenuRowView(newFocus);
67                                 if (newParent != null) {
68                                     if (DEBUG) Log.d(TAG, "Focus changed to " + newParent);
69                                     // When the row is selected, the row view itself has the focus
70                                     // because the row
71                                     // is collapsed. To make the child of the row have the focus,
72                                     // requestFocus()
73                                     // should be called again after the row is expanded. It's done
74                                     // in
75                                     // setSelectedPosition().
76                                     setSelectedPositionSmooth(mMenuRowViews.indexOf(newParent));
77                                 }
78                             }
79                         });
80         mLayoutManager = new MenuLayoutManager(context, this);
81     }
82 
83     @Override
setMenuRows(List<MenuRow> menuRows)84     public void setMenuRows(List<MenuRow> menuRows) {
85         mMenuRows.clear();
86         mMenuRows.addAll(menuRows);
87         for (MenuRow row : menuRows) {
88             MenuRowView view = createMenuRowView(row);
89             mMenuRowViews.add(view);
90             addView(view);
91         }
92         mLayoutManager.setMenuRowsAndViews(mMenuRows, mMenuRowViews);
93     }
94 
createMenuRowView(MenuRow row)95     private MenuRowView createMenuRowView(MenuRow row) {
96         MenuRowView view = (MenuRowView) mLayoutInflater.inflate(row.getLayoutResId(), this, false);
97         view.onBind(row);
98         row.setMenuRowView(view);
99         return view;
100     }
101 
102     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)103     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
104         mLayoutManager.layout(left, top, right, bottom);
105     }
106 
107     @Override
onShow( @enuShowReason int reason, String rowIdToSelect, final Runnable runnableAfterShow)108     public void onShow(
109             @MenuShowReason int reason, String rowIdToSelect, final Runnable runnableAfterShow) {
110         if (DEBUG) {
111             Log.d(TAG, "onShow(reason=" + reason + ", rowIdToSelect=" + rowIdToSelect + ")");
112         }
113         mShowReason = reason;
114         if (getVisibility() == VISIBLE) {
115             if (rowIdToSelect != null) {
116                 int position = getItemPosition(rowIdToSelect);
117                 if (position >= 0) {
118                     MenuRowView rowView = mMenuRowViews.get(position);
119                     rowView.initialize(reason);
120                     setSelectedPosition(position);
121                 }
122             }
123             return;
124         }
125         initializeChildren();
126         update(true);
127         int position = getItemPosition(rowIdToSelect);
128         if (position == -1 || !mMenuRows.get(position).isVisible()) {
129             // Channels row is always visible.
130             position = getItemPosition(ChannelsRow.ID);
131         }
132         setSelectedPosition(position);
133         // Change the visibility as late as possible to avoid the unnecessary animation.
134         setVisibility(VISIBLE);
135         // Make the selected row have the focus.
136         requestFocus();
137         if (runnableAfterShow != null) {
138             getViewTreeObserver()
139                     .addOnGlobalLayoutListener(
140                             new OnGlobalLayoutListener() {
141                                 @Override
142                                 public void onGlobalLayout() {
143                                     getViewTreeObserver().removeOnGlobalLayoutListener(this);
144                                     // Start show animation after layout finishes for smooth
145                                     // animation because the
146                                     // layout can take long time.
147                                     runnableAfterShow.run();
148                                 }
149                             });
150         }
151         mLayoutManager.onMenuShow();
152     }
153 
154     @Override
onHide()155     public void onHide() {
156         if (getVisibility() == GONE) {
157             return;
158         }
159         mLayoutManager.onMenuHide();
160         setVisibility(GONE);
161     }
162 
163     @Override
isVisible()164     public boolean isVisible() {
165         return getVisibility() == VISIBLE;
166     }
167 
168     @Override
update(boolean menuActive)169     public boolean update(boolean menuActive) {
170         if (menuActive) {
171             for (MenuRow row : mMenuRows) {
172                 row.update();
173             }
174             mLayoutManager.onMenuRowUpdated();
175             return true;
176         }
177         return false;
178     }
179 
180     @Override
update(String rowId, boolean menuActive)181     public boolean update(String rowId, boolean menuActive) {
182         if (menuActive) {
183             MenuRow row = getMenuRow(rowId);
184             if (row != null) {
185                 row.update();
186                 mLayoutManager.onMenuRowUpdated();
187                 return true;
188             }
189         }
190         return false;
191     }
192 
193     @Override
onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)194     protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
195         int selectedPosition = mLayoutManager.getSelectedPosition();
196         // When the menu shows up, the selected row should have focus.
197         AccessibilityManager mAccessibilityManager =
198                 getContext().getSystemService(AccessibilityManager.class);
199         if (selectedPosition >= 0 && selectedPosition < mMenuRowViews.size()) {
200             if(mAccessibilityManager.isEnabled())
201                 mMenuRowViews.get(selectedPosition)
202                         .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
203             return mMenuRowViews.get(selectedPosition).requestFocus();
204         }
205         return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
206     }
207 
208     @Override
focusableViewAvailable(View v)209     public void focusableViewAvailable(View v) {
210         // Workaround of b/30788222 and b/32074688.
211         // The re-layout of RecyclerView gives the focus to the card view even when the menu is not
212         // visible. Don't report focusable view when the menu is not visible.
213         if (getVisibility() == VISIBLE) {
214             super.focusableViewAvailable(v);
215         }
216     }
217 
setSelectedPosition(int position)218     private void setSelectedPosition(int position) {
219         mLayoutManager.setSelectedPosition(position);
220     }
221 
setSelectedPositionSmooth(int position)222     private void setSelectedPositionSmooth(int position) {
223         mLayoutManager.setSelectedPositionSmooth(position);
224     }
225 
initializeChildren()226     private void initializeChildren() {
227         for (MenuRowView view : mMenuRowViews) {
228             view.initialize(mShowReason);
229         }
230     }
231 
getMenuRow(String rowId)232     private MenuRow getMenuRow(String rowId) {
233         for (MenuRow item : mMenuRows) {
234             if (rowId.equals(item.getId())) {
235                 return item;
236             }
237         }
238         return null;
239     }
240 
getItemPosition(String rowIdToSelect)241     private int getItemPosition(String rowIdToSelect) {
242         if (rowIdToSelect == null) {
243             return -1;
244         }
245         int position = 0;
246         for (MenuRow item : mMenuRows) {
247             if (rowIdToSelect.equals(item.getId())) {
248                 return position;
249             }
250             ++position;
251         }
252         return -1;
253     }
254 
255     @Override
focusSearch(View focused, int direction)256     public View focusSearch(View focused, int direction) {
257         // The bounds of the views move and overlap with each other during the animation. In this
258         // situation, the framework can't perform the correct focus navigation. So the menu view
259         // should search by itself.
260         if (direction == View.FOCUS_UP || direction == View.FOCUS_DOWN) {
261             return getUpDownFocus(focused, direction);
262         }
263         return super.focusSearch(focused, direction);
264     }
265 
getUpDownFocus(View focused, int direction)266     private View getUpDownFocus(View focused, int direction) {
267         View newView = super.focusSearch(focused, direction);
268         MenuRowView oldfocusedParent = getParentMenuRowView(focused);
269         MenuRowView newFocusedParent = getParentMenuRowView(newView);
270         int selectedPosition = mLayoutManager.getSelectedPosition();
271         int start, delta;
272         if (direction == View.FOCUS_UP) {
273             start = selectedPosition - 1;
274             delta = -1;
275         } else {
276             start = selectedPosition + 1;
277             delta = 1;
278         }
279         if (newFocusedParent != oldfocusedParent) {
280             // The focus leaves from the current menu row view.
281             int count = mMenuRowViews.size();
282             int i = start;
283             while (i < count && i >= 0) {
284                 MenuRowView view = mMenuRowViews.get(i);
285                 if (view.getVisibility() == View.VISIBLE) {
286                     mMenuRows.get(i).setIsReselected(false);
287                     return view;
288                 }
289                 i += delta;
290             }
291         }
292         mMenuRows.get(selectedPosition).setIsReselected(true);
293         return newView;
294     }
295 
getParentMenuRowView(View view)296     private MenuRowView getParentMenuRowView(View view) {
297         if (view == null) {
298             return null;
299         }
300         ViewParent parent = view.getParent();
301         if (parent == MenuView.this) {
302             return (MenuRowView) view;
303         }
304         if (parent instanceof View) {
305             return getParentMenuRowView((View) parent);
306         }
307         return null;
308     }
309 }
310