SwipeableListView.java revision ebdfd98264104cb5a6888acd663970b7c0b31382
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.animation.Animator;
21import android.animation.AnimatorListenerAdapter;
22import android.content.Context;
23import android.content.res.Configuration;
24import android.util.AttributeSet;
25import android.view.MotionEvent;
26import android.view.View;
27import android.view.ViewConfiguration;
28import android.widget.ListView;
29
30import com.android.mail.R;
31import com.android.mail.browse.ConversationCursor;
32import com.android.mail.browse.ConversationItemView;
33import com.android.mail.providers.Conversation;
34import com.android.mail.providers.Folder;
35import com.android.mail.providers.UIProvider.ConversationColumns;
36import com.android.mail.ui.SwipeHelper.Callback;
37import com.android.mail.utils.LogTag;
38import com.google.common.base.Objects;
39
40import java.util.ArrayList;
41import java.util.Collection;
42
43public class SwipeableListView extends ListView implements Callback{
44    private SwipeHelper mSwipeHelper;
45    private boolean mEnableSwipe = false;
46
47    public static final String LOG_TAG = LogTag.getLogTag();
48
49    private ConversationSelectionSet mConvSelectionSet;
50    private int mSwipeAction;
51    private Folder mFolder;
52
53    public SwipeableListView(Context context) {
54        this(context, null);
55    }
56
57    public SwipeableListView(Context context, AttributeSet attrs) {
58        this(context, attrs, -1);
59    }
60
61    public SwipeableListView(Context context, AttributeSet attrs, int defStyle) {
62        super(context, attrs, defStyle);
63        float densityScale = getResources().getDisplayMetrics().density;
64        float pagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
65        float scrollSlop = context.getResources().getInteger(R.integer.swipeScrollSlop);
66        float minSwipe = context.getResources().getDimension(R.dimen.min_swipe);
67        float minVert = context.getResources().getDimension(R.dimen.min_vert);
68        float minLock = context.getResources().getDimension(R.dimen.min_lock);
69        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop,
70                scrollSlop, minSwipe, minVert, minLock);
71    }
72
73    @Override
74    protected void onConfigurationChanged(Configuration newConfig) {
75        super.onConfigurationChanged(newConfig);
76        float densityScale = getResources().getDisplayMetrics().density;
77        mSwipeHelper.setDensityScale(densityScale);
78        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
79        mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
80    }
81
82    /**
83     * Enable swipe gestures.
84     */
85    public void enableSwipe(boolean enable) {
86        mEnableSwipe = enable;
87    }
88
89    public boolean isSwipeEnabled() {
90        return mEnableSwipe;
91    }
92
93    public void setSwipeAction(int action) {
94        mSwipeAction = action;
95    }
96
97    public void setSelectionSet(ConversationSelectionSet set) {
98        mConvSelectionSet = set;
99    }
100
101    public void setCurrentFolder(Folder folder) {
102        mFolder = folder;
103    }
104
105    @Override
106    public ConversationSelectionSet getSelectionSet() {
107        return mConvSelectionSet;
108    }
109
110    @Override
111    public boolean onInterceptTouchEvent(MotionEvent ev) {
112        if (mEnableSwipe) {
113            return mSwipeHelper.onInterceptTouchEvent(ev)
114                    || super.onInterceptTouchEvent(ev);
115        } else {
116            return super.onInterceptTouchEvent(ev);
117        }
118    }
119
120    @Override
121    public boolean onTouchEvent(MotionEvent ev) {
122        if (mEnableSwipe) {
123            return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev);
124        } else {
125            return super.onTouchEvent(ev);
126        }
127    }
128
129    @Override
130    public View getChildAtPosition(MotionEvent ev) {
131        // find the view under the pointer, accounting for GONE views
132        final int count = getChildCount();
133        int touchY = (int) ev.getY();
134        int childIdx = 0;
135        View slidingChild;
136        for (; childIdx < count; childIdx++) {
137            slidingChild = getChildAt(childIdx);
138            if (slidingChild.getVisibility() == GONE) {
139                continue;
140            }
141            if (touchY >= slidingChild.getTop() && touchY <= slidingChild.getBottom()) {
142                return slidingChild;
143            }
144        }
145        return null;
146    }
147
148    @Override
149    public boolean canChildBeDismissed(SwipeableItemView v) {
150        View view = v.getView();
151        return view instanceof ConversationItemView || view instanceof LeaveBehindItem;
152    }
153
154    @Override
155    public void onChildDismissed(SwipeableItemView v) {
156        View view = v.getView();
157        if (view instanceof ConversationItemView) {
158        dismissChildren((ConversationItemView) v, null);
159        } else if (view instanceof LeaveBehindItem) {
160            ((LeaveBehindItem)view).commit();
161        }
162    }
163
164    @Override
165    public void onChildrenDismissed(SwipeableItemView target,
166            Collection<ConversationItemView> views) {
167        assert(target instanceof ConversationItemView);
168        dismissChildren((ConversationItemView) target.getView(), views);
169    }
170
171    private void dismissChildren(final ConversationItemView target,
172            final Collection<ConversationItemView> conversationViews) {
173        final Context context = getContext();
174        final AnimatedAdapter adapter = ((AnimatedAdapter) getAdapter());
175        final UndoOperation undoOp;
176        if (conversationViews != null) {
177            final ArrayList<Conversation> conversations = new ArrayList<Conversation>(
178                    conversationViews.size());
179            Conversation conversation;
180            for (ConversationItemView view : conversationViews) {
181                if (view.getConversation().id != target.getConversation().id) {
182                    conversation = view.getConversation();
183                    conversation.localDeleteOnUpdate = true;
184                    conversations.add(conversation);
185                }
186            }
187            undoOp = new UndoOperation(
188                    conversationViews != null ? (conversations.size() + 1) : 1, mSwipeAction);
189            handleLeaveBehind(target, undoOp, context);
190            adapter.delete(conversations, new DestructiveAction() {
191                @Override
192                public void performAction() {
193                    ConversationCursor cc = (ConversationCursor)adapter.getCursor();
194                    switch (mSwipeAction) {
195                        case R.id.archive:
196                            cc.archive(context, conversations);
197                            break;
198                        case R.id.change_folder:
199                            Collection<Folder> folders = getFolders(conversations);
200                            cc.updateStrings(
201                                    context,
202                                    conversations,
203                                    Conversation.UPDATE_FOLDER_COLUMNS,
204                                    new String[] {
205                                            Folder.getUriString(folders),
206                                            Folder.getSerializedFolderString(mFolder, folders)
207                                    });
208                            break;
209                        case R.id.delete:
210                            cc.delete(context, conversations);
211                            break;
212                    }
213                }
214            });
215        } else {
216            undoOp = new UndoOperation(1, mSwipeAction);
217            target.getConversation().position = target.getParent() != null ?
218                    getPositionForView(target) : -1;
219            handleLeaveBehind(target, undoOp, context);
220        }
221    }
222
223    private Collection<Folder> getFolders(Collection<Conversation> conversations) {
224        ArrayList<Folder> folders = new ArrayList<Folder>();
225        for (Conversation conversation : conversations) {
226            folders.addAll(Folder.forFoldersString(conversation.rawFolders));
227        }
228        for (Folder folder : folders) {
229            if (Objects.equal(folder.uri, mFolder.uri)) {
230                folders.remove(folder);
231            }
232        }
233        return folders;
234    }
235
236    private void handleLeaveBehind(ConversationItemView target, UndoOperation undoOp,
237            Context context) {
238        Conversation conv = target.getConversation();
239        final AnimatedAdapter adapter = ((AnimatedAdapter) getAdapter());
240        adapter.setupLeaveBehind(conv, undoOp, conv.position);
241        ConversationCursor cc = (ConversationCursor)adapter.getCursor();
242        switch (mSwipeAction) {
243            case R.id.change_folder:
244                Collection<Conversation> convs = Conversation.listOf(conv);
245                Collection<Folder> folders = getFolders(convs);
246                cc.mostlyDestructiveUpdate(
247                        context,
248                        convs,
249                        Conversation.UPDATE_FOLDER_COLUMNS,
250                        new String[] {
251                                Folder.getUriString(folders),
252                                Folder.getSerializedFolderString(mFolder, folders)
253                        });
254                break;
255            case R.id.archive:
256                cc.mostlyArchive(context, Conversation.listOf(conv));
257                break;
258            case R.id.delete:
259                cc.mostlyDelete(context, Conversation.listOf(conv));
260                break;
261        }
262        adapter.notifyDataSetChanged();
263        if (mConvSelectionSet != null && !mConvSelectionSet.isEmpty()
264                && mConvSelectionSet.contains(conv)) {
265            mConvSelectionSet.toggle(null, conv);
266        }
267    }
268
269    @Override
270    public void onBeginDrag(View v) {
271        // We do this so the underlying ScrollView knows that it won't get
272        // the chance to intercept events anymore
273        requestDisallowInterceptTouchEvent(true);
274    }
275
276    @Override
277    public void onDragCancelled(SwipeableItemView v) {
278    }
279
280    /**
281     * Archive items using the swipe away animation before shrinking them away.
282     */
283    public void archiveItems(ArrayList<ConversationItemView> views,
284            final DestructiveAction listener) {
285        if (views == null || views.size() == 0) {
286            return;
287        }
288        final ArrayList<Conversation> conversations = new ArrayList<Conversation>();
289        for (ConversationItemView view : views) {
290            conversations.add(view.getConversation());
291        }
292        mSwipeHelper.dismissChildren(views.get(0), views, new AnimatorListenerAdapter() {
293            @Override
294            public void onAnimationEnd(Animator animation) {
295                ((AnimatedAdapter) getAdapter()).delete(conversations, listener);
296            }
297        });
298    }
299
300    public interface SwipeCompleteListener {
301        public void onSwipeComplete(Collection<Conversation> conversations);
302    }
303}