diff options
author | Yorke Lee <yorkelee@google.com> | 2013-08-13 10:10:57 -0700 |
---|---|---|
committer | Yorke Lee <yorkelee@google.com> | 2013-08-16 10:56:25 -0700 |
commit | 11ee58b1b8711c6d3b2ade6a71835b6c102a08a7 (patch) | |
tree | 5d12046a569043637fba7fbd72d1adeed570dc3d | |
parent | ba6b3366f21c47454b3ba5189e94bb6bf58fd11d (diff) |
Use swipe helper for swiping
* Use SwipeHelper for swiping, in SwipeableListView for regular favorites.
SwipeHelper and SwipeableListView are copied from DeskClock, with minor
modifications (to prevent swiping or call log items, and all contact buttons).
* Make ContactTileRow implement SwipeHelperCallback so that tiled favorites
can be swiped.
* Remove PhoneFavoriteGestureListener
* Add selectable item backgrounds to undo buttons on removal dialog
* Moved common code shared by PhoneFavoriteRegularRowView and
PhoneFavoriteSquareTileView to PhoneFavoriteTileView
* Standardize layout ids for phone_favorite_regular_row_view and phone_favorite_tile_view
* Add long click listener to PhoneFavoriteTileView to trigger the start of a drag
and drop operation
* Remove any contact entries that are in the removal dialog phase if the app is paused
Bug: 10257340
Bug: 10341201
Bug: 10328093
Bug: 10290239
Bug: 10262721
Bug: 10257340
Change-Id: I20448048b658759f6de75d643d2150be5a6ba8af
-rw-r--r-- | res/layout/phone_favorite_regular_row_view.xml | 8 | ||||
-rw-r--r-- | res/layout/phone_favorite_tile_view.xml | 12 | ||||
-rw-r--r-- | res/layout/phone_favorites_fragment.xml | 2 | ||||
-rw-r--r-- | res/values/animation_constants.xml | 30 | ||||
-rw-r--r-- | res/values/ids.xml | 22 | ||||
-rw-r--r-- | src/com/android/dialer/list/PhoneFavoriteDragAndDropListeners.java | 87 | ||||
-rw-r--r-- | src/com/android/dialer/list/PhoneFavoriteFragment.java | 13 | ||||
-rw-r--r-- | src/com/android/dialer/list/PhoneFavoriteRegularRowView.java | 24 | ||||
-rw-r--r-- | src/com/android/dialer/list/PhoneFavoriteSquareTileView.java | 18 | ||||
-rw-r--r-- | src/com/android/dialer/list/PhoneFavoriteTileView.java | 197 | ||||
-rw-r--r-- | src/com/android/dialer/list/PhoneFavoritesTileAdapter.java | 197 | ||||
-rw-r--r-- | src/com/android/dialer/list/SwipeHelper.java | 469 | ||||
-rw-r--r-- | src/com/android/dialer/list/SwipeableListView.java | 162 |
13 files changed, 972 insertions, 269 deletions
diff --git a/res/layout/phone_favorite_regular_row_view.xml b/res/layout/phone_favorite_regular_row_view.xml index 137d3da60..8ac01d666 100644 --- a/res/layout/phone_favorite_regular_row_view.xml +++ b/res/layout/phone_favorite_regular_row_view.xml @@ -17,7 +17,7 @@ <!-- Layout parameters are set programmatically. --> <view xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/contact_tile_frequent_phone" + android:id="@+id/contact_tile" class="com.android.dialer.list.PhoneFavoriteRegularRowView"> <RelativeLayout @@ -33,7 +33,7 @@ android:layout_height="64dip" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" - android:nextFocusRight="@id/contact_tile_frequent_phone" + android:nextFocusRight="@id/contact_tile" android:scaleType="centerCrop" android:focusable="true" /> @@ -88,7 +88,9 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:clickable="true" - android:layout_marginRight="30dp" + android:paddingStart="30dp" + android:paddingEnd="30dp" + android:background="?android:attr/selectableItemBackground" android:gravity="center_vertical"> <ImageView diff --git a/res/layout/phone_favorite_tile_view.xml b/res/layout/phone_favorite_tile_view.xml index 6242b96a2..a0ef1af0e 100644 --- a/res/layout/phone_favorite_tile_view.xml +++ b/res/layout/phone_favorite_tile_view.xml @@ -18,11 +18,11 @@ android:paddingBottom="1dip" android:paddingRight="1dip" android:paddingEnd="1dip" - android:background="@color/background_dialer_light" + android:id="@+id/contact_tile" class="com.android.dialer.list.PhoneFavoriteSquareTileView" > <RelativeLayout - android:id="@+id/contact_tile_favorite_card" + android:id="@+id/contact_favorite_card" android:layout_width="match_parent" android:layout_height="match_parent" android:focusable="true"> @@ -86,18 +86,19 @@ </RelativeLayout> <LinearLayout - android:id="@+id/favorite_tile_remove_dialogue" + android:id="@+id/favorite_remove_dialogue" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:gravity="center_horizontal" + android:background="@color/background_dialer_light" android:alpha="0.0" android:visibility="gone"> <TextView - android:id="@+id/favorite_tile_remove_dialogue_text" + android:id="@+id/favorite_remove_dialogue_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/favorite_hidden" @@ -112,11 +113,12 @@ android:textAlignment="center" /> <LinearLayout - android:id="@+id/favorite_tile_remove_undo_button" + android:id="@+id/favorite_remove_undo_button" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_alignParentLeft="true" + android:background="?android:attr/selectableItemBackground" android:clickable="true" android:layout_weight="1" android:layout_gravity="bottom"> diff --git a/res/layout/phone_favorites_fragment.xml b/res/layout/phone_favorites_fragment.xml index 2b6bbe447..6a3320bb7 100644 --- a/res/layout/phone_favorites_fragment.xml +++ b/res/layout/phone_favorites_fragment.xml @@ -28,7 +28,7 @@ android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> - <ListView + <com.android.dialer.list.SwipeableListView android:id="@+id/contact_tile_list" android:layout_width="match_parent" android:layout_height="match_parent" diff --git a/res/values/animation_constants.xml b/res/values/animation_constants.xml new file mode 100644 index 000000000..77b762739 --- /dev/null +++ b/res/values/animation_constants.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<resources> + <integer name="fade_duration">250</integer> + + <!-- Swipe constants --> + <integer name="swipe_escape_velocity">100</integer> + <integer name="escape_animation_duration">200</integer> + <integer name="max_escape_animation_duration">400</integer> + <integer name="max_dismiss_velocity">2000</integer> + <integer name="snap_animation_duration">350</integer> + <integer name="swipe_scroll_slop">2</integer> + <dimen name="min_swipe">5dip</dimen> + <dimen name="min_vert">10dip</dimen> + <dimen name="min_lock">20dip</dimen> +</resources>
\ No newline at end of file diff --git a/res/values/ids.xml b/res/values/ids.xml new file mode 100644 index 000000000..2b095043a --- /dev/null +++ b/res/values/ids.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<resources> + <item type="id" + name="is_swipeable_tag" /> + <item type="id" + name="contact_entry_index_tag" /> +</resources>
\ No newline at end of file diff --git a/src/com/android/dialer/list/PhoneFavoriteDragAndDropListeners.java b/src/com/android/dialer/list/PhoneFavoriteDragAndDropListeners.java index 034dc0ab2..dfc3c39ca 100644 --- a/src/com/android/dialer/list/PhoneFavoriteDragAndDropListeners.java +++ b/src/com/android/dialer/list/PhoneFavoriteDragAndDropListeners.java @@ -15,12 +15,9 @@ */ package com.android.dialer.list; -import android.content.ClipData; import android.graphics.Rect; 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; @@ -34,90 +31,6 @@ 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("", ""); - mView.setPressed(false); - if (mView instanceof PhoneFavoriteRegularRowView) { - // If the view is regular row, start drag the row view. - final View.DragShadowBuilder shadowBuilder = - new View.DragShadowBuilder(mView.getParentRow()); - mView.getParentRow().startDrag(data, shadowBuilder, null, 0); - } else { - // If the view is a tile view, start drag the tile. - final View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(mView); - 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(mView.getLeft() + x1, - mView.getTop() + 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. */ diff --git a/src/com/android/dialer/list/PhoneFavoriteFragment.java b/src/com/android/dialer/list/PhoneFavoriteFragment.java index a1406d242..948c70aa8 100644 --- a/src/com/android/dialer/list/PhoneFavoriteFragment.java +++ b/src/com/android/dialer/list/PhoneFavoriteFragment.java @@ -143,7 +143,7 @@ public class PhoneFavoriteFragment extends Fragment implements OnItemClickListen private CallLogQueryHandler mCallLogQueryHandler; private TextView mEmptyView; - private ListView mListView; + private SwipeableListView mListView; private View mShowAllContactsButton; /** @@ -191,6 +191,7 @@ public class PhoneFavoriteFragment extends Fragment implements OnItemClickListen super.onResume(); mCallLogQueryHandler.fetchCalls(CallLogQueryHandler.CALL_TYPE_ALL); mCallLogAdapter.setLoading(true); + getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE).forceLoad(); } @Override @@ -199,12 +200,13 @@ public class PhoneFavoriteFragment extends Fragment implements OnItemClickListen final View listLayout = inflater.inflate( R.layout.phone_favorites_fragment, container, false); - mListView = (ListView) listLayout.findViewById(R.id.contact_tile_list); + mListView = (SwipeableListView) listLayout.findViewById(R.id.contact_tile_list); mListView.setItemsCanFocus(true); mListView.setOnItemClickListener(this); mListView.setVerticalScrollBarEnabled(false); mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT); mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY); + mListView.setOnItemSwipeListener(mContactTileAdapter); mLoadingView = inflater.inflate(R.layout.phone_loading_contacts, mListView, false); mShowAllContactsButton = inflater.inflate(R.layout.show_all_contact_button, mListView, @@ -312,4 +314,11 @@ public class PhoneFavoriteFragment extends Fragment implements OnItemClickListen @Override public void fetchCalls() { } + + @Override + public void onPause() { + // If there are any pending contact entries that are to be removed, remove them + mContactTileAdapter.removePendingContactEntry(); + super.onPause(); + } } diff --git a/src/com/android/dialer/list/PhoneFavoriteRegularRowView.java b/src/com/android/dialer/list/PhoneFavoriteRegularRowView.java index 14c1043f4..f9c2c84b6 100644 --- a/src/com/android/dialer/list/PhoneFavoriteRegularRowView.java +++ b/src/com/android/dialer/list/PhoneFavoriteRegularRowView.java @@ -19,11 +19,11 @@ import android.content.Context; import android.content.res.Resources; import android.util.AttributeSet; import android.view.GestureDetector; +import android.view.View; 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; @@ -40,7 +40,7 @@ public class PhoneFavoriteRegularRowView extends PhoneFavoriteTileView { protected void onFinishInflate() { super.onFinishInflate(); - mFavoriteContactCard = findViewById(R.id.contact_favorite_card); + final View favoriteContactCard = findViewById(R.id.contact_favorite_card); final int rowPaddingStart; final int rowPaddingEnd; @@ -57,26 +57,8 @@ public class PhoneFavoriteRegularRowView extends PhoneFavoriteTileView { rowPaddingBottom = resources.getDimensionPixelSize( R.dimen.favorites_row_bottom_padding); - mFavoriteContactCard.setPaddingRelative(rowPaddingStart, rowPaddingTop, rowPaddingEnd, + favoriteContactCard.setPaddingRelative(rowPaddingStart, rowPaddingTop, rowPaddingEnd, rowPaddingBottom); - - mRemovalDialogue = findViewById(R.id.favorite_remove_dialogue); - mUndoRemovalButton = findViewById(R.id.favorite_remove_undo_button); - - mGestureDetector = new GestureDetector(getContext(), - new PhoneFavoriteGestureListener(this)); - } - - @Override - protected void onAttachedToWindow() { - mParentRow = (ContactTileRow) getParent(); - mParentRow.setOnDragListener(new PhoneFavoriteDragListener(mParentRow, - mParentRow.getTileAdapter())); - } - - @Override - protected boolean isDarkTheme() { - return false; } @Override diff --git a/src/com/android/dialer/list/PhoneFavoriteSquareTileView.java b/src/com/android/dialer/list/PhoneFavoriteSquareTileView.java index fe07d188e..3bfe9a99d 100644 --- a/src/com/android/dialer/list/PhoneFavoriteSquareTileView.java +++ b/src/com/android/dialer/list/PhoneFavoriteSquareTileView.java @@ -25,7 +25,6 @@ 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; /** @@ -44,9 +43,6 @@ public class PhoneFavoriteSquareTileView extends PhoneFavoriteTileView { 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 @@ -58,20 +54,6 @@ public class PhoneFavoriteSquareTileView extends PhoneFavoriteTileView { 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 diff --git a/src/com/android/dialer/list/PhoneFavoriteTileView.java b/src/com/android/dialer/list/PhoneFavoriteTileView.java index 8903e4b9b..dc82f73a4 100644 --- a/src/com/android/dialer/list/PhoneFavoriteTileView.java +++ b/src/com/android/dialer/list/PhoneFavoriteTileView.java @@ -16,8 +16,11 @@ */ package com.android.dialer.list; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; +import android.content.ClipData; import android.content.Context; import android.text.TextUtils; import android.util.AttributeSet; @@ -29,7 +32,9 @@ import android.view.View; 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.PhoneFavoriteDragAndDropListeners.PhoneFavoriteDragListener; import com.android.dialer.list.PhoneFavoritesTileAdapter.ContactTileRow; +import com.android.dialer.list.PhoneFavoritesTileAdapter.ViewTypes; /** * A light version of the {@link com.android.contacts.common.list.ContactTileView} that is used in @@ -47,11 +52,11 @@ public abstract class PhoneFavoriteTileView extends ContactTileView { private static final int ANIMATION_LENGTH = 300; /** The view that holds the front layer of the favorite contact card. */ - protected View mFavoriteContactCard; + private View mFavoriteContactCard; /** The view that holds the background layer of the removal dialogue. */ - protected View mRemovalDialogue; + private View mRemovalDialogue; /** Undo button for undoing favorite removal. */ - protected View mUndoRemovalButton; + private View mUndoRemovalButton; /** The view that holds the list view row. */ protected ContactTileRow mParentRow; @@ -60,8 +65,6 @@ public abstract class PhoneFavoriteTileView extends ContactTileView { /** 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); @@ -72,6 +75,42 @@ public abstract class PhoneFavoriteTileView extends ContactTileView { } @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mFavoriteContactCard = findViewById(com.android.dialer.R.id.contact_favorite_card); + mRemovalDialogue = findViewById(com.android.dialer.R.id.favorite_remove_dialogue); + mUndoRemovalButton = findViewById(com.android.dialer.R.id + .favorite_remove_undo_button); + + mUndoRemovalButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + undoRemove(); + } + }); + + setOnLongClickListener(new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + setPressed(false); + final PhoneFavoriteTileView view = (PhoneFavoriteTileView) v; + final ClipData data = ClipData.newPlainText("", ""); + if (view instanceof PhoneFavoriteRegularRowView) { + // If the view is regular row, start drag the row view. + final View.DragShadowBuilder shadowBuilder = + new View.DragShadowBuilder(view.getParentRow()); + view.getParentRow().startDrag(data, shadowBuilder, null, 0); + } else { + // If the view is a tile view, start drag the tile. + final View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view); + view.startDrag(data, shadowBuilder, null, 0); + } + return true; + } + }); + } + + @Override public void loadFromContact(ContactEntry entry) { super.loadFromContact(entry); mPhoneNumberString = null; // ... in case we're reusing the view @@ -89,74 +128,23 @@ public abstract class PhoneFavoriteTileView extends ContactTileView { } } - /** - * 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); - } - } - - /** - * 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; + public void displayRemovalDialog() { + mRemovalDialogue.setVisibility(VISIBLE); + mRemovalDialogue.setAlpha(0f); + final int animationLength = ANIMATION_LENGTH; + final AnimatorSet animSet = new AnimatorSet(); + final ObjectAnimator fadeIn = ObjectAnimator.ofFloat(mRemovalDialogue, "alpha", + 1.f).setDuration(animationLength); - 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); - animationLength = Math.max(0, animationLength); - final ObjectAnimator fadeOut = ObjectAnimator.ofFloat(mFavoriteContactCard, "alpha", - 0.f).setDuration(animationLength); - final ObjectAnimator moveAway = ObjectAnimator.ofFloat(mFavoriteContactCard, - "translationX", getWidth()).setDuration(animationLength); + if (mParentRow.getItemViewType() == ViewTypes.FREQUENT) { 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(); + animSet.playTogether(fadeIn, backgroundFadeIn); + } else { + animSet.playTogether(fadeIn); } + + animSet.start(); } /** @@ -177,24 +165,20 @@ public abstract class PhoneFavoriteTileView extends ContactTileView { 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() { + animSet.addListener(new AnimatorListenerAdapter() { @Override - public void onClick(View view) { - undoRemove(); + public void onAnimationEnd(Animator animation) { + if (mParentRow.getItemViewType() == ViewTypes.FREQUENT) { + SwipeHelper.setSwipeable(mParentRow, true); + } else { + SwipeHelper.setSwipeable(PhoneFavoriteTileView.this, true); + } } }); + + + // Signals the PhoneFavoritesTileAdapter to undo the potential delete. + mParentRow.getTileAdapter().undoPotentialRemoveEntryIndex(); } /** @@ -210,11 +194,24 @@ public abstract class PhoneFavoriteTileView extends ContactTileView { } @Override + protected void onAttachedToWindow() { + mParentRow = (ContactTileRow) getParent(); + mParentRow.setOnDragListener(new PhoneFavoriteDragListener(mParentRow, + mParentRow.getTileAdapter())); + } + + @Override + protected boolean isDarkTheme() { + return false; + } + + @Override protected OnClickListener createClickListener() { return new OnClickListener() { @Override public void onClick(View v) { - if (mListener == null) return; + // When the removal dialog is present, don't allow a click to call + if (mListener == null || mRemovalDialogue.isShown()) return; if (TextUtils.isEmpty(mPhoneNumberString)) { // Copy "superclass" implementation mListener.onContactSelected(getLookupUri(), MoreContactUtils @@ -230,34 +227,4 @@ public abstract class PhoneFavoriteTileView extends ContactTileView { } }; } - - @Override - 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 b4e00fb3b..c7c8ae83c 100644 --- a/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java +++ b/src/com/android/dialer/list/PhoneFavoritesTileAdapter.java @@ -29,7 +29,9 @@ import android.provider.ContactsContract.PinnedPositions; import android.text.TextUtils; import android.util.Log; import android.util.LongSparseArray; +import android.view.MotionEvent; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.FrameLayout; @@ -40,6 +42,8 @@ import com.android.contacts.common.R; import com.android.contacts.common.list.ContactEntry; import com.android.contacts.common.list.ContactTileAdapter.DisplayType; import com.android.contacts.common.list.ContactTileView; +import com.android.dialer.list.SwipeHelper.OnItemGestureListener; +import com.android.dialer.list.SwipeHelper.SwipeHelperCallback; import com.android.internal.annotations.VisibleForTesting; import com.google.common.collect.ComparisonChain; @@ -56,7 +60,8 @@ import java.util.PriorityQueue; * This adapter has been rewritten to only support a maximum of one row for favorites. * */ -public class PhoneFavoritesTileAdapter extends BaseAdapter { +public class PhoneFavoritesTileAdapter extends BaseAdapter implements + SwipeHelper.OnItemGestureListener { private static final String TAG = PhoneFavoritesTileAdapter.class.getSimpleName(); private static final boolean DEBUG = false; @@ -393,24 +398,42 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { @Override public ArrayList<ContactEntry> getItem(int position) { ArrayList<ContactEntry> resultList = new ArrayList<ContactEntry>(mColumnCount); - int contactIndex = position * mColumnCount; + + final int entryIndex = getFirstContactEntryIndexForPosition(position); + + final int viewType = getItemViewType(position); + + final int columnCount; + if (viewType == ViewTypes.TOP) { + columnCount = mColumnCount; + } else { + columnCount = 1; + } + + for (int i = 0; i < columnCount; i++) { + final ContactEntry entry = getContactEntryFromCache(entryIndex + i); + if (entry == null) break; // less than mColumnCount contacts + resultList.add(entry); + } + + return resultList; + } + + /* + * Given a position in the adapter, returns the index of the first contact entry that is to be + * in that row. + */ + private int getFirstContactEntryIndexForPosition(int position) { final int maxContactsInTiles = getMaxContactsInTiles(); if (position < getRowCount(maxContactsInTiles)) { // Contacts that appear as tiles - for (int columnCounter = 0; columnCounter < mColumnCount && - contactIndex != maxContactsInTiles; columnCounter++) { - resultList.add(getContactEntryFromCache(contactIndex)); - contactIndex++; - } + return position * mColumnCount; } else { // Contacts that appear as rows // 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(getContactEntryFromCache(contactIndex)); + return maxContactsInTiles + position - 1; } - - return resultList; } @Override @@ -606,13 +629,16 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { * * @return True is an item is removed. False is there is no item to be removed. */ - public boolean removeContactEntry() { + public boolean removePendingContactEntry() { + boolean removed = false; if (mPotentialRemoveEntryIndex >= 0 && mPotentialRemoveEntryIndex < mContactEntries.size()) { - final ContactEntry entry = mContactEntries.get(mPotentialRemoveEntryIndex); + final ContactEntry entry = mContactEntries.remove(mPotentialRemoveEntryIndex); unstarAndUnpinContact(entry.lookupKey); - return true; + notifyDataSetChanged(); + removed = true; } - return false; + cleanTempVariables(); + return removed; } /** @@ -636,7 +662,9 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { * Acts as a row item composed of {@link ContactTileView} * */ - public class ContactTileRow extends FrameLayout { + public class ContactTileRow extends FrameLayout implements SwipeHelperCallback { + public static final int CONTACT_ENTRY_INDEX_TAG = R.id.contact_entry_index_tag; + private int mItemViewType; private int mLayoutResId; private final int mRowPaddingStart; @@ -644,6 +672,8 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { private final int mRowPaddingTop; private final int mRowPaddingBottom; private int mPosition; + private SwipeHelper mSwipeHelper; + private OnItemGestureListener mOnItemSwipeListener; public ContactTileRow(Context context, int itemViewType, int position) { super(context); @@ -680,6 +710,20 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { // Remove row (but not children) from accessibility node tree. setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + + if (mItemViewType == ViewTypes.FREQUENT) { + // ListView handles swiping for this item + SwipeHelper.setSwipeable(this, true); + } else if (mItemViewType == ViewTypes.TOP) { + // The contact tile row has its own swipe helpers, that makes each individual + // tile swipeable. + final float densityScale = getResources().getDisplayMetrics().density; + final float pagingTouchSlop = ViewConfiguration.get(context) + .getScaledPagingTouchSlop(); + mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this, densityScale, + pagingTouchSlop); + mOnItemSwipeListener = PhoneFavoritesTileAdapter.this; + } } /** @@ -723,20 +767,29 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { contactTile = (PhoneFavoriteTileView) getChildAt(childIndex); } contactTile.loadFromContact(entry); - contactTile.setId(childIndex); + + int entryIndex = -1; switch (mItemViewType) { case ViewTypes.TOP: // Setting divider visibilities contactTile.setPaddingRelative(0, 0, childIndex >= mColumnCount - 1 ? 0 : mPaddingInPixels, 0); + entryIndex = getFirstContactEntryIndexForPosition(mPosition) + childIndex; + SwipeHelper.setSwipeable(contactTile, true); break; case ViewTypes.FREQUENT: contactTile.setHorizontalDividerVisibility( isLastRow ? View.GONE : View.VISIBLE); + entryIndex = getFirstContactEntryIndexForPosition(mPosition); + SwipeHelper.setSwipeable(this, true); break; default: break; } + // tag the tile with the index of the contact entry it is associated with + if (entryIndex != -1) { + contactTile.setTag(CONTACT_ENTRY_INDEX_TAG, entryIndex); + } contactTile.setupFavoriteContactCard(); } @@ -879,6 +932,97 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { public int getPosition() { return mPosition; } + + @Override + public View getChildAtPosition(MotionEvent ev) { + // find the view under the pointer, accounting for GONE views + final int count = getChildCount(); + final int touchX = (int) ev.getX(); + View slidingChild; + for (int childIdx = 0; childIdx < count; childIdx++) { + slidingChild = getChildAt(childIdx); + if (slidingChild.getVisibility() == GONE) { + continue; + } + if (touchX >= slidingChild.getLeft() && touchX <= slidingChild.getRight()) { + if (SwipeHelper.isSwipeable(slidingChild)) { + // If this view is swipable, then return it. If not, because the removal + // dialog is currently showing, then return a null view, which will simply + // be ignored by the swipe helper. + return slidingChild; + } else { + return null; + } + } + } + return null; + } + + @Override + public View getChildContentView(View v) { + return v.findViewById(R.id.contact_favorite_card); + } + + @Override + public void onScroll() {} + + @Override + public boolean canChildBeDismissed(View v) { + return true; + } + + @Override + public void onBeginDrag(View v) { + removePendingContactEntry(); + final int index = indexOfChild(v); + // Move tile to front so that any overlap will be hidden behind its siblings + if (index > 0) { + detachViewFromParent(index); + attachViewToParent(v, 0, v.getLayoutParams()); + } + + // We do this so the underlying ScrollView knows that it won't get + // the chance to intercept events anymore + requestDisallowInterceptTouchEvent(true); + } + + @Override + public void onChildDismissed(View v) { + if (v != null) { + if (mOnItemSwipeListener != null) { + mOnItemSwipeListener.onSwipe(v); + } + } + } + + @Override + public void onDragCancelled(View v) {} + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (mSwipeHelper != null) { + return mSwipeHelper.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev); + } else { + return super.onInterceptTouchEvent(ev); + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mSwipeHelper != null) { + return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev); + } else { + return super.onTouchEvent(ev); + } + } + + public int getItemViewType() { + return mItemViewType; + } + + public void setOnItemSwipeListener(OnItemGestureListener listener) { + mOnItemSwipeListener = listener; + } } /** @@ -1004,4 +1148,23 @@ public class PhoneFavoritesTileAdapter extends BaseAdapter { public static final int FREQUENT = 0; public static final int TOP = 1; } + + @Override + public void onSwipe(View view) { + final PhoneFavoriteTileView tileView = (PhoneFavoriteTileView) view.findViewById( + R.id.contact_tile); + // When the view is in the removal dialog, it should no longer be swipeable + SwipeHelper.setSwipeable(view, false); + tileView.displayRemovalDialog(); + + final Integer entryIndex = (Integer) tileView.getTag( + ContactTileRow.CONTACT_ENTRY_INDEX_TAG); + + setPotentialRemoveEntryIndex(entryIndex); + } + + @Override + public void onTouch() { + removePendingContactEntry(); + } } diff --git a/src/com/android/dialer/list/SwipeHelper.java b/src/com/android/dialer/list/SwipeHelper.java new file mode 100644 index 000000000..3bcad2d95 --- /dev/null +++ b/src/com/android/dialer/list/SwipeHelper.java @@ -0,0 +1,469 @@ +/* + * Copyright (C) 2012 Google Inc. + * Licensed to 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.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.RectF; +import android.util.Log; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.animation.LinearInterpolator; + +import com.android.dialer.R; + +/** + * Copy of packages/apps/UnifiedEmail - com.android.mail.ui.SwipeHelper with changes. + */ +public class SwipeHelper { + static final String TAG = SwipeHelper.class.getSimpleName(); + private static final boolean DEBUG_INVALIDATE = false; + private static final boolean CONSTRAIN_SWIPE = true; + private static final boolean FADE_OUT_DURING_SWIPE = true; + private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true; + private static final boolean LOG_SWIPE_DISMISS_VELOCITY = false; // STOPSHIP - DEBUG ONLY + + public static final int IS_SWIPEABLE_TAG = R.id.is_swipeable_tag; + public static final Object IS_SWIPEABLE = new Object(); + + public static final int X = 0; + public static final int Y = 1; + + private static LinearInterpolator sLinearInterpolator = new LinearInterpolator(); + + private static int SWIPE_ESCAPE_VELOCITY = -1; + private static int DEFAULT_ESCAPE_ANIMATION_DURATION; + private static int MAX_ESCAPE_ANIMATION_DURATION; + private static int MAX_DISMISS_VELOCITY; + private static int SNAP_ANIM_LEN; + private static int SWIPE_SCROLL_SLOP; + private static float MIN_SWIPE; + private static float MIN_VERT; + private static float MIN_LOCK; + + public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width + // where fade starts + static final float ALPHA_FADE_END = 0.7f; // fraction of thumbnail width + // beyond which alpha->0 + private static final float FACTOR = 1.2f; + + private static final int PROTECTION_PADDING = 50; + + private float mMinAlpha = 0.3f; + + private float mPagingTouchSlop; + private final SwipeHelperCallback mCallback; + private final int mSwipeDirection; + private final VelocityTracker mVelocityTracker; + + private float mInitialTouchPosX; + private boolean mDragging; + private View mCurrView; + private View mCurrAnimView; + private boolean mCanCurrViewBeDimissed; + private float mDensityScale; + private float mLastY; + private float mInitialTouchPosY; + + private float mStartAlpha; + private boolean mProtected = false; + + public SwipeHelper(Context context, int swipeDirection, SwipeHelperCallback callback, float densityScale, + float pagingTouchSlop) { + mCallback = callback; + mSwipeDirection = swipeDirection; + mVelocityTracker = VelocityTracker.obtain(); + mDensityScale = densityScale; + mPagingTouchSlop = pagingTouchSlop; + if (SWIPE_ESCAPE_VELOCITY == -1) { + Resources res = context.getResources(); + SWIPE_ESCAPE_VELOCITY = res.getInteger(R.integer.swipe_escape_velocity); + DEFAULT_ESCAPE_ANIMATION_DURATION = res.getInteger(R.integer.escape_animation_duration); + MAX_ESCAPE_ANIMATION_DURATION = res.getInteger(R.integer.max_escape_animation_duration); + MAX_DISMISS_VELOCITY = res.getInteger(R.integer.max_dismiss_velocity); + SNAP_ANIM_LEN = res.getInteger(R.integer.snap_animation_duration); + SWIPE_SCROLL_SLOP = res.getInteger(R.integer.swipe_scroll_slop); + MIN_SWIPE = res.getDimension(R.dimen.min_swipe); + MIN_VERT = res.getDimension(R.dimen.min_vert); + MIN_LOCK = res.getDimension(R.dimen.min_lock); + } + } + + public void setDensityScale(float densityScale) { + mDensityScale = densityScale; + } + + public void setPagingTouchSlop(float pagingTouchSlop) { + mPagingTouchSlop = pagingTouchSlop; + } + + private float getVelocity(VelocityTracker vt) { + return mSwipeDirection == X ? vt.getXVelocity() : + vt.getYVelocity(); + } + + private ObjectAnimator createTranslationAnimation(View v, float newPos) { + ObjectAnimator anim = ObjectAnimator.ofFloat(v, + mSwipeDirection == X ? "translationX" : "translationY", newPos); + return anim; + } + + private ObjectAnimator createDismissAnimation(View v, float newPos, int duration) { + ObjectAnimator anim = createTranslationAnimation(v, newPos); + anim.setInterpolator(sLinearInterpolator); + anim.setDuration(duration); + return anim; + } + + private float getPerpendicularVelocity(VelocityTracker vt) { + return mSwipeDirection == X ? vt.getYVelocity() : + vt.getXVelocity(); + } + + private void setTranslation(View v, float translate) { + if (mSwipeDirection == X) { + v.setTranslationX(translate); + } else { + v.setTranslationY(translate); + } + } + + private float getSize(View v) { + return mSwipeDirection == X ? v.getMeasuredWidth() : + v.getMeasuredHeight(); + } + + public void setMinAlpha(float minAlpha) { + mMinAlpha = minAlpha; + } + + private float getAlphaForOffset(View view) { + float viewSize = getSize(view); + final float fadeSize = ALPHA_FADE_END * viewSize; + float result = mStartAlpha; + float pos = view.getTranslationX(); + if (pos >= viewSize * ALPHA_FADE_START) { + result = mStartAlpha - (pos - viewSize * ALPHA_FADE_START) / fadeSize; + } else if (pos < viewSize * (mStartAlpha - ALPHA_FADE_START)) { + result = mStartAlpha + (viewSize * ALPHA_FADE_START + pos) / fadeSize; + } + return Math.max(mMinAlpha, result); + } + + // invalidate the view's own bounds all the way up the view hierarchy + public static void invalidateGlobalRegion(View view) { + invalidateGlobalRegion( + view, + new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom())); + } + + // invalidate a rectangle relative to the view's coordinate system all the way up the view + // hierarchy + public static void invalidateGlobalRegion(View view, RectF childBounds) { + // childBounds.offset(view.getTranslationX(), view.getTranslationY()); + if (DEBUG_INVALIDATE) + Log.v(TAG, "-------------"); + while (view.getParent() != null && view.getParent() instanceof View) { + view = (View) view.getParent(); + view.getMatrix().mapRect(childBounds); + view.invalidate((int) Math.floor(childBounds.left), + (int) Math.floor(childBounds.top), + (int) Math.ceil(childBounds.right), + (int) Math.ceil(childBounds.bottom)); + if (DEBUG_INVALIDATE) { + Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left) + + "," + (int) Math.floor(childBounds.top) + + "," + (int) Math.ceil(childBounds.right) + + "," + (int) Math.ceil(childBounds.bottom)); + } + } + } + + public boolean onInterceptTouchEvent(MotionEvent ev) { + final int action = ev.getAction(); + switch (action) { + case MotionEvent.ACTION_DOWN: + mLastY = ev.getY(); + mDragging = false; + mCurrView = mCallback.getChildAtPosition(ev); + mVelocityTracker.clear(); + if (mCurrView != null) { + mCurrAnimView = mCallback.getChildContentView(mCurrView); + mStartAlpha = mCurrAnimView.getAlpha(); + mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); + mVelocityTracker.addMovement(ev); + mInitialTouchPosX = ev.getX(); + mInitialTouchPosY = ev.getY(); + } + break; + case MotionEvent.ACTION_MOVE: + if (mCurrView != null) { + // Check the movement direction. + if (mLastY >= 0 && !mDragging) { + float currY = ev.getY(); + float currX = ev.getX(); + float deltaY = Math.abs(currY - mInitialTouchPosY); + float deltaX = Math.abs(currX - mInitialTouchPosX); + if (deltaY > SWIPE_SCROLL_SLOP && deltaY > (FACTOR * deltaX)) { + mLastY = ev.getY(); + mCallback.onScroll(); + return false; + } + } + mVelocityTracker.addMovement(ev); + float pos = ev.getX(); + float delta = pos - mInitialTouchPosX; + if (Math.abs(delta) > mPagingTouchSlop) { + mCallback.onBeginDrag(mCallback.getChildContentView(mCurrView)); + mDragging = true; + mInitialTouchPosX = ev.getX() - mCurrAnimView.getTranslationX(); + mInitialTouchPosY = ev.getY(); + } + } + mLastY = ev.getY(); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mDragging = false; + mCurrView = null; + mCurrAnimView = null; + mLastY = -1; + break; + } + return mDragging; + } + + /** + * @param view The view to be dismissed + * @param velocity The desired pixels/second speed at which the view should + * move + */ + private void dismissChild(final View view, float velocity) { + final View animView = mCallback.getChildContentView(view); + final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); + float newPos = determinePos(animView, velocity); + int duration = determineDuration(animView, newPos, velocity); + + animView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + ObjectAnimator anim = createDismissAnimation(animView, newPos, duration); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCallback.onChildDismissed(mCurrView); + animView.setLayerType(View.LAYER_TYPE_NONE, null); + } + }); + anim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { + animView.setAlpha(getAlphaForOffset(animView)); + } + invalidateGlobalRegion(animView); + } + }); + anim.start(); + } + + private int determineDuration(View animView, float newPos, float velocity) { + int duration = MAX_ESCAPE_ANIMATION_DURATION; + if (velocity != 0) { + duration = Math + .min(duration, + (int) (Math.abs(newPos - animView.getTranslationX()) * 1000f / Math + .abs(velocity))); + } else { + duration = DEFAULT_ESCAPE_ANIMATION_DURATION; + } + return duration; + } + + private float determinePos(View animView, float velocity) { + float newPos = 0; + if (velocity < 0 || (velocity == 0 && animView.getTranslationX() < 0) + // if we use the Menu to dismiss an item in landscape, animate up + || (velocity == 0 && animView.getTranslationX() == 0 && mSwipeDirection == Y)) { + newPos = -getSize(animView); + } else { + newPos = getSize(animView); + } + return newPos; + } + + public void snapChild(final View view, float velocity) { + final View animView = mCallback.getChildContentView(view); + final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); + ObjectAnimator anim = createTranslationAnimation(animView, 0); + int duration = SNAP_ANIM_LEN; + anim.setDuration(duration); + anim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { + animView.setAlpha(getAlphaForOffset(animView)); + } + invalidateGlobalRegion(animView); + } + }); + anim.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + animView.setAlpha(mStartAlpha); + mCallback.onDragCancelled(mCurrView); + } + + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + }); + anim.start(); + } + + public boolean onTouchEvent(MotionEvent ev) { + if (!mDragging || mProtected) { + return false; + } + mVelocityTracker.addMovement(ev); + final int action = ev.getAction(); + switch (action) { + case MotionEvent.ACTION_OUTSIDE: + case MotionEvent.ACTION_MOVE: + if (mCurrView != null) { + float deltaX = ev.getX() - mInitialTouchPosX; + float deltaY = Math.abs(ev.getY() - mInitialTouchPosY); + // If the user has gone vertical and not gone horizontalish AT + // LEAST minBeforeLock, switch to scroll. Otherwise, cancel + // the swipe. + if (!mDragging && deltaY > MIN_VERT && (Math.abs(deltaX)) < MIN_LOCK + && deltaY > (FACTOR * Math.abs(deltaX))) { + mCallback.onScroll(); + return false; + } + float minDistance = MIN_SWIPE; + if (Math.abs(deltaX) < minDistance) { + // Don't start the drag until at least X distance has + // occurred. + return true; + } + // don't let items that can't be dismissed be dragged more + // than maxScrollDistance + if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) { + float size = getSize(mCurrAnimView); + float maxScrollDistance = 0.15f * size; + if (Math.abs(deltaX) >= size) { + deltaX = deltaX > 0 ? maxScrollDistance : -maxScrollDistance; + } else { + deltaX = maxScrollDistance + * (float) Math.sin((deltaX / size) * (Math.PI / 2)); + } + } + setTranslation(mCurrAnimView, deltaX); + if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) { + mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView)); + } + invalidateGlobalRegion(mCallback.getChildContentView(mCurrView)); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (mCurrView != null) { + float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; + mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); + float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; + float velocity = getVelocity(mVelocityTracker); + float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker); + + // Decide whether to dismiss the current view + // Tweak constants below as required to prevent erroneous + // swipe/dismiss + float translation = Math.abs(mCurrAnimView.getTranslationX()); + float currAnimViewSize = getSize(mCurrAnimView); + // Long swipe = translation of .4 * width + boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH + && translation > 0.4 * currAnimViewSize; + // Fast swipe = > escapeVelocity and translation of .1 * + // width + boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) + && (Math.abs(velocity) > Math.abs(perpendicularVelocity)) + && (velocity > 0) == (mCurrAnimView.getTranslationX() > 0) + && translation > 0.05 * currAnimViewSize; + if (LOG_SWIPE_DISMISS_VELOCITY) { + Log.v(TAG, "Swipe/Dismiss: " + velocity + "/" + escapeVelocity + "/" + + perpendicularVelocity + ", x: " + translation + "/" + + currAnimViewSize); + } + + boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) + && (childSwipedFastEnough || childSwipedFarEnough); + + if (dismissChild) { + dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); + } else { + snapChild(mCurrView, velocity); + } + } + break; + } + return true; + } + + public static void setSwipeable(View view, boolean swipeable) { + view.setTag(IS_SWIPEABLE_TAG, swipeable ? IS_SWIPEABLE : null); + } + + public static boolean isSwipeable(View view) { + return IS_SWIPEABLE == view.getTag(IS_SWIPEABLE_TAG); + } + + public interface SwipeHelperCallback { + View getChildAtPosition(MotionEvent ev); + + View getChildContentView(View v); + + void onScroll(); + + boolean canChildBeDismissed(View v); + + void onBeginDrag(View v); + + void onChildDismissed(View v); + + void onDragCancelled(View v); + + } + + public interface OnItemGestureListener { + public void onSwipe(View view); + + public void onTouch(); + } +}
\ No newline at end of file diff --git a/src/com/android/dialer/list/SwipeableListView.java b/src/com/android/dialer/list/SwipeableListView.java new file mode 100644 index 000000000..504b1403b --- /dev/null +++ b/src/com/android/dialer/list/SwipeableListView.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2012 Google Inc. + * Licensed to 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.res.Configuration; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.ListView; + +import com.android.dialer.R; +import com.android.dialer.list.PhoneFavoritesTileAdapter.ContactTileRow; +import com.android.dialer.list.SwipeHelper.OnItemGestureListener; +import com.android.dialer.list.SwipeHelper.SwipeHelperCallback; + +/** + * Copy of packages/apps/UnifiedEmail - com.android.mail.ui.Swipeable with changes. + */ +public class SwipeableListView extends ListView implements SwipeHelperCallback { + private SwipeHelper mSwipeHelper; + private boolean mEnableSwipe = true; + + public static final String LOG_TAG = SwipeableListView.class.getSimpleName(); + + private OnItemGestureListener mOnItemGestureListener; + + public SwipeableListView(Context context) { + this(context, null); + } + + public SwipeableListView(Context context, AttributeSet attrs) { + this(context, attrs, -1); + } + + public SwipeableListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + float densityScale = getResources().getDisplayMetrics().density; + float pagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop(); + mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this, densityScale, + pagingTouchSlop); + setItemsCanFocus(true); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + float densityScale = getResources().getDisplayMetrics().density; + mSwipeHelper.setDensityScale(densityScale); + float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); + mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); + } + + /** + * Enable swipe gestures. + */ + public void enableSwipe(boolean enable) { + mEnableSwipe = enable; + } + + public boolean isSwipeEnabled() { + return mEnableSwipe; + } + + public void setOnItemSwipeListener(OnItemGestureListener listener) { + mOnItemGestureListener = listener; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (mEnableSwipe) { + return mSwipeHelper.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev); + } else { + return super.onInterceptTouchEvent(ev); + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mOnItemGestureListener != null) { + mOnItemGestureListener.onTouch(); + } + if (mEnableSwipe) { + return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev); + } else { + return super.onTouchEvent(ev); + } + } + + @Override + public View getChildAtPosition(MotionEvent ev) { + // find the view under the pointer, accounting for GONE views + final int count = getChildCount(); + final int touchY = (int) ev.getY(); + View slidingChild; + for (int childIdx = 0; childIdx < count; childIdx++) { + slidingChild = getChildAt(childIdx); + if (slidingChild.getVisibility() == GONE) { + continue; + } + if (touchY >= slidingChild.getTop() && touchY <= slidingChild.getBottom()) { + if (SwipeHelper.isSwipeable(slidingChild)) { + // If this view is swipable in this listview, then return it. Otherwise + // return a null view, which will simply be ignored by the swipe helper. + return slidingChild; + } else { + return null; + } + } + } + return null; + } + + @Override + public View getChildContentView(View view) { + return view.findViewById(R.id.contact_favorite_card); + } + + @Override + public void onScroll() {} + + @Override + public boolean canChildBeDismissed(View v) { + return SwipeHelper.isSwipeable(v); + } + + @Override + public void onChildDismissed(final View v) { + if (v != null) { + if (mOnItemGestureListener != null) { + mOnItemGestureListener.onSwipe(v); + } + } + } + + @Override + public void onDragCancelled(View v) {} + + @Override + public void onBeginDrag(View v) { + // We do this so the underlying ScrollView knows that it won't get + // the chance to intercept events anymore + requestDisallowInterceptTouchEvent(true); + } +}
\ No newline at end of file |