blob: add4a774ab84e22c802dd1f86ef7bf782969ef0d [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.menu;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewParent;
import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.FrameLayout;
import com.android.tv.menu.Menu.MenuShowReason;
import java.util.ArrayList;
import java.util.List;
/** A view that represents TV main menu. */
public class MenuView extends FrameLayout implements IMenuView {
static final String TAG = MenuView.class.getSimpleName();
static final boolean DEBUG = false;
private final LayoutInflater mLayoutInflater;
private final List<MenuRow> mMenuRows = new ArrayList<>();
private final List<MenuRowView> mMenuRowViews = new ArrayList<>();
@MenuShowReason private int mShowReason = Menu.REASON_NONE;
private final MenuLayoutManager mLayoutManager;
public MenuView(Context context) {
this(context, null, 0);
}
public MenuView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MenuView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mLayoutInflater = LayoutInflater.from(context);
// Set hardware layer type for smooth animation of lots of views.
setLayerType(LAYER_TYPE_HARDWARE, null);
getViewTreeObserver()
.addOnGlobalFocusChangeListener(
new OnGlobalFocusChangeListener() {
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
MenuRowView newParent = getParentMenuRowView(newFocus);
if (newParent != null) {
if (DEBUG) Log.d(TAG, "Focus changed to " + newParent);
// When the row is selected, the row view itself has the focus
// because the row
// is collapsed. To make the child of the row have the focus,
// requestFocus()
// should be called again after the row is expanded. It's done
// in
// setSelectedPosition().
setSelectedPositionSmooth(mMenuRowViews.indexOf(newParent));
}
}
});
mLayoutManager = new MenuLayoutManager(context, this);
}
@Override
public void setMenuRows(List<MenuRow> menuRows) {
mMenuRows.clear();
mMenuRows.addAll(menuRows);
for (MenuRow row : menuRows) {
MenuRowView view = createMenuRowView(row);
mMenuRowViews.add(view);
addView(view);
}
mLayoutManager.setMenuRowsAndViews(mMenuRows, mMenuRowViews);
}
private MenuRowView createMenuRowView(MenuRow row) {
MenuRowView view = (MenuRowView) mLayoutInflater.inflate(row.getLayoutResId(), this, false);
view.onBind(row);
row.setMenuRowView(view);
return view;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
mLayoutManager.layout(left, top, right, bottom);
}
@Override
public void onShow(
@MenuShowReason int reason, String rowIdToSelect, final Runnable runnableAfterShow) {
if (DEBUG) {
Log.d(TAG, "onShow(reason=" + reason + ", rowIdToSelect=" + rowIdToSelect + ")");
}
mShowReason = reason;
if (getVisibility() == VISIBLE) {
if (rowIdToSelect != null) {
int position = getItemPosition(rowIdToSelect);
if (position >= 0) {
MenuRowView rowView = mMenuRowViews.get(position);
rowView.initialize(reason);
setSelectedPosition(position);
}
}
return;
}
initializeChildren();
update(true);
int position = getItemPosition(rowIdToSelect);
if (position == -1 || !mMenuRows.get(position).isVisible()) {
// Channels row is always visible.
position = getItemPosition(ChannelsRow.ID);
}
setSelectedPosition(position);
// Change the visibility as late as possible to avoid the unnecessary animation.
setVisibility(VISIBLE);
// Make the selected row have the focus.
requestFocus();
if (runnableAfterShow != null) {
getViewTreeObserver()
.addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
// Start show animation after layout finishes for smooth
// animation because the
// layout can take long time.
runnableAfterShow.run();
}
});
}
mLayoutManager.onMenuShow();
}
@Override
public void onHide() {
if (getVisibility() == GONE) {
return;
}
mLayoutManager.onMenuHide();
setVisibility(GONE);
}
@Override
public boolean isVisible() {
return getVisibility() == VISIBLE;
}
@Override
public boolean update(boolean menuActive) {
if (menuActive) {
for (MenuRow row : mMenuRows) {
row.update();
}
mLayoutManager.onMenuRowUpdated();
return true;
}
return false;
}
@Override
public boolean update(String rowId, boolean menuActive) {
if (menuActive) {
MenuRow row = getMenuRow(rowId);
if (row != null) {
row.update();
mLayoutManager.onMenuRowUpdated();
return true;
}
}
return false;
}
@Override
protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
int selectedPosition = mLayoutManager.getSelectedPosition();
// When the menu shows up, the selected row should have focus.
if (selectedPosition >= 0 && selectedPosition < mMenuRowViews.size()) {
return mMenuRowViews.get(selectedPosition).requestFocus();
}
return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
}
@Override
public void focusableViewAvailable(View v) {
// Workaround of b/30788222 and b/32074688.
// The re-layout of RecyclerView gives the focus to the card view even when the menu is not
// visible. Don't report focusable view when the menu is not visible.
if (getVisibility() == VISIBLE) {
super.focusableViewAvailable(v);
}
}
private void setSelectedPosition(int position) {
mLayoutManager.setSelectedPosition(position);
}
private void setSelectedPositionSmooth(int position) {
mLayoutManager.setSelectedPositionSmooth(position);
}
private void initializeChildren() {
for (MenuRowView view : mMenuRowViews) {
view.initialize(mShowReason);
}
}
private MenuRow getMenuRow(String rowId) {
for (MenuRow item : mMenuRows) {
if (rowId.equals(item.getId())) {
return item;
}
}
return null;
}
private int getItemPosition(String rowIdToSelect) {
if (rowIdToSelect == null) {
return -1;
}
int position = 0;
for (MenuRow item : mMenuRows) {
if (rowIdToSelect.equals(item.getId())) {
return position;
}
++position;
}
return -1;
}
@Override
public View focusSearch(View focused, int direction) {
// The bounds of the views move and overlap with each other during the animation. In this
// situation, the framework can't perform the correct focus navigation. So the menu view
// should search by itself.
if (direction == View.FOCUS_UP || direction == View.FOCUS_DOWN) {
return getUpDownFocus(focused, direction);
}
return super.focusSearch(focused, direction);
}
private View getUpDownFocus(View focused, int direction) {
View newView = super.focusSearch(focused, direction);
MenuRowView oldfocusedParent = getParentMenuRowView(focused);
MenuRowView newFocusedParent = getParentMenuRowView(newView);
int selectedPosition = mLayoutManager.getSelectedPosition();
int start, delta;
if (direction == View.FOCUS_UP) {
start = selectedPosition - 1;
delta = -1;
} else {
start = selectedPosition + 1;
delta = 1;
}
if (newFocusedParent != oldfocusedParent) {
// The focus leaves from the current menu row view.
int count = mMenuRowViews.size();
int i = start;
while (i < count && i >= 0) {
MenuRowView view = mMenuRowViews.get(i);
if (view.getVisibility() == View.VISIBLE) {
mMenuRows.get(i).setIsReselected(false);
return view;
}
i += delta;
}
}
mMenuRows.get(selectedPosition).setIsReselected(true);
return newView;
}
private MenuRowView getParentMenuRowView(View view) {
if (view == null) {
return null;
}
ViewParent parent = view.getParent();
if (parent == MenuView.this) {
return (MenuRowView) view;
}
if (parent instanceof View) {
return getParentMenuRowView((View) parent);
}
return null;
}
}