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