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