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