Add TitleView and use it to complete VerticalGridFragment.

b/15432740

Change-Id: I00740c265e76b93b943d402be452c74d889510e4
diff --git a/v17/leanback/res/layout/lb_browse_title.xml b/v17/leanback/res/layout/lb_browse_title.xml
index bc6a808..75068d8 100644
--- a/v17/leanback/res/layout/lb_browse_title.xml
+++ b/v17/leanback/res/layout/lb_browse_title.xml
@@ -14,7 +14,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<android.support.v17.leanback.widget.TitleView xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/browse_title_group"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
@@ -22,30 +22,5 @@
     android:paddingRight="?attr/browsePaddingRight"
     android:paddingLeft="?attr/browsePaddingLeft"
     android:paddingBottom="?attr/browsePaddingTop"
-    android:clipToPadding="false"
-    android:clipChildren="false">
+    style="?attr/browseTitleViewStyle" />
 
-    <ImageView
-        android:id="@+id/browse_badge"
-        android:layout_width="@dimen/lb_browse_title_text_width"
-        android:layout_height="@dimen/lb_browse_title_height"
-        android:layout_gravity="center_vertical|right"
-        android:src="@null"
-        android:visibility="gone"
-        style="?attr/browseTitleIconStyle"/>
-
-    <TextView
-        android:id="@+id/browse_title"
-        android:layout_width="@dimen/lb_browse_title_text_width"
-        android:layout_height="@dimen/lb_browse_title_height"
-        android:layout_gravity="center_vertical|right"
-        style="?attr/browseTitleTextStyle"/>
-
-    <android.support.v17.leanback.widget.SearchOrbView
-        android:id="@+id/browse_orb"
-        android:layout_height="wrap_content"
-        android:layout_width="wrap_content"
-        android:layout_gravity="center_vertical|left"
-        android:visibility="invisible" />
-
-</FrameLayout>
diff --git a/v17/leanback/res/layout/lb_title_view.xml b/v17/leanback/res/layout/lb_title_view.xml
new file mode 100644
index 0000000..590a683
--- /dev/null
+++ b/v17/leanback/res/layout/lb_title_view.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2014 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.
+-->
+<merge xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <ImageView
+        android:id="@+id/browse_badge"
+        android:layout_width="@dimen/lb_browse_title_text_width"
+        android:layout_height="@dimen/lb_browse_title_height"
+        android:layout_gravity="center_vertical|right"
+        android:src="@null"
+        android:visibility="gone"
+        style="?attr/browseTitleIconStyle"/>
+
+    <TextView
+        android:id="@+id/browse_title"
+        android:layout_width="@dimen/lb_browse_title_text_width"
+        android:layout_height="@dimen/lb_browse_title_height"
+        android:layout_gravity="center_vertical|right"
+        style="?attr/browseTitleTextStyle"/>
+
+    <android.support.v17.leanback.widget.SearchOrbView
+        android:id="@+id/browse_orb"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:layout_gravity="center_vertical|left"
+        android:visibility="invisible" />
+
+</merge>
diff --git a/v17/leanback/res/values/attrs.xml b/v17/leanback/res/values/attrs.xml
index a0fed63..533ba7e 100644
--- a/v17/leanback/res/values/attrs.xml
+++ b/v17/leanback/res/values/attrs.xml
@@ -115,6 +115,11 @@
         </attr>
     </declare-styleable>
 
+    <declare-styleable name="lbTitleView">
+        <!-- Defining color of the search affordance -->
+        <attr name="searchAffordanceColor" format="reference|color" />
+    </declare-styleable>
+
     <declare-styleable name="LeanbackTheme">
 
         <!-- left padding of BrowseFragment, RowsFragment, DetailsFragment -->
@@ -138,6 +143,9 @@
         <!-- BrowseFragment Title icon style -->
         <attr name="browseTitleIconStyle" format="reference" />
 
+        <!-- BrowseFragment TitleView style -->
+        <attr name="browseTitleViewStyle" format="reference" />
+
         <!-- vertical grid style inside HeadersFragment -->
         <attr name="headersVerticalGridStyle" format="reference" />
         <!-- header style inside HeadersFragment -->
diff --git a/v17/leanback/res/values/styles.xml b/v17/leanback/res/values/styles.xml
index 535f7e3..1074bb0 100644
--- a/v17/leanback/res/values/styles.xml
+++ b/v17/leanback/res/values/styles.xml
@@ -74,6 +74,10 @@
         <item name="android:background">@color/lb_basic_card_bg_color</item>
     </style>
 
+    <style name="Widget.Leanback.TitleView" >
+        <item name="searchAffordanceColor">?attr/defaultSearchColor</item>
+    </style>
+
     <style name="Widget.Leanback.Title" />
 
     <style name="Widget.Leanback.Title.Text">
diff --git a/v17/leanback/res/values/themes.xml b/v17/leanback/res/values/themes.xml
index a481488..aab296a 100644
--- a/v17/leanback/res/values/themes.xml
+++ b/v17/leanback/res/values/themes.xml
@@ -41,6 +41,8 @@
 
         <item name="browseTitleTextStyle">@style/Widget.Leanback.Title.Text</item>
         <item name="browseTitleIconStyle">@style/Widget.Leanback.Title.Icon</item>
+        <item name="browseTitleViewStyle">@style/Widget.Leanback.TitleView</item>
+
         <item name="rowHeaderStyle">@style/Widget.Leanback.Row.Header</item>
         <item name="rowHoverCardTitleStyle">@style/Widget.Leanback.Row.HoverCardTitle</item>
         <item name="rowHoverCardDescriptionStyle">@style/Widget.Leanback.Row.HoverCardDescription</item>
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
index 2d5f9be..02a584a 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
@@ -17,6 +17,7 @@
 import android.support.v17.leanback.widget.HorizontalGridView;
 import android.support.v17.leanback.widget.Presenter;
 import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.TitleView;
 import android.support.v17.leanback.widget.VerticalGridView;
 import android.support.v17.leanback.widget.Row;
 import android.support.v17.leanback.widget.ObjectAdapter;
@@ -24,7 +25,6 @@
 import android.support.v17.leanback.widget.OnItemClickedListener;
 import android.support.v17.leanback.widget.SearchOrbView;
 import android.util.Log;
-import android.util.TypedValue;
 import android.app.Activity;
 import android.app.Fragment;
 import android.app.FragmentManager;
@@ -36,9 +36,6 @@
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
 import android.view.ViewGroup.MarginLayoutParams;
-import android.view.animation.DecelerateInterpolator;
-import android.widget.ImageView;
-import android.widget.TextView;
 import android.graphics.Color;
 import android.graphics.drawable.Drawable;
 
@@ -194,10 +191,7 @@
     private boolean mBrandColorSet;
 
     private BrowseFrameLayout mBrowseFrame;
-    private ImageView mBadgeView;
-    private TextView mTitleView;
-    private ViewGroup mBrowseTitle;
-    private SearchOrbView mSearchOrbView;
+    private TitleView mTitleView;
     private boolean mShowingTitle = true;
     private boolean mHeadersBackStackEnabled = true;
     private String mWithHeadersBackStackName;
@@ -362,8 +356,8 @@
      */
     public void setOnSearchClickedListener(View.OnClickListener listener) {
         mExternalOnSearchClickedListener = listener;
-        if (mSearchOrbView != null) {
-            mSearchOrbView.setOnOrbClickedListener(listener);
+        if (mTitleView != null) {
+            mTitleView.setOnSearchClickedListener(listener);
         }
     }
 
@@ -373,9 +367,8 @@
     public void setSearchAffordanceColor(int color) {
         mSearchAffordanceColor = color;
         mSearchAffordanceColorSet = true;
-
-        if (mSearchOrbView != null) {
-            mSearchOrbView.setOrbColor(mSearchAffordanceColor);
+        if (mTitleView != null) {
+            mTitleView.setSearchAffordanceColor(mSearchAffordanceColor);
         }
     }
 
@@ -384,17 +377,13 @@
      * Can be called only after an activity has been attached.
      */
     public int getSearchAffordanceColor() {
-        if (getActivity() == null) {
-            throw new IllegalStateException("Activity must be attached");
-        }
-
         if (mSearchAffordanceColorSet) {
             return mSearchAffordanceColor;
         }
-
-        TypedValue outValue = new TypedValue();
-        getActivity().getTheme().resolveAttribute(R.attr.defaultSearchColor, outValue, true);
-        return getResources().getColor(outValue.resourceId);
+        if (mTitleView == null) {
+            throw new IllegalStateException("Fragment views not yet created");
+        }
+        return mTitleView.getSearchAffordanceColor();
     }
 
     /**
@@ -472,6 +461,7 @@
             // If headers fragment is disabled, just return null.
             if (!mCanShowHeaders) return null;
 
+            final View searchOrbView = mTitleView.getSearchAffordanceView();
             // if headers is running transition,  focus stays
             if (isInHeadersTransition()) return focused;
             if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
@@ -485,13 +475,13 @@
                     return focused;
                 }
                 return mRowsFragment.getVerticalGridView();
-            } else if (focused == mSearchOrbView && direction == View.FOCUS_DOWN) {
+            } else if (focused == searchOrbView && direction == View.FOCUS_DOWN) {
                 return mShowingHeaders ? mHeadersFragment.getVerticalGridView() :
                     mRowsFragment.getVerticalGridView();
 
-            } else if (focused != mSearchOrbView && mSearchOrbView.getVisibility() == View.VISIBLE
+            } else if (focused != searchOrbView && searchOrbView.getVisibility() == View.VISIBLE
                     && direction == View.FOCUS_UP) {
-                return mSearchOrbView;
+                return searchOrbView;
 
             } else {
                 return null;
@@ -577,16 +567,14 @@
         mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
         mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener);
 
-        mBrowseTitle = (ViewGroup) root.findViewById(R.id.browse_title_group);
-        mTitleView = (TextView) mBrowseTitle.findViewById(R.id.browse_title);
-        mTitleView.setText(mTitle);
-        mBadgeView = (ImageView) mBrowseTitle.findViewById(R.id.browse_badge);
-        setBadgeViewImage();
-
-        mSearchOrbView = (SearchOrbView) mBrowseTitle.findViewById(R.id.browse_orb);
-        mSearchOrbView.setOrbColor(getSearchAffordanceColor());
+        mTitleView = (TitleView) root.findViewById(R.id.browse_title_group);
+        mTitleView.setTitle(mTitle);
+        mTitleView.setBadgeDrawable(mBadgeDrawable);
+        if (mSearchAffordanceColorSet) {
+            mTitleView.setSearchAffordanceColor(mSearchAffordanceColor);
+        }
         if (mExternalOnSearchClickedListener != null) {
-            mSearchOrbView.setOnOrbClickedListener(mExternalOnSearchClickedListener);
+            mTitleView.setOnSearchClickedListener(mExternalOnSearchClickedListener);
         }
 
         if (mBrandColorSet) {
@@ -596,13 +584,13 @@
         mSceneWithTitle = sTransitionHelper.createScene(mBrowseFrame, new Runnable() {
             @Override
             public void run() {
-                showTitle(true);
+                TitleTransitionHelper.showTitle(mTitleView, true);
             }
         });
         mSceneWithoutTitle = sTransitionHelper.createScene(mBrowseFrame, new Runnable() {
             @Override
             public void run() {
-                showTitle(false);
+                TitleTransitionHelper.showTitle(mTitleView, false);
             }
         });
         mSceneWithHeaders = sTransitionHelper.createScene(mBrowseFrame, new Runnable() {
@@ -617,10 +605,8 @@
                 showHeaders(false);
             }
         });
-        mTitleUpTransition = sTransitionHelper.createChangeBounds(false);
-        sTransitionHelper.setInterpolator(mTitleUpTransition, new DecelerateInterpolator(4));
-        mTitleDownTransition = sTransitionHelper.createChangeBounds(false);
-        sTransitionHelper.setInterpolator(mTitleDownTransition, new DecelerateInterpolator());
+        mTitleUpTransition = TitleTransitionHelper.createTransitionTitleUp(sTransitionHelper);
+        mTitleDownTransition = TitleTransitionHelper.createTransitionTitleDown(sTransitionHelper);
 
         sTransitionHelper.excludeChildren(mTitleUpTransition, R.id.browse_headers, true);
         sTransitionHelper.excludeChildren(mTitleDownTransition, R.id.browse_headers, true);
@@ -682,12 +668,6 @@
         }
     }
 
-    private void showTitle(boolean show) {
-        MarginLayoutParams lp = (MarginLayoutParams) mBrowseTitle.getLayoutParams();
-        lp.topMargin = show ? 0 : -mBrowseTitle.getHeight();
-        mBrowseTitle.setLayoutParams(lp);
-    }
-
     private void showHeaders(boolean show) {
         if (DEBUG) Log.v(TAG, "showHeaders " + show);
         mHeadersFragment.setHeadersEnabled(show);
@@ -834,21 +814,9 @@
     public void setBadgeDrawable(Drawable drawable) {
         if (mBadgeDrawable != drawable) {
             mBadgeDrawable = drawable;
-            setBadgeViewImage();
-        }
-    }
-
-    private void setBadgeViewImage() {
-        if (mBadgeView == null) {
-            return;
-        }
-        mBadgeView.setImageDrawable(mBadgeDrawable);
-        if (mBadgeDrawable != null) {
-            mBadgeView.setVisibility(View.VISIBLE);
-            mTitleView.setVisibility(View.GONE);
-        } else {
-            mBadgeView.setVisibility(View.GONE);
-            mTitleView.setVisibility(View.VISIBLE);
+            if (mTitleView != null) {
+                mTitleView.setBadgeDrawable(drawable);
+            }
         }
     }
 
@@ -865,7 +833,7 @@
     public void setTitle(String title) {
         mTitle = title;
         if (mTitleView != null) {
-            mTitleView.setText(title);
+            mTitleView.setTitle(title);
         }
     }
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/TitleTransitionHelper.java b/v17/leanback/src/android/support/v17/leanback/app/TitleTransitionHelper.java
new file mode 100644
index 0000000..288c9eb
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/TitleTransitionHelper.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014 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 android.support.v17.leanback.app;
+
+import android.support.v17.leanback.widget.TitleView;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+class TitleTransitionHelper {
+
+    private static Interpolator createTransitionInterpolatorUp() {
+        return new DecelerateInterpolator(4);
+    }
+
+    private static Interpolator createTransitionInterpolatorDown() {
+        return new DecelerateInterpolator();
+    }
+
+    static public Object createTransitionTitleUp(TransitionHelper helper) {
+        Object transition = helper.createChangeBounds(false);
+        helper.setInterpolator(transition, createTransitionInterpolatorUp());
+        return transition;
+    }
+
+    static public Object createTransitionTitleDown(TransitionHelper helper) {
+        Object transition = helper.createChangeBounds(false);
+        helper.setInterpolator(transition, createTransitionInterpolatorDown());
+        return transition;
+    }
+
+    static public void showTitle(TitleView view, boolean show) {
+        MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
+        lp.topMargin = show ? 0 : -view.getHeight();
+        view.setLayoutParams(lp);
+    }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java b/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
index 8758381..1bee762 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
@@ -15,6 +15,7 @@
 
 import android.support.v17.leanback.R;
 import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.TitleView;
 import android.support.v17.leanback.widget.VerticalGridPresenter;
 import android.support.v17.leanback.widget.ObjectAdapter;
 import android.support.v17.leanback.widget.OnItemClickedListener;
@@ -55,15 +56,15 @@
     private View.OnClickListener mExternalOnSearchClickedListener;
     private int mSelectedPosition = -1;
 
-    private ImageView mBadgeView;
-    private TextView mTitleView;
-    private ViewGroup mBrowseTitle;
-    private SearchOrbView mSearchOrbView;
+    private TitleView mTitleView;
+    private int mSearchAffordanceColor;
+    private boolean mSearchAffordanceColorSet;
     private boolean mShowingTitle = true;
 
     // transition related
     private static TransitionHelper sTransitionHelper = TransitionHelper.getInstance();
-    private Object mTitleTransition;
+    private Object mTitleUpTransition;
+    private Object mTitleDownTransition;
     private Object mSceneWithTitle;
     private Object mSceneWithoutTitle;
 
@@ -127,7 +128,9 @@
     public void setBadgeDrawable(Drawable drawable) {
         if (drawable != mBadgeDrawable) {
             mBadgeDrawable = drawable;
-            setBadgeViewImage();
+            if (mTitleView != null) {
+                mTitleView.setBadgeDrawable(drawable);
+            }
         }
     }
 
@@ -144,7 +147,7 @@
     public void setTitle(String title) {
         mTitle = title;
         if (mTitleView != null) {
-            mTitleView.setText(mTitle);
+            mTitleView.setTitle(mTitle);
         }
     }
 
@@ -215,11 +218,11 @@
             if (!mGridViewHolder.getGridView().hasPreviousViewInSameRow(position)) {
                 // if has no sibling in front of it,  show title
                 if (!mShowingTitle) {
-                    sTransitionHelper.runTransition(mSceneWithTitle, mTitleTransition);
+                    sTransitionHelper.runTransition(mSceneWithTitle, mTitleDownTransition);
                     mShowingTitle = true;
                 }
             } else if (mShowingTitle) {
-                sTransitionHelper.runTransition(mSceneWithoutTitle, mTitleTransition);
+                sTransitionHelper.runTransition(mSceneWithoutTitle, mTitleUpTransition);
                 mShowingTitle = false;
             }
             mSelectedPosition = position;
@@ -255,38 +258,50 @@
      */
     public void setOnSearchClickedListener(View.OnClickListener listener) {
         mExternalOnSearchClickedListener = listener;
-        if (mSearchOrbView != null) {
-            mSearchOrbView.setOnOrbClickedListener(listener);
+        if (mTitleView != null) {
+            mTitleView.setOnSearchClickedListener(listener);
         }
     }
 
-    private void setBadgeViewImage() {
-        if (mBadgeView == null) {
-            return;
-        }
-        mBadgeView.setImageDrawable(mBadgeDrawable);
-        if (mBadgeDrawable != null) {
-            mBadgeView.setVisibility(View.VISIBLE);
-            mTitleView.setVisibility(View.GONE);
-        } else {
-            mBadgeView.setVisibility(View.GONE);
-            mTitleView.setVisibility(View.VISIBLE);
+    /**
+     * Sets the color used to draw the search affordance.
+     */
+    public void setSearchAffordanceColor(int color) {
+        mSearchAffordanceColor = color;
+        mSearchAffordanceColorSet = true;
+        if (mTitleView != null) {
+            mTitleView.setSearchAffordanceColor(mSearchAffordanceColor);
         }
     }
 
+    /**
+     * Returns the color used to draw the search affordance.
+     */
+    public int getSearchAffordanceColor() {
+        if (mSearchAffordanceColorSet) {
+            return mSearchAffordanceColor;
+        }
+        if (mTitleView == null) {
+            throw new IllegalStateException("Fragment views not yet created");
+        }
+        return mTitleView.getSearchAffordanceColor();
+    }
+
+
     private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
             new BrowseFrameLayout.OnFocusSearchListener() {
         @Override
         public View onFocusSearch(View focused, int direction) {
             if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
 
-            if (focused == mSearchOrbView && (
+            final View searchOrbView = mTitleView.getSearchAffordanceView();
+            if (focused == searchOrbView && (
                     direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT)) {
                 return mGridViewHolder.view;
 
-            } else if (focused != mSearchOrbView && mSearchOrbView.getVisibility() == View.VISIBLE
+            } else if (focused != searchOrbView && searchOrbView.getVisibility() == View.VISIBLE
                     && direction == View.FOCUS_UP) {
-                return mSearchOrbView;
+                return searchOrbView;
 
             } else {
                 return null;
@@ -303,43 +318,34 @@
         mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
         mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
 
-        mBrowseTitle = (ViewGroup) root.findViewById(R.id.browse_title_group);
-        mBadgeView = (ImageView) mBrowseTitle.findViewById(R.id.browse_badge);
-        mTitleView = (TextView) mBrowseTitle.findViewById(R.id.browse_title);
-        mSearchOrbView = (SearchOrbView) mBrowseTitle.findViewById(R.id.browse_orb);
-        if (mExternalOnSearchClickedListener != null) {
-            mSearchOrbView.setOnOrbClickedListener(mExternalOnSearchClickedListener);
+        mTitleView = (TitleView) root.findViewById(R.id.browse_title_group);
+        mTitleView.setBadgeDrawable(mBadgeDrawable);
+        mTitleView.setTitle(mTitle);
+        if (mSearchAffordanceColorSet) {
+            mTitleView.setSearchAffordanceColor(mSearchAffordanceColor);
         }
-
-        setBadgeViewImage();
-        mTitleView.setText(mTitle);
+        if (mExternalOnSearchClickedListener != null) {
+            mTitleView.setOnSearchClickedListener(mExternalOnSearchClickedListener);
+        }
 
         mSceneWithTitle = sTransitionHelper.createScene(root, new Runnable() {
             @Override
             public void run() {
-                showTitle(true);
+                TitleTransitionHelper.showTitle(mTitleView, true);
             }
         });
         mSceneWithoutTitle = sTransitionHelper.createScene(root, new Runnable() {
             @Override
             public void run() {
-                showTitle(false);
+                TitleTransitionHelper.showTitle(mTitleView, false);
             }
         });
-        mTitleTransition = sTransitionHelper.createTransitionSet(false);
-        Object fade = sTransitionHelper.createFadeTransition(
-                TransitionHelper.FADE_IN | TransitionHelper.FADE_OUT);
-        Object changeBounds = sTransitionHelper.createChangeBounds(false);
-        sTransitionHelper.addTransition(mTitleTransition, fade);
-        sTransitionHelper.addTransition(mTitleTransition, changeBounds);
-        sTransitionHelper.excludeChildren(mTitleTransition, R.id.browse_grid_dock, true);
-        return root;
-    }
+        mTitleUpTransition = TitleTransitionHelper.createTransitionTitleUp(sTransitionHelper);
+        mTitleDownTransition = TitleTransitionHelper.createTransitionTitleDown(sTransitionHelper);
+        sTransitionHelper.excludeChildren(mTitleUpTransition, R.id.browse_grid_dock, true);
+        sTransitionHelper.excludeChildren(mTitleDownTransition, R.id.browse_grid_dock, true);
 
-    private void showTitle(boolean show) {
-        MarginLayoutParams lp = (MarginLayoutParams) mBrowseTitle.getLayoutParams();
-        lp.topMargin = show ? 0 : -mBrowseTitle.getHeight();
-        mBrowseTitle.setLayoutParams(lp);
+        return root;
     }
 
     @Override
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SearchOrbView.java b/v17/leanback/src/android/support/v17/leanback/widget/SearchOrbView.java
index 86ad1c6..34c2026 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/SearchOrbView.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/SearchOrbView.java
@@ -150,4 +150,8 @@
             enableOrbColorAnimation(true);
         }
     }
+
+    public int getOrbColor() {
+        return mSearchOrbColor;
+    }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/TitleView.java b/v17/leanback/src/android/support/v17/leanback/widget/TitleView.java
new file mode 100644
index 0000000..d38e394
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/TitleView.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2014 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 android.support.v17.leanback.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.support.v17.leanback.R;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ * Title view for a leanback fragment.
+ * @hide
+ */
+public class TitleView extends FrameLayout {
+
+    private ImageView mBadgeView;
+    private TextView mTextView;
+    private SearchOrbView mSearchOrbView;
+
+    public TitleView(Context context) {
+        this(context, null);
+    }
+
+    public TitleView(Context context, AttributeSet attrs) {
+        this(context, attrs, R.style.Widget_Leanback_Title);
+    }
+
+    public TitleView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        LayoutInflater inflater = LayoutInflater.from(context);
+        View rootView = inflater.inflate(R.layout.lb_title_view, this);
+
+        mBadgeView = (ImageView) rootView.findViewById(R.id.browse_badge);
+        mTextView = (TextView) rootView.findViewById(R.id.browse_title);
+        mSearchOrbView = (SearchOrbView) rootView.findViewById(R.id.browse_orb);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbTitleView);
+        int color = a.getColor(R.styleable.lbTitleView_searchAffordanceColor, 0);
+        a.recycle();
+        mSearchOrbView.setOrbColor(color);
+
+        setClipToPadding(false);
+        setClipChildren(false);
+    }
+
+    /**
+     * Sets the title text.
+     */
+    public void setTitle(String titleText) {
+        mTextView.setText(titleText);
+    }
+
+    /**
+     * Returns the title text.
+     */
+    public CharSequence getTitle() {
+        return mTextView.getText();
+    }
+
+    /**
+     * Sets the badge drawable.
+     * If non-null, the drawable is displayed instead of the title text.
+     */
+    public void setBadgeDrawable(Drawable drawable) {
+        mBadgeView.setImageDrawable(drawable);
+        if (drawable != null) {
+            mBadgeView.setVisibility(View.VISIBLE);
+            mTextView.setVisibility(View.GONE);
+        } else {
+            mBadgeView.setVisibility(View.GONE);
+            mTextView.setVisibility(View.VISIBLE);
+        }
+    }
+
+    /**
+     * Returns the badge drawable.
+     */
+    public Drawable getBadgeDrawable() {
+        return mBadgeView.getDrawable();
+    }
+
+    /**
+     * Sets the listener to be called when the search affordance is clicked.
+     */
+    public void setOnSearchClickedListener(View.OnClickListener listener) {
+        mSearchOrbView.setOnOrbClickedListener(listener);
+    }
+
+    /**
+     *  Returns the view for the search affordance.
+     */
+    public View getSearchAffordanceView() {
+        return mSearchOrbView;
+    }
+
+    /**
+     * Sets the color used to draw the search affordance.
+     */
+    public void setSearchAffordanceColor(int color) {
+        mSearchOrbView.setOrbColor(color);
+    }
+
+    /**
+     * Returns the color used to draw the search affordance.
+     */
+    public int getSearchAffordanceColor() {
+        return mSearchOrbView.getOrbColor();
+    }
+}