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