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