Fade photos in contact-details and quick-contacts.
Introduces new helper class, ImageViewDrawableSetter, which remembers
the previously-set drawable, so it can transition from that to a new
one.
Change-Id: Ie6ee6d5ccc376cc9d5b7aa945f2622b173bf09e8
diff --git a/src/com/android/contacts/detail/ContactDetailFragment.java b/src/com/android/contacts/detail/ContactDetailFragment.java
index 33e2ab4..b059ef4 100644
--- a/src/com/android/contacts/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailFragment.java
@@ -40,6 +40,7 @@
import com.android.contacts.util.Constants;
import com.android.contacts.util.DataStatus;
import com.android.contacts.util.DateUtils;
+import com.android.contacts.util.ImageViewDrawableSetter;
import com.android.contacts.util.PhoneCapabilityTester;
import com.android.contacts.util.StructuredPostalUtils;
import com.android.internal.telephony.ITelephony;
@@ -144,6 +145,8 @@
private Uri mPrimaryPhoneUri = null;
private ViewEntryDimensions mViewEntryDimensions;
+ private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
+
private Button mQuickFixButton;
private QuickFix mQuickFix;
private String mDefaultCountryIso;
@@ -435,8 +438,8 @@
if (mShowStaticPhoto) {
mStaticPhotoContainer.setVisibility(View.VISIBLE);
ImageView photoView = (ImageView) mStaticPhotoContainer.findViewById(R.id.photo);
- OnClickListener listener = ContactDetailDisplayUtils.setPhoto(mContext,
- mContactData, photoView, false);
+ OnClickListener listener = mPhotoSetter.setupContactPhotoForClick(
+ mContext, mContactData, photoView, false);
if (mPhotoTouchOverlay != null) {
mPhotoTouchOverlay.setVisibility(View.VISIBLE);
mPhotoTouchOverlay.setOnClickListener(listener);
@@ -1489,9 +1492,9 @@
// Set the photo if it should be displayed
if (viewCache.photoView != null) {
- OnClickListener listener = ContactDetailDisplayUtils.setPhoto(mContext,
- mContactData, viewCache.photoView,
- !PhoneCapabilityTester.isUsingTwoPanes(mContext));
+ final boolean expandOnClick = !PhoneCapabilityTester.isUsingTwoPanes(mContext);
+ OnClickListener listener = mPhotoSetter.setupContactPhotoForClick(
+ mContext, mContactData, viewCache.photoView, expandOnClick);
viewCache.enableTouchInterceptor(listener);
}
diff --git a/src/com/android/contacts/detail/ContactDetailTabCarousel.java b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
index 6cd48e3..008136d 100644
--- a/src/com/android/contacts/detail/ContactDetailTabCarousel.java
+++ b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
@@ -19,6 +19,7 @@
import com.android.contacts.ContactLoader;
import com.android.contacts.R;
import com.android.contacts.util.PhoneCapabilityTester;
+import com.android.contacts.util.ImageViewDrawableSetter;
import android.content.Context;
import android.content.res.Resources;
@@ -60,6 +61,7 @@
private TextView mStatusView;
private ImageView mStatusPhotoView;
private OnClickListener mPhotoClickListener;
+ private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
private Listener mListener;
@@ -408,16 +410,15 @@
* from the outside to fully setup the View
*/
public void loadData(ContactLoader.Result contactData) {
- if (contactData == null) {
- return;
- }
+ if (contactData == null) return;
- // TODO: Move this into the {@link CarouselTab} class when the updates fragment code is more
- // finalized
- mPhotoClickListener = ContactDetailDisplayUtils.setPhoto(mContext, contactData, mPhotoView,
- !PhoneCapabilityTester.isUsingTwoPanes(mContext));
- ContactDetailDisplayUtils.setSocialSnippet(mContext, contactData, mStatusView,
- mStatusPhotoView);
+ // TODO: Move this into the {@link CarouselTab} class when the updates
+ // fragment code is more finalized.
+ final boolean expandOnClick = !PhoneCapabilityTester.isUsingTwoPanes(mContext);
+ mPhotoClickListener = mPhotoSetter.setupContactPhotoForClick(
+ mContext, contactData, mPhotoView, expandOnClick);
+ ContactDetailDisplayUtils.setSocialSnippet(
+ mContext, contactData, mStatusView, mStatusPhotoView);
}
/**
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index b603e42..04afb89 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -23,6 +23,7 @@
import com.android.contacts.model.AccountTypeManager;
import com.android.contacts.model.DataKind;
import com.android.contacts.util.DataStatus;
+import com.android.contacts.util.ImageViewDrawableSetter;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
@@ -112,6 +113,8 @@
private ImageButton mOpenDetailsPushLayerButton;
private ViewPager mListPager;
+ private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
+
/**
* Keeps the default action per mimetype. Empty if no default actions are set
*/
@@ -296,13 +299,7 @@
final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
context.getApplicationContext());
final ImageView photoView = (ImageView) mPhotoContainer.findViewById(R.id.photo);
- final byte[] photo = data.getPhotoBinaryData();
- if (photo != null) {
- photoView.setImageBitmap(BitmapFactory.decodeByteArray(photo, 0, photo.length));
- } else {
- photoView.setImageResource(
- ContactPhotoManager.getDefaultAvatarResId(true, false));
- }
+ mPhotoSetter.setupContactPhoto(data, photoView);
for (Entity entity : data.getEntities()) {
final ContentValues entityValues = entity.getEntityValues();
diff --git a/src/com/android/contacts/util/ImageViewDrawableSetter.java b/src/com/android/contacts/util/ImageViewDrawableSetter.java
new file mode 100644
index 0000000..412d162
--- /dev/null
+++ b/src/com/android/contacts/util/ImageViewDrawableSetter.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.net.Uri;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageView;
+import com.android.contacts.ContactPhotoManager;
+import com.android.contacts.ContactLoader.Result;
+import com.android.contacts.activities.PhotoSelectionActivity;
+import com.android.contacts.model.EntityDeltaList;
+
+import java.util.Arrays;
+
+/**
+ * Initialized with a target ImageView. When provided with a compressed image
+ * (i.e. a byte[]), it appropriately updates the ImageView's Drawable.
+ */
+public class ImageViewDrawableSetter {
+ private ImageView mTarget;
+ private byte[] mCompressed;
+ private Drawable mPreviousDrawable;
+ private static final String TAG = "ImageViewDrawableSetter";
+
+ public ImageViewDrawableSetter() {
+
+ }
+
+ public ImageViewDrawableSetter(ImageView target) {
+ mTarget = target;
+ }
+
+ public void setupContactPhoto(Result contactData, ImageView photoView) {
+ setTarget(photoView);
+ Bitmap bitmap = setCompressedImage(contactData.getPhotoBinaryData());
+ }
+
+ public OnClickListener setupContactPhotoForClick(Context context, Result contactData,
+ ImageView photoView, boolean expandPhotoOnClick) {
+ setTarget(photoView);
+ Bitmap bitmap = setCompressedImage(contactData.getPhotoBinaryData());
+ return setupClickListener(context, contactData, bitmap, expandPhotoOnClick);
+ }
+
+ /**
+ * Re-initialize to use new target. As a result, the next time a new image
+ * is set, it will immediately be applied to the target (there will be no
+ * fade transition).
+ */
+ private void setTarget(ImageView target) {
+ if (mTarget != target) {
+ mTarget = target;
+ mCompressed = null;
+ mPreviousDrawable = null;
+ }
+ }
+
+ private Bitmap setCompressedImage(byte[] compressed) {
+ if (mPreviousDrawable == null) {
+ // If we don't already have a drawable, skip the exit-early test
+ // below; otherwise we might not end up setting the default image.
+ } else if (mPreviousDrawable != null && Arrays.equals(mCompressed, compressed)) {
+ // TODO: the worst case is when the arrays are equal but not
+ // identical. This takes about 1ms (more with high-res photos). A
+ // possible optimization is to sparsely sample chunks of the arrays
+ // to compare.
+ return previousBitmap();
+ }
+
+ final Drawable newDrawable = (compressed == null)
+ ? defaultDrawable()
+ : decodedBitmapDrawable(compressed);
+
+ // Remember this for next time, so that we can check if it changed.
+ mCompressed = compressed;
+
+ // If we don't have a new Drawable, something went wrong... bail out.
+ if (newDrawable == null) return previousBitmap();
+
+ if (mPreviousDrawable == null) {
+ // Set the new one immediately.
+ mTarget.setImageDrawable(newDrawable);
+ } else {
+ // Set up a transition from the previous Drawable to the new one.
+ final Drawable[] beforeAndAfter = new Drawable[2];
+ beforeAndAfter[0] = mPreviousDrawable;
+ beforeAndAfter[1] = newDrawable;
+ final TransitionDrawable transition = new TransitionDrawable(beforeAndAfter);
+ mTarget.setImageDrawable(transition);
+ transition.startTransition(200);
+ }
+
+ // Remember this for next time, so that we can transition from it to the
+ // new one.
+ mPreviousDrawable = newDrawable;
+
+ return previousBitmap();
+ }
+
+ private Bitmap previousBitmap() {
+ return (mPreviousDrawable == null)
+ ? null
+ : ((BitmapDrawable) mPreviousDrawable).getBitmap();
+ }
+
+ private static final class PhotoClickListener implements OnClickListener {
+
+ private final Context mContext;
+ private final Result mContactData;
+ private final Bitmap mPhotoBitmap;
+ private final byte[] mPhotoBytes;
+ private final boolean mExpandPhotoOnClick;
+ public PhotoClickListener(Context context, Result contactData, Bitmap photoBitmap,
+ byte[] photoBytes, boolean expandPhotoOnClick) {
+ mContext = context;
+ mContactData = contactData;
+ mPhotoBitmap = photoBitmap;
+ mPhotoBytes = photoBytes;
+ mExpandPhotoOnClick = expandPhotoOnClick;
+ }
+
+ @Override
+ public void onClick(View v) {
+ // Assemble the intent.
+ EntityDeltaList delta = EntityDeltaList.fromIterator(
+ mContactData.getEntities().iterator());
+
+ // Find location and bounds of target view, adjusting based on the
+ // assumed local density.
+ final float appScale =
+ mContext.getResources().getCompatibilityInfo().applicationScale;
+ final int[] pos = new int[2];
+ v.getLocationOnScreen(pos);
+
+ final Rect rect = new Rect();
+ rect.left = (int) (pos[0] * appScale + 0.5f);
+ rect.top = (int) (pos[1] * appScale + 0.5f);
+ rect.right = (int) ((pos[0] + v.getWidth()) * appScale + 0.5f);
+ rect.bottom = (int) ((pos[1] + v.getHeight()) * appScale + 0.5f);
+
+ Uri photoUri = null;
+ if (mContactData.getPhotoUri() != null) {
+ photoUri = Uri.parse(mContactData.getPhotoUri());
+ }
+ Intent photoSelectionIntent = PhotoSelectionActivity.buildIntent(mContext,
+ photoUri, mPhotoBitmap, mPhotoBytes, rect, delta, mContactData.isUserProfile(),
+ mContactData.isDirectoryEntry(), mExpandPhotoOnClick);
+ // Cache the bitmap directly, so the activity can pull it from the photo manager.
+ if (mPhotoBitmap != null) {
+ ContactPhotoManager.getInstance(mContext).cacheBitmap(
+ photoUri, mPhotoBitmap, mPhotoBytes);
+ }
+ mContext.startActivity(photoSelectionIntent);
+ }
+ }
+
+ private OnClickListener setupClickListener(Context context, Result contactData, Bitmap bitmap,
+ boolean expandPhotoOnClick) {
+ if (mTarget == null) return null;
+
+ OnClickListener clickListener = new PhotoClickListener(
+ context, contactData, bitmap, mCompressed, expandPhotoOnClick);
+ mTarget.setOnClickListener(clickListener);
+ return clickListener;
+ }
+
+ /**
+ * Obtain the default drawable for a contact when no photo is available.
+ */
+ private Drawable defaultDrawable() {
+ Resources resources = mTarget.getResources();
+ final int resId = ContactPhotoManager.getDefaultAvatarResId(true, false);
+ try {
+ return resources.getDrawable(resId);
+ } catch (NotFoundException e) {
+ Log.wtf(TAG, "Cannot load default avatar resource.");
+ return null;
+ }
+ }
+
+ private BitmapDrawable decodedBitmapDrawable(byte[] compressed) {
+ Resources rsrc = mTarget.getResources();
+ Bitmap bitmap = BitmapFactory.decodeByteArray(compressed, 0, compressed.length);
+ return new BitmapDrawable(rsrc, bitmap);
+ }
+
+}