/* * 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.content.res.Resources; import android.graphics.Rect; import android.support.annotation.NonNull; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.widget.LinearLayout; import android.widget.TextView; import com.android.tv.R; import com.android.tv.menu.Menu.MenuShowReason; public abstract class MenuRowView extends LinearLayout { private static final String TAG = "MenuRowView"; private static final boolean DEBUG = false; private TextView mTitleView; private View mContentsView; private final float mTitleViewAlphaDeselected; private final float mTitleViewScaleSelected; /** * The lastly focused view. It is used to keep the focus while navigating the menu rows and * reset when the menu is popped up. */ private View mLastFocusView; private MenuRow mRow; private final OnFocusChangeListener mOnFocusChangeListener = new OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { onChildFocusChange(v, hasFocus); } }; /** Returns the alpha value of the title view when it's deselected. */ public float getTitleViewAlphaDeselected() { return mTitleViewAlphaDeselected; } /** Returns the scale value of the title view when it's selected. */ public float getTitleViewScaleSelected() { return mTitleViewScaleSelected; } public MenuRowView(Context context) { this(context, null); } public MenuRowView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MenuRowView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public MenuRowView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); Resources res = context.getResources(); TypedValue outValue = new TypedValue(); res.getValue(R.dimen.menu_row_title_alpha_deselected, outValue, true); mTitleViewAlphaDeselected = outValue.getFloat(); float textSizeSelected = res.getDimensionPixelSize(R.dimen.menu_row_title_text_size_selected); float textSizeDeselected = res.getDimensionPixelSize(R.dimen.menu_row_title_text_size_deselected); mTitleViewScaleSelected = textSizeSelected / textSizeDeselected; this.setAccessibilityDelegate( new AccessibilityDelegate() { @Override public void sendAccessibilityEvent(View host, int eventType) { super.sendAccessibilityEvent(host, eventType); if ((eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED || eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) && !mRow.isReselected()) { requestChildFocus(); } } } ); } @Override protected void onFinishInflate() { super.onFinishInflate(); mTitleView = (TextView) findViewById(R.id.title); mContentsView = findViewById(getContentsViewId()); if (mContentsView.isFocusable()) { mContentsView.setOnFocusChangeListener(mOnFocusChangeListener); } if (mContentsView instanceof ViewGroup) { setOnFocusChangeListenerToChildren((ViewGroup) mContentsView); } // Make contents view invisible in order that the view participates in the initial layout. // The visibility is set to GONE after the first layout finishes. // If not, we can't see the contents view animation for the first time it is shown. // TODO: Find a better way to resolve this issue. mContentsView.setVisibility(INVISIBLE); } private void setOnFocusChangeListenerToChildren(ViewGroup parent) { int childCount = parent.getChildCount(); for (int i = 0; i < childCount; ++i) { View child = parent.getChildAt(i); if (child.isFocusable()) { child.setOnFocusChangeListener(mOnFocusChangeListener); } if (child instanceof ViewGroup) { setOnFocusChangeListenerToChildren((ViewGroup) child); } } } protected abstract int getContentsViewId(); /** Returns the title view. */ public final TextView getTitleView() { return mTitleView; } /** Returns the contents view. */ public final View getContentsView() { return mContentsView; } /** * Initialize this view. e.g. Set the initial selection. This method is called when the main * menu is visible. Subclass of {@link MenuRowView} should override this to set correct * mLastFocusView. * * @param reason A reason why this is initialized. See {@link MenuShowReason} */ public void initialize(@MenuShowReason int reason) { mLastFocusView = null; } protected Menu getMenu() { return mRow == null ? null : mRow.getMenu(); } public void onBind(MenuRow row) { if (DEBUG) Log.d(TAG, "onBind: row=" + row); mRow = row; mTitleView.setText(row.getTitle()); } @Override protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { // Expand view here so initial focused item can be shown. return getInitialFocusView().requestFocus(); } @NonNull private View getInitialFocusView() { if (mLastFocusView == null) { return mContentsView; } return mLastFocusView; } /** * Sets the view which needs to have focus when this row appears. Subclasses should call this in * {@link #initialize} if needed. */ protected void setInitialFocusView(@NonNull View v) { mLastFocusView = v; } /** Subclasses should implement this to request focus on child. */ protected abstract void requestChildFocus(); /** * Called when the focus of a child view is changed. The inherited class should override this * method instead of calling {@link * android.view.View#setOnFocusChangeListener(android.view.View.OnFocusChangeListener)}. */ protected void onChildFocusChange(View v, boolean hasFocus) { if (hasFocus) { mLastFocusView = v; } } /** Returns the ID of row object bound to this view. */ public String getRowId() { return mRow == null ? null : mRow.getId(); } /** * Called when this row is selected. * * @param showTitle If {@code true}, the title is not hidden immediately after the row is * selected even though hideTitleWhenSelected() is {@code true}. */ public void onSelected(boolean showTitle) { if (mRow.hideTitleWhenSelected() && !showTitle) { // Title view should participate in the layout even though it is not visible. mTitleView.setVisibility(INVISIBLE); } else { mTitleView.setVisibility(VISIBLE); mTitleView.setAlpha(1.0f); mTitleView.setScaleX(mTitleViewScaleSelected); mTitleView.setScaleY(mTitleViewScaleSelected); } // Making the content view visible will cause it to set a focus item // So we store mLastFocusView and reset it View lastFocusView = mLastFocusView; mContentsView.setVisibility(VISIBLE); mLastFocusView = lastFocusView; } /** Called when this row is deselected. */ public void onDeselected() { mTitleView.setVisibility(VISIBLE); mTitleView.setAlpha(mTitleViewAlphaDeselected); mTitleView.setScaleX(1.0f); mTitleView.setScaleY(1.0f); mContentsView.setVisibility(GONE); } /** Returns the preferred height of the contents view. The top/bottom padding is excluded. */ public int getPreferredContentsHeight() { return mRow.getHeight(); } }