• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 Google Inc.
3  * Licensed to The Android Open Source Project.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.mail.ui;
19 
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.content.res.Configuration;
23 import android.graphics.Rect;
24 import android.net.Uri;
25 import android.util.AttributeSet;
26 import android.view.MotionEvent;
27 import android.view.View;
28 import android.view.ViewConfiguration;
29 import android.widget.AbsListView;
30 import android.widget.AbsListView.OnScrollListener;
31 import android.widget.ListView;
32 
33 import com.android.mail.R;
34 import com.android.mail.analytics.Analytics;
35 import com.android.mail.browse.ConversationCursor;
36 import com.android.mail.browse.ConversationItemView;
37 import com.android.mail.browse.SwipeableConversationItemView;
38 import com.android.mail.providers.Account;
39 import com.android.mail.providers.Conversation;
40 import com.android.mail.providers.Folder;
41 import com.android.mail.providers.FolderList;
42 import com.android.mail.ui.SwipeHelper.Callback;
43 import com.android.mail.utils.LogTag;
44 import com.android.mail.utils.LogUtils;
45 import com.android.mail.utils.Utils;
46 
47 import java.util.ArrayList;
48 import java.util.Collection;
49 import java.util.HashMap;
50 
51 public class SwipeableListView extends ListView implements Callback, OnScrollListener {
52     private static final long INVALID_CONVERSATION_ID = -1;
53 
54     private final SwipeHelper mSwipeHelper;
55     /**
56      * Are swipes enabled on all items? (Each individual item can still prevent swiping.)<br>
57      * When swiping is disabled, the UI still reacts to the gesture to acknowledge it.
58      */
59     private boolean mEnableSwipe = false;
60     /**
61      * When set, we prevent the SwipeHelper from kicking in at all. This
62      * short-circuits {@link #mEnableSwipe}.
63      */
64     private boolean mPreventSwipesEntirely = false;
65 
66     public static final String LOG_TAG = LogTag.getLogTag();
67 
68     private ConversationCheckedSet mConvCheckedSet;
69     private int mSwipeAction;
70     private Account mAccount;
71     private Folder mFolder;
72     private ListItemSwipedListener mSwipedListener;
73     private boolean mScrolling;
74 
75     private SwipeListener mSwipeListener;
76 
77     private long mSelectedConversationId = INVALID_CONVERSATION_ID;
78 
79     // Instantiated through view inflation
80     @SuppressWarnings("unused")
SwipeableListView(Context context)81     public SwipeableListView(Context context) {
82         this(context, null);
83     }
84 
SwipeableListView(Context context, AttributeSet attrs)85     public SwipeableListView(Context context, AttributeSet attrs) {
86         this(context, attrs, -1);
87     }
88 
SwipeableListView(Context context, AttributeSet attrs, int defStyle)89     public SwipeableListView(Context context, AttributeSet attrs, int defStyle) {
90         super(context, attrs, defStyle);
91         float densityScale = getResources().getDisplayMetrics().density;
92         float pagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
93         mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this, densityScale,
94                 pagingTouchSlop);
95         mScrolling = false;
96     }
97 
98     @Override
onConfigurationChanged(Configuration newConfig)99     protected void onConfigurationChanged(Configuration newConfig) {
100         super.onConfigurationChanged(newConfig);
101         float densityScale = getResources().getDisplayMetrics().density;
102         mSwipeHelper.setDensityScale(densityScale);
103         float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
104         mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
105     }
106 
107     @Override
onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)108     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
109         LogUtils.d(Utils.VIEW_DEBUGGING_TAG,
110                 "START CLF-ListView.onFocusChanged layoutRequested=%s root.layoutRequested=%s",
111                 isLayoutRequested(), getRootView().isLayoutRequested());
112         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
113         LogUtils.d(Utils.VIEW_DEBUGGING_TAG, new Error(),
114                 "FINISH CLF-ListView.onFocusChanged layoutRequested=%s root.layoutRequested=%s",
115                 isLayoutRequested(), getRootView().isLayoutRequested());
116     }
117 
118     /**
119      * Enable swipe gestures.
120      */
enableSwipe(boolean enable)121     public void enableSwipe(boolean enable) {
122         mEnableSwipe = enable;
123     }
124 
125     /**
126      * Completely ignore any horizontal swiping gestures.
127      */
preventSwipesEntirely()128     public void preventSwipesEntirely() {
129         mPreventSwipesEntirely = true;
130     }
131 
132     /**
133      * Reverses a prior call to {@link #preventSwipesEntirely()}.
134      */
stopPreventingSwipes()135     public void stopPreventingSwipes() {
136         mPreventSwipesEntirely = false;
137     }
138 
setSwipeAction(int action)139     public void setSwipeAction(int action) {
140         mSwipeAction = action;
141     }
142 
setListItemSwipedListener(ListItemSwipedListener listener)143     public void setListItemSwipedListener(ListItemSwipedListener listener) {
144         mSwipedListener = listener;
145     }
146 
getSwipeAction()147     public int getSwipeAction() {
148         return mSwipeAction;
149     }
150 
setCheckedSet(ConversationCheckedSet set)151     public void setCheckedSet(ConversationCheckedSet set) {
152         mConvCheckedSet = set;
153     }
154 
setCurrentAccount(Account account)155     public void setCurrentAccount(Account account) {
156         mAccount = account;
157     }
158 
setCurrentFolder(Folder folder)159     public void setCurrentFolder(Folder folder) {
160         mFolder = folder;
161     }
162 
163     @Override
getCheckedSet()164     public ConversationCheckedSet getCheckedSet() {
165         return mConvCheckedSet;
166     }
167 
168     @Override
onInterceptTouchEvent(MotionEvent ev)169     public boolean onInterceptTouchEvent(MotionEvent ev) {
170         if (mScrolling) {
171             return super.onInterceptTouchEvent(ev);
172         } else {
173             return (!mPreventSwipesEntirely && mSwipeHelper.onInterceptTouchEvent(ev))
174                     || super.onInterceptTouchEvent(ev);
175         }
176     }
177 
178     @Override
onTouchEvent(MotionEvent ev)179     public boolean onTouchEvent(MotionEvent ev) {
180         return (!mPreventSwipesEntirely && mSwipeHelper.onTouchEvent(ev)) || super.onTouchEvent(ev);
181     }
182 
183     @Override
getChildAtPosition(MotionEvent ev)184     public View getChildAtPosition(MotionEvent ev) {
185         // find the view under the pointer, accounting for GONE views
186         final int count = getChildCount();
187         final int touchY = (int) ev.getY();
188         int childIdx = 0;
189         View slidingChild;
190         for (; childIdx < count; childIdx++) {
191             slidingChild = getChildAt(childIdx);
192             if (slidingChild.getVisibility() == GONE) {
193                 continue;
194             }
195             if (touchY >= slidingChild.getTop() && touchY <= slidingChild.getBottom()) {
196                 if (slidingChild instanceof SwipeableConversationItemView) {
197                     return ((SwipeableConversationItemView) slidingChild).getSwipeableItemView();
198                 }
199                 return slidingChild;
200             }
201         }
202         return null;
203     }
204 
205     @Override
canChildBeDismissed(SwipeableItemView v)206     public boolean canChildBeDismissed(SwipeableItemView v) {
207         return mEnableSwipe && v.canChildBeDismissed();
208     }
209 
210     @Override
onChildDismissed(SwipeableItemView v)211     public void onChildDismissed(SwipeableItemView v) {
212         if (v != null) {
213             v.dismiss();
214         }
215     }
216 
217     // Call this whenever a new action is taken; this forces a commit of any
218     // existing destructive actions.
commitDestructiveActions(boolean animate)219     public void commitDestructiveActions(boolean animate) {
220         final AnimatedAdapter adapter = getAnimatedAdapter();
221         if (adapter != null) {
222             adapter.commitLeaveBehindItems(animate);
223         }
224     }
225 
dismissChild(final ConversationItemView target)226     public void dismissChild(final ConversationItemView target) {
227         // Notifies the SwipeListener that a swipe has ended.
228         if (mSwipeListener != null) {
229             mSwipeListener.onEndSwipe();
230         }
231 
232         final ToastBarOperation undoOp;
233 
234         undoOp = new ToastBarOperation(1, mSwipeAction, ToastBarOperation.UNDO, false /* batch */,
235                 mFolder);
236         Conversation conv = target.getConversation();
237         target.getConversation().position = findConversation(target, conv);
238         final AnimatedAdapter adapter = getAnimatedAdapter();
239         if (adapter == null) {
240             return;
241         }
242         adapter.setupLeaveBehind(conv, undoOp, conv.position, target.getHeight());
243         ConversationCursor cc = (ConversationCursor) adapter.getCursor();
244         Collection<Conversation> convList = Conversation.listOf(conv);
245         ArrayList<Uri> folderUris;
246         ArrayList<Boolean> adds;
247 
248         Analytics.getInstance().sendMenuItemEvent("list_swipe", mSwipeAction, null, 0);
249 
250         if (mSwipeAction == R.id.remove_folder) {
251             FolderOperation folderOp = new FolderOperation(mFolder, false);
252             HashMap<Uri, Folder> targetFolders = Folder
253                     .hashMapForFolders(conv.getRawFolders());
254             targetFolders.remove(folderOp.mFolder.folderUri.fullUri);
255             final FolderList folders = FolderList.copyOf(targetFolders.values());
256             conv.setRawFolders(folders);
257             final ContentValues values = new ContentValues();
258             folderUris = new ArrayList<Uri>();
259             folderUris.add(mFolder.folderUri.fullUri);
260             adds = new ArrayList<Boolean>();
261             adds.add(Boolean.FALSE);
262             ConversationCursor.addFolderUpdates(folderUris, adds, values);
263             ConversationCursor.addTargetFolders(targetFolders.values(), values);
264             cc.mostlyDestructiveUpdate(Conversation.listOf(conv), values);
265         } else if (mSwipeAction == R.id.archive) {
266             cc.mostlyArchive(convList);
267         } else if (mSwipeAction == R.id.delete) {
268             cc.mostlyDelete(convList);
269         } else if (mSwipeAction == R.id.discard_outbox) {
270             cc.moveFailedIntoDrafts(convList);
271         }
272         if (mSwipedListener != null) {
273             mSwipedListener.onListItemSwiped(convList);
274         }
275         adapter.notifyDataSetChanged();
276         if (mConvCheckedSet != null && !mConvCheckedSet.isEmpty()
277                 && mConvCheckedSet.contains(conv)) {
278             mConvCheckedSet.toggle(conv);
279             // Don't commit destructive actions if the item we just removed from
280             // the selection set is the item we just destroyed!
281             if (!conv.isMostlyDead() && mConvCheckedSet.isEmpty()) {
282                 commitDestructiveActions(true);
283             }
284         }
285     }
286 
287     @Override
onBeginDrag(View v)288     public void onBeginDrag(View v) {
289         // We do this so the underlying ScrollView knows that it won't get
290         // the chance to intercept events anymore
291         requestDisallowInterceptTouchEvent(true);
292         cancelDismissCounter();
293 
294         // Notifies the SwipeListener that a swipe has begun.
295         if (mSwipeListener != null) {
296             mSwipeListener.onBeginSwipe();
297         }
298     }
299 
300     @Override
onDragCancelled(SwipeableItemView v)301     public void onDragCancelled(SwipeableItemView v) {
302         final AnimatedAdapter adapter = getAnimatedAdapter();
303         if (adapter != null) {
304             adapter.startDismissCounter();
305             adapter.cancelFadeOutLastLeaveBehindItemText();
306         }
307 
308         // Notifies the SwipeListener that a swipe has ended.
309         if (mSwipeListener != null) {
310             mSwipeListener.onEndSwipe();
311         }
312     }
313 
314     /**
315      * Archive items using the swipe away animation before shrinking them away.
316      */
destroyItems(Collection<Conversation> convs, final ListItemsRemovedListener listener)317     public boolean destroyItems(Collection<Conversation> convs,
318             final ListItemsRemovedListener listener) {
319         if (convs == null) {
320             LogUtils.e(LOG_TAG, "SwipeableListView.destroyItems: null conversations.");
321             return false;
322         }
323         final AnimatedAdapter adapter = getAnimatedAdapter();
324         if (adapter == null) {
325             LogUtils.e(LOG_TAG, "SwipeableListView.destroyItems: Cannot destroy: adapter is null.");
326             return false;
327         }
328         adapter.swipeDelete(convs, listener);
329         return true;
330     }
331 
findConversation(ConversationItemView view, Conversation conv)332     public int findConversation(ConversationItemView view, Conversation conv) {
333         int position = INVALID_POSITION;
334         long convId = conv.id;
335         try {
336             position = getPositionForView(view);
337         } catch (Exception e) {
338             position = INVALID_POSITION;
339             LogUtils.w(LOG_TAG, e, "Exception finding position; using alternate strategy");
340         }
341         if (position == INVALID_POSITION) {
342             // Try the other way!
343             Conversation foundConv;
344             long foundId;
345             for (int i = 0; i < getChildCount(); i++) {
346                 View child = getChildAt(i);
347                 if (child instanceof SwipeableConversationItemView) {
348                     foundConv = ((SwipeableConversationItemView) child).getSwipeableItemView()
349                             .getConversation();
350                     foundId = foundConv.id;
351                     if (foundId == convId) {
352                         position = i + getFirstVisiblePosition();
353                         break;
354                     }
355                 }
356             }
357         }
358         return position;
359     }
360 
getAnimatedAdapter()361     private AnimatedAdapter getAnimatedAdapter() {
362         return (AnimatedAdapter) getAdapter();
363     }
364 
365     @Override
performItemClick(View view, int pos, long id)366     public boolean performItemClick(View view, int pos, long id) {
367         // Superclass method modifies the selection set
368         final boolean handled = super.performItemClick(view, pos, id);
369 
370         // Commit any existing destructive actions when the user selects a
371         // conversation to view.
372         commitDestructiveActions(true);
373         return handled;
374     }
375 
376     @Override
onScroll()377     public void onScroll() {
378         commitDestructiveActions(true);
379     }
380 
381     public interface ListItemsRemovedListener {
onListItemsRemoved()382         public void onListItemsRemoved();
383     }
384 
385     public interface ListItemSwipedListener {
onListItemSwiped(Collection<Conversation> conversations)386         public void onListItemSwiped(Collection<Conversation> conversations);
387     }
388 
389     @Override
onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)390     public final void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
391             int totalItemCount) {
392     }
393 
394     @Override
onScrollStateChanged(final AbsListView view, final int scrollState)395     public void onScrollStateChanged(final AbsListView view, final int scrollState) {
396         mScrolling = scrollState != OnScrollListener.SCROLL_STATE_IDLE;
397 
398         if (!mScrolling) {
399             final Context c = getContext();
400             if (c instanceof ControllableActivity) {
401                 final ControllableActivity activity = (ControllableActivity) c;
402                 activity.onAnimationEnd(null /* adapter */);
403             } else {
404                 LogUtils.wtf(LOG_TAG, "unexpected context=%s", c);
405             }
406         }
407     }
408 
isScrolling()409     public boolean isScrolling() {
410         return mScrolling;
411     }
412 
413     /**
414      * Set the currently selected (focused by the list view) position.
415      */
setSelectedConversation(Conversation conv)416     public void setSelectedConversation(Conversation conv) {
417         if (conv == null) {
418             return;
419         }
420 
421         mSelectedConversationId = conv.id;
422     }
423 
isConversationSelected(Conversation conv)424     public boolean isConversationSelected(Conversation conv) {
425         return mSelectedConversationId != INVALID_CONVERSATION_ID && conv != null
426                 && mSelectedConversationId == conv.id;
427     }
428 
429     /**
430      * This is only used for debugging/logging purposes. DO NOT call this function to try to get
431      * the currently selected position. Use {@link #mSelectedConversationId} instead.
432      */
getSelectedConversationPosDebug()433     public int getSelectedConversationPosDebug() {
434         for (int i = getFirstVisiblePosition(); i < getLastVisiblePosition(); i++) {
435             final Object item = getItemAtPosition(i);
436             if (item instanceof ConversationCursor) {
437                 final Conversation c = ((ConversationCursor) item).getConversation();
438                 if (c.id == mSelectedConversationId) {
439                     return i;
440                 }
441             }
442         }
443         return ListView.INVALID_POSITION;
444     }
445 
446     @Override
onTouchModeChanged(boolean isInTouchMode)447     public void onTouchModeChanged(boolean isInTouchMode) {
448         super.onTouchModeChanged(isInTouchMode);
449         if (!isInTouchMode) {
450             // We need to invalidate going from touch mode -> keyboard mode because the currently
451             // selected item might have changed in touch mode. However, since from the framework's
452             // perspective the selected position doesn't matter in touch mode, when we enter
453             // keyboard mode via up/down arrow, the list view will ONLY invalidate the newly
454             // selected item and not the currently selected item. As a result, we might get an
455             // inconsistent UI where it looks like both the old and new selected items are focused.
456             final int index = getSelectedItemPosition();
457             if (index != ListView.INVALID_POSITION) {
458                 final View child = getChildAt(index - getFirstVisiblePosition());
459                 if (child != null) {
460                     child.invalidate();
461                 }
462             }
463         }
464     }
465 
466     @Override
cancelDismissCounter()467     public void cancelDismissCounter() {
468         AnimatedAdapter adapter = getAnimatedAdapter();
469         if (adapter != null) {
470             adapter.cancelDismissCounter();
471         }
472     }
473 
474     @Override
getLastSwipedItem()475     public LeaveBehindItem getLastSwipedItem() {
476         AnimatedAdapter adapter = getAnimatedAdapter();
477         if (adapter != null) {
478             return adapter.getLastLeaveBehindItem();
479         }
480         return null;
481     }
482 
setSwipeListener(SwipeListener swipeListener)483     public void setSwipeListener(SwipeListener swipeListener) {
484         mSwipeListener = swipeListener;
485     }
486 
487     public interface SwipeListener {
onBeginSwipe()488         public void onBeginSwipe();
onEndSwipe()489         public void onEndSwipe();
490     }
491 }
492