From 316b4713b2f8f26f393ecc4bb4760512a4a9f096 Mon Sep 17 00:00:00 2001 From: Christine Chen Date: Mon, 15 Jul 2013 18:31:22 -0700 Subject: Adds Drag and Drop UI to the Dialer main view. - Adds drag and drop listner. - Changes the FavoritesTileAdapter to use an array stored in cache to populate the view. - Adds animation for drag and drop. - Adds swipe to delete an entry. Change-Id: I0717fb3d256b2ab2353f86a998de07edb24e9b4c --- .../dialer/list/NewPhoneFavoriteFragment.java | 1 + .../list/PhoneFavoriteDragAndDropListeners.java | 196 ++++++++++ .../dialer/list/PhoneFavoriteRegularRowView.java | 70 ++-- .../dialer/list/PhoneFavoriteSquareTileView.java | 82 +++++ .../android/dialer/list/PhoneFavoriteTileView.java | 233 ++++++++++-- .../dialer/list/PhoneFavoritesTileAdapter.java | 395 ++++++++++++++++++--- 6 files changed, 854 insertions(+), 123 deletions(-) create mode 100644 src/com/android/dialer/list/PhoneFavoriteDragAndDropListeners.java create mode 100644 src/com/android/dialer/list/PhoneFavoriteSquareTileView.java (limited to 'src/com/android/dialer') diff --git a/src/com/android/dialer/list/NewPhoneFavoriteFragment.java b/src/com/android/dialer/list/NewPhoneFavoriteFragment.java index ba438cff3..eba931021 100644 --- a/src/com/android/dialer/list/NewPhoneFavoriteFragment.java +++ b/src/com/android/dialer/list/NewPhoneFavoriteFragment.java @@ -26,6 +26,7 @@ import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; diff --git a/src/com/android/dialer/list/PhoneFavoriteDragAndDropListeners.java b/src/com/android/dialer/list/PhoneFavoriteDragAndDropListeners.java new file mode 100644 index 000000000..846b2a74f --- /dev/null +++ b/src/com/android/dialer/list/PhoneFavoriteDragAndDropListeners.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2013 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.dialer.list; + +import android.content.ClipData; +import android.util.Log; +import android.view.DragEvent; +import android.view.GestureDetector.SimpleOnGestureListener; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnDragListener; + +import com.android.dialer.list.PhoneFavoritesTileAdapter.ContactTileRow; + +/** + * Implements the OnLongClickListener and OnDragListener for phone's favorite tiles and rows. + */ +public class PhoneFavoriteDragAndDropListeners { + + private static final String TAG = PhoneFavoriteDragAndDropListeners.class.getSimpleName(); + private static final boolean DEBUG = false; + + private static final float FLING_HEIGHT_PORTION = 1.f / 4.f; + private static final float FLING_WIDTH_PORTION = 1.f / 6.f; + + public static class PhoneFavoriteGestureListener extends SimpleOnGestureListener { + private static final float FLING_VELOCITY_MINIMUM = 5.0f; + private float mFlingHorizontalThreshold; + private float mFlingVerticalThreshold; + private final PhoneFavoriteTileView mView; + + public PhoneFavoriteGestureListener(View view) { + super(); + mView = (PhoneFavoriteTileView) view; + } + + @Override + public void onLongPress(MotionEvent event) { + final ClipData data = ClipData.newPlainText("", ""); + final View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(mView); + mView.setPressed(false); + if (mView instanceof PhoneFavoriteRegularRowView) { + // If the view is regular row, start drag the row view. + // TODO: move the padding so we can start drag the original view. + mView.getParentRow().startDrag(data, shadowBuilder, null, 0); + } else { + // If the view is a tile view, start drag the tile. + mView.startDrag(data, shadowBuilder, null, 0); + } + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + final float x1 = e1.getX(); + final float x2 = e2.getX(); + // Temporarily disables parents from getting this event so the listview does not scroll. + mView.getParent().requestDisallowInterceptTouchEvent(true); + mView.setScrollOffset(x2 - x1); + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + // Sets fling trigger threshold. + mFlingVerticalThreshold = (float) mView.getHeight() * FLING_HEIGHT_PORTION; + mFlingHorizontalThreshold = (float) mView.getWidth() * FLING_WIDTH_PORTION; + final float x1 = e1.getX(); + final float x2 = e2.getX(); + final float y1 = e1.getY(); + final float y2 = e2.getY(); + + mView.setPressed(false); + + if (Math.abs(y1 - y2) < mFlingVerticalThreshold && + Math.abs(x2 - x1) > mFlingHorizontalThreshold && + Math.abs(velocityX) > FLING_VELOCITY_MINIMUM) { + // If fling is triggered successfully, end the scroll and setup removal dialogue. + final int removeIndex = mView.getParentRow().getItemIndex(x1, y1); + mView.setScrollEnd(false); + mView.setupRemoveDialogue(); + mView.getParentRow().getTileAdapter().setPotentialRemoveEntryIndex(removeIndex); + + return true; + } else { + mView.setScrollEnd(true); + return false; + } + } + + @Override + public boolean onDown(MotionEvent e) { + mView.setPressed(true); + // Signals that the view will accept further events. + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + mView.performClick(); + return true; + } + } + + /** + * Implements the OnDragListener to handle drag events. + */ + public static class PhoneFavoriteDragListener implements OnDragListener { + /** Location of the drag event. */ + private float mX = 0; + private float mY = 0; + private final ContactTileRow mContactTileRow; + private final PhoneFavoritesTileAdapter mTileAdapter; + + public PhoneFavoriteDragListener(ContactTileRow contactTileRow, + PhoneFavoritesTileAdapter tileAdapter) { + super(); + mContactTileRow = contactTileRow; + mTileAdapter = tileAdapter; + } + + @Override + public boolean onDrag(View v, DragEvent event) { + // Handles drag events. + switch (event.getAction()) { + case DragEvent.ACTION_DRAG_STARTED: + break; + case DragEvent.ACTION_DRAG_ENTERED: + break; + case DragEvent.ACTION_DRAG_EXITED: + break; + case DragEvent.ACTION_DROP: + // Gets the location of the drag with respect to the whole Dialer view. + mX = event.getX() + v.getLeft(); + mY = event.getY() + v.getTop(); + + // Indicates a drag has finished. + if (mTileAdapter != null && mContactTileRow != null) { + mTileAdapter.setInDragging(false); + + // Finds out at which position of the list the Contact is being dropped. + final int dropIndex = mContactTileRow.getItemIndex(mX, mY); + if (DEBUG) { + Log.v(TAG, "Stop dragging " + String.valueOf(dropIndex)); + } + + // Adds the dragged contact to the drop position. + mTileAdapter.dropContactEntry(dropIndex); + } + break; + case DragEvent.ACTION_DRAG_ENDED: + break; + case DragEvent.ACTION_DRAG_LOCATION: + // Gets the current drag location with respect to the whole Dialer view. + mX = event.getX() + v.getLeft(); + mY = event.getY() + v.getTop(); + if (DEBUG) { + Log.v(TAG, String.valueOf(mX) + "; " + String.valueOf(mY)); + } + + if (mTileAdapter != null && mContactTileRow != null) { + // If there is no drag in process, initializes the drag. + if (!mTileAdapter.getInDragging()) { + // Finds out which item is being dragged. + final int dragIndex = mContactTileRow.getItemIndex(mX, mY); + if (DEBUG) { + Log.v(TAG, "Start dragging " + String.valueOf(dragIndex)); + } + + // Indicates a drag has started. + mTileAdapter.setInDragging(true); + + // Temporarily pops out the Contact entry. + mTileAdapter.popContactEntry(dragIndex); + } + } + break; + default: + break; + } + return true; + } + } +} diff --git a/src/com/android/dialer/list/PhoneFavoriteRegularRowView.java b/src/com/android/dialer/list/PhoneFavoriteRegularRowView.java index 2f5921eaf..6d9fdcbbb 100644 --- a/src/com/android/dialer/list/PhoneFavoriteRegularRowView.java +++ b/src/com/android/dialer/list/PhoneFavoriteRegularRowView.java @@ -16,67 +16,51 @@ package com.android.dialer.list; import android.content.Context; -import android.text.TextUtils; import android.util.AttributeSet; -import android.view.View; +import android.view.GestureDetector; -import com.android.contacts.common.MoreContactUtils; -import com.android.contacts.common.list.ContactEntry; -import com.android.contacts.common.list.ContactTileView; import com.android.contacts.common.util.ViewUtil; +import com.android.dialer.R; +import com.android.dialer.list.PhoneFavoriteDragAndDropListeners.PhoneFavoriteDragListener; +import com.android.dialer.list.PhoneFavoriteDragAndDropListeners.PhoneFavoriteGestureListener; + +import com.android.dialer.list.PhoneFavoritesTileAdapter.ContactTileRow; -/** - * A light version of the {@link com.android.contacts.common.list.ContactTileView} that is used in Dialtacts - * for frequently called contacts. Slightly different behavior from superclass... - * when you tap it, you want to call the frequently-called number for the - * contact, even if that is not the default number for that contact. - */ -public class PhoneFavoriteRegularRowView extends ContactTileView { - private String mPhoneNumberString; + +public class PhoneFavoriteRegularRowView extends PhoneFavoriteTileView { + private static final String TAG = PhoneFavoriteRegularRowView.class.getSimpleName(); + private static final boolean DEBUG = false; public PhoneFavoriteRegularRowView(Context context, AttributeSet attrs) { super(context, attrs); } @Override - protected boolean isDarkTheme() { - return false; + protected void onFinishInflate() { + super.onFinishInflate(); + + mFavoriteContactCard = findViewById(R.id.contact_favorite_card); + mRemovalDialogue = findViewById(R.id.favorite_remove_dialogue); + mUndoRemovalButton = findViewById(R.id.favorite_remove_undo_button); + + mGestureDetector = new GestureDetector(getContext(), + new PhoneFavoriteGestureListener(this)); } @Override - protected int getApproximateImageSize() { - return ViewUtil.getConstantPreLayoutWidth(getQuickContact()); + protected void onAttachedToWindow() { + mParentRow = (ContactTileRow) getParent(); + mParentRow.setOnDragListener(new PhoneFavoriteDragListener(mParentRow, + mParentRow.getTileAdapter())); } @Override - public void loadFromContact(ContactEntry entry) { - super.loadFromContact(entry); - mPhoneNumberString = null; // ... in case we're reusing the view - if (entry != null) { - // Grab the phone-number to call directly... see {@link onClick()} - mPhoneNumberString = entry.phoneNumber; - } + protected boolean isDarkTheme() { + return false; } @Override - protected OnClickListener createClickListener() { - return new OnClickListener() { - @Override - public void onClick(View v) { - if (mListener == null) return; - if (TextUtils.isEmpty(mPhoneNumberString)) { - // Copy "superclass" implementation - mListener.onContactSelected(getLookupUri(), MoreContactUtils - .getTargetRectFromView( - mContext, PhoneFavoriteRegularRowView.this)); - } else { - // When you tap a frequently-called contact, you want to - // call them at the number that you usually talk to them - // at (i.e. the one displayed in the UI), regardless of - // whether that's their default number. - mListener.onCallNumberDirectly(mPhoneNumberString); - } - } - }; + protected int getApproximateImageSize() { + return ViewUtil.getConstantPreLayoutWidth(getQuickContact()); } } diff --git a/src/com/android/dialer/list/PhoneFavoriteSquareTileView.java b/src/com/android/dialer/list/PhoneFavoriteSquareTileView.java new file mode 100644 index 000000000..fe07d188e --- /dev/null +++ b/src/com/android/dialer/list/PhoneFavoriteSquareTileView.java @@ -0,0 +1,82 @@ +/* + + * 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. + */ +package com.android.dialer.list; + +import android.content.Context; +import android.content.Intent; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.View; +import android.widget.ImageButton; + +import com.android.contacts.common.R; +import com.android.dialer.list.PhoneFavoriteDragAndDropListeners.PhoneFavoriteDragListener; +import com.android.dialer.list.PhoneFavoriteDragAndDropListeners.PhoneFavoriteGestureListener; +import com.android.dialer.list.PhoneFavoritesTileAdapter.ContactTileRow; + +/** + * Displays the contact's picture overlayed with their name + * in a perfect square. It also has an additional touch target for a secondary action. + */ +public class PhoneFavoriteSquareTileView extends PhoneFavoriteTileView { + private static final String TAG = PhoneFavoriteSquareTileView.class.getSimpleName(); + private ImageButton mSecondaryButton; + + public PhoneFavoriteSquareTileView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mFavoriteContactCard = findViewById(com.android.dialer.R.id.contact_tile_favorite_card); + mRemovalDialogue = findViewById(com.android.dialer.R.id.favorite_tile_remove_dialogue); + mUndoRemovalButton = findViewById(com.android.dialer.R.id.favorite_tile_remove_undo_button); + mSecondaryButton = (ImageButton) findViewById(R.id.contact_tile_secondary_button); + mSecondaryButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(Intent.ACTION_VIEW, getLookupUri()); + // Secondary target will be visible only from phone's favorite screen, then + // we want to launch it as a separate People task. + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + getContext().startActivity(intent); + } + }); + + mGestureDetector = new GestureDetector(getContext(), + new PhoneFavoriteGestureListener(this)); + } + + @Override + protected void onAttachedToWindow() { + mParentRow = (ContactTileRow) getParent(); + setOnDragListener(new PhoneFavoriteDragListener(mParentRow, mParentRow.getTileAdapter())); + } + + @Override + protected boolean isDarkTheme() { + return false; + } + + @Override + protected int getApproximateImageSize() { + // The picture is the full size of the tile (minus some padding, but we can be generous) + return mListener.getApproximateTileWidth(); + } +} diff --git a/src/com/android/dialer/list/PhoneFavoriteTileView.java b/src/com/android/dialer/list/PhoneFavoriteTileView.java index d87e2a837..9a1577a27 100644 --- a/src/com/android/dialer/list/PhoneFavoriteTileView.java +++ b/src/com/android/dialer/list/PhoneFavoriteTileView.java @@ -1,4 +1,5 @@ /* + * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,51 +16,239 @@ */ package com.android.dialer.list; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.Context; -import android.content.Intent; +import android.text.TextUtils; import android.util.AttributeSet; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; import android.view.View; -import android.widget.ImageButton; -import com.android.contacts.common.R; +import com.android.contacts.common.MoreContactUtils; +import com.android.contacts.common.list.ContactEntry; import com.android.contacts.common.list.ContactTileView; +import com.android.dialer.list.PhoneFavoritesTileAdapter.ContactTileRow; /** - * Displays the contact's picture overlayed with their name - * in a perfect square. It also has an additional touch target for a secondary action. + * A light version of the {@link com.android.contacts.common.list.ContactTileView} that is used in + * Dialtacts for frequently called contacts. Slightly different behavior from superclass when you + * tap it, you want to call the frequently-called number for the contact, even if that is not the + * default number for that contact. This abstract class is the super class to both the row and tile + * view. */ -public class PhoneFavoriteTileView extends ContactTileView { - private ImageButton mSecondaryButton; +public abstract class PhoneFavoriteTileView extends ContactTileView { + + private static final String TAG = PhoneFavoriteTileView.class.getSimpleName(); + private static final boolean DEBUG = false; + + /** Length of all animations in miniseconds. */ + private static final int ANIMATION_LENGTH = 300; + + /** The view that holds the front layer of the favorite contact card. */ + protected View mFavoriteContactCard; + /** The view that holds the background layer of the removal dialogue. */ + protected View mRemovalDialogue; + /** Undo button for undoing favorite removal. */ + protected View mUndoRemovalButton; + /** The view that holds the list view row. */ + protected ContactTileRow mParentRow; + + /** Users' most frequent phone number. */ + private String mPhoneNumberString; + + /** Custom gesture detector.*/ + protected GestureDetector mGestureDetector; + /** Indicator of whether a scroll has started. */ + private boolean mInScroll; public PhoneFavoriteTileView(Context context, AttributeSet attrs) { super(context, attrs); } + public ContactTileRow getParentRow() { + return mParentRow; + } + @Override - protected void onFinishInflate() { - super.onFinishInflate(); + public void loadFromContact(ContactEntry entry) { + super.loadFromContact(entry); + mPhoneNumberString = null; // ... in case we're reusing the view + if (entry != null) { + // Grab the phone-number to call directly... see {@link onClick()} + mPhoneNumberString = entry.phoneNumber; + } + } + + /** + * Gets the latest scroll gesture offset. + */ + public void setScrollOffset(float offset) { + // Sets the mInScroll variable to indicate a scroll is in progress. + if (!mInScroll) { + mInScroll = true; + } + + // Changes the view to follow user's scroll position. + shiftViewWithScroll(offset); + } + + /** + * Shifts the view to follow user's scroll position. + */ + private void shiftViewWithScroll(float offset) { + if (mInScroll) { + // Shifts the foreground card to follow users' scroll gesture. + mFavoriteContactCard.setTranslationX(offset); + + // Changes transparency of the foreground and background color + final float alpha = 1.f - Math.abs((offset)) / getWidth(); + final float cappedAlpha = Math.min(Math.max(alpha, 0.f), 1.f); + mFavoriteContactCard.setAlpha(cappedAlpha); + } + } - mSecondaryButton = (ImageButton) findViewById(R.id.contact_tile_secondary_button); - mSecondaryButton.setOnClickListener(new OnClickListener() { + /** + * Sets the scroll has finished. + * + * @param isUnfinishedFling True if it is triggered from the onFling method, but the fling was + * too short or too slow, or from the scroll that does not trigger fling. + */ + public void setScrollEnd(boolean isUnfinishedFling) { + mInScroll = false; + + if (isUnfinishedFling) { + // If the fling is too short or too slow, or it is from a scroll, bring back the + // favorite contact card. + final ObjectAnimator fadeIn = ObjectAnimator.ofFloat(mFavoriteContactCard, "alpha", + 1.f).setDuration(ANIMATION_LENGTH); + final ObjectAnimator moveBack = ObjectAnimator.ofFloat(mFavoriteContactCard, + "translationX", 0.f).setDuration(ANIMATION_LENGTH); + final ObjectAnimator backgroundFadeOut = ObjectAnimator.ofInt( + mParentRow.getBackground(), "alpha", 255).setDuration(ANIMATION_LENGTH); + final AnimatorSet animSet = new AnimatorSet(); + animSet.playTogether(fadeIn, moveBack, backgroundFadeOut); + animSet.start(); + } else { + // If the fling is fast and far enough, move away the favorite contact card, bring the + // favorite removal view to the foreground to ask user to confirm removal. + int animationLength = (int) ((1 - Math.abs(mFavoriteContactCard.getTranslationX()) / + getWidth()) * ANIMATION_LENGTH); + final ObjectAnimator fadeOut = ObjectAnimator.ofFloat(mFavoriteContactCard, "alpha", + 0.f).setDuration(animationLength); + final ObjectAnimator moveAway = ObjectAnimator.ofFloat(mFavoriteContactCard, + "translationX", getWidth()).setDuration(animationLength); + final ObjectAnimator backgroundFadeIn = ObjectAnimator.ofInt( + mParentRow.getBackground(), "alpha", 0).setDuration(animationLength); + if (mFavoriteContactCard.getTranslationX() < 0) { + moveAway.setFloatValues(-getWidth()); + } + final AnimatorSet animSet = new AnimatorSet(); + animSet.playTogether(fadeOut, moveAway, backgroundFadeIn); + animSet.start(); + } + } + + /** + * Signals the user wants to undo removing the favorite contact. + */ + public void undoRemove() { + // Makes the removal dialogue invisible. + mRemovalDialogue.setAlpha(0.0f); + mRemovalDialogue.setVisibility(GONE); + + // Animates back the favorite contact card. + final ObjectAnimator fadeIn = ObjectAnimator.ofFloat(mFavoriteContactCard, "alpha", 1.f). + setDuration(ANIMATION_LENGTH); + final ObjectAnimator moveBack = ObjectAnimator.ofFloat(mFavoriteContactCard, "translationX", + 0.f).setDuration(ANIMATION_LENGTH); + final ObjectAnimator backgroundFadeOut = ObjectAnimator.ofInt(mParentRow.getBackground(), + "alpha", 255).setDuration(ANIMATION_LENGTH); + final AnimatorSet animSet = new AnimatorSet(); + animSet.playTogether(fadeIn, moveBack, backgroundFadeOut); + animSet.start(); + + // Signals the PhoneFavoritesTileAdapter to undo the potential delete. + mParentRow.getTileAdapter().undoPotentialRemoveEntryIndex(); + } + + /** + * Sets up the removal dialogue. + */ + public void setupRemoveDialogue() { + mRemovalDialogue.setVisibility(VISIBLE); + mRemovalDialogue.setAlpha(1.0f); + + mUndoRemovalButton.setOnClickListener(new OnClickListener() { @Override - public void onClick(View v) { - Intent intent = new Intent(Intent.ACTION_VIEW, getLookupUri()); - // Secondary target will be visible only from phone's favorite screen, then - // we want to launch it as a separate People task. - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - getContext().startActivity(intent); + public void onClick(View view) { + undoRemove(); } }); } + /** + * Sets up the favorite contact card. + */ + public void setupFavoriteContactCard() { + if (mRemovalDialogue != null) { + mRemovalDialogue.setVisibility(GONE); + mRemovalDialogue.setAlpha(0.f); + } + mFavoriteContactCard.setAlpha(1.0f); + mFavoriteContactCard.setTranslationX(0.f); + } + @Override - protected boolean isDarkTheme() { - return false; + protected OnClickListener createClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + if (mListener == null) return; + if (TextUtils.isEmpty(mPhoneNumberString)) { + // Copy "superclass" implementation + mListener.onContactSelected(getLookupUri(), MoreContactUtils + .getTargetRectFromView( + mContext, PhoneFavoriteTileView.this)); + } else { + // When you tap a frequently-called contact, you want to + // call them at the number that you usually talk to them + // at (i.e. the one displayed in the UI), regardless of + // whether that's their default number. + mListener.onCallNumberDirectly(mPhoneNumberString); + } + } + }; } @Override - protected int getApproximateImageSize() { - // The picture is the full size of the tile (minus some padding, but we can be generous) - return mListener.getApproximateTileWidth(); + public boolean onTouchEvent(MotionEvent event) { + if (DEBUG) { + Log.v(TAG, event.toString()); + } + switch (event.getAction()) { + // If the scroll has finished without triggering a fling, handles it here. + case MotionEvent.ACTION_UP: + setPressed(false); + if (mInScroll) { + if (!mGestureDetector.onTouchEvent(event)) { + setScrollEnd(true); + } + return true; + } + break; + // When user starts a new gesture, clean up the pending removals. + case MotionEvent.ACTION_DOWN: + mParentRow.getTileAdapter().removeContactEntry(); + break; + // When user continues with a new gesture, cleans up all the temp variables. + case MotionEvent.ACTION_CANCEL: + mParentRow.getTileAdapter().cleanTempVariables(); + break; + default: + break; + } + return mGestureDetector.onTouchEvent(event); } } diff --git a/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java b/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java index 6a4476df5..0a08f2caf 100644 --- a/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java +++ b/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java @@ -15,11 +15,13 @@ */ package com.android.dialer.list; +import android.animation.ObjectAnimator; import android.content.ContentUris; import android.content.Context; import android.content.res.Resources; import android.database.Cursor; -import android.graphics.drawable.Drawable; +import android.graphics.Color; +import android.graphics.Rect; import android.net.Uri; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; @@ -33,7 +35,6 @@ import com.android.contacts.common.ContactPhotoManager; import com.android.contacts.common.ContactTileLoaderFactory; import com.android.contacts.common.R; import com.android.contacts.common.list.ContactEntry; -import com.android.contacts.common.list.ContactTileAdapter; import com.android.contacts.common.list.ContactTileView; import java.util.ArrayList; @@ -45,14 +46,33 @@ import java.util.ArrayList; * */ public class PhoneFavoritesTileAdapter extends BaseAdapter { - private static final String TAG = ContactTileAdapter.class.getSimpleName(); + private static final String TAG = PhoneFavoritesTileAdapter.class.getSimpleName(); + private static final boolean DEBUG = false; 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 Context mContext; private Resources mResources; - protected Cursor mContactCursor = null; + + /** Contact data stored in cache. This is used to populate the associated view. */ + protected ArrayList mContactEntries = null; + /** Back up of the temporarily removed Contact during dragging. */ + private ContactEntry mDraggedEntry = null; + /** Position of the temporarily removed contact in the cache. */ + private int mDraggedEntryIndex = -1; + /** New position of the temporarily removed contact in the cache. */ + private int mDropEntryIndex = -1; + /** Position of the contact pending removal. */ + private int mPotentialRemoveEntryIndex = -1; + private ContactPhotoManager mPhotoManager; protected int mNumFrequents; protected int mNumStarred; @@ -78,22 +98,36 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { private boolean mIsQuickContactEnabled = false; private final int mPaddingInPixels; - public PhoneFavoritesTileAdapter(Context context, ContactTileView.Listener listener, int numCols) { + /** Indicates whether a drag is in process. */ + private boolean mInDragging = false; + + public PhoneFavoritesTileAdapter(Context context, ContactTileView.Listener listener, + int numCols) { this(context, listener, numCols, ROW_LIMIT_DEFAULT); } - public PhoneFavoritesTileAdapter(Context context, ContactTileView.Listener listener, int numCols, - int maxTiledRows) { + public PhoneFavoritesTileAdapter(Context context, ContactTileView.Listener listener, + int numCols, int maxTiledRows) { mListener = listener; mContext = context; mResources = context.getResources(); mColumnCount = numCols; mNumFrequents = 0; mMaxTiledRows = maxTiledRows; - + mContactEntries = new ArrayList(); // Converting padding in dips to padding in pixels 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(); } @@ -113,6 +147,20 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { mIsQuickContactEnabled = enableQuickContact; } + /** + * Indicates whether a drag is in process. + * + * @param inDragging Boolean variable indicating whether there is a drag in process. + */ + public void setInDragging(boolean inDragging) { + mInDragging = inDragging; + } + + /** Gets whether the drag is in process. */ + public boolean getInDragging() { + return mInDragging; + } + /** * Sets the column indices for expected {@link Cursor} * based on {@link DisplayType}. @@ -148,13 +196,49 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { * Else use {@link ContactTileLoaderFactory} */ public void setContactCursor(Cursor cursor) { - mContactCursor = cursor; - mNumStarred = getNumStarredContacts(cursor); + if (cursor != null && !cursor.isClosed()) { + mNumStarred = getNumStarredContacts(cursor); + saveNumFrequentsFromCursor(cursor); + saveCursorToCache(cursor); - saveNumFrequentsFromCursor(cursor); + // cause a refresh of any views that rely on this data + notifyDataSetChanged(); + } + } - // cause a refresh of any views that rely on this data - notifyDataSetChanged(); + /** + * Saves the cursor data to the cache, to speed up UI changes. + * + * @param cursor Returned cursor with data to populate the view. + */ + private void saveCursorToCache(Cursor cursor) { + mContactEntries.clear(); + try { + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + final long id = cursor.getLong(mIdIndex); + final String photoUri = cursor.getString(mPhotoUriIndex); + final String lookupKey = cursor.getString(mLookupIndex); + + final ContactEntry contact = new ContactEntry(); + final String name = cursor.getString(mNameIndex); + contact.name = (name != null) ? name : mResources.getString(R.string.missing_name); + contact.status = cursor.getString(mStatusIndex); + contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null); + contact.lookupKey = ContentUris.withAppendedId( + Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id); + + // Set phone number and label + final int phoneNumberType = cursor.getInt(mPhoneNumberTypeIndex); + final String phoneNumberCustomLabel = cursor.getString(mPhoneNumberLabelIndex); + contact.phoneLabel = (String) Phone.getTypeLabel(mResources, phoneNumberType, + phoneNumberCustomLabel); + contact.phoneNumber = cursor.getString(mPhoneNumberIndex); + mContactEntries.add(contact); + } + } finally { + cursor.close(); + } } /** @@ -164,10 +248,6 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { * Returns 0 if {@link DisplayType#FREQUENT_ONLY} */ protected int getNumStarredContacts(Cursor cursor) { - if (cursor == null || cursor.isClosed()) { - throw new IllegalStateException("Unable to access cursor"); - } - cursor.moveToPosition(-1); while (cursor.moveToNext()) { if (cursor.getInt(mStarredIndex) == 0) { @@ -180,32 +260,15 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { return cursor.getCount(); } - protected ContactEntry createContactEntryFromCursor(Cursor cursor, int position) { - // If the loader was canceled we will be given a null cursor. - // In that case, show an empty list of contacts. - if (cursor == null || cursor.isClosed() || cursor.getCount() <= position) return null; - - cursor.moveToPosition(position); - long id = cursor.getLong(mIdIndex); - String photoUri = cursor.getString(mPhotoUriIndex); - String lookupKey = cursor.getString(mLookupIndex); - - ContactEntry contact = new ContactEntry(); - String name = cursor.getString(mNameIndex); - contact.name = (name != null) ? name : mResources.getString(R.string.missing_name); - contact.status = cursor.getString(mStatusIndex); - contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null); - contact.lookupKey = ContentUris.withAppendedId( - Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id); - - // Set phone number and label - int phoneNumberType = cursor.getInt(mPhoneNumberTypeIndex); - String phoneNumberCustomLabel = cursor.getString(mPhoneNumberLabelIndex); - contact.phoneLabel = (String) Phone.getTypeLabel(mResources, phoneNumberType, - phoneNumberCustomLabel); - contact.phoneNumber = cursor.getString(mPhoneNumberIndex); - - return contact; + /** + * Loads a contact from the cached list. + * + * @param position Position of the Contact. + * @return Contact at the requested position. + */ + protected ContactEntry getContactEntryFromCache(int position) { + if (mContactEntries.size() <= position) return null; + return mContactEntries.get(position); } /** @@ -217,7 +280,7 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { @Override public int getCount() { - if (mContactCursor == null || mContactCursor.isClosed()) { + if (mContactEntries == null) { return 0; } @@ -244,6 +307,14 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { return mColumnCount * mMaxTiledRows; } + protected int getRowIndex(int entryIndex) { + if (entryIndex < mMaxTiledRows * mColumnCount) { + return entryIndex / mColumnCount; + } else { + return entryIndex - mMaxTiledRows * mColumnCount + mMaxTiledRows; + } + } + public int getColumnCount() { return mColumnCount; } @@ -261,7 +332,7 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { // Contacts that appear as tiles for (int columnCounter = 0; columnCounter < mColumnCount && contactIndex != maxContactsInTiles; columnCounter++) { - resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex)); + resultList.add(getContactEntryFromCache(contactIndex)); contactIndex++; } } else { @@ -269,7 +340,7 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { // The actual position of the contact in the cursor is simply total the number of // tiled contacts + the given position contactIndex = maxContactsInTiles + position - 1; - resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex)); + resultList.add(getContactEntryFromCache(contactIndex)); } return resultList; @@ -294,8 +365,75 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { return true; } + @Override + public void notifyDataSetChanged() { + if (DEBUG) { + Log.v(TAG, "nofigyDataSetChanged"); + } + 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) { + if (mInDragging) { + // If the one item above the row is being dragged, animates all following items to + // move up. If the item is a favorite tile, animate it to appear from right. + if (position >= getRowIndex(mDraggedEntryIndex)) { + if (itemViewType == ViewTypes.FREQUENT) { + mTranslateVerticalAnimation.setTarget(contactTileRowView); + mTranslateVerticalAnimation.setFloatValues(contactTileRowView.getHeight(), 0); + mTranslateVerticalAnimation.clone().start(); + } else { + contactTileRowView.animateTilesAppearLeft(mDraggedEntryIndex - + position * mColumnCount); + } + } + } else 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) { + Log.v(TAG, "get view for " + String.valueOf(position)); int itemViewType = getItemViewType(position); ContactTileRow contactTileRowView = (ContactTileRow) convertView; @@ -304,10 +442,13 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { if (contactTileRowView == null) { // Creating new row if needed - contactTileRowView = new ContactTileRow(mContext, itemViewType); + contactTileRowView = new ContactTileRow(mContext, itemViewType, position); } - contactTileRowView.configureRow(contactList, position == getCount() - 1); + contactTileRowView.configureRow(contactList, position, position == getCount() - 1); + + configureAnimationToView(contactTileRowView, position, itemViewType); + return contactTileRowView; } @@ -346,23 +487,101 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { return getRowCount(mNumStarred); } + /** + * Temporarily removes a contact from the list for UI refresh. Stores data for this contact + * in the back-up variable. + * + * @param index Position of the contact to be removed. + */ + public void popContactEntry(int index) { + if (index >= 0 && index < mContactEntries.size()) { + mDraggedEntry = mContactEntries.get(index); + mDraggedEntryIndex = index; + mContactEntries.remove(index); + notifyDataSetChanged(); + } + } + + /** + * Drops the temporarily removed contact to the desired location in the list. + * + * @param index Location where the contact will be dropped. + */ + public void dropContactEntry(int index) { + if (mDraggedEntry != null) { + if (index >= 0 && index <= mContactEntries.size()) { + mContactEntries.add(index, mDraggedEntry); + mDropEntryIndex = index; + } else if (mDraggedEntryIndex >= 0 && mDraggedEntryIndex <= mContactEntries.size()) { + /** If the index is invalid, falls back to the original position of the contact. */ + mContactEntries.add(mDraggedEntryIndex, mDraggedEntry); + mDropEntryIndex = mDraggedEntryIndex; + } + mDraggedEntry = null; + notifyDataSetChanged(); + } + } + + /** + * Sets an item to for pending removal. If the user does not click the undo button, the item + * will be removed at the next interaction. + * + * @param index Index of the item to be removed. + */ + public void setPotentialRemoveEntryIndex(int index) { + mPotentialRemoveEntryIndex = index; + } + + /** + * Removes a contact entry from the cache. + * + * @return True is an item is removed. False is there is no item to be removed. + */ + public boolean removeContactEntry() { + if (mPotentialRemoveEntryIndex >= 0 && mPotentialRemoveEntryIndex < mContactEntries.size()) { + mContactEntries.remove(mPotentialRemoveEntryIndex); + notifyDataSetChanged(); + return true; + } + return false; + } + + /** + * Resets the item for pending removal. + */ + public void undoPotentialRemoveEntryIndex() { + mPotentialRemoveEntryIndex = -1; + } + + /** + * Clears all temporary variables at a new interaction. + */ + public void cleanTempVariables() { + mDraggedEntryIndex = -1; + mDropEntryIndex = -1; + mDraggedEntry = null; + mPotentialRemoveEntryIndex = -1; + } + /** * Acts as a row item composed of {@link ContactTileView} * * TODO: FREQUENT doesn't really need it. Just let {@link #getView} return */ - private class ContactTileRow extends FrameLayout { + public class ContactTileRow extends FrameLayout { private int mItemViewType; private int mLayoutResId; private final int mRowPaddingStart; private final int mRowPaddingEnd; private final int mRowPaddingTop; private final int mRowPaddingBottom; + private int mPosition; - public ContactTileRow(Context context, int itemViewType) { + public ContactTileRow(Context context, int itemViewType, int position) { super(context); mItemViewType = itemViewType; mLayoutResId = getLayoutResourceId(mItemViewType); + mPosition = position; final Resources resources = mContext.getResources(); mRowPaddingStart = resources.getDimensionPixelSize( @@ -386,8 +605,9 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { /** * Configures the row to add {@link ContactEntry}s information to the views */ - public void configureRow(ArrayList list, boolean isLastRow) { + public void configureRow(ArrayList list, int position, boolean isLastRow) { int columnCount = mItemViewType == ViewTypes.FREQUENT ? 1 : mColumnCount; + mPosition = position; // Adding tiles to row and filling in contact information for (int columnCounter = 0; columnCounter < columnCount; columnCounter++) { @@ -398,11 +618,11 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { } private void addTileFromEntry(ContactEntry entry, int childIndex, boolean isLastRow) { - final ContactTileView contactTile; + final PhoneFavoriteTileView contactTile; if (getChildCount() <= childIndex) { - contactTile = (ContactTileView) inflate(mContext, mLayoutResId, null); + contactTile = (PhoneFavoriteTileView) inflate(mContext, mLayoutResId, null); // Note: the layoutparam set here is only actually used for FREQUENT. // We override onMeasure() for STARRED and we don't care the layout param there. final Resources resources = mContext.getResources(); @@ -411,18 +631,17 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { ViewGroup.LayoutParams.WRAP_CONTENT); params.setMargins( - resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), - 0, - resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), - 0); + resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), 0, + resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), 0); contactTile.setLayoutParams(params); contactTile.setPhotoManager(mPhotoManager); contactTile.setListener(mListener); addView(contactTile); } else { - contactTile = (ContactTileView) getChildAt(childIndex); + contactTile = (PhoneFavoriteTileView) getChildAt(childIndex); } contactTile.loadFromContact(entry); + contactTile.setId(childIndex); switch (mItemViewType) { case ViewTypes.TOP: // Setting divider visibilities @@ -436,6 +655,7 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { default: break; } + contactTile.setupFavoriteContactCard(); } @Override @@ -518,6 +738,65 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { } setMeasuredDimension(width, imageSize + getPaddingTop() + getPaddingBottom()); } + + /** + * Gets the index of the item at the specified coordinates. + * + * @param itemX X-coordinate of the selected item. + * @param itemY Y-coordinate of the selected item. + * @return Index of the selected item in the cached array. + */ + public int getItemIndex(float itemX, float itemY) { + if (mPosition < mMaxTiledRows) { + final Rect childRect = new Rect(); + if (DEBUG) { + Log.v(TAG, String.valueOf(itemX) + " " + String.valueOf(itemY)); + } + for (int i = 0; i < getChildCount(); ++i) { + /** If the row contains multiple tiles, checks each tile to see if the point + * is contained in the tile. */ + getChildAt(i).getHitRect(childRect); + if (DEBUG) { + Log.v(TAG, childRect.toString()); + } + if (childRect.contains((int)itemX, (int)itemY)) { + /** If the point is contained in the rectangle, computes the index of the + * item in the cached array. */ + return i + (mPosition) * mColumnCount; + } + } + } else { + /** If the selected item is one of the rows, compute the index. */ + return (mPosition - mMaxTiledRows) + mColumnCount * mMaxTiledRows; + } + return -1; + } + + public PhoneFavoritesTileAdapter getTileAdapter() { + 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; + } } protected static class ViewTypes { -- cgit v1.2.3