diff options
author | Yorke Lee <yorkelee@google.com> | 2013-08-05 20:34:40 -0700 |
---|---|---|
committer | Yorke Lee <yorkelee@google.com> | 2013-08-22 08:56:13 -0700 |
commit | c36684277aa45085999284bfe71cb8be71b3a464 (patch) | |
tree | f5aab137e7bf3390dfb8e9007b9e289a550ee90d /src | |
parent | 2c0ea3b35b9983619c12ab0a4f50e14cc70f801e (diff) |
Rewrite animation logic
* Remove old animation-related code. In the past, animations would be applied to a
view everytime getView was called. This is no longer the case so it fixes the issue of
animations triggering everytime the list was scrolled, dialpad opened, etc.
* Make PhoneFavoriteMergedAdapter (and PhoneFavoritesTileAdapter) return stable IDs, so
that they can be used for animations. The ID schemes are described below:
(N + 1) to -2: CallLogAdapterItems, where N is equal to the number of call log items
-1: All contacts button
0 to (N -1): Rows of tiled contacts, where N is equal to the max rows of tiled contacts
N to infinity: Rows of regular contacts. Their item id is calculated by N + contact_id,
where contact_id is guaranteed to never be negative.
* Perform animations by saving each view's offset before the data set changes, and then
applying a translation animation to them based on their new offsets in the updated list view.
This is the same method described by the framework team at :
http://graphics-geek.blogspot.com/2013/06/devbytes-animating-listview-deletion.html
In our case, we need to perform both horizontal and vertical animations because of the
contact tile favorites.
Bug: 10294203
Change-Id: I3ea4ff9995c539267410a264dbbea5ffa02bc6e3
Diffstat (limited to 'src')
4 files changed, 281 insertions, 102 deletions
diff --git a/src/com/android/dialer/list/PhoneFavoriteFragment.java b/src/com/android/dialer/list/PhoneFavoriteFragment.java index 1a78c5fae..efbee9b63 100644 --- a/src/com/android/dialer/list/PhoneFavoriteFragment.java +++ b/src/com/android/dialer/list/PhoneFavoriteFragment.java @@ -29,6 +29,7 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; +import android.view.ViewTreeObserver; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AbsListView; @@ -41,6 +42,7 @@ import android.widget.TextView; import com.android.contacts.common.ContactPhotoManager; import com.android.contacts.common.ContactTileLoaderFactory; import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.list.ContactEntry; import com.android.contacts.common.list.ContactTileView; import com.android.contacts.common.list.PhoneNumberListAdapter; import com.android.dialer.DialtactsActivity; @@ -48,6 +50,10 @@ import com.android.dialer.R; import com.android.dialer.calllog.ContactInfoHelper; import com.android.dialer.calllog.CallLogAdapter; import com.android.dialer.calllog.CallLogQueryHandler; +import com.android.dialer.list.PhoneFavoritesTileAdapter.ContactTileRow; + +import java.util.ArrayList; +import java.util.HashMap; /** * Fragment for Phone UI's favorite screen. @@ -58,9 +64,13 @@ import com.android.dialer.calllog.CallLogQueryHandler; * A contact filter header is also inserted between those adapters' results. */ public class PhoneFavoriteFragment extends Fragment implements OnItemClickListener, - CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher { + CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher, + PhoneFavoritesTileAdapter.OnDataSetChangedForAnimationListener { + private static final String TAG = PhoneFavoriteFragment.class.getSimpleName(); - private static final boolean DEBUG = false; + private static final boolean DEBUG = true; + + private static final int ANIMATION_DURATION = 300; /** * Used with LoaderManager. @@ -146,6 +156,9 @@ public class PhoneFavoriteFragment extends Fragment implements OnItemClickListen private SwipeableListView mListView; private View mShowAllContactsButton; + private final HashMap<Long, Integer> mItemIdTopMap = new HashMap<Long, Integer>(); + private final HashMap<Long, Integer> mItemIdLeftMap = new HashMap<Long, Integer>(); + /** * Layout used when contacts load is slower than expected and thus "loading" view should be * shown. @@ -168,6 +181,7 @@ public class PhoneFavoriteFragment extends Fragment implements OnItemClickListen // that will be available on onCreateView(). mContactTileAdapter = new PhoneFavoritesTileAdapter(activity, mContactTileAdapterListener, + this, getResources().getInteger(R.integer.contact_tile_column_count_in_favorites_new), 1); mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity)); @@ -321,4 +335,166 @@ public class PhoneFavoriteFragment extends Fragment implements OnItemClickListen mContactTileAdapter.removePendingContactEntry(); super.onPause(); } + + /** + * Saves the current view offsets into memory + */ + @SuppressWarnings("unchecked") + private void saveOffsets(long... idsInPlace) { + final int firstVisiblePosition = mListView.getFirstVisiblePosition(); + if (DEBUG) { + Log.d(TAG, "Child count : " + mListView.getChildCount()); + } + for (int i = 0; i < mListView.getChildCount(); i++) { + final View child = mListView.getChildAt(i); + final int position = firstVisiblePosition + i; + final long itemId = mAdapter.getItemId(position); + final int itemViewType = mAdapter.getItemViewType(position); + if (itemViewType == PhoneFavoritesTileAdapter.ViewTypes.TOP) { + // This is a tiled row, so save horizontal offsets instead + saveHorizontalOffsets((ContactTileRow) child, (ArrayList<ContactEntry>) + mAdapter.getItem(position), idsInPlace); + } + if (DEBUG) { + Log.d(TAG, "Saving itemId: " + itemId + " for listview child " + i + " Top: " + + child.getTop()); + } + mItemIdTopMap.put(itemId, child.getTop()); + } + } + + private void saveHorizontalOffsets(ContactTileRow row, ArrayList<ContactEntry> list, + long... idsInPlace) { + for (int i = 0; i < list.size(); i++) { + final View child = row.getChildAt(i); + final ContactEntry entry = list.get(i); + final long itemId = mContactTileAdapter.getAdjustedItemId(entry.id); + if (DEBUG) { + Log.d(TAG, "Saving itemId: " + itemId + " for tileview child " + i + " Left: " + + child.getTop()); + } + mItemIdLeftMap.put(itemId, child.getLeft()); + } + } + + /* + * Performs a animations for a row of tiles + */ + private void performHorizontalAnimations(ContactTileRow row, ArrayList<ContactEntry> list, + long[] idsInPlace) { + if (mItemIdLeftMap.isEmpty()) { + return; + } + for (int i = 0; i < list.size(); i++) { + final View child = row.getChildAt(i); + final ContactEntry entry = list.get(i); + final long itemId = mContactTileAdapter.getAdjustedItemId(entry.id); + + // Skip animation for this view if the caller specified that it should be + // kept in place + if (containsId(idsInPlace, itemId)) continue; + + Integer startLeft = mItemIdLeftMap.get(itemId); + int left = child.getLeft(); + if (DEBUG) { + Log.d(TAG, "Found itemId: " + itemId + " for tileview child " + i + + " Left: " + left); + } + if (startLeft != null) { + if (startLeft != left) { + int delta = startLeft - left; + child.setTranslationX(delta); + child.animate().setDuration(ANIMATION_DURATION).translationX(0); + } + } + // No need to worry about horizontal offsets of new views that come into view since + // there is no horizontal scrolling involved. + } + } + + /* + * Performs animations for the list view. If the list item is a row of tiles, horizontal + * animations will be performed instead. + */ + private void animateListView(final long... idsInPlace) { + if (mItemIdTopMap.isEmpty()) { + // Don't do animations if the database is being queried for the first time and + // the previous item offsets have not been cached, or the user hasn't done anything + // (dragging, swiping etc) that requires an animation. + return; + } + final ViewTreeObserver observer = mListView.getViewTreeObserver(); + observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @SuppressWarnings("unchecked") + @Override + public boolean onPreDraw() { + observer.removeOnPreDrawListener(this); + final int firstVisiblePosition = mListView.getFirstVisiblePosition(); + for (int i = 0; i < mListView.getChildCount(); i++) { + final View child = mListView.getChildAt(i); + int position = firstVisiblePosition + i; + final int itemViewType = mAdapter.getItemViewType(position); + if (itemViewType == PhoneFavoritesTileAdapter.ViewTypes.TOP) { + // This is a tiled row, so perform horizontal animations instead + performHorizontalAnimations((ContactTileRow) child, ( + ArrayList<ContactEntry>) mAdapter.getItem(position), idsInPlace); + } + + final long itemId = mAdapter.getItemId(position); + + // Skip animation for this view if the caller specified that it should be + // kept in place + if (containsId(idsInPlace, itemId)) continue; + + Integer startTop = mItemIdTopMap.get(itemId); + final int top = child.getTop(); + if (DEBUG) { + Log.d(TAG, "Found itemId: " + itemId + " for listview child " + i + + " Top: " + top); + } + int delta = 0; + if (startTop != null) { + if (startTop != top) { + delta = startTop - top; + } + } else if (!mItemIdLeftMap.containsKey(itemId)) { + // Animate new views along with the others. The catch is that they did not + // exist in the start state, so we must calculate their starting position + // based on neighboring views. + int childHeight = child.getHeight() + mListView.getDividerHeight(); + startTop = top + (i > 0 ? childHeight : -childHeight); + delta = startTop - top; + } + + if (delta != 0) { + child.setTranslationY(delta); + child.animate().setDuration(ANIMATION_DURATION).translationY(0); + } + } + mItemIdTopMap.clear(); + mItemIdLeftMap.clear(); + return true; + } + }); + } + + private boolean containsId(long[] ids, long target) { + // Linear search on array is fine because this is typically only 0-1 elements long + for (int i = 0; i < ids.length; i++) { + if (ids[i] == target) { + return true; + } + } + return false; + } + + @Override + public void onDataSetChangedForAnimation(long... idsInPlace) { + animateListView(idsInPlace); + } + + @Override + public void cacheOffsetsForDatasetChange() { + saveOffsets(); + } } diff --git a/src/com/android/dialer/list/PhoneFavoriteMergedAdapter.java b/src/com/android/dialer/list/PhoneFavoriteMergedAdapter.java index ce2b6276e..cbb94b22a 100644 --- a/src/com/android/dialer/list/PhoneFavoriteMergedAdapter.java +++ b/src/com/android/dialer/list/PhoneFavoriteMergedAdapter.java @@ -42,6 +42,7 @@ public class PhoneFavoriteMergedAdapter extends BaseAdapter { private static final String TAG = PhoneFavoriteMergedAdapter.class.getSimpleName(); + private static final int ALL_CONTACTS_BUTTON_ITEM_ID = -1; private final PhoneFavoritesTileAdapter mContactTileAdapter; private final CallLogAdapter mCallLogAdapter; private final View mLoadingView; @@ -95,9 +96,35 @@ public class PhoneFavoriteMergedAdapter extends BaseAdapter { return mContactTileAdapter.getItem(position); } + /** + * In order to ensure that items have stable ids (for animation purposes), we need to + * guarantee that every single item has a unique ID, even across data set changes. + * + * These are the ranges of IDs reserved for each item type. + * + * -(N + 1) to -2: CallLogAdapterItems, where N is equal to the number of call log items + * -1: All contacts button + * 0 to (N -1): Rows of tiled contacts, where N is equal to the max rows of tiled contacts + * N to infinity: Rows of regular contacts. Their item id is calculated by N + contact_id, + * where contact_id is guaranteed to never be negative. + */ @Override public long getItemId(int position) { - return position; + final int callLogAdapterCount = mCallLogAdapter.getCount(); + if (position < callLogAdapterCount) { + // Call log items are not animated, so reusing their position for IDs is fine. + return ALL_CONTACTS_BUTTON_ITEM_ID - 1 - position; + } else if (position < (callLogAdapterCount + mContactTileAdapter.getCount())) { + return mContactTileAdapter.getItemId(position - callLogAdapterCount); + } else { + // All contacts button + return ALL_CONTACTS_BUTTON_ITEM_ID; + } + } + + @Override + public boolean hasStableIds() { + return true; } @Override diff --git a/src/com/android/dialer/list/PhoneFavoriteTileView.java b/src/com/android/dialer/list/PhoneFavoriteTileView.java index dc82f73a4..8887a2c47 100644 --- a/src/com/android/dialer/list/PhoneFavoriteTileView.java +++ b/src/com/android/dialer/list/PhoneFavoriteTileView.java @@ -144,6 +144,18 @@ public abstract class PhoneFavoriteTileView extends ContactTileView { animSet.playTogether(fadeIn); } + animSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mParentRow.setHasTransientState(true); + }; + + @Override + public void onAnimationEnd(Animator animation) { + mParentRow.setHasTransientState(false); + } + }); + animSet.start(); } @@ -164,19 +176,22 @@ public abstract class PhoneFavoriteTileView extends ContactTileView { "alpha", 255).setDuration(ANIMATION_LENGTH); final AnimatorSet animSet = new AnimatorSet(); animSet.playTogether(fadeIn, moveBack, backgroundFadeOut); - animSet.start(); animSet.addListener(new AnimatorListenerAdapter() { @Override + public void onAnimationStart(Animator animation) { + mParentRow.setHasTransientState(true); + } + @Override public void onAnimationEnd(Animator animation) { if (mParentRow.getItemViewType() == ViewTypes.FREQUENT) { SwipeHelper.setSwipeable(mParentRow, true); } else { SwipeHelper.setSwipeable(PhoneFavoriteTileView.this, true); } + mParentRow.setHasTransientState(false); } }); - - + animSet.start(); // Signals the PhoneFavoritesTileAdapter to undo the potential delete. mParentRow.getTileAdapter().undoPotentialRemoveEntryIndex(); } diff --git a/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java b/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java index 88520dd24..263794f67 100644 --- a/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java +++ b/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java @@ -50,6 +50,7 @@ import com.google.common.collect.ComparisonChain; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.PriorityQueue; @@ -67,14 +68,9 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter implements public static final int ROW_LIMIT_DEFAULT = 1; - /** Time period for an animation. */ - private static final int ANIMATION_LENGTH = 300; - - private final ObjectAnimator mTranslateHorizontalAnimation; - private final ObjectAnimator mTranslateVerticalAnimation; - private final ObjectAnimator mAlphaAnimation; - private ContactTileView.Listener mListener; + private OnDataSetChangedForAnimationListener mDataSetChangedListener; + private Context mContext; private Resources mResources; @@ -88,6 +84,7 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter implements private int mDropEntryIndex = -1; /** Position of the contact pending removal. */ private int mPotentialRemoveEntryIndex = -1; + private long mIdToKeepInPlace = -1; private boolean mAwaitingRemove = false; @@ -134,13 +131,15 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter implements } }; - public PhoneFavoritesTileAdapter(Context context, ContactTileView.Listener listener, - int numCols) { - this(context, listener, numCols, ROW_LIMIT_DEFAULT); - } + public interface OnDataSetChangedForAnimationListener { + public void onDataSetChangedForAnimation(long... idsInPlace); + public void cacheOffsetsForDatasetChange(); + }; public PhoneFavoritesTileAdapter(Context context, ContactTileView.Listener listener, + OnDataSetChangedForAnimationListener dataSetChangedListener, int numCols, int maxTiledRows) { + mDataSetChangedListener = dataSetChangedListener; mListener = listener; mContext = context; mResources = context.getResources(); @@ -152,15 +151,6 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter implements mPaddingInPixels = mContext.getResources() .getDimensionPixelSize(R.dimen.contact_tile_divider_padding); - // Initiates all animations. - mAlphaAnimation = ObjectAnimator.ofFloat(null, "alpha", 1.f).setDuration(ANIMATION_LENGTH); - - mTranslateHorizontalAnimation = ObjectAnimator.ofFloat(null, "translationX", 0.f). - setDuration(ANIMATION_LENGTH); - - mTranslateVerticalAnimation = ObjectAnimator.ofFloat(null, "translationY", 0.f).setDuration( - ANIMATION_LENGTH); - bindColumnIndices(); } @@ -229,11 +219,21 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter implements public void setContactCursor(Cursor cursor) { if (cursor != null && !cursor.isClosed()) { mNumStarred = getNumStarredContacts(cursor); + if (mAwaitingRemove) { + mDataSetChangedListener.cacheOffsetsForDatasetChange(); + } + saveNumFrequentsFromCursor(cursor); saveCursorToCache(cursor); - // cause a refresh of any views that rely on this data notifyDataSetChanged(); + // about to start redraw + if (mIdToKeepInPlace != -1) { + mDataSetChangedListener.onDataSetChangedForAnimation(mIdToKeepInPlace); + } else { + mDataSetChangedListener.onDataSetChangedForAnimation(); + } + mIdToKeepInPlace = -1; } } @@ -440,15 +440,36 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter implements } } + /** + * For the top row of tiled contacts, the item id is the position of the row of + * contacts. + * For frequent contacts, the item id is the maximum number of rows of tiled contacts + + * the actual contact id. Since contact ids are always greater than 0, this guarantees that + * all items within this adapter will always have unique ids. + */ @Override public long getItemId(int position) { - // As we show several selectable items for each ListView row, - // we can not determine a stable id. But as we don't rely on ListView's selection, - // this should not be a problem. - return position; + if (getItemViewType(position) == ViewTypes.FREQUENT) { + return getAdjustedItemId(getItem(position).get(0).id); + } else { + return position; + } + } + + /** + * Calculates the stable itemId for a particular entry based on its contactID + */ + public long getAdjustedItemId(long id) { + return mMaxTiledRows + id; + } + + @Override + public boolean hasStableIds() { + return true; } @Override + public boolean areAllItemsEnabled() { // No dividers, so all items are enabled. return true; @@ -467,54 +488,6 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter implements super.notifyDataSetChanged(); } - /** - * Configures the animation for each view. - * - * @param contactTileRowView The row to be animated. - * @param position The position of the row. - * @param itemViewType The type of the row. - */ - private void configureAnimationToView(ContactTileRow contactTileRowView, int position, - int itemViewType) { - // No need to animate anything if we are just entering a drag, because the blank - // entry takes the place of the dragged entry anyway. - if (mInDragging) return; - if (mDropEntryIndex != -1) { - // If one item is dropped in front the row, animate all following rows to shift down. - // If the item is a favorite tile, animate it to appear from left. - if (position >= getRowIndex(mDropEntryIndex)) { - if (itemViewType == ViewTypes.FREQUENT) { - if (position == getRowIndex(mDropEntryIndex) || position == mMaxTiledRows) { - contactTileRowView.setVisibility(View.VISIBLE); - mAlphaAnimation.setTarget(contactTileRowView); - mAlphaAnimation.clone().start(); - } else { - mTranslateVerticalAnimation.setTarget(contactTileRowView); - mTranslateVerticalAnimation.setFloatValues(-contactTileRowView.getHeight(), - 0); - mTranslateVerticalAnimation.clone().start(); - } - } else { - contactTileRowView.animateTilesAppearRight(mDropEntryIndex + 1 - - position * mColumnCount); - } - } - } else if (mPotentialRemoveEntryIndex != -1) { - // If one item is to be removed above this row, animate the row to shift up. If it is - // a favorite contact tile, animate it to appear from right. - if (position >= getRowIndex(mPotentialRemoveEntryIndex)) { - if (itemViewType == ViewTypes.FREQUENT) { - mTranslateVerticalAnimation.setTarget(contactTileRowView); - mTranslateVerticalAnimation.setFloatValues(contactTileRowView.getHeight(), 0); - mTranslateVerticalAnimation.clone().start(); - } else { - contactTileRowView.animateTilesAppearLeft( - mPotentialRemoveEntryIndex - position * mColumnCount); - } - } - } - } - @Override public View getView(int position, View convertView, ViewGroup parent) { if (DEBUG) { @@ -533,8 +506,6 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter implements contactTileRowView.configureRow(contactList, position, position == getCount() - 1); - configureAnimationToView(contactTileRowView, position, itemViewType); - return contactTileRowView; } @@ -572,6 +543,7 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter implements if (index >= 0 && index < mContactEntries.size()) { mDraggedEntry = mContactEntries.get(index); mContactEntries.set(index, ContactEntry.BLANK_ENTRY); + ContactEntry.BLANK_ENTRY.id = mDraggedEntry.id; mDraggedEntryIndex = index; notifyDataSetChanged(); } @@ -590,6 +562,8 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter implements // When we receive a new cursor the list of contact entries will automatically be // populated with the dragged ContactEntry at the correct spot. mDropEntryIndex = index; + mIdToKeepInPlace = getAdjustedItemId(mDraggedEntry.id); + mDataSetChangedListener.cacheOffsetsForDatasetChange(); changed = true; } else if (mDraggedEntryIndex >= 0 && mDraggedEntryIndex <= mContactEntries.size()) { /** If the index is invalid, falls back to the original position of the contact. */ @@ -636,9 +610,8 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter implements public boolean removePendingContactEntry() { boolean removed = false; if (mPotentialRemoveEntryIndex >= 0 && mPotentialRemoveEntryIndex < mContactEntries.size()) { - final ContactEntry entry = mContactEntries.remove(mPotentialRemoveEntryIndex); + final ContactEntry entry = mContactEntries.get(mPotentialRemoveEntryIndex); unstarAndUnpinContact(entry.lookupKey); - notifyDataSetChanged(); removed = true; mAwaitingRemove = true; } @@ -699,7 +672,6 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter implements R.dimen.favorites_row_start_padding); mRowPaddingEnd = resources.getDimensionPixelSize( R.dimen.favorites_row_end_padding); - } else { // For row views, padding is set on the view itself. mRowPaddingTop = 0; @@ -744,6 +716,13 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter implements columnCounter < list.size() ? list.get(columnCounter) : null; addTileFromEntry(entry, columnCounter, isLastRow); } + if (columnCount == 1) { + if (list.get(0) == ContactEntry.BLANK_ENTRY) { + setVisibility(View.INVISIBLE); + } else { + setVisibility(View.VISIBLE); + } + } setPressed(false); getBackground().setAlpha(255); } @@ -916,24 +895,6 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter implements return PhoneFavoritesTileAdapter.this; } - public void animateTilesAppearLeft(int index) { - for (int i = index; i < getChildCount(); ++i) { - View childView = getChildAt(i); - mTranslateHorizontalAnimation.setTarget(childView); - mTranslateHorizontalAnimation.setFloatValues(childView.getWidth(), 0); - mTranslateHorizontalAnimation.clone().start(); - } - } - - public void animateTilesAppearRight(int index) { - for (int i = index; i < getChildCount(); ++i) { - View childView = getChildAt(i); - mTranslateHorizontalAnimation.setTarget(childView); - mTranslateHorizontalAnimation.setFloatValues(-childView.getWidth(), 0); - mTranslateHorizontalAnimation.clone().start(); - } - } - public int getPosition() { return mPosition; } |