/* * Copyright (C) 2011 Google Inc. * * 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.example.google.tv.leftnavbar; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.app.ActionBar; import android.content.Context; import android.content.res.Resources; import android.util.AttributeSet; import android.view.FocusFinder; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import java.util.ArrayList; /** * Left navigation panel hosting various controls such as tabs. */ public class LeftNavView extends LinearLayout { private final HomeDisplay mHome; private final TabDisplay mTabs; private final OptionsDisplay mOptions; private final SpinnerDisplay mSpinner; private final int mWidthCollapsed; private final int mWidthExpanded; private final int mApparentWidthCollapsed; private final int mApparentWidthExpanded; private final VisibilityController mVisibilityController; private final int mAnimationDuration; private int mDisplayOptions; private int mNavigationMode; private boolean mExpanded; private boolean mAnimationsEnabled; private ValueAnimator mWidthAnimator; public LeftNavView(Context context, AttributeSet attrs) { super(context, attrs); mVisibilityController = new VisibilityController(this); LayoutInflater.from(context).inflate(R.layout.left_nav, this, true); setOrientation(VERTICAL); mHome = new HomeDisplay(context, this, null).setVisible(false); mTabs = new TabDisplay(context, this, null).setVisible(false); mOptions = new OptionsDisplay(context, this, null).setVisible(false); mSpinner = new SpinnerDisplay(context, this, null).setVisible(false); Resources res = context.getResources(); mWidthCollapsed = res.getDimensionPixelSize( R.dimen.left_nav_collapsed_width); mWidthExpanded = res.getDimensionPixelSize( R.dimen.left_nav_expanded_width); mApparentWidthCollapsed = res.getDimensionPixelSize( R.dimen.left_nav_collapsed_apparent_width); mApparentWidthExpanded = res.getDimensionPixelSize( R.dimen.left_nav_expanded_apparent_width); mAnimationDuration = res.getInteger( android.R.integer.config_shortAnimTime); mNavigationMode = ActionBar.NAVIGATION_MODE_STANDARD; setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); } @Override protected void onFinishInflate() { super.onFinishInflate(); // Add header / footer views. addView(mHome.getView(), 0); // Central section falls here. addView(mOptions.getView(), 2); // Add views to the central section. ViewGroup main = getMainSection(); main.addView(mTabs.getView()); main.addView(mSpinner.getView()); } private ViewGroup getMainSection() { return (ViewGroup) findViewById(R.id.main); } public boolean setVisible(boolean visible, boolean animated) { return mVisibilityController.setVisible(visible, animated && mAnimationsEnabled); } public boolean isVisible() { return mVisibilityController.isVisible(); } public void setOnClickHomeListener(View.OnClickListener listener) { mHome.setOnClickHomeListener(listener); } @Override public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { if (direction == FOCUS_FORWARD) { // When setting the initial focus for the window, all views should be considered // focusable. super.addFocusables(views, direction, focusableMode); return; } if (direction != FOCUS_LEFT && !hasFocus()) { // We don't want to gain focus unless it's coming from the right or we already have // focus. return; } if (!hasFocus()) { // Try to focus a navigation mode first. int initialCount = views.size(); switch (mNavigationMode) { case ActionBar.NAVIGATION_MODE_TABS: mTabs.getView().addFocusables(views, direction, focusableMode); break; case ActionBar.NAVIGATION_MODE_LIST: mSpinner.getView().addFocusables(views, direction, focusableMode); break; default: if (hasCustomView()) { getCustomView().addFocusables(views, direction, focusableMode); } break; } if (views.size() > initialCount) { // Some focusable elements were added. return; } } super.addFocusables(views, direction, focusableMode); } @Override public View focusSearch(View focused, int direction) { if (hasFocus() && direction != FOCUS_RIGHT) { // If we have focus, we should only relinquish focus if it is moving to the right. // Otherwise we restrict the focus search to our children. return FocusFinder.getInstance().findNextFocus(this, focused, direction); } else { return super.focusSearch(focused, direction); } } protected void onDescendantFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (has(mDisplayOptions, LeftNavBar.DISPLAY_AUTO_EXPAND)) { setExpanded(hasFocus); } } /*@Override protected void onDescendantFocusChanged(boolean hasFocus) { super.onDescendantFocusChanged(hasFocus); if (has(mDisplayOptions, ActionBar.DISPLAY_AUTO_EXPAND)) { setExpanded(hasFocus); } }*/ public int getDisplayOptions() { return mDisplayOptions; } /** * Sets the display options and returns the options which have changed. */ public int setDisplayOptions(int options) { int changes = options ^ mDisplayOptions; mDisplayOptions = options; if (has(changes, ActionBar.DISPLAY_SHOW_HOME)) { mHome.setVisible(has(options, ActionBar.DISPLAY_SHOW_HOME)); } if (has(changes, ActionBar.DISPLAY_USE_LOGO) || has(changes, LeftNavBar.DISPLAY_USE_LOGO_WHEN_EXPANDED)) { setHomeMode(); } if (has(changes, ActionBar.DISPLAY_HOME_AS_UP)) { mHome.setAsUp(has(options, ActionBar.DISPLAY_HOME_AS_UP)); } if (has(changes, ActionBar.DISPLAY_SHOW_CUSTOM)) { setCustomViewVisibility(has(mDisplayOptions, ActionBar.DISPLAY_SHOW_CUSTOM)); } if (has(changes, LeftNavBar.DISPLAY_AUTO_EXPAND) || has(changes, LeftNavBar.DISPLAY_ALWAYS_EXPANDED)) { setExpandedState(); } return changes; } private void setHomeMode() { HomeDisplay.Mode mode; if (has(mDisplayOptions, LeftNavBar.DISPLAY_USE_LOGO_WHEN_EXPANDED)) { mode = HomeDisplay.Mode.BOTH; } else if (has(mDisplayOptions, ActionBar.DISPLAY_USE_LOGO)) { mode = HomeDisplay.Mode.LOGO; } else { mode = HomeDisplay.Mode.ICON; } mHome.setImageMode(mode); } private void setExpandedState() { if (has(mDisplayOptions, LeftNavBar.DISPLAY_AUTO_EXPAND)) { setExpanded(hasFocus(), false); } else { setExpanded(has(mDisplayOptions, LeftNavBar.DISPLAY_ALWAYS_EXPANDED), false); } } private void setExpanded(boolean expanded) { setExpanded(expanded, mAnimationsEnabled && isVisible()); } private void setExpanded(final boolean expanded, boolean animated) { if (mExpanded == expanded) { return; } if (animated) { if (mWidthAnimator != null) { mWidthAnimator.cancel(); } mWidthAnimator = ValueAnimator.ofInt( getLayoutParams().width, // Starting value. expanded ? mWidthExpanded : mWidthCollapsed); mWidthAnimator.setDuration(mAnimationDuration); mWidthAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { public void onAnimationUpdate(ValueAnimator animation) { setViewWidth((Integer) animation.getAnimatedValue()); } }); mWidthAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animator) { if (!expanded) { setContentExpanded(false); } } @Override public void onAnimationEnd(Animator animator) { if (expanded) { setContentExpanded(true); } } }); mWidthAnimator.start(); } else { setViewWidth(expanded ? mWidthExpanded : mWidthCollapsed); setContentExpanded(expanded); } mExpanded = expanded; } private void setContentExpanded(boolean expanded) { mTabs.setExpanded(expanded); mOptions.setExpanded(expanded); mHome.setExpanded(expanded); mSpinner.setExpanded(expanded); if (hasCustomView()) { getCustomView().setActivated(expanded); } } private void setViewWidth(int width) { ViewGroup.LayoutParams params = getLayoutParams(); params.width = width; setLayoutParams(params); } /** * Returns the "steady state" width for the view, taking into account all shadowing effects. */ public int getApparentWidth(boolean ignoreHiddenState) { if (!isVisible() && !ignoreHiddenState) { return 0; } boolean isCollapsed = has(mDisplayOptions, LeftNavBar.DISPLAY_AUTO_EXPAND) || !has(mDisplayOptions, LeftNavBar.DISPLAY_ALWAYS_EXPANDED); return isCollapsed ? mApparentWidthCollapsed : mApparentWidthExpanded; } public void setAnimationsEnabled(boolean enabled) { mAnimationsEnabled = enabled; } private static boolean has(int changes, int option) { return (changes & option) != 0; } public TabDisplay getTabs() { return mTabs; } public SpinnerDisplay getSpinner() { return mSpinner; } public void setNavigationMode(int mode) { if (mNavigationMode == mode) { return; } setNavigationModeVisibility(mNavigationMode, false); setNavigationModeVisibility(mode, true); mNavigationMode = mode; } private void setNavigationModeVisibility(int mode, boolean visible) { switch (mode) { case ActionBar.NAVIGATION_MODE_TABS: mTabs.setVisible(visible); break; case ActionBar.NAVIGATION_MODE_LIST: mSpinner.setVisible(visible); break; default: break; } } public int getNavigationMode() { return mNavigationMode; } public void showOptionsMenu(Boolean show) { mOptions.setVisible(show); } public void setCustomView(View view) { ViewGroup main = getMainSection(); CustomViewWrapper current = getCustomViewWrapper(); if (current != null) { current.detach(); main.removeView(current); } if (view != null) { view.setActivated(mExpanded); main.addView(new CustomViewWrapper(getContext(), view)); setCustomViewVisibility(has(mDisplayOptions, ActionBar.DISPLAY_SHOW_CUSTOM)); } } private boolean hasCustomView() { return getCustomViewWrapper() != null; } private boolean hasVisibleCustomView() { return hasCustomView() && getCustomViewWrapper().getVisibility() == VISIBLE; } public View getCustomView() { CustomViewWrapper wrapper = getCustomViewWrapper(); return wrapper != null ? wrapper.getView() : null; } private CustomViewWrapper getCustomViewWrapper() { ViewGroup main = getMainSection(); // The custom view comes after the tabs and the spinner. if (main.getChildCount() == 3) { return (CustomViewWrapper) main.getChildAt(2); } return null; } private void setCustomViewVisibility(boolean visible) { View current = getCustomViewWrapper(); if (current != null) { current.setVisibility(visible ? VISIBLE : GONE); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (hasVisibleCustomView()) { getCustomViewWrapper().onPostMeasure(this); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (hasVisibleCustomView()) { getCustomViewWrapper().onPostLayout(this); } } /** * Wrapper around custom views to allow them to use the layout parameters defined in the * ActionBar class and still be displayed within this view group. */ private static final class CustomViewWrapper extends ViewGroup { private final View mView; CustomViewWrapper(Context context, View view) { super(context); setLayoutParams(new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); mView = view; if (!(view.getLayoutParams() instanceof ActionBar.LayoutParams)) { view.setLayoutParams(generateDefaultLayoutParams()); } addView(view); } View getView() { return mView; } void detach() { removeView(mView); } /** * Locates the top bound of the space available for custom views, expressed in the * coordinate system of {@code parent}. */ private int findTopOfAvailableSpace(LeftNavView parent) { //int top = parent.mPaddingTop; int top = parent.getPaddingTop(); if (parent.mHome.isVisible()) { top += parent.mHome.getView().getMeasuredHeight(); } switch (parent.mNavigationMode) { case ActionBar.NAVIGATION_MODE_TABS: top += parent.mTabs.getView().getMeasuredHeight(); break; case ActionBar.NAVIGATION_MODE_LIST: top += parent.mSpinner.getView().getMeasuredHeight(); break; default: break; } return top; } /** * Locates the bottom bound of the space available for custom views, expressed in the * coordinate system of {@code parent}. */ private int findBottomOfAvailableSpace(LeftNavView parent) { //int bottom = parent.getMeasuredHeight() - parent.mPaddingBottom; int bottom = parent.getMeasuredHeight() - parent.getPaddingBottom(); if (parent.mOptions.isVisible()) { bottom -= parent.mOptions.getView().getMeasuredHeight(); } return bottom; } private void checkDimensionsConsistency(int value, int expected) { if (value != expected) { throw new IllegalStateException("Inconsistent dimensions!"); } } /** * Should be called after the rest of the left nav has been measured, so that custom views * can be sized according to the space left and the desired gravity. */ void onPostMeasure(LeftNavView parent) { // Dimensions of the entire parent. int totalWidth = parent.getMeasuredWidth(); int totalHeight = parent.getMeasuredHeight(); // Coordinates of the top and bottom of the available space. int topOfAvailableSpace = findTopOfAvailableSpace(parent); int bottomOfAvailableSpace = findBottomOfAvailableSpace(parent); // Dimensions of the available space. int availableWidth = totalWidth - parent.getPaddingLeft() - parent.getPaddingRight(); int availableHeight = bottomOfAvailableSpace - topOfAvailableSpace; // Space available in each half of the parent. // This is used when attempting to vertically center a custom view which has its height // as "match parent": in this case its size will be limited by the smallest of the two // spaces. int availableInTopHalf = totalHeight / 2 - topOfAvailableSpace; int availableInBottomHalf = bottomOfAvailableSpace - totalHeight / 2; // Sanity checks. if (getMeasuredWidth() != 0) { checkDimensionsConsistency(availableWidth, getMeasuredWidth()); } if (getMeasuredHeight() != 0) { checkDimensionsConsistency(availableHeight, getMeasuredHeight()); } ActionBar.LayoutParams params = (ActionBar.LayoutParams) mView.getLayoutParams(); int horizontalMargin = params.leftMargin + params.rightMargin; int verticalMargin = params.topMargin + params.bottomMargin; int widthMode = params.width != LayoutParams.WRAP_CONTENT ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST; int widthValue = params.width >= 0 ? Math.min(params.width, availableWidth) : availableWidth; widthValue = Math.max(0, widthValue - horizontalMargin); int heightMode = params.height != LayoutParams.WRAP_CONTENT ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST; int heightValue = params.height >= 0 ? Math.min(params.height, availableHeight) : availableHeight; heightValue = Math.max(0, heightValue - verticalMargin); int vGravity = params.gravity & Gravity.VERTICAL_GRAVITY_MASK; if (vGravity == Gravity.CENTER_VERTICAL && params.height == ViewGroup.LayoutParams.MATCH_PARENT && availableInTopHalf > 0 && availableInBottomHalf > 0) { // Attempt to center if there's enough space to center. heightValue = Math.min(availableInTopHalf, availableInBottomHalf) * 2; } mView.measure( MeasureSpec.makeMeasureSpec(widthValue, widthMode), MeasureSpec.makeMeasureSpec(heightValue, heightMode)); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // Nothing to do here, all the work is done in onPostLayout. } /** * Should be called as the last layout step to properly position the custom view. */ void onPostLayout(LeftNavView parent) { int width = mView.getMeasuredWidth(); int height = mView.getMeasuredHeight(); ActionBar.LayoutParams params = (ActionBar.LayoutParams) mView.getLayoutParams(); // Expressed within the coordinate system of the present view. int xPosition = 0; // Horizontal alignment is always performed within the available space. // int containerWidth = mRight - mLeft; int containerWidth = getRight() - getLeft(); switch (params.gravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: xPosition = (containerWidth - width) / 2; break; case Gravity.LEFT: xPosition = params.leftMargin; break; case Gravity.RIGHT: xPosition = containerWidth - width - params.rightMargin; break; } int vGravity = params.gravity & Gravity.VERTICAL_GRAVITY_MASK; // For "center vertical" the view gets centered within the parent, not within the // available space. /* int superContainerHeight = parent.mBottom - parent.mTop - parent.mPaddingTop - parent.mPaddingBottom;*/ int superContainerHeight = parent.getBottom() - parent.getTop() - parent.getPaddingTop() - parent.getPaddingBottom(); int superCenteredTop = (superContainerHeight - height) / 2 + parent.getPaddingTop(); // The coordinates of the wrapper in parent's coordinate system. int top = findTopOfAvailableSpace(parent); int bottom = findBottomOfAvailableSpace(parent); // Sanity checks. if (getBottom() - getTop() != 0) { checkDimensionsConsistency(bottom - top, getBottom() - getTop()); } // See if we actually have room to truly center; if not push against top or bottom. if (vGravity == Gravity.CENTER_VERTICAL) { if (superCenteredTop < top) { vGravity = Gravity.TOP; } else if (superCenteredTop + height > bottom) { vGravity = Gravity.BOTTOM; } } // Expressed within the coordinate system of the present view. int yPosition = 0; int containerHeight = bottom - top; switch (vGravity) { case Gravity.CENTER_VERTICAL: yPosition = superCenteredTop - top; break; case Gravity.TOP: yPosition = params.topMargin; break; case Gravity.BOTTOM: yPosition = containerHeight - height - params.bottomMargin; break; } mView.layout(xPosition, yPosition, xPosition + width, yPosition + height); } @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { return new ActionBar.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); } } }