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.content.res.Resources;
21 import android.graphics.Rect;
22 import android.support.annotation.NonNull;
23 import android.util.AttributeSet;
24 import android.util.Log;
25 import android.util.TypedValue;
26 import android.view.View;
27 import android.view.ViewGroup;
28 import android.widget.LinearLayout;
29 import android.widget.TextView;
30 
31 import com.android.tv.R;
32 import com.android.tv.menu.Menu.MenuShowReason;
33 
34 public abstract class MenuRowView extends LinearLayout {
35     private static final String TAG = "MenuRowView";
36     private static final boolean DEBUG = false;
37 
38     private TextView mTitleView;
39     private View mContentsView;
40 
41     private final float mTitleViewAlphaDeselected;
42     private final float mTitleViewScaleSelected;
43 
44     /**
45      * The lastly focused view. It is used to keep the focus while navigating the menu rows and
46      * reset when the menu is popped up.
47      */
48     private View mLastFocusView;
49     private MenuRow mRow;
50 
51     private final OnFocusChangeListener mOnFocusChangeListener = new OnFocusChangeListener() {
52         @Override
53         public void onFocusChange(View v, boolean hasFocus) {
54             onChildFocusChange(v, hasFocus);
55         }
56     };
57 
58     /**
59      * Returns the alpha value of the title view when it's deselected.
60      */
getTitleViewAlphaDeselected()61     public float getTitleViewAlphaDeselected() {
62         return mTitleViewAlphaDeselected;
63     }
64 
65     /**
66      * Returns the scale value of the title view when it's selected.
67      */
getTitleViewScaleSelected()68     public float getTitleViewScaleSelected() {
69         return mTitleViewScaleSelected;
70     }
71 
MenuRowView(Context context)72     public MenuRowView(Context context) {
73         this(context, null);
74     }
75 
MenuRowView(Context context, AttributeSet attrs)76     public MenuRowView(Context context, AttributeSet attrs) {
77         this(context, attrs, 0);
78     }
79 
MenuRowView(Context context, AttributeSet attrs, int defStyleAttr)80     public MenuRowView(Context context, AttributeSet attrs, int defStyleAttr) {
81         this(context, attrs, defStyleAttr, 0);
82     }
83 
MenuRowView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)84     public MenuRowView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
85         super(context, attrs, defStyleAttr, defStyleRes);
86         Resources res = context.getResources();
87         TypedValue outValue = new TypedValue();
88         res.getValue(R.dimen.menu_row_title_alpha_deselected, outValue, true);
89         mTitleViewAlphaDeselected = outValue.getFloat();
90         float textSizeSelected =
91                 res.getDimensionPixelSize(R.dimen.menu_row_title_text_size_selected);
92         float textSizeDeselected =
93                 res.getDimensionPixelSize(R.dimen.menu_row_title_text_size_deselected);
94         mTitleViewScaleSelected = textSizeSelected / textSizeDeselected;
95     }
96 
97     @Override
onFinishInflate()98     protected void onFinishInflate() {
99         super.onFinishInflate();
100         mTitleView = (TextView) findViewById(R.id.title);
101         mContentsView = findViewById(getContentsViewId());
102         if (mContentsView.isFocusable()) {
103             mContentsView.setOnFocusChangeListener(mOnFocusChangeListener);
104         }
105         if (mContentsView instanceof ViewGroup) {
106             setOnFocusChangeListenerToChildren((ViewGroup) mContentsView);
107         }
108         // Make contents view invisible in order that the view participates in the initial layout.
109         // The visibility is set to GONE after the first layout finishes.
110         // If not, we can't see the contents view animation for the first time it is shown.
111         // TODO: Find a better way to resolve this issue.
112         mContentsView.setVisibility(INVISIBLE);
113     }
114 
setOnFocusChangeListenerToChildren(ViewGroup parent)115     private void setOnFocusChangeListenerToChildren(ViewGroup parent) {
116         int childCount = parent.getChildCount();
117         for (int i = 0; i < childCount; ++i) {
118             View child = parent.getChildAt(i);
119             if (child.isFocusable()) {
120                 child.setOnFocusChangeListener(mOnFocusChangeListener);
121             }
122             if (child instanceof ViewGroup) {
123                 setOnFocusChangeListenerToChildren((ViewGroup) child);
124             }
125         }
126     }
127 
getContentsViewId()128     abstract protected int getContentsViewId();
129 
130     /**
131      * Returns the title view.
132      */
getTitleView()133     public final TextView getTitleView() {
134         return mTitleView;
135     }
136 
137     /**
138      * Returns the contents view.
139      */
getContentsView()140     public final View getContentsView() {
141         return mContentsView;
142     }
143 
144     /**
145      * Initialize this view. e.g. Set the initial selection.
146      * This method is called when the main menu is visible.
147      * Subclass of {@link MenuRowView} should override this to set correct mLastFocusView.
148      *
149      * @param reason A reason why this is initialized. See {@link MenuShowReason}
150      */
initialize(@enuShowReason int reason)151     public void initialize(@MenuShowReason int reason) {
152         mLastFocusView = null;
153     }
154 
getMenu()155     protected Menu getMenu() {
156         return mRow == null ? null : mRow.getMenu();
157     }
158 
onBind(MenuRow row)159     public void onBind(MenuRow row) {
160         if (DEBUG) Log.d(TAG, "onBind: row=" + row);
161         mRow = row;
162         mTitleView.setText(row.getTitle());
163     }
164 
165     @Override
onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)166     protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
167         // Expand view here so initial focused item can be shown.
168         return getInitialFocusView().requestFocus();
169     }
170 
171     @NonNull
getInitialFocusView()172     private View getInitialFocusView() {
173         if (mLastFocusView == null) {
174             return mContentsView;
175         }
176         return mLastFocusView;
177     }
178 
179     /**
180      * Sets the view which needs to have focus when this row appears.
181      * Subclasses should call this in {@link #initialize} if needed.
182      */
setInitialFocusView(@onNull View v)183     protected void setInitialFocusView(@NonNull View v) {
184         mLastFocusView = v;
185     }
186 
187     /**
188      * Called when the focus of a child view is changed.
189      * The inherited class should override this method instead of calling
190      * {@link android.view.View#setOnFocusChangeListener(android.view.View.OnFocusChangeListener)}.
191      */
onChildFocusChange(View v, boolean hasFocus)192     protected void onChildFocusChange(View v, boolean hasFocus) {
193         if (hasFocus) {
194             mLastFocusView = v;
195         }
196     }
197 
198     /**
199      * Returns the ID of row object bound to this view.
200      */
getRowId()201     public String getRowId() {
202         return mRow == null ? null : mRow.getId();
203     }
204 
205     /**
206      * Called when this row is selected.
207      *
208      * @param showTitle If {@code true}, the title is not hidden immediately after the row is
209      * selected even though hideTitleWhenSelected() is {@code true}.
210      */
onSelected(boolean showTitle)211     public void onSelected(boolean showTitle) {
212         if (mRow.hideTitleWhenSelected() && !showTitle) {
213             // Title view should participate in the layout even though it is not visible.
214             mTitleView.setVisibility(INVISIBLE);
215         } else {
216             mTitleView.setVisibility(VISIBLE);
217             mTitleView.setAlpha(1.0f);
218             mTitleView.setScaleX(mTitleViewScaleSelected);
219             mTitleView.setScaleY(mTitleViewScaleSelected);
220         }
221         // Making the content view visible will cause it to set a focus item
222         // So we store mLastFocusView and reset it
223         View lastFocusView = mLastFocusView;
224         mContentsView.setVisibility(VISIBLE);
225         mLastFocusView = lastFocusView;
226     }
227 
228     /**
229      * Called when this row is deselected.
230      */
onDeselected()231     public void onDeselected() {
232         mTitleView.setVisibility(VISIBLE);
233         mTitleView.setAlpha(mTitleViewAlphaDeselected);
234         mTitleView.setScaleX(1.0f);
235         mTitleView.setScaleY(1.0f);
236         mContentsView.setVisibility(GONE);
237     }
238 
239     /**
240      * Returns the preferred height of the contents view. The top/bottom padding is excluded.
241      */
getPreferredContentsHeight()242     public int getPreferredContentsHeight() {
243         return mRow.getHeight();
244     }
245 }
246