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