SwipeableListView.java revision 599e7f8bf95d2f21a966cbff1bf72adf77a90a33
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
18package com.android.mail.ui;
19
20import android.content.ContentValues;
21import android.content.Context;
22import android.content.res.Configuration;
23import android.graphics.Rect;
24import android.net.Uri;
25import android.util.AttributeSet;
26import android.widget.AbsListView;
27import android.widget.AbsListView.OnScrollListener;
28import android.view.MotionEvent;
29import android.view.View;
30import android.view.ViewConfiguration;
31import android.widget.ListView;
32
33import com.android.mail.R;
34import com.android.mail.browse.ConversationCursor;
35import com.android.mail.browse.ConversationItemView;
36import com.android.mail.browse.SwipeableConversationItemView;
37import com.android.mail.providers.Account;
38import com.android.mail.providers.Conversation;
39import com.android.mail.providers.Folder;
40import com.android.mail.providers.FolderList;
41import com.android.mail.providers.UIProvider.ConversationListIcon;
42import com.android.mail.ui.SwipeHelper.Callback;
43import com.android.mail.utils.LogTag;
44import com.android.mail.utils.LogUtils;
45import com.android.mail.utils.Utils;
46
47import java.util.ArrayList;
48import java.util.Collection;
49import java.util.HashMap;
50
51public class SwipeableListView extends ListView implements Callback, OnScrollListener {
52    private final SwipeHelper mSwipeHelper;
53    private boolean mEnableSwipe = false;
54
55    public static final String LOG_TAG = LogTag.getLogTag();
56
57    private ConversationSelectionSet mConvSelectionSet;
58    private int mSwipeAction;
59    private Account mAccount;
60    private Folder mFolder;
61    private ListItemSwipedListener mSwipedListener;
62    private boolean mScrolling;
63
64    private SwipeListener mSwipeListener;
65
66    // Instantiated through view inflation
67    @SuppressWarnings("unused")
68    public SwipeableListView(Context context) {
69        this(context, null);
70    }
71
72    public SwipeableListView(Context context, AttributeSet attrs) {
73        this(context, attrs, -1);
74    }
75
76    public SwipeableListView(Context context, AttributeSet attrs, int defStyle) {
77        super(context, attrs, defStyle);
78        float densityScale = getResources().getDisplayMetrics().density;
79        float pagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
80        mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this, densityScale,
81                pagingTouchSlop);
82        setOnScrollListener(this);
83    }
84
85    @Override
86    protected void onConfigurationChanged(Configuration newConfig) {
87        super.onConfigurationChanged(newConfig);
88        float densityScale = getResources().getDisplayMetrics().density;
89        mSwipeHelper.setDensityScale(densityScale);
90        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
91        mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
92    }
93
94    @Override
95    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
96        LogUtils.d(Utils.VIEW_DEBUGGING_TAG,
97                "START CLF-ListView.onFocusChanged layoutRequested=%s root.layoutRequested=%s",
98                isLayoutRequested(), getRootView().isLayoutRequested());
99        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
100        LogUtils.d(Utils.VIEW_DEBUGGING_TAG, new Error(),
101                "FINISH CLF-ListView.onFocusChanged layoutRequested=%s root.layoutRequested=%s",
102                isLayoutRequested(), getRootView().isLayoutRequested());
103    }
104
105    /**
106     * Enable swipe gestures.
107     */
108    public void enableSwipe(boolean enable) {
109        mEnableSwipe = enable;
110    }
111
112    public void setSwipeAction(int action) {
113        mSwipeAction = action;
114    }
115
116    public void setSwipedListener(ListItemSwipedListener listener) {
117        mSwipedListener = listener;
118    }
119
120    public int getSwipeAction() {
121        return mSwipeAction;
122    }
123
124    public void setSelectionSet(ConversationSelectionSet set) {
125        mConvSelectionSet = set;
126    }
127
128    public void setCurrentAccount(Account account) {
129        mAccount = account;
130    }
131
132    public void setCurrentFolder(Folder folder) {
133        mFolder = folder;
134    }
135
136    @Override
137    public ConversationSelectionSet getSelectionSet() {
138        return mConvSelectionSet;
139    }
140
141    @Override
142    public boolean onInterceptTouchEvent(MotionEvent ev) {
143        if (mScrolling || !mEnableSwipe) {
144            return super.onInterceptTouchEvent(ev);
145        } else {
146            return mSwipeHelper.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev);
147        }
148    }
149
150    @Override
151    public boolean onTouchEvent(MotionEvent ev) {
152        if (mEnableSwipe) {
153            return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev);
154        } else {
155            return super.onTouchEvent(ev);
156        }
157    }
158
159    @Override
160    public View getChildAtPosition(MotionEvent ev) {
161        // find the view under the pointer, accounting for GONE views
162        final int count = getChildCount();
163        int touchY = (int) ev.getY();
164        int childIdx = 0;
165        View slidingChild;
166        for (; childIdx < count; childIdx++) {
167            slidingChild = getChildAt(childIdx);
168            if (slidingChild.getVisibility() == GONE) {
169                continue;
170            }
171            if (touchY >= slidingChild.getTop() && touchY <= slidingChild.getBottom()) {
172                if (slidingChild instanceof SwipeableConversationItemView) {
173                    return ((SwipeableConversationItemView) slidingChild).getSwipeableItemView();
174                }
175                return slidingChild;
176            }
177        }
178        return null;
179    }
180
181    @Override
182    public boolean canChildBeDismissed(SwipeableItemView v) {
183        return v.canChildBeDismissed();
184    }
185
186    @Override
187    public void onChildDismissed(SwipeableItemView v) {
188        if (v != null) {
189            v.dismiss();
190        }
191    }
192
193    // Call this whenever a new action is taken; this forces a commit of any
194    // existing destructive actions.
195    public void commitDestructiveActions(boolean animate) {
196        final AnimatedAdapter adapter = getAnimatedAdapter();
197        if (adapter != null) {
198            adapter.commitLeaveBehindItems(animate);
199        }
200    }
201
202    public void dismissChild(final ConversationItemView target) {
203        final ToastBarOperation undoOp;
204
205        undoOp = new ToastBarOperation(1, mSwipeAction, ToastBarOperation.UNDO, false /* batch */,
206                mFolder);
207        Conversation conv = target.getConversation();
208        target.getConversation().position = findConversation(target, conv);
209        final AnimatedAdapter adapter = getAnimatedAdapter();
210        if (adapter == null) {
211            return;
212        }
213        adapter.setupLeaveBehind(conv, undoOp, conv.position, target.getHeight());
214        ConversationCursor cc = (ConversationCursor) adapter.getCursor();
215        Collection<Conversation> convList = Conversation.listOf(conv);
216        ArrayList<Uri> folderUris;
217        ArrayList<Boolean> adds;
218        switch (mSwipeAction) {
219            case R.id.remove_folder:
220                FolderOperation folderOp = new FolderOperation(mFolder, false);
221                HashMap<Uri, Folder> targetFolders = Folder
222                        .hashMapForFolders(conv.getRawFolders());
223                targetFolders.remove(folderOp.mFolder.uri);
224                final FolderList folders = FolderList.copyOf(targetFolders.values());
225                conv.setRawFolders(folders);
226                final ContentValues values = new ContentValues();
227                folderUris = new ArrayList<Uri>();
228                folderUris.add(mFolder.uri);
229                adds = new ArrayList<Boolean>();
230                adds.add(Boolean.FALSE);
231                ConversationCursor.addFolderUpdates(folderUris, adds, values);
232                ConversationCursor.addTargetFolders(targetFolders.values(), values);
233                cc.mostlyDestructiveUpdate(Conversation.listOf(conv), values);
234                break;
235            case R.id.archive:
236                cc.mostlyArchive(convList);
237                break;
238            case R.id.delete:
239                cc.mostlyDelete(convList);
240                break;
241        }
242        if (mSwipedListener != null) {
243            mSwipedListener.onListItemSwiped(convList);
244        }
245        adapter.notifyDataSetChanged();
246        if (mConvSelectionSet != null && !mConvSelectionSet.isEmpty()
247                && mConvSelectionSet.contains(conv)) {
248            mConvSelectionSet.toggle(conv);
249            // Don't commit destructive actions if the item we just removed from
250            // the selection set is the item we just destroyed!
251            if (!conv.isMostlyDead() && mConvSelectionSet.isEmpty()) {
252                commitDestructiveActions(true);
253            }
254        }
255    }
256
257    @Override
258    public void onBeginDrag(View v) {
259        // We do this so the underlying ScrollView knows that it won't get
260        // the chance to intercept events anymore
261        requestDisallowInterceptTouchEvent(true);
262        cancelDismissCounter();
263
264        // Notifies {@link ConversationListView} to disable pull to refresh since once
265        // an item in the list view has been picked up, we don't want any vertical movement
266        // to also trigger refresh.
267        if (mSwipeListener != null) {
268            mSwipeListener.onBeginSwipe();
269        }
270    }
271
272    @Override
273    public void onDragCancelled(SwipeableItemView v) {
274        final AnimatedAdapter adapter = getAnimatedAdapter();
275        if (adapter != null) {
276            adapter.startDismissCounter();
277            adapter.cancelFadeOutLastLeaveBehindItemText();
278        }
279    }
280
281    /**
282     * Archive items using the swipe away animation before shrinking them away.
283     */
284    public boolean destroyItems(Collection<Conversation> convs,
285            final ListItemsRemovedListener listener) {
286        if (convs == null) {
287            LogUtils.e(LOG_TAG, "SwipeableListView.destroyItems: null conversations.");
288            return false;
289        }
290        final AnimatedAdapter adapter = getAnimatedAdapter();
291        if (adapter == null) {
292            LogUtils.e(LOG_TAG, "SwipeableListView.destroyItems: Cannot destroy: adapter is null.");
293            return false;
294        }
295        adapter.swipeDelete(convs, listener);
296        return true;
297    }
298
299    public int findConversation(ConversationItemView view, Conversation conv) {
300        int position = INVALID_POSITION;
301        long convId = conv.id;
302        try {
303            position = getPositionForView(view);
304        } catch (Exception e) {
305            position = INVALID_POSITION;
306            LogUtils.w(LOG_TAG, e, "Exception finding position; using alternate strategy");
307        }
308        if (position == INVALID_POSITION) {
309            // Try the other way!
310            Conversation foundConv;
311            long foundId;
312            for (int i = 0; i < getChildCount(); i++) {
313                View child = getChildAt(i);
314                if (child instanceof SwipeableConversationItemView) {
315                    foundConv = ((SwipeableConversationItemView) child).getSwipeableItemView()
316                            .getConversation();
317                    foundId = foundConv.id;
318                    if (foundId == convId) {
319                        position = i + getFirstVisiblePosition();
320                        break;
321                    }
322                }
323            }
324        }
325        return position;
326    }
327
328    private AnimatedAdapter getAnimatedAdapter() {
329        return (AnimatedAdapter) getAdapter();
330    }
331
332    @Override
333    public boolean performItemClick(View view, int pos, long id) {
334        int previousPosition = getCheckedItemPosition();
335        boolean selectionSetEmpty = mConvSelectionSet.isEmpty();
336
337        // Superclass method modifies the selection set
338        boolean handled = super.performItemClick(view, pos, id);
339
340        // If we are in CAB mode with no checkboxes then a click shouldn't
341        // activate the new item, it should only add it to the selection set
342        boolean showSenderImage = mAccount != null
343                && (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
344        if (!showSenderImage && !selectionSetEmpty && previousPosition != -1) {
345            setItemChecked(previousPosition, true);
346        }
347        // Commit any existing destructive actions when the user selects a
348        // conversation to view.
349        commitDestructiveActions(true);
350        return handled;
351    }
352
353    @Override
354    public void onScroll() {
355        commitDestructiveActions(true);
356    }
357
358    public interface ListItemsRemovedListener {
359        public void onListItemsRemoved();
360    }
361
362    public interface ListItemSwipedListener {
363        public void onListItemSwiped(Collection<Conversation> conversations);
364    }
365
366    @Override
367    public void onScroll(AbsListView arg0, int arg1, int arg2, int arg3) {
368        // Do nothing.
369    }
370
371    @Override
372    public void onScrollStateChanged(AbsListView arg0, int scrollState) {
373        switch (scrollState) {
374            case OnScrollListener.SCROLL_STATE_IDLE:
375                mScrolling = false;
376                break;
377            default:
378                mScrolling = true;
379        }
380    }
381
382    @Override
383    public void cancelDismissCounter() {
384        AnimatedAdapter adapter = getAnimatedAdapter();
385        if (adapter != null) {
386            adapter.cancelDismissCounter();
387        }
388    }
389
390    @Override
391    public LeaveBehindItem getLastSwipedItem() {
392        AnimatedAdapter adapter = getAnimatedAdapter();
393        if (adapter != null) {
394            return adapter.getLastLeaveBehindItem();
395        }
396        return null;
397    }
398
399    public void setSwipeListener(SwipeListener swipeListener) {
400        mSwipeListener = swipeListener;
401    }
402
403    public interface SwipeListener {
404        public void onBeginSwipe();
405    }
406}
407