Re-enable swipe.
Swipes all checked items at once
Tap highlight
Properly colored background
Doesnt swipe non checked items when there are checked items
Change-Id: Id71e331d35f75ee02813dee8376d764386221868
diff --git a/proguard.flags b/proguard.flags
index c3f7930..8a0a1d0 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -43,4 +43,9 @@
*** setListLeft(...);
*** setListAlpha(...);
*** setConversationLeft(...);
+}
+
+-keepclasseswithmembers class com.android.mail.browse.ConversationItemView {
+ *** setAnimatedHeight(...);
+ *** setItemAlpha(...);
}
\ No newline at end of file
diff --git a/res/drawable/conversation_read_selector.xml b/res/drawable/conversation_read_selector.xml
index 37f8fee..ebe2537 100644
--- a/res/drawable/conversation_read_selector.xml
+++ b/res/drawable/conversation_read_selector.xml
@@ -22,5 +22,7 @@
android:drawable="@drawable/list_focused_holo" />
<item android:state_selected="true"
android:drawable="@drawable/list_selected_holo" />
+ <item android:state_activated="true"
+ android:drawable="@drawable/list_selected_holo" />
<item android:drawable="@drawable/list_read_holo" />
</selector>
diff --git a/res/drawable/conversation_unread_selector.xml b/res/drawable/conversation_unread_selector.xml
index b087ea9..8988638 100644
--- a/res/drawable/conversation_unread_selector.xml
+++ b/res/drawable/conversation_unread_selector.xml
@@ -22,5 +22,7 @@
android:drawable="@drawable/list_focused_holo" />
<item android:state_selected="true"
android:drawable="@drawable/list_selected_holo" />
+ <item android:state_activated="true"
+ android:drawable="@drawable/list_selected_holo" />
<item android:drawable="@drawable/list_unread_holo" />
</selector>
diff --git a/res/layout/conversation_list.xml b/res/layout/conversation_list.xml
index 1a76303..3f4044f 100644
--- a/res/layout/conversation_list.xml
+++ b/res/layout/conversation_list.xml
@@ -20,7 +20,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/between_chrome"
android:layout_width="match_parent"
- android:layout_height="match_parent" >
+ android:layout_height="match_parent"
+ android:background="@color/conversation_list_background_color" >
<!-- Note: intentionally not called "empty" because we call
setEmptyView programmatically-->
diff --git a/res/layout/conversation_list_footer_view.xml b/res/layout/conversation_list_footer_view.xml
index a9f7f97..1b50630 100644
--- a/res/layout/conversation_list_footer_view.xml
+++ b/res/layout/conversation_list_footer_view.xml
@@ -21,7 +21,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
- android:layout_height="wrap_content">
+ android:layout_height="wrap_content"
+ android:background="@android:color/white">
<LinearLayout android:id="@+id/network_error"
android:layout_width="match_parent"
diff --git a/res/values/animation_constants.xml b/res/values/animation_constants.xml
index 3ed2bc1..6a72d37 100644
--- a/res/values/animation_constants.xml
+++ b/res/values/animation_constants.xml
@@ -24,4 +24,5 @@
<integer name="fade_duration">250</integer>
<integer name="dialog_animationDefaultDur">220</integer>
<integer name="dialog_animationShortDur">150</integer>
+ <integer name="undo_animation_duration">300</integer>
</resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 78dfdac..63211f6 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -35,6 +35,7 @@
<color name="light_text_color">#ff666666</color>
<color name="default_folder_background_color">#f1f5ec</color>
<color name="default_folder_foreground_color">#888888</color>
+ <color name="conversation_list_background_color">#bbbbbb</color>
<!-- Compose colors -->
<color name="compose_label_text">#aaaaaa</color>
diff --git a/res/values/constants.xml b/res/values/constants.xml
index 6421109..e00c550 100644
--- a/res/values/constants.xml
+++ b/res/values/constants.xml
@@ -41,6 +41,7 @@
<integer name="conversation_view_weight">1</integer>
<!-- Max unread count to show for a folder -->
<integer name="maxUnreadCount">999</integer>
+ <integer name="swipeScrollSlop">2</integer>
<!-- <integer name="widget_refresh_delay_ms">4000</integer>-->
<integer name="widget_folder_refresh_delay_ms">500</integer>
<item type="id" name="folder"/>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 4ab697b..47e6c01 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -190,6 +190,8 @@
<style name="ConversationListFade" parent="@android:style/Widget.Holo.Light.ListView">
<item name="android:cacheColorHint">@android:color/transparent</item>
+ <item name="android:divider">#dddddd</item>
+ <item name="android:dividerHeight">1dip</item>
</style>
<style name="StarStyle">
diff --git a/src/com/android/mail/browse/ConversationItemView.java b/src/com/android/mail/browse/ConversationItemView.java
index c83a85a..7dd476a 100644
--- a/src/com/android/mail/browse/ConversationItemView.java
+++ b/src/com/android/mail/browse/ConversationItemView.java
@@ -19,6 +19,10 @@
import com.google.common.annotations.VisibleForTesting;
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.Animator.AnimatorListener;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
@@ -48,6 +52,9 @@
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.Checkable;
import android.widget.ListView;
import com.android.mail.R;
@@ -108,6 +115,7 @@
private static int TOUCH_SLOP;
private static int sDateBackgroundHeight;
private static int sStandardScaledDimen;
+ private static int sUndoAnimationDuration;
private static CharacterStyle sLightTextStyle;
private static CharacterStyle sNormalTextStyle;
@@ -136,16 +144,15 @@
private final Context mContext;
- private String mAccount;
- private ConversationItemViewModel mHeader;
+ public ConversationItemViewModel mHeader;
private ViewMode mViewMode;
private boolean mDownEvent;
private boolean mChecked = false;
- private static int sFadedColor = -1;
private static int sFadedActivatedColor = -1;
private ConversationSelectionSet mSelectedConversationSet;
private Folder mDisplayedFolder;
private boolean mPriorityMarkersEnabled;
+ private int mAnimatedHeight = -1;
private static Bitmap MORE_FOLDERS;
static {
@@ -290,7 +297,6 @@
mContext = context.getApplicationContext();
mTabletDevice = Utils.useTabletUI(mContext);
- mAccount = account;
Resources res = mContext.getResources();
if (CHECKMARK_OFF == null) {
@@ -341,6 +347,7 @@
TOUCH_SLOP = res.getDimensionPixelSize(R.dimen.touch_slop);
sDateBackgroundHeight = res.getDimensionPixelSize(R.dimen.date_background_height);
sStandardScaledDimen = res.getDimensionPixelSize(R.dimen.standard_scaled_dimen);
+ sUndoAnimationDuration = res.getInteger(R.integer.undo_animation_duration);
// Initialize static color.
sNormalTextStyle = new StyleSpan(Typeface.NORMAL);
@@ -348,17 +355,19 @@
}
}
- public void bind(Cursor cursor, String account, ViewMode viewMode,
- ConversationSelectionSet set, Folder folder) {
- mAccount = account;
+ public void bind(Cursor cursor, ViewMode viewMode, ConversationSelectionSet set,
+ Folder folder) {
mViewMode = viewMode;
- mHeader = ConversationItemViewModel.forCursor(account, cursor);
+ mHeader = ConversationItemViewModel.forCursor(cursor);
mSelectedConversationSet = set;
mDisplayedFolder = folder;
setContentDescription(mHeader.getContentDescription(mContext));
requestLayout();
}
+ /**
+ * Get the Conversation object associated with this view.
+ */
public Conversation getConversation() {
return mHeader.conversation;
}
@@ -755,10 +764,14 @@
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int width = measureWidth(widthMeasureSpec);
- int height = measureHeight(heightMeasureSpec,
- ConversationItemViewCoordinates.getMode(mContext, mViewMode));
- setMeasuredDimension(width, height);
+ if (mAnimatedHeight == -1) {
+ int width = measureWidth(widthMeasureSpec);
+ int height = measureHeight(heightMeasureSpec,
+ ConversationItemViewCoordinates.getMode(mContext, mViewMode));
+ setMeasuredDimension(width, height);
+ } else {
+ setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mAnimatedHeight);
+ }
}
/**
@@ -983,7 +996,7 @@
conv.position = mChecked ? ((ListView)getParent()).getPositionForView(this)
: Conversation.NO_POSITION;
if (mSelectedConversationSet != null) {
- mSelectedConversationSet.toggle(conv);
+ mSelectedConversationSet.toggle(this, conv);
}
// We update the background after the checked state has changed now that
// we have a selected background asset. Setting the background usually
@@ -993,9 +1006,16 @@
}
/**
+ * Return if the checkbox for this item is checked.
+ */
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ /**
* Toggle the star on this view and update the conversation.
*/
- private void toggleStar() {
+ public void toggleStar() {
mHeader.starred = !mHeader.starred;
mHeader.starBitmap = mHeader.starred ? STAR_ON : STAR_OFF;
postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX
@@ -1005,52 +1025,112 @@
mHeader.conversation.updateBoolean(mContext, ConversationColumns.STARRED, mHeader.starred);
}
- private boolean touchCheckmark(float x, float y) {
+ private boolean isTouchInCheckmark(float x, float y) {
// Everything before senders and include a touch slop.
return mHeader.checkboxVisible && x < mCoordinates.sendersX + TOUCH_SLOP;
}
- private boolean touchStar(float x, float y) {
+ private boolean isTouchInStar(float x, float y) {
// Everything after the star and include a touch slop.
return x > mCoordinates.starX - TOUCH_SLOP;
}
+ /**
+ * ConversationItemView is given the first chance to handle touch events.
+ */
@Override
public boolean onTouchEvent(MotionEvent event) {
- boolean handled = false;
+ boolean handled = true;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownEvent = true;
- if (touchCheckmark(x, y) || touchStar(x, y)) {
- handled = true;
- }
+ // In order to allow the down event and subsequent move events
+ // to bubble to the swipe handler, we need to return that all
+ // down events are handled.
+ handled = true;
break;
-
case MotionEvent.ACTION_CANCEL:
mDownEvent = false;
break;
-
case MotionEvent.ACTION_UP:
if (mDownEvent) {
- if (touchCheckmark(x, y)) {
+ // ConversationItemView gets the first chance to handle up
+ // events if there was a down event and there was no move
+ // event in between. In this case, ConversationItemView
+ // received the down event, and then an up event in the
+ // same location (+/- slop). Treat this as a click on the
+ // view or on a specific part of the view.
+ if (isTouchInCheckmark(x, y)) {
// Touch on the check mark
toggleCheckMark();
- } else if (touchStar(x, y)) {
+ } else if (isTouchInStar(x, y)) {
// Touch on the star
toggleStar();
+ } else {
+ ListView list = (ListView)getParent();
+ int pos = list.getPositionForView(this);
+ list.performItemClick(this, pos, mHeader.conversation.id);
}
handled = true;
+ } else {
+ // There was no down event that this view was made aware of,
+ // therefore it cannot handle it.
+ handled = false;
}
break;
}
if (!handled) {
+ // Let View try to handle it as well.
handled = super.onTouchEvent(event);
}
return handled;
}
+
+ /**
+ * Grow the height of the item and fade it in when bringing a conversation
+ * back from a destructive action.
+ *
+ * @param listener
+ */
+ public void startUndoAnimation(final AnimatorListener listener) {
+ setMinimumHeight(140);
+ final int start = 0 ;
+ final int end = 140;
+ ObjectAnimator undoAnimator = ObjectAnimator.ofInt(this, "animatedHeight", start, end);
+ Animator fadeAnimator = ObjectAnimator.ofFloat(this, "itemAlpha", 0, 1.0f);
+ mAnimatedHeight = start;
+ undoAnimator.setInterpolator(new DecelerateInterpolator(2.0f));
+ undoAnimator.setDuration(sUndoAnimationDuration);
+ AnimatorSet transitionSet = new AnimatorSet();
+ transitionSet.playTogether(undoAnimator, fadeAnimator);
+ transitionSet.addListener(listener);
+ transitionSet.start();
+ }
+
+ // Used by animator
+ @SuppressWarnings("unused")
+ public void setItemAlpha(float alpha) {
+ setAlpha(alpha);
+ invalidate();
+ }
+
+ // Used by animator
+ @SuppressWarnings("unused")
+ public void setAnimatedHeight(int height) {
+ mAnimatedHeight = height;
+ requestLayout();
+ }
+
+ /**
+ * Get the current position of this conversation item in the list.
+ */
+ public int getPosition() {
+ return mHeader != null && mHeader.conversation != null ?
+ mHeader.conversation.position : -1;
+ }
}
diff --git a/src/com/android/mail/browse/ConversationItemViewModel.java b/src/com/android/mail/browse/ConversationItemViewModel.java
index e4490ea..b80490d 100644
--- a/src/com/android/mail/browse/ConversationItemViewModel.java
+++ b/src/com/android/mail/browse/ConversationItemViewModel.java
@@ -130,7 +130,7 @@
}
}
- static ConversationItemViewModel forCursor(String account, Cursor cursor) {
+ static ConversationItemViewModel forCursor(Cursor cursor) {
ConversationItemViewModel header = new ConversationItemViewModel();
if (cursor != null) {
header.faded = false;
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index 17e3d2f..4731761 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -106,7 +106,7 @@
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (!isPositionAnimating(view) && !isPositionFooter(view)) {
- ((ConversationItemView) view).bind(cursor, mSelectedAccount.name, mViewMode,
+ ((ConversationItemView) view).bind(cursor, mViewMode,
mBatchConversations, mFolder);
}
}
@@ -229,14 +229,24 @@
}
Conversation conversation = new Conversation((ConversationCursor) getItem(position));
conversation.position = position;
- final AnimatingItemView view = (convertView == null) ? new AnimatingItemView(mContext)
- : (AnimatingItemView) convertView;
- view.startAnimation(conversation, this, mUndo);
- return view;
+ if (mUndo) {
+ // The undo animation consists of fading in the conversation that
+ // had been destroyed.
+ ConversationItemView convView = (ConversationItemView) super.getView(position, null,
+ parent);
+ convView.startUndoAnimation(this);
+ return convView;
+ } else {
+ // Destroying a conversation just shows a blank shrinking item.
+ final AnimatingItemView view = new AnimatingItemView(mContext);
+ view.startAnimation(conversation, this);
+ return view;
+ }
}
private boolean isPositionAnimating(int position) {
- return mDeletingItems.contains(position);
+ return mDeletingItems.contains(position)
+ || (mUndo && mLastDeletingItems.contains(position));
}
private boolean isPositionAnimating(View view) {
@@ -249,22 +259,36 @@
@Override
public void onAnimationStart(Animator animation) {
- // TODO Auto-generated method stub
+ if (mUndo) {
+ mDeletingItems.clear();
+ mLastDeletingItems.clear();
+ mUndo = false;
+ }
}
@Override
public void onAnimationEnd(Animator animation) {
- if (!mDeletingItems.isEmpty()) {
- // See if we have received all the animations we expected; if so,
- // call the listener and reset it.
- int position = ((AnimatingItemView)
- ((ObjectAnimator) animation).getTarget()).getData().position;
+ if (mUndo && !mLastDeletingItems.isEmpty()) {
+ // See if we have received all the animations we expected; if
+ // so, call the listener and reset it.
+ int position = ((ConversationItemView) ((ObjectAnimator) animation).getTarget())
+ .getPosition();
+ mLastDeletingItems.remove(position);
+ if (mLastDeletingItems.isEmpty()) {
+ if (mActionCompleteListener != null) {
+ mActionCompleteListener.onActionComplete();
+ mActionCompleteListener = null;
+ }
+ mUndo = false;
+ }
+ } else if (!mDeletingItems.isEmpty()) {
+ // See if we have received all the animations we expected; if
+ // so, call the listener and reset it.
+ AnimatingItemView target = ((AnimatingItemView) ((ObjectAnimator) animation)
+ .getTarget());
+ int position = target.getData().position;
mDeletingItems.remove(position);
if (mDeletingItems.isEmpty()) {
- if (mUndo) {
- mLastDeletingItems.clear();
- mUndo = false;
- }
if (mActionCompleteListener != null) {
mActionCompleteListener.onActionComplete();
mActionCompleteListener = null;
@@ -274,6 +298,16 @@
}
@Override
+ public boolean areAllItemsEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return !isPositionAnimating(position);
+ }
+
+ @Override
public void onAnimationCancel(Animator animation) {
onAnimationEnd(animation);
}
diff --git a/src/com/android/mail/ui/AnimatingItemView.java b/src/com/android/mail/ui/AnimatingItemView.java
index 65fc181..482986e 100644
--- a/src/com/android/mail/ui/AnimatingItemView.java
+++ b/src/com/android/mail/ui/AnimatingItemView.java
@@ -17,35 +17,23 @@
package com.android.mail.ui;
-import com.android.mail.R;
-
import android.animation.Animator.AnimatorListener;
import android.animation.ObjectAnimator;
import android.content.Context;
-import android.util.AttributeSet;
import android.view.animation.DecelerateInterpolator;
import android.widget.LinearLayout;
-import com.android.mail.browse.ConversationItemView;
import com.android.mail.providers.Conversation;
public class AnimatingItemView extends LinearLayout {
+ public AnimatingItemView(Context context) {
+ super(context);
+ }
+
private Conversation mData;
private ObjectAnimator mAnimator;
private int mAnimatedHeight;
- public AnimatingItemView(Context context) {
- this(context, null);
- }
-
- public AnimatingItemView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public AnimatingItemView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
/**
* Start the animation on an animating view.
* @param item the conversation to animate
@@ -53,14 +41,12 @@
* @param undo true if an operation is being undone. We animate the item away during delete.
* Undoing populates the item.
*/
- public void startAnimation(Conversation item, AnimatorListener listener, boolean undo) {
+ public void startAnimation(Conversation item, AnimatorListener listener) {
mData = item;
setMinimumHeight(140);
- final int start = undo ? 0 : 140;
- final int end = undo ? 140 : 0;
- if (!undo) {
- setBackgroundResource(R.drawable.list_activated_holo);
- }
+ final int start = 140;
+ final int end = 0;
+
mAnimator = ObjectAnimator.ofInt(this, "animatedHeight", start, end);
mAnimatedHeight = start;
mAnimator.setInterpolator(new DecelerateInterpolator(2.0f));
@@ -69,12 +55,6 @@
mAnimator.start();
}
- public AnimatingItemView(Context context, Conversation item, AnimatorListener listener,
- boolean undo) {
- // The context stays the same when views are recycled.
- this(context);
- startAnimation(item, listener, undo);
- }
public Conversation getData() {
return mData;
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index 9f343b8..1d7983a 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -219,6 +219,7 @@
null);
mListAdapter.addFooter(mFooterView);
mListView.setAdapter(mListAdapter);
+ mListView.setSelectionSet(mActivity.getSelectedSet());
mListAdapter.hideFooter();
mListView.setSwipeCompleteListener(this);
// Don't need to add ourselves to our own set observer.
@@ -511,6 +512,9 @@
break;
}
mListAdapter.notifyDataSetChanged();
+ if (!mActivity.getSelectedSet().isEmpty()) {
+ mActivity.getSelectedSet().clear();
+ }
onUndoAvailable(new UndoOperation(conversations.size(), mSwipeAction));
}
diff --git a/src/com/android/mail/ui/ConversationSelectionSet.java b/src/com/android/mail/ui/ConversationSelectionSet.java
index 4af5497..9f26fe3 100644
--- a/src/com/android/mail/ui/ConversationSelectionSet.java
+++ b/src/com/android/mail/ui/ConversationSelectionSet.java
@@ -23,6 +23,7 @@
import android.os.Parcel;
import android.os.Parcelable;
+import com.android.mail.browse.ConversationItemView;
import com.android.mail.providers.Conversation;
import java.util.ArrayList;
import java.util.Collection;
@@ -45,8 +46,8 @@
Parcelable[] conversations = source.readParcelableArray(
Conversation.class.getClassLoader());
for (Parcelable parceled : conversations) {
- Conversation conversation = (Conversation) parceled;
- result.put(conversation.id, conversation);
+ Conversation conversation = (Conversation) parceled;
+ result.put(conversation.id, conversation);
}
return result;
}
@@ -60,6 +61,9 @@
private final HashMap<Long, Conversation> mInternalMap =
new HashMap<Long, Conversation>();
+ private final HashMap<Long, ConversationItemView> mInternalViewMap =
+ new HashMap<Long, ConversationItemView>();
+
@VisibleForTesting
final ArrayList<ConversationSetObserver> mObservers = new ArrayList<ConversationSetObserver>();
@@ -77,6 +81,7 @@
*/
public synchronized void clear() {
boolean initiallyNotEmpty = !mInternalMap.isEmpty();
+ mInternalViewMap.clear();
mInternalMap.clear();
if (mInternalMap.isEmpty() && initiallyNotEmpty) {
@@ -139,10 +144,25 @@
return mInternalMap.isEmpty();
}
- /** @see java.util.HashMap#put */
private synchronized void put(Long id, Conversation info) {
final boolean initiallyEmpty = mInternalMap.isEmpty();
mInternalMap.put(id, info);
+ // Fill out the view map with null. The sizes will match, but
+ // we won't have any views available yet to store.
+ mInternalViewMap.put(id, null);
+
+ ArrayList<ConversationSetObserver> observersCopy = Lists.newArrayList(mObservers);
+ dispatchOnChange(observersCopy);
+ if (initiallyEmpty) {
+ dispatchOnBecomeUnempty(observersCopy);
+ }
+ }
+
+ /** @see java.util.HashMap#put */
+ private synchronized void put(Long id, ConversationItemView info) {
+ boolean initiallyEmpty = mInternalMap.isEmpty();
+ mInternalViewMap.put(id, info);
+ mInternalMap.put(id, info.mHeader.conversation);
ArrayList<ConversationSetObserver> observersCopy = Lists.newArrayList(mObservers);
dispatchOnChange(observersCopy);
@@ -154,6 +174,8 @@
/** @see java.util.HashMap#remove */
private synchronized void remove(Long id) {
final boolean initiallyNotEmpty = !mInternalMap.isEmpty();
+
+ mInternalViewMap.remove(id);
mInternalMap.remove(id);
ArrayList<ConversationSetObserver> observersCopy = Lists.newArrayList(mObservers);
@@ -186,12 +208,12 @@
* selected.
* @param conversation
*/
- public void toggle(Conversation conversation) {
+ public void toggle(ConversationItemView view, Conversation conversation) {
long conversationId = conversation.id;
if (containsKey(conversationId)) {
remove(conversationId);
} else {
- put(conversationId, conversation);
+ put(conversationId, view);
}
}
@@ -221,4 +243,8 @@
Conversation[] values = values().toArray(new Conversation[size()]);
dest.writeParcelableArray(values, flags);
}
+
+ public Collection<ConversationItemView> views() {
+ return mInternalViewMap.values();
+ }
}
diff --git a/src/com/android/mail/ui/SwipeHelper.java b/src/com/android/mail/ui/SwipeHelper.java
index 3e5d917..b660804 100644
--- a/src/com/android/mail/ui/SwipeHelper.java
+++ b/src/com/android/mail/ui/SwipeHelper.java
@@ -19,6 +19,7 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
@@ -29,9 +30,13 @@
import android.view.VelocityTracker;
import android.view.View;
+import com.android.mail.browse.ConversationItemView;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
public class SwipeHelper {
static final String TAG = "com.android.systemui.SwipeHelper";
- private static final boolean DEBUG = false;
private static final boolean DEBUG_INVALIDATE = false;
private static final boolean SLOW_ANIMATIONS = false; // DEBUG;
private static final boolean CONSTRAIN_SWIPE = true;
@@ -62,18 +67,22 @@
private float mInitialTouchPos;
private boolean mDragging;
- private View mCurrView;
+ private ConversationItemView mCurrView;
private View mCurrAnimView;
private boolean mCanCurrViewBeDimissed;
private float mDensityScale;
+ private float mLastY;
+ private Collection<ConversationItemView> mAssociatedViews;
+ private final float mScrollSlop;
public SwipeHelper(int swipeDirection, Callback callback, float densityScale,
- float pagingTouchSlop) {
+ float pagingTouchSlop, float scrollSlop) {
mCallback = callback;
mSwipeDirection = swipeDirection;
mVelocityTracker = VelocityTracker.obtain();
mDensityScale = densityScale;
mPagingTouchSlop = pagingTouchSlop;
+ mScrollSlop = scrollSlop;
}
public void setDensityScale(float densityScale) {
@@ -103,6 +112,13 @@
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();
@@ -169,11 +185,11 @@
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);
+ mCurrView = (ConversationItemView)mCallback.getChildAtPosition(ev);
mVelocityTracker.clear();
if (mCurrView != null) {
mCurrAnimView = mCallback.getChildContentView(mCurrView);
@@ -184,60 +200,65 @@
break;
case MotionEvent.ACTION_MOVE:
if (mCurrView != null) {
+ // Check the movement direction.
+ if (mLastY >= 0) {
+ float currY = ev.getY();
+ if (Math.abs(currY - mLastY) > mScrollSlop) {
+ mLastY = ev.getY();
+ return false;
+ }
+ }
mVelocityTracker.addMovement(ev);
float pos = getPos(ev);
float delta = pos - mInitialTouchPos;
if (Math.abs(delta) > mPagingTouchSlop) {
- mCallback.onBeginDrag(mCurrView);
- mDragging = true;
- mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView);
+ if (mCallback.getSelectionSet().isEmpty()
+ || (!mCallback.getSelectionSet().isEmpty()
+ && mCurrView.isChecked())) {
+ mCallback.onBeginDrag(mCurrView);
+ mDragging = true;
+ mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView);
+ }
}
}
+ mLastY = ev.getY();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mDragging = false;
mCurrView = null;
mCurrAnimView = null;
+ mLastY = -1;
break;
}
return mDragging;
}
+ public void setAssociatedViews(Collection<ConversationItemView> associated) {
+ mAssociatedViews = associated;
+ }
+
+ public void clearAssociatedViews() {
+ mAssociatedViews = null;
+ }
+
/**
* @param view The view to be dismissed
- * @param velocity The desired pixels/second speed at which the view should move
+ * @param velocity The desired pixels/second speed at which the view should
+ * move
*/
- public void dismissChild(final View view, float velocity) {
- final View animView = mCallback.getChildContentView(view);
- final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
- float newPos;
-
- if (velocity < 0
- || (velocity == 0 && getTranslation(animView) < 0)
- // if we use the Menu to dismiss an item in landscape, animate up
- || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) {
- newPos = -getSize(animView);
- } else {
- newPos = getSize(animView);
- }
- int duration = MAX_ESCAPE_ANIMATION_DURATION;
- if (velocity != 0) {
- duration = Math.min(duration,
- (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math
- .abs(velocity)));
- } else {
- duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
- }
+ private void dismissChild(final View view, float velocity) {
+ final View animView = mCurrView;
+ final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
+ float newPos = determinePos(animView, velocity);
+ int duration = determineDuration(animView, newPos, velocity);
animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
- ObjectAnimator anim = createTranslationAnimation(animView, newPos);
- anim.setInterpolator(sLinearInterpolator);
- anim.setDuration(duration);
+ ObjectAnimator anim = createDismissAnimation(animView, newPos, duration);
anim.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
mCallback.onChildDismissed(view);
- animView.setLayerType(View.LAYER_TYPE_NONE, null);
+ mCurrView.setLayerType(View.LAYER_TYPE_NONE, null);
}
});
anim.addUpdateListener(new AnimatorUpdateListener() {
@@ -251,6 +272,63 @@
anim.start();
}
+ private void dismissChildren(final Collection<ConversationItemView> views, float velocity) {
+ AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
+ public void onAnimationEnd(Animator animation) {
+ mCallback.onChildrenDismissed(views);
+ mCurrView.setLayerType(View.LAYER_TYPE_NONE, null);
+ }
+ };
+ final View animView = mCurrView;
+ final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
+ float newPos = determinePos(animView, velocity);
+ int duration = determineDuration(animView, newPos, velocity);
+ ArrayList<Animator> animations = new ArrayList<Animator>();
+ ObjectAnimator anim;
+ for (final ConversationItemView view : views) {
+ view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ anim = createDismissAnimation(view, newPos, duration);
+ anim.addUpdateListener(new AnimatorUpdateListener() {
+ public void onAnimationUpdate(ValueAnimator animation) {
+ if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
+ view.setAlpha(getAlphaForOffset(view));
+ }
+ invalidateGlobalRegion(view);
+ }
+ });
+ animations.add(anim);
+ }
+ AnimatorSet transitionSet = new AnimatorSet();
+ transitionSet.playTogether(animations);
+ transitionSet.addListener(listener);
+ transitionSet.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 - getTranslation(animView)) * 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 && getTranslation(animView) < 0)
+ // if we use the Menu to dismiss an item in landscape, animate up
+ || (velocity == 0 && getTranslation(animView) == 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(animView);
@@ -272,7 +350,6 @@
if (!mDragging) {
return false;
}
-
mVelocityTracker.addMovement(ev);
final int action = ev.getAction();
switch (action) {
@@ -291,9 +368,21 @@
delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2));
}
}
- setTranslation(mCurrAnimView, delta);
+ if (mAssociatedViews != null && mAssociatedViews.size() > 1) {
+ for (View v : mAssociatedViews) {
+ setTranslation(v, delta);
+ }
+ } else {
+ setTranslation(mCurrAnimView, delta);
+ }
if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) {
- mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView));
+ if (mAssociatedViews != null && mAssociatedViews.size() > 1) {
+ for (View v : mAssociatedViews) {
+ v.setAlpha(getAlphaForOffset(mCurrAnimView));
+ }
+ } else {
+ mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView));
+ }
}
invalidateGlobalRegion(mCurrView);
}
@@ -318,12 +407,23 @@
(childSwipedFastEnough || childSwipedFarEnough);
if (dismissChild) {
- // flingadingy
- dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
+ if (mAssociatedViews != null && mAssociatedViews.size() > 1) {
+ dismissChildren(mAssociatedViews, childSwipedFastEnough ?
+ velocity : 0f);
+ } else {
+ dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
+ }
} else {
// snappity
mCallback.onDragCancelled(mCurrView);
- snapChild(mCurrView, velocity);
+
+ if (mAssociatedViews != null && mAssociatedViews.size() > 1) {
+ for (View v : mAssociatedViews) {
+ snapChild(v, velocity);
+ }
+ } else {
+ snapChild(mCurrView, velocity);
+ }
}
}
break;
@@ -342,6 +442,10 @@
void onChildDismissed(View v);
+ void onChildrenDismissed(Collection<ConversationItemView> v);
+
void onDragCancelled(View v);
+
+ ConversationSelectionSet getSelectionSet();
}
}
diff --git a/src/com/android/mail/ui/SwipeableListView.java b/src/com/android/mail/ui/SwipeableListView.java
index b910c48..fbec5bd 100644
--- a/src/com/android/mail/ui/SwipeableListView.java
+++ b/src/com/android/mail/ui/SwipeableListView.java
@@ -28,18 +28,17 @@
import com.android.mail.browse.ConversationItemView;
import com.android.mail.providers.Conversation;
import com.android.mail.ui.SwipeHelper.Callback;
-import com.android.mail.ui.UndoBarView.UndoListener;
import com.android.mail.utils.LogUtils;
import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
import java.util.Collection;
public class SwipeableListView extends ListView implements Callback {
private SwipeHelper mSwipeHelper;
private SwipeCompleteListener mSwipeCompleteListener;
- // TODO(mindyp) disable for original droidfood build.
- private boolean ENABLE_SWIPE = false;
+ private boolean ENABLE_SWIPE = true;
private ListAdapter mDebugAdapter;
private int mDebugLastCount;
@@ -48,6 +47,8 @@
public static final String LOG_TAG = new LogUtils().getLogTag();
+ private ConversationSelectionSet mConvSelectionSet;
+
public SwipeableListView(Context context) {
this(context, null);
}
@@ -59,13 +60,24 @@
public SwipeableListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
float densityScale = getResources().getDisplayMetrics().density;
- mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, densityScale);
+ float scrollSlop = context.getResources().getInteger(R.integer.swipeScrollSlop);
+ mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, densityScale,
+ scrollSlop);
}
public void setSwipeCompleteListener(SwipeCompleteListener listener) {
mSwipeCompleteListener = listener;
}
+ public void setSelectionSet(ConversationSelectionSet set) {
+ mConvSelectionSet = set;
+ }
+
+ @Override
+ public ConversationSelectionSet getSelectionSet() {
+ return mConvSelectionSet;
+ }
+
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ENABLE_SWIPE) {
@@ -119,7 +131,6 @@
public View getChildAtPosition(MotionEvent ev) {
// find the view under the pointer, accounting for GONE views
final int count = getChildCount();
- int y = 0;
int touchY = (int) ev.getY();
int childIdx = 0;
View slidingChild;
@@ -128,8 +139,9 @@
if (slidingChild.getVisibility() == GONE) {
continue;
}
- y += slidingChild.getMeasuredHeight();
- if (touchY < y) return slidingChild;
+ if (touchY >= slidingChild.getTop() && touchY <= slidingChild.getBottom()) {
+ return slidingChild;
+ }
}
return null;
}
@@ -146,18 +158,32 @@
@Override
public void onChildDismissed(View v) {
- if (v instanceof ConversationItemView) {
- Conversation c = ((ConversationItemView) v).getConversation();
- c.position = getPositionForView(v);
- AnimatedAdapter adapter = ((AnimatedAdapter) getAdapter());
- final ImmutableList<Conversation> conversations = ImmutableList.of(c);
- adapter.delete(conversations, new ActionCompleteListener() {
- @Override
- public void onActionComplete() {
- mSwipeCompleteListener.onSwipeComplete(conversations);
- }
- });
+ dismissChildren(ImmutableList.of(getConversation(v)));
+ }
+
+ @Override
+ public void onChildrenDismissed(Collection<ConversationItemView> views) {
+ final ArrayList<Conversation> conversations = new ArrayList<Conversation>();
+ for (ConversationItemView view : views) {
+ conversations.add(getConversation(view));
}
+ dismissChildren(conversations);
+ }
+
+ private Conversation getConversation(View view) {
+ Conversation c = ((ConversationItemView) view).getConversation();
+ c.position = getPositionForView(view);
+ return c;
+ }
+
+ private void dismissChildren(final Collection<Conversation> conversations) {
+ AnimatedAdapter adapter = ((AnimatedAdapter) getAdapter());
+ adapter.delete(conversations, new ActionCompleteListener() {
+ @Override
+ public void onActionComplete() {
+ mSwipeCompleteListener.onSwipeComplete(conversations);
+ }
+ });
}
@Override
@@ -165,12 +191,17 @@
// We do this so the underlying ScrollView knows that it won't get
// the chance to intercept events anymore
requestDisallowInterceptTouchEvent(true);
- v.setActivated(true);
+ // If there are selected conversations, we are dismissing an entire
+ // associated set.
+ // Otherwise, the SwipeHelper will just get rid of the single item it
+ // received touch events for.
+ mSwipeHelper.setAssociatedViews(mConvSelectionSet != null ? mConvSelectionSet.views()
+ : null);
}
@Override
public void onDragCancelled(View v) {
- v.setActivated(false);
+ mSwipeHelper.setAssociatedViews(null);
}
public interface SwipeCompleteListener {