Bag o' QC UX improvements

UX and I spent a couple days trying out different tweaks to QC.
This is the result.

Main Changes:
-landscape
-better blending, and interpolation of blended values
-different scrim animation length
-updated colors & dimensions
-scaling of title TextView during scroll
-EdgeEffect color is now dynamic
-Drop shadow size

Bug: 15725269
Change-Id: Ib992b41692704d3d932527cef715693ed7a7f4cc
diff --git a/res/layout-land/quickcontact_activity.xml b/res/layout-land/quickcontact_activity.xml
index 552f568..65fcd7d 100644
--- a/res/layout-land/quickcontact_activity.xml
+++ b/res/layout-land/quickcontact_activity.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2011 The Android Open Source Project
+<!-- 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.
@@ -13,45 +13,29 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<LinearLayout
+<com.android.contacts.widget.MultiShrinkScroller
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:ex="http://schemas.android.com/apk/res-auto"
-    android:id="@android:id/content"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:padding="32dip"
-    android:orientation="horizontal">
-    <view
-        class="com.android.contacts.common.widget.ProportionalLayout"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        ex:ratio="1.0"
-        ex:direction="heightToWidth">
-        <FrameLayout
-            android:layout_width="match_parent"
-            android:layout_height="match_parent">
-            <ImageView
-                android:id="@+id/photo"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:scaleType="centerCrop"
-                android:contentDescription="@string/description_contact_photo" />
-            <!-- Need to set a non null background on Toolbar in order for MenuItem
-                ripples to be drawn on this view, instead of another-->
-            <Toolbar
-                android:layout_width="match_parent"
-                android:layout_height="?android:attr/actionBarSize"
-                android:background="#00000000"
-                android:id="@+id/toolbar"/>
-        </FrameLayout>
-    </view>
-    <com.android.contacts.quickcontact.ExpandingEntryCardView
-        style="@style/ExpandingEntryCardStyle"
-        android:id="@+id/communication_card"
-        android:layout_marginTop="@dimen/communication_card_marginTop"
-        android:visibility="gone" />
-    <com.android.contacts.quickcontact.ExpandingEntryCardView
-        style="@style/ExpandingEntryCardStyle"
-        android:id="@+id/recent_card"
-        android:visibility="gone" />
-</LinearLayout>
\ No newline at end of file
+    android:orientation="vertical"
+    android:id="@+id/multiscroller"
+    android:focusable="true"
+    android:focusableInTouchMode="true"
+    android:descendantFocusability="afterDescendants" >
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/quickcontact_starting_empty_height"
+        android:id="@+id/transparent_view" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <include layout="@layout/quickcontact_header" />
+
+        <include layout="@layout/quickcontact_content" />
+
+    </LinearLayout>
+
+</com.android.contacts.widget.MultiShrinkScroller>
\ No newline at end of file
diff --git a/res/layout/expanding_entry_card_item.xml b/res/layout/expanding_entry_card_item.xml
index 890f2da..c038d1b 100644
--- a/res/layout/expanding_entry_card_item.xml
+++ b/res/layout/expanding_entry_card_item.xml
@@ -40,8 +40,7 @@
         android:layout_alignParentTop="true"
         android:layout_toRightOf="@+id/icon"
         android:singleLine="true"
-        android:textColor="@android:color/black"
-        android:textStyle="bold" />
+        android:textColor="@android:color/black" />
 
     <TextView
         android:id="@+id/sub_header"
diff --git a/res/layout/quickcontact_activity.xml b/res/layout/quickcontact_activity.xml
index 13b8d9b..7b81ea2 100644
--- a/res/layout/quickcontact_activity.xml
+++ b/res/layout/quickcontact_activity.xml
@@ -29,54 +29,8 @@
         android:layout_height="@dimen/quickcontact_starting_empty_height"
         android:id="@+id/transparent_view" />
 
-    <FrameLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:background="@color/card_margin_color"
-        android:id="@+id/toolbar_parent">
+    <include layout="@layout/quickcontact_header" />
 
-        <ImageView
-            android:id="@+id/photo"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:scaleType="centerCrop"
-            android:contentDescription="@string/description_contact_photo" />
-
-        <!-- Need to set a non null background on Toolbar in order for MenuItem
-            ripples to be drawn on this view, instead of another-->
-        <Toolbar
-            android:layout_width="match_parent"
-            android:layout_height="?android:attr/actionBarSize"
-            android:background="#00000000"
-            android:id="@+id/toolbar"/>
-
-    </FrameLayout>
-
-    <com.android.contacts.widget.TouchlessScrollView
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:fillViewport="true"
-        android:id="@+id/content_scroller"
-        android:background="@color/card_margin_color">
-
-        <!-- All the cards should be inserted into this LinearLayout -->
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:orientation="vertical"
-            android:id="@+id/card_container">
-            <com.android.contacts.quickcontact.ExpandingEntryCardView
-                style="@style/ExpandingEntryCardStyle"
-                android:id="@+id/communication_card"
-                android:layout_marginTop="@dimen/communication_card_marginTop"
-                android:visibility="gone" />
-
-           <com.android.contacts.quickcontact.ExpandingEntryCardView
-                style="@style/ExpandingEntryCardStyle"
-                android:id="@+id/recent_card"
-                android:visibility="gone" />
-        </LinearLayout>
-
-    </com.android.contacts.widget.TouchlessScrollView>
+    <include layout="@layout/quickcontact_content" />
 
 </com.android.contacts.widget.MultiShrinkScroller>
\ No newline at end of file
diff --git a/res/layout/quickcontact_content.xml b/res/layout/quickcontact_content.xml
new file mode 100644
index 0000000..b5b2a83
--- /dev/null
+++ b/res/layout/quickcontact_content.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.
+-->
+<com.android.contacts.widget.TouchlessScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:fillViewport="true"
+    android:id="@+id/content_scroller"
+    android:background="@color/card_margin_color">
+
+    <!-- All the cards should be inserted into this LinearLayout -->
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        android:id="@+id/card_container">
+        <com.android.contacts.quickcontact.ExpandingEntryCardView
+            style="@style/ExpandingEntryCardStyle"
+            android:id="@+id/communication_card"
+            android:layout_marginTop="@dimen/communication_card_marginTop"
+            android:visibility="gone" />
+
+        <com.android.contacts.quickcontact.ExpandingEntryCardView
+            style="@style/ExpandingEntryCardStyle"
+            android:id="@+id/recent_card"
+            android:visibility="gone" />
+    </LinearLayout>
+
+</com.android.contacts.widget.TouchlessScrollView>
\ No newline at end of file
diff --git a/res/layout/quickcontact_header.xml b/res/layout/quickcontact_header.xml
new file mode 100644
index 0000000..0d06917
--- /dev/null
+++ b/res/layout/quickcontact_header.xml
@@ -0,0 +1,49 @@
+<?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.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@color/card_margin_color"
+    android:id="@+id/toolbar_parent">
+
+    <com.android.contacts.widget.QuickContactImageView
+        android:id="@+id/photo"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scaleType="centerCrop"
+        android:contentDescription="@string/description_contact_photo" />
+
+    <!-- Need to set a non null background on Toolbar in order for MenuItem
+        ripples to be drawn on this view, instead of another-->
+    <Toolbar
+        android:layout_width="match_parent"
+        android:layout_height="?android:attr/actionBarSize"
+        android:background="#00000000"
+        android:id="@+id/toolbar"/>
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textColor="@color/actionbar_text_color"
+        android:maxLines="@integer/quickcontact_title_lines"
+        android:ellipsize="end"
+        android:layout_gravity="bottom"
+        android:textSize="@dimen/quickcontact_maximum_title_size"
+        android:id="@+id/large_title"/>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/quickcontact_title_placeholder.xml b/res/layout/quickcontact_title_placeholder.xml
new file mode 100644
index 0000000..31d83ff
--- /dev/null
+++ b/res/layout/quickcontact_title_placeholder.xml
@@ -0,0 +1,31 @@
+<?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.
+  -->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent" >
+
+    <!-- Marks the location and size of the Activity title -->
+    <TextView
+        android:id="@+id/placeholder_textview"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        style="@android:style/TextAppearance.Material.Widget.ActionBar.Title" />
+
+</FrameLayout>
+
+
diff --git a/res/values-land/bools.xml b/res/values-land/bools.xml
new file mode 100644
index 0000000..bd0650f
--- /dev/null
+++ b/res/values-land/bools.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+<resources>
+
+    <bool name="quickcontact_two_panel">true</bool>
+
+</resources>
diff --git a/res/values-land/integers.xml b/res/values-land/integers.xml
index 010a5b7..08e1fe3 100644
--- a/res/values-land/integers.xml
+++ b/res/values-land/integers.xml
@@ -18,4 +18,7 @@
     <integer name="contact_tile_column_count_in_favorites">5</integer>
 
     <integer name="contact_tile_column_count">4</integer>
+
+    <!-- Number of lines the QuickContact title can have -->
+    <integer name="quickcontact_title_lines">2</integer>
 </resources>
diff --git a/res/values-land/vals.xml b/res/values-land/vals.xml
new file mode 100644
index 0000000..ebcae31
--- /dev/null
+++ b/res/values-land/vals.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<resources>
+    <!-- The ratio of width:height for the contact's photo -->
+    <item name="quickcontact_photo_ratio" type="vals" format="float">0.7</item>
+</resources>
diff --git a/res/values/bools.xml b/res/values/bools.xml
new file mode 100644
index 0000000..663845a
--- /dev/null
+++ b/res/values/bools.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+<resources>
+
+    <bool name="quickcontact_two_panel">false</bool>
+
+</resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 962fe97..89f39b8 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -39,16 +39,16 @@
     <color name="contacts_accent_color">#00acc1</color>
 
     <!-- Color of the separator between entries in an ExpandingEntryCardView -->
-    <color name="expanding_entry_card_item_separator_color">#eeeeee</color>
+    <color name="expanding_entry_card_item_separator_color">#e4e4e4</color>
 
     <!-- Color of the text on an ExpandingEntryCard button -->
     <color name="expanding_entry_card_button_text_color">@android:color/black</color>
 
     <!-- Background color for an ExpandingEntryCard -->
-    <color name="expanding_entry_card_background_color">#f7f8f9</color>
+    <color name="expanding_entry_card_background_color">#ffffff</color>
 
     <!-- Color of the margin for cards -->
-    <color name="card_margin_color">#ffbbbbbb</color>
+    <color name="card_margin_color">#f4f4f4</color>
 
     <color name="call_arrow_green">#2aad6f</color>
     <color name="call_arrow_red">#ff2e58</color>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 99bc345..9d8ba70 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -18,8 +18,8 @@
 
     <!-- Initial height of transparent space above QuickContacts -->
     <dimen name="quickcontact_starting_empty_height">150dp</dimen>
-    <!-- Initial height of QuickContact's header/avatar-photo -->
-    <dimen name="quickcontact_starting_header_height">200dp</dimen>
+    <!-- Initial size of QuickContact's title size -->
+    <dimen name="quickcontact_maximum_title_size">36dp</dimen>
 
     <!-- Top padding of the entire contact editor  -->
     <dimen name="editor_padding_top">0dip</dimen>
@@ -147,9 +147,9 @@
     <dimen name="expanding_entry_card_marginBottom">8dp</dimen>
 
     <!-- Elevation of an ExpandingEntryCard, for the sake of shadow casting -->
-    <dimen name="expanding_entry_card_elevation">2dp</dimen>
+    <dimen name="expanding_entry_card_elevation">1dp</dimen>
     <!-- Elevation of the QuickContact's Toolbar, for the sake of shadow casting -->
-    <dimen name="quick_contact_toolbar_elevation">6dp</dimen>
+    <dimen name="quick_contact_toolbar_elevation">4.5dp</dimen>
 
     <!-- Top margin for the communication card, used to add space from header. -->
     <dimen name="communication_card_marginTop">8dp</dimen>
diff --git a/res/values/integers.xml b/res/values/integers.xml
index a6b43d7..ff34d11 100644
--- a/res/values/integers.xml
+++ b/res/values/integers.xml
@@ -20,4 +20,7 @@
 
     <!-- Determines the number of columns in a ContactTileRow -->
     <integer name="contact_tile_column_count">2</integer>
+
+    <!-- Number of lines the QuickContact title can have -->
+    <integer name="quickcontact_title_lines">1</integer>
 </resources>
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index 303dae5..1453ff4 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -123,11 +123,13 @@
 
     private static final String TAG = "QuickContact";
 
+    private static final String KEY_THEME_COLOR = "theme_color";
+
     private static final int ANIMATION_SLIDE_OPEN_DURATION = 250;
-    private static final int ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION = 75;
+    private static final int ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION = 150;
     private static final int REQUEST_CODE_CONTACT_EDITOR_ACTIVITY = 1;
     private static final float SYSTEM_BAR_BRIGHTNESS_FACTOR = 0.7f;
-    private static final int SHIM_COLOR = Color.argb(0x7F, 0, 0, 0);
+    private static final int SCRIM_COLOR = Color.argb(0xB2, 0, 0, 0);
 
     /** This is the Intent action to install a shortcut in the launcher. */
     private static final String ACTION_INSTALL_SHORTCUT =
@@ -149,7 +151,7 @@
     private MultiShrinkScroller mScroller;
     private SelectAccountDialogFragmentListener mSelectAccountFragmentListener;
     private AsyncTask<Void, Void, Void> mEntriesAndActionsTask;
-    private ColorDrawable mWindowShim;
+    private ColorDrawable mWindowScrim;
     private boolean mIsWaitingForOtherPieceOfExitAnimation;
     private boolean mIsExitAnimationInProgress;
 
@@ -158,6 +160,7 @@
 
     private Contact mContactData;
     private ContactLoader mContactLoader;
+    private PorterDuffColorFilter mColorFilter;
 
     private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
 
@@ -292,7 +295,7 @@
         public void onStartScrollOffBottom() {
             // Remove the window shim now that we are starting an Activity exit animation.
             final int duration = getResources().getInteger(android.R.integer.config_shortAnimTime);
-            final ObjectAnimator animator = ObjectAnimator.ofInt(mWindowShim, "alpha", 0xFF, 0);
+            final ObjectAnimator animator = ObjectAnimator.ofInt(mWindowScrim, "alpha", 0xFF, 0);
             animator.addListener(mExitWindowShimAnimationListener);
             animator.setDuration(duration).start();
             mIsWaitingForOtherPieceOfExitAnimation = true;
@@ -354,15 +357,18 @@
 
         final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
         setActionBar(toolbar);
-        setHeaderNameText(R.string.missing_name);
+        getActionBar().setTitle(null);
+        // Put a TextView with a known resource id into the ActionBar. This allows us to easily
+        // find the correct TextView location & size later.
+        toolbar.addView(getLayoutInflater().inflate(R.layout.quickcontact_title_placeholder, null));
 
         mHasAlreadyBeenOpened = savedInstanceState != null;
 
-        mWindowShim = new ColorDrawable(SHIM_COLOR);
-        getWindow().setBackgroundDrawable(mWindowShim);
+        mWindowScrim = new ColorDrawable(SCRIM_COLOR);
+        getWindow().setBackgroundDrawable(mWindowScrim);
         if (!mHasAlreadyBeenOpened) {
             final int duration = getResources().getInteger(android.R.integer.config_shortAnimTime);
-            ObjectAnimator.ofInt(mWindowShim, "alpha", 0, 0xFF).setDuration(duration).start();
+            ObjectAnimator.ofInt(mWindowScrim, "alpha", 0, 0xFF).setDuration(duration).start();
         }
 
         if (mScroller != null) {
@@ -377,6 +383,8 @@
             }
         }
 
+        setHeaderNameText(R.string.missing_name);
+
         mSelectAccountFragmentListener= (SelectAccountDialogFragmentListener) getFragmentManager()
                 .findFragmentByTag(FRAGMENT_TAG_SELECT_ACCOUNT);
         if (mSelectAccountFragmentListener == null) {
@@ -387,6 +395,21 @@
         }
         mSelectAccountFragmentListener.setQuickContactActivity(this);
 
+        if (savedInstanceState != null) {
+            final int color = savedInstanceState.getInt(KEY_THEME_COLOR, 0);
+            if (color != 0) {
+                // Wait for pre draw. Setting the header tint before the MultiShrinkScroller has
+                // been measured will cause incorrect tinting calculations.
+                SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ true,
+                        new Runnable() {
+                            @Override
+                            public void run() {
+                                setThemeColor(color);
+                            }
+                        });
+            }
+        }
+
         Trace.endSection();
     }
 
@@ -406,6 +429,14 @@
         processIntent(intent);
     }
 
+    @Override
+    public void onSaveInstanceState(Bundle savedInstanceState) {
+        super.onSaveInstanceState(savedInstanceState);
+        if (mColorFilter != null) {
+            savedInstanceState.putInt(KEY_THEME_COLOR, mColorFilter.getColor());
+        }
+    }
+
     private void processIntent(Intent intent) {
         Uri lookupUri = intent.getData();
 
@@ -450,13 +481,17 @@
 
     /** Assign this string to the view if it is not empty. */
     private void setHeaderNameText(int resId) {
-        getActionBar().setTitle(getText(resId));
+        if (mScroller != null) {
+            mScroller.setTitle(String.valueOf(getText(resId)));
+        }
     }
 
     /** Assign this string to the view if it is not empty. */
     private void setHeaderNameText(CharSequence value) {
         if (!TextUtils.isEmpty(value)) {
-            getActionBar().setTitle(value);
+            if (mScroller != null) {
+                mScroller.setTitle(value.toString());
+            }
         }
     }
 
@@ -700,24 +735,7 @@
                     return colorFromBitmap(bitmap);
                 }
                 if (imageViewDrawable instanceof LetterTileDrawable) {
-                    // LetterTileDrawable doesn't normally draw unless it is visible. Therefore,
-                    // we need to directly ask it for its color via getColor(). We could directly
-                    // return this color. However, in the future Palette#generate() may incorporate
-                    // saturation boosting. So I want to use Palette#generate() for the sake of
-                    // consistency.
-                    final LetterTileDrawable tileDrawable = (LetterTileDrawable) imageViewDrawable;
-                    final int PALETTE_BITMAP_SIZE = 1;
-                    final Bitmap bitmap = Bitmap.createBitmap(PALETTE_BITMAP_SIZE,
-                            PALETTE_BITMAP_SIZE, Bitmap.Config.ARGB_8888);
-                    // If Palette can not extract a primary color, our UX person says we are better
-                    // off using the LetterTileDrawable's non vibrant color than falling back
-                    // to the app's default color.
-                    final int color = colorFromBitmap(bitmap);
-                    if (color == 0) {
-                        return tileDrawable.getColor();
-                    } else {
-                        return color;
-                    }
+                    return ((LetterTileDrawable) imageViewDrawable).getColor();
                 }
                 return 0;
             }
@@ -725,34 +743,38 @@
             @Override
             protected void onPostExecute(Integer color) {
                 super.onPostExecute(color);
-                // Check that the Photo has not changed. If it has changed, the new tint color
-                // needs to be extracted
-                if (imageViewDrawable == mPhotoView.getDrawable()) {
-                    // If the color is invalid, use the predefined default
-                    if (color == 0) {
-                        color = getResources().getColor(R.color.actionbar_background_color);
-                    }
-                    // TODO: animate from the previous tint.
-                    mScroller.setHeaderTintColor(color);
-
-                    // Create a darker version of the actionbar color. HSV is device dependent
-                    // and not perceptually-linear. Therefore, we can't say mStatusBarColor is
-                    // 70% as bright as the action bar color. We can only say: it is a bit darker.
-                    final float hsvComponents[] = new float[3];
-                    Color.colorToHSV(color, hsvComponents);
-                    hsvComponents[2] *= SYSTEM_BAR_BRIGHTNESS_FACTOR;
-                    mStatusBarColor = Color.HSVToColor(hsvComponents);
-
-                    updateStatusBarColor();
-                    final ColorFilter colorFilter =
-                            new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP);
-                    mCommunicationCard.setColorAndFilter(color, colorFilter);
-                    mRecentCard.setColorAndFilter(color, colorFilter);
+                // Make sure the color is valid. Also check that the Photo has not changed. If it
+                // has changed, the new tint color needs to be extracted
+                if (color != 0 && imageViewDrawable == mPhotoView.getDrawable()) {
+                    setThemeColor(color);
                 }
             }
         }.execute();
     }
 
+    private void setThemeColor(int color) {
+        // If the color is invalid, use the predefined default
+        if (color == 0) {
+            color = getResources().getColor(R.color.actionbar_background_color);
+        }
+        // TODO: animate from the previous tint.
+        mScroller.setHeaderTintColor(color);
+
+        // Create a darker version of the actionbar color. HSV is device dependent
+        // and not perceptually-linear. Therefore, we can't say mStatusBarColor is
+        // 70% as bright as the action bar color. We can only say: it is a bit darker.
+        final float hsvComponents[] = new float[3];
+        Color.colorToHSV(color, hsvComponents);
+        hsvComponents[2] *= SYSTEM_BAR_BRIGHTNESS_FACTOR;
+        mStatusBarColor = Color.HSVToColor(hsvComponents);
+
+        updateStatusBarColor();
+        mColorFilter =
+                new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP);
+        mCommunicationCard.setColorAndFilter(color, mColorFilter);
+        mRecentCard.setColorAndFilter(color, mColorFilter);
+    }
+
     private void updateStatusBarColor() {
         if (mScroller == null) {
             return;
diff --git a/src/com/android/contacts/widget/MultiShrinkScroller.java b/src/com/android/contacts/widget/MultiShrinkScroller.java
index 96a3d0b..3666420 100644
--- a/src/com/android/contacts/widget/MultiShrinkScroller.java
+++ b/src/com/android/contacts/widget/MultiShrinkScroller.java
@@ -11,10 +11,14 @@
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
+import android.graphics.Color;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Point;
+import android.graphics.Rect;
 import android.hardware.display.DisplayManagerGlobal;
 import android.util.AttributeSet;
+import android.util.TypedValue;
 import android.view.Display;
 import android.view.DisplayInfo;
 import android.view.MotionEvent;
@@ -22,12 +26,14 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewConfiguration;
+import android.view.WindowManager;
 import android.view.animation.Interpolator;
 import android.widget.EdgeEffect;
-import android.widget.ImageView;
+import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 import android.widget.Scroller;
 import android.widget.ScrollView;
+import android.widget.TextView;
 
 /**
  * A custom {@link ViewGroup} that operates similarly to a {@link ScrollView}, except with multiple
@@ -54,6 +60,11 @@
      */
     private static final int EXIT_FLING_ANIMATION_DURATION_MS = 300;
 
+    /**
+     * In portrait mode, the height:width ratio of the photo's starting height.
+     */
+    private static final float INTERMEDIATE_HEADER_HEIGHT_RATIO = 0.5f;
+
     private float[] mLastEventPosition = { 0, 0 };
     private VelocityTracker mVelocityTracker;
     private boolean mIsBeingDragged = false;
@@ -62,24 +73,49 @@
     private ScrollView mScrollView;
     private View mScrollViewChild;
     private View mToolbar;
-    private ImageView mPhotoView;
+    private QuickContactImageView mPhotoView;
     private View mPhotoViewContainer;
     private View mTransparentView;
     private MultiShrinkScrollerListener mListener;
+    private TextView mLargeTextView;
+    /** Contains desired location/size of the title, once the header is fully compressed */
+    private TextView mInvisiblePlaceholderTextView;
     private int mHeaderTintColor;
     private int mMaximumHeaderHeight;
+    private int mMinimumHeaderHeight;
+    private int mIntermediateHeaderHeight;
+    private int mMaximumHeaderTextSize;
 
     private final Scroller mScroller;
     private final EdgeEffect mEdgeGlowBottom;
     private final int mTouchSlop;
     private final int mMaximumVelocity;
     private final int mMinimumVelocity;
-    private final int mIntermediateHeaderHeight;
-    private final int mMinimumHeaderHeight;
     private final int mTransparentStartHeight;
     private final float mToolbarElevation;
-    private final PorterDuffColorFilter mColorFilter
-            = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP);
+    private final boolean mIsTwoPanel;
+    final Rect mLargeTextViewRect = new Rect();
+    final Rect mInvisiblePlaceholderTextViewRect = new Rect();
+
+    // Objects used to perform color filtering on the header. These are stored as fields for
+    // the sole purpose of avoiding "new" operations inside animation loops.
+    private final ColorMatrix mWhitenessColorMatrix = new ColorMatrix();
+    private final  ColorMatrixColorFilter mColorFilter = new ColorMatrixColorFilter(
+            mWhitenessColorMatrix);
+    private final ColorMatrix mColorMatrix = new ColorMatrix();
+    private final float[] mAlphaMatrixValues = {
+            0, 0, 0, 0, 0,
+            0, 0, 0, 0, 0,
+            0, 0, 0, 0, 0,
+            0, 0, 0, 1, 0
+    };
+    private final ColorMatrix mMultiplyBlendMatrix = new ColorMatrix();
+    private final float[] mMultiplyBlendMatrixValues = {
+            0, 0, 0, 0, 0,
+            0, 0, 0, 0, 0,
+            0, 0, 0, 0, 0,
+            0, 0, 0, 1, 0
+    };
 
     public interface MultiShrinkScrollerListener {
         void onScrolledOffBottom();
@@ -148,12 +184,11 @@
         mTouchSlop = configuration.getScaledTouchSlop();
         mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
         mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
-        mIntermediateHeaderHeight = (int) getResources().getDimension(
-                R.dimen.quickcontact_starting_header_height);
         mTransparentStartHeight = (int) getResources().getDimension(
                 R.dimen.quickcontact_starting_empty_height);
         mToolbarElevation = mContext.getResources().getDimension(
                 R.dimen.quick_contact_toolbar_elevation);
+        mIsTwoPanel = mContext.getResources().getBoolean(R.bool.quickcontact_two_panel);
 
         final TypedArray attributeArray = context.obtainStyledAttributes(
                 new int[]{android.R.attr.actionBarSize});
@@ -170,10 +205,11 @@
         mToolbar = findViewById(R.id.toolbar_parent);
         mPhotoViewContainer = findViewById(R.id.toolbar_parent);
         mTransparentView = findViewById(R.id.transparent_view);
+        mLargeTextView = (TextView) findViewById(R.id.large_title);
+        mInvisiblePlaceholderTextView = (TextView) findViewById(R.id.placeholder_textview);
         mListener = listener;
 
-        mPhotoView = (ImageView) findViewById(R.id.photo);
-        setHeaderHeight(mIntermediateHeaderHeight);
+        mPhotoView = (QuickContactImageView) findViewById(R.id.photo);
         mPhotoView.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
@@ -181,15 +217,49 @@
             }
         });
 
-        SchedulingUtils.doOnPreDraw(this, /* drawNextFrame = */ true, new Runnable() {
+        final WindowManager windowManager = (WindowManager) getContext().getSystemService(
+                Context.WINDOW_SERVICE);
+        final Point windowSize = new Point();
+        windowManager.getDefaultDisplay().getSize(windowSize);
+        if (!mIsTwoPanel) {
+            // We never want the height of the photo view to exceed its width.
+            mMaximumHeaderHeight = windowSize.x;
+            mIntermediateHeaderHeight = (int) (mMaximumHeaderHeight
+                    * INTERMEDIATE_HEADER_HEIGHT_RATIO);
+        }
+        setHeaderHeight(mIntermediateHeaderHeight);
+
+        SchedulingUtils.doOnPreDraw(this, /* drawNextFrame = */ false, new Runnable() {
             @Override
             public void run() {
-                // We never want the height of the photo view to exceed its width.
-                mMaximumHeaderHeight = mToolbar.getWidth();
+                mMaximumHeaderTextSize = mLargeTextView.getHeight();
+                // Unlike Window width, we can't know the usable window height until predraw
+                // has occured. Therefore, setting these constraints must be done inside
+                // onPreDraw for the two panel layout. Fortunately, the two panel layout
+                // doesn't need these values anywhere else inside the activity's creation.
+                if (mIsTwoPanel) {
+                    mMaximumHeaderHeight = getHeight();
+                    mMinimumHeaderHeight = mMaximumHeaderHeight;
+                    mIntermediateHeaderHeight = mMaximumHeaderHeight;
+                    final TypedValue photoRatio = new TypedValue();
+                    getResources().getValue(R.vals.quickcontact_photo_ratio, photoRatio,
+                            /* resolveRefs = */ true);
+                    final LayoutParams layoutParams
+                            = (LayoutParams) mPhotoViewContainer.getLayoutParams();
+                    layoutParams.height = mMaximumHeaderHeight;
+                    layoutParams.width = (int) (mMaximumHeaderHeight * photoRatio.getFloat());
+                    mPhotoViewContainer.setLayoutParams(layoutParams);
+                }
+
+                updateHeaderTextSize();
             }
         });
     }
 
+    public void setTitle(String title) {
+        mLargeTextView.setText(title);
+    }
+
     @Override
     public boolean onInterceptTouchEvent(MotionEvent event) {
         // The only time we want to intercept touch events is when we are being dragged.
@@ -257,14 +327,13 @@
                 mReceivedDown = false;
 
                 if (mIsBeingDragged) {
-                    final int heightScrollViewChild = mScrollViewChild.getHeight();
-                    final int pulledToY = mScrollView.getScrollY() + (int) delta;
-                    if (pulledToY > heightScrollViewChild - mScrollView.getHeight()
-                            && mToolbar.getHeight() == mMinimumHeaderHeight) {
+                    final int distanceFromMaxScrolling = getMaximumScrollUpwards() - getScroll();
+                    if (delta > distanceFromMaxScrolling) {
                         // The ScrollView is being pulled upwards while there is no more
                         // content offscreen, and the view port is already fully expanded.
                         mEdgeGlowBottom.onPull(delta / getHeight(), 1 - event.getX() / getWidth());
                     }
+
                     if (!mEdgeGlowBottom.isFinished()) {
                         postInvalidateOnAnimation();
                     }
@@ -285,6 +354,9 @@
     public void setHeaderTintColor(int color) {
         mHeaderTintColor = color;
         updatePhotoTintAndDropShadow();
+        // We want to use the same amount of alpha on the new tint color as the previous tint color.
+        final int edgeEffectAlpha = Color.alpha(mEdgeGlowBottom.getColor());
+        mEdgeGlowBottom.setColor((color & 0xffffff) | Color.argb(edgeEffectAlpha, 0, 0, 0));
     }
 
     /**
@@ -399,6 +471,7 @@
             scrollDown(delta);
         }
         updatePhotoTintAndDropShadow();
+        updateHeaderTextSize();
         final boolean isFullscreen = getScrollNeededToBeFullScreen() <= 0;
         if (mListener != null) {
             if (wasFullscreen && !isFullscreen) {
@@ -419,6 +492,7 @@
         toolbarLayoutParams.height = height;
         mToolbar.setLayoutParams(toolbarLayoutParams);
         updatePhotoTintAndDropShadow();
+        updateHeaderTextSize();
     }
 
     @NeededForReflection
@@ -510,7 +584,12 @@
                     height + getMaximumScrollUpwards() - getScroll());
 
             canvas.rotate(180, width, 0);
-            mEdgeGlowBottom.setSize(width, height);
+            if (mIsTwoPanel) {
+                // Only show the EdgeEffect on the bottom of the ScrollView.
+                mEdgeGlowBottom.setSize(mScrollView.getWidth(), height);
+            } else {
+                mEdgeGlowBottom.setSize(width, height);
+            }
             if (mEdgeGlowBottom.draw(canvas)) {
                 postInvalidateOnAnimation();
             }
@@ -535,11 +614,18 @@
     }
 
     private int getMaximumScrollUpwards() {
-        return mTransparentStartHeight
-                // How much the Header view can compress
-                + mIntermediateHeaderHeight - mMinimumHeaderHeight
-                // How much the ScrollView can scroll. 0, if child is smaller than ScrollView.
-                + Math.max(0, mScrollViewChild.getHeight() - getHeight() + mMinimumHeaderHeight);
+        if (!mIsTwoPanel) {
+            return mTransparentStartHeight
+                    // How much the Header view can compress
+                    + mIntermediateHeaderHeight - mMinimumHeaderHeight
+                    // How much the ScrollView can scroll. 0, if child is smaller than ScrollView.
+                    + Math.max(0, mScrollViewChild.getHeight() - getHeight()
+                    + mMinimumHeaderHeight);
+        } else {
+            return mTransparentStartHeight
+                    // How much the ScrollView can scroll. 0, if child is smaller than ScrollView.
+                    + Math.max(0, mScrollViewChild.getHeight() - getHeight());
+        }
     }
 
     private int getTransparentViewHeight() {
@@ -602,27 +688,167 @@
         }
     }
 
+    /**
+     * Set the header size and padding, based on the current scroll position.
+     */
+    private void updateHeaderTextSize() {
+        if (mIsTwoPanel) {
+            // The text size stays constant on two panel layouts.
+            return;
+        }
+
+        // The pivot point for scaling should be middle of the starting side.
+        if (isLayoutRtl()) {
+            mLargeTextView.setPivotX(mLargeTextView.getWidth());
+        } else {
+            mLargeTextView.setPivotX(0);
+        }
+        mLargeTextView.setPivotY(mLargeTextView.getHeight() / 2);
+
+        final int START_TEXT_SCALING_THRESHOLD_COEFFICIENT = 2;
+        final int threshold = START_TEXT_SCALING_THRESHOLD_COEFFICIENT * mMinimumHeaderHeight;
+        final int toolbarHeight = mToolbar.getLayoutParams().height;
+        if (toolbarHeight >= threshold) {
+            // Keep the text at maximum size since the header is smaller than threshold.
+            mLargeTextView.setScaleX(1);
+            mLargeTextView.setScaleY(1);
+            configureLargeTitlePadding();
+            return;
+        }
+        final float ratio = (toolbarHeight  - mMinimumHeaderHeight)
+                / (float)(threshold - mMinimumHeaderHeight);
+        final float minimumSize = mInvisiblePlaceholderTextView.getHeight();
+        final float scale = (minimumSize + (mMaximumHeaderTextSize - minimumSize) * ratio)
+                / mMaximumHeaderTextSize;
+
+        mLargeTextView.setScaleX(scale);
+        mLargeTextView.setScaleY(scale);
+        configureLargeTitlePadding();
+    }
+
+    /**
+     * Configure the padding around mLargeTextView so that it will look appropriate once it
+     * finishes moving into its target location/size.
+     */
+    private void configureLargeTitlePadding() {
+        mToolbar.getBoundsOnScreen(mLargeTextViewRect);
+        mInvisiblePlaceholderTextView.getBoundsOnScreen(mInvisiblePlaceholderTextViewRect);
+        final int neededPaddingStart;
+        if (isLayoutRtl()) {
+            neededPaddingStart = mInvisiblePlaceholderTextViewRect.right - mLargeTextViewRect.right;
+        } else {
+            neededPaddingStart = mInvisiblePlaceholderTextViewRect.left - mLargeTextViewRect.left;
+        }
+
+        // Distance between top of toolbar to the center of the target rectangle.
+        final int desiredTopToCenter = (
+                mInvisiblePlaceholderTextViewRect.top + mInvisiblePlaceholderTextViewRect.bottom)
+                / 2 - mLargeTextViewRect.top;
+        // Additional padding needed on the mLargeTextView so that it has the same amount of
+        // padding as the target rectangle.
+        final int additionalBottomPaddingNeeded = desiredTopToCenter - mLargeTextView.getHeight()
+                / 2;
+
+        final FrameLayout.LayoutParams layoutParams
+                = (FrameLayout.LayoutParams) mLargeTextView.getLayoutParams();
+        layoutParams.bottomMargin = additionalBottomPaddingNeeded;
+        layoutParams.setMarginStart(neededPaddingStart);
+        mLargeTextView.setLayoutParams(layoutParams);
+    }
+
     private void updatePhotoTintAndDropShadow() {
         // We need to use toolbarLayoutParams to determine the height, since the layout
         // params can be updated before the height change is reflected inside the View#getHeight().
         final int toolbarHeight = mToolbar.getLayoutParams().height;
+
+        if (toolbarHeight <= mMinimumHeaderHeight && !mIsTwoPanel) {
+            mPhotoViewContainer.setElevation(mToolbarElevation);
+        } else {
+            mPhotoViewContainer.setElevation(0);
+        }
+
         // Reuse an existing mColorFilter (to avoid GC pauses) to change the photo's tint.
         mPhotoView.clearColorFilter();
-        if (toolbarHeight >= mMaximumHeaderHeight) {
-            mPhotoViewContainer.setElevation(0);
-            return;
+
+        final float ratio;
+        final float intermediateRatio;
+        if (!mIsTwoPanel) {
+            // Ratio of current size to maximum size of the header.
+            ratio =  (toolbarHeight  - mMinimumHeaderHeight)
+                    / (float) (mMaximumHeaderHeight - mMinimumHeaderHeight) ;
+            // The value that "ratio" will have when the header is at its
+            // starting/intermediate size.
+            intermediateRatio = (mIntermediateHeaderHeight - mMinimumHeaderHeight)
+                    / (float) (mMaximumHeaderHeight - mMinimumHeaderHeight);
+        } else {
+            // Set ratio and intermediateRatio to the same arbitrary value, so that
+            // the math below considers us to be in the intermediate position. The specific
+            // values are not very important.
+            ratio = 0.5f;
+            intermediateRatio = 0.5f;
         }
-        if (toolbarHeight <= mMinimumHeaderHeight) {
-            mColorFilter.setColor(mHeaderTintColor);
-            mPhotoView.setColorFilter(mColorFilter);
-            mPhotoViewContainer.setElevation(mToolbarElevation);
+
+        final float linearBeforeMiddle = Math.max(1 - (1 - ratio) / intermediateRatio, 0);
+
+        // Want a function with a derivative of 0 at x=0. I don't want it to grow too
+        // slowly before x=0.5. x^1.1 satisfies both requirements.
+        final float EXPONENT_ALMOST_ONE = 1.1f;
+        final float semiLinearBeforeMiddle = (float) Math.pow(linearBeforeMiddle,
+                EXPONENT_ALMOST_ONE);
+        mColorMatrix.reset();
+        mColorMatrix.setSaturation(semiLinearBeforeMiddle);
+        mColorMatrix.postConcat(alphaMatrix(ratio, Color.WHITE));
+
+        final float colorAlpha;
+        if (mPhotoView.isBasedOffLetterTile()) {
+            // Since the letter tile only has white and grey, tint it more slowly. Otherwise
+            // it will be completely invisible before we reach the intermediate point.
+            final float SLOWING_FACTOR = 1.6f;
+            float linearBeforeMiddleish = Math.max(1 - (1 - ratio) / intermediateRatio
+                    / SLOWING_FACTOR, 0);
+            colorAlpha = 1 - (float) Math.pow(linearBeforeMiddleish, EXPONENT_ALMOST_ONE);
+            mColorMatrix.postConcat(alphaMatrix(colorAlpha, mHeaderTintColor));
+        } else {
+            colorAlpha = 1 - semiLinearBeforeMiddle;
+            mColorMatrix.postConcat(multiplyBlendMatrix(mHeaderTintColor, colorAlpha));
         }
-        mPhotoViewContainer.setElevation(0);
-        final int alphaBits = 0xff - 0xff * (toolbarHeight  - mMinimumHeaderHeight)
-                / (mMaximumHeaderHeight - mMinimumHeaderHeight);
-        final int color = alphaBits << 24 | (mHeaderTintColor & 0xffffff);
-        mColorFilter.setColor(color);
+
+        mColorFilter.setColorMatrix(mColorMatrix);
         mPhotoView.setColorFilter(mColorFilter);
+        // Tell the photo view what tint we are trying to achieve. Depending on the type of
+        // drawable used, the photo view may or may not use this tint.
+        mPhotoView.setTint(((int) (0xFF * colorAlpha)) << 24 | (mHeaderTintColor & 0xffffff));
+    }
+
+    /**
+     * Simulates alpha blending an image with {@param color}.
+     */
+    private ColorMatrix alphaMatrix(float alpha, int color) {
+        mAlphaMatrixValues[0] = Color.red(color) * alpha / 255;
+        mAlphaMatrixValues[6] = Color.green(color) * alpha / 255;
+        mAlphaMatrixValues[12] = Color.blue(color) * alpha / 255;
+        mAlphaMatrixValues[4] = 255 * (1 - alpha);
+        mAlphaMatrixValues[9] = 255 * (1 - alpha);
+        mAlphaMatrixValues[14] = 255 * (1 - alpha);
+        mWhitenessColorMatrix.set(mAlphaMatrixValues);
+        return mWhitenessColorMatrix;
+    }
+
+    /**
+     * Simulates multiply blending an image with a single {@param color}.
+     *
+     * Multiply blending is [Sa * Da, Sc * Dc]. See {@link android.graphics.PorterDuff}.
+     */
+    private ColorMatrix multiplyBlendMatrix(int color, float alpha) {
+        mMultiplyBlendMatrixValues[0] = multiplyBlend(Color.red(color), alpha);
+        mMultiplyBlendMatrixValues[6] = multiplyBlend(Color.green(color), alpha);
+        mMultiplyBlendMatrixValues[12] = multiplyBlend(Color.blue(color), alpha);
+        mMultiplyBlendMatrix.set(mMultiplyBlendMatrixValues);
+        return mMultiplyBlendMatrix;
+    }
+
+    private float multiplyBlend(int color, float alpha) {
+        return color * alpha / 255.0f + (1 - alpha);
     }
 
     private void updateLastEventPosition(MotionEvent event) {
diff --git a/src/com/android/contacts/widget/QuickContactImageView.java b/src/com/android/contacts/widget/QuickContactImageView.java
new file mode 100644
index 0000000..9dbf85e
--- /dev/null
+++ b/src/com/android/contacts/widget/QuickContactImageView.java
@@ -0,0 +1,85 @@
+package com.android.contacts.widget;
+
+
+import com.android.contacts.common.lettertiles.LetterTileDrawable;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Xfermode;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * An {@link ImageView} designed to display QuickContact's contact photo. In addition to
+ * supporting {@link ImageView#setColorFilter} this also performs a second color blending with
+ * the tint set in {@link #setTint}. This requires a second draw pass.
+ */
+public class QuickContactImageView extends ImageView {
+
+    private Xfermode mMode = new PorterDuffXfermode(Mode.MULTIPLY);
+    private int mTintColor;
+    private BitmapDrawable mBitmapDrawable;
+    private Drawable mOriginalDrawable;
+
+    public QuickContactImageView(Context context) {
+        this(context, null);
+    }
+
+    public QuickContactImageView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public QuickContactImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public QuickContactImageView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    public void setTint(int color) {
+        mTintColor = color;
+        postInvalidate();
+    }
+
+    public boolean isBasedOffLetterTile() {
+        return mOriginalDrawable instanceof LetterTileDrawable;
+    }
+
+    @Override
+    public void setImageDrawable(Drawable drawable) {
+        // There is no way to avoid all this casting. Blending modes aren't equally
+        // supported for all drawable types.
+        if (drawable == null || drawable instanceof BitmapDrawable) {
+            mBitmapDrawable = (BitmapDrawable) drawable;
+        } else if (drawable instanceof LetterTileDrawable) {
+            // TODO: set a desired hardcoded BitmapDrawable here
+            mBitmapDrawable = null;
+        } else {
+            throw new IllegalArgumentException("Does not support this type of drawable");
+
+        }
+        mOriginalDrawable = drawable;
+        super.setImageDrawable(mBitmapDrawable);
+    }
+
+    @Override
+    public Drawable getDrawable() {
+        return mOriginalDrawable;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (isBasedOffLetterTile()) {
+            // The LetterTileDrawable's bitmaps have a lot of pixels with alpha=0. These
+            // look stupid unless we fill in the background and use a different blending mode.
+            canvas.drawColor(((LetterTileDrawable) mOriginalDrawable).getColor());
+        }
+        super.onDraw(canvas);
+    }
+}