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
17package com.android.tv.menu;
18
19import android.content.Context;
20import android.graphics.Rect;
21import android.util.AttributeSet;
22import android.util.Log;
23import android.view.LayoutInflater;
24import android.view.View;
25import android.view.ViewParent;
26import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
27import android.widget.FrameLayout;
28
29import com.android.tv.menu.Menu.MenuShowReason;
30
31import java.util.ArrayList;
32import java.util.List;
33
34/**
35 * A view that represents TV main menu.
36 */
37public 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