SwipeableListView.java revision a59283a9856b9356b058575e89dfe3f17fffa529
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.Context;
21import android.content.res.Configuration;
22import android.graphics.Rect;
23import android.net.Uri;
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.browse.SwipeableConversationItemView;
34import com.android.mail.providers.Conversation;
35import com.android.mail.providers.Folder;
36import com.android.mail.ui.SwipeHelper.Callback;
37import com.android.mail.utils.LogTag;
38import com.android.mail.utils.LogUtils;
39import com.android.mail.utils.Utils;
40
41import java.util.ArrayList;
42import java.util.Collection;
43import java.util.HashMap;
44
45public class SwipeableListView extends ListView implements Callback {
46    private SwipeHelper mSwipeHelper;
47    private boolean mEnableSwipe = false;
48
49    public static final String LOG_TAG = LogTag.getLogTag();
50
51    private ConversationSelectionSet mConvSelectionSet;
52    private int mSwipeAction;
53    private Folder mFolder;
54
55    public SwipeableListView(Context context) {
56        this(context, null);
57    }
58
59    public SwipeableListView(Context context, AttributeSet attrs) {
60        this(context, attrs, -1);
61    }
62
63    public SwipeableListView(Context context, AttributeSet attrs, int defStyle) {
64        super(context, attrs, defStyle);
65        float densityScale = getResources().getDisplayMetrics().density;
66        float pagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
67        mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this, densityScale,
68                pagingTouchSlop);
69    }
70
71    @Override
72    protected void onConfigurationChanged(Configuration newConfig) {
73        super.onConfigurationChanged(newConfig);
74        float densityScale = getResources().getDisplayMetrics().density;
75        mSwipeHelper.setDensityScale(densityScale);
76        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
77        mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
78    }
79
80    @Override
81    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
82        LogUtils.d(Utils.VIEW_DEBUGGING_TAG,
83                "START CLF-ListView.onFocusChanged layoutRequested=%s root.layoutRequested=%s",
84                isLayoutRequested(), getRootView().isLayoutRequested());
85        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
86        LogUtils.d(Utils.VIEW_DEBUGGING_TAG, new Error(),
87                "FINISH CLF-ListView.onFocusChanged layoutRequested=%s root.layoutRequested=%s",
88                isLayoutRequested(), getRootView().isLayoutRequested());
89    }
90
91    @Override
92    public void requestLayout() {
93        Utils.checkRequestLayout(this);
94        super.requestLayout();
95    }
96
97    /**
98     * Enable swipe gestures.
99     */
100    public void enableSwipe(boolean enable) {
101        mEnableSwipe = enable;
102    }
103
104    public boolean isSwipeEnabled() {
105        return mEnableSwipe;
106    }
107
108    public void setSwipeAction(int action) {
109        mSwipeAction = action;
110    }
111
112    public int getSwipeAction() {
113        return mSwipeAction;
114    }
115
116    public void setSelectionSet(ConversationSelectionSet set) {
117        mConvSelectionSet = set;
118    }
119
120    public void setCurrentFolder(Folder folder) {
121        mFolder = folder;
122    }
123
124    @Override
125    public ConversationSelectionSet getSelectionSet() {
126        return mConvSelectionSet;
127    }
128
129    @Override
130    public boolean onInterceptTouchEvent(MotionEvent ev) {
131        if (mEnableSwipe) {
132            return mSwipeHelper.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev);
133        } else {
134            return super.onInterceptTouchEvent(ev);
135        }
136    }
137
138    @Override
139    public boolean onTouchEvent(MotionEvent ev) {
140        if (mEnableSwipe) {
141            return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev);
142        } else {
143            return super.onTouchEvent(ev);
144        }
145    }
146
147    @Override
148    public View getChildAtPosition(MotionEvent ev) {
149        // find the view under the pointer, accounting for GONE views
150        final int count = getChildCount();
151        int touchY = (int) ev.getY();
152        int childIdx = 0;
153        View slidingChild;
154        for (; childIdx < count; childIdx++) {
155            slidingChild = getChildAt(childIdx);
156            if (slidingChild.getVisibility() == GONE) {
157                continue;
158            }
159            if (touchY >= slidingChild.getTop() && touchY <= slidingChild.getBottom()) {
160                if (slidingChild instanceof SwipeableConversationItemView) {
161                    return ((SwipeableConversationItemView) slidingChild).getSwipeableItemView();
162                }
163                return slidingChild;
164            }
165        }
166        return null;
167    }
168
169    @Override
170    public boolean canChildBeDismissed(SwipeableItemView v) {
171        return v.canChildBeDismissed();
172    }
173
174    @Override
175    public void onChildDismissed(SwipeableItemView v) {
176        if (v != null) {
177            v.dismiss();
178        }
179    }
180
181    // Call this whenever a new action is taken; this forces a commit of any
182    // existing destructive actions.
183    public void commitDestructiveActions(boolean animate) {
184        final AnimatedAdapter adapter = getAnimatedAdapter();
185        if (adapter != null) {
186            adapter.commitLeaveBehindItems(animate);
187        }
188    }
189
190    public void dismissChild(final ConversationItemView target) {
191        final Context context = getContext();
192        final ToastBarOperation undoOp;
193
194        undoOp = new ToastBarOperation(1, mSwipeAction, ToastBarOperation.UNDO, false);
195        Conversation conv = target.getConversation();
196        target.getConversation().position = findConversation(target, conv);
197        final AnimatedAdapter adapter = getAnimatedAdapter();
198        if (adapter == null) {
199            return;
200        }
201        adapter.setupLeaveBehind(conv, undoOp, conv.position);
202        ConversationCursor cc = (ConversationCursor) adapter.getCursor();
203        switch (mSwipeAction) {
204            case R.id.remove_folder:
205                FolderOperation folderOp = new FolderOperation(mFolder, false);
206                HashMap<Uri, Folder> targetFolders = Folder
207                        .hashMapForFolders(conv.getRawFolders());
208                targetFolders.remove(folderOp.mFolder.uri);
209                conv.setRawFolders(Folder.getSerializedFolderString(targetFolders.values()));
210                cc.mostlyDestructiveUpdate(context, Conversation.listOf(conv),
211                        Conversation.UPDATE_FOLDER_COLUMN, conv.getRawFoldersString());
212                break;
213            case R.id.archive:
214                cc.mostlyArchive(context, Conversation.listOf(conv));
215                break;
216            case R.id.delete:
217                cc.mostlyDelete(context, Conversation.listOf(conv));
218                break;
219        }
220        adapter.notifyDataSetChanged();
221        if (mConvSelectionSet != null && !mConvSelectionSet.isEmpty()
222                && mConvSelectionSet.contains(conv)) {
223            mConvSelectionSet.toggle(null, conv);
224            // Don't commit destructive actions if the item we just removed from
225            // the selection set is the item we just destroyed!
226            if (!conv.isMostlyDead() && mConvSelectionSet.isEmpty()) {
227                commitDestructiveActions(true);
228            }
229        }
230    }
231
232    @Override
233    public void onBeginDrag(View v) {
234        // We do this so the underlying ScrollView knows that it won't get
235        // the chance to intercept events anymore
236        requestDisallowInterceptTouchEvent(true);
237        SwipeableConversationItemView view = null;
238        if (v instanceof ConversationItemView) {
239            view = (SwipeableConversationItemView) v.getParent();
240        }
241        if (view != null) {
242            view.addBackground(getContext());
243            view.setBackgroundVisibility(View.VISIBLE);
244        }
245    }
246
247    @Override
248    public void onDragCancelled(SwipeableItemView v) {
249        SwipeableConversationItemView view = null;
250        if (v instanceof ConversationItemView) {
251            view = (SwipeableConversationItemView) ((View) v).getParent();
252        }
253        if (view != null) {
254            view.removeBackground();
255        }
256    }
257
258    /**
259     * Archive items using the swipe away animation before shrinking them away.
260     */
261    public void destroyItems(final ArrayList<ConversationItemView> views,
262            final DestructiveAction listener) {
263        if (views == null || views.size() == 0) {
264            return;
265        }
266        // Need to find the items in the LIST!
267        final ArrayList<Conversation> conversations = new ArrayList<Conversation>();
268        for (ConversationItemView view : views) {
269            Conversation conv = view.getConversation();
270            conv.position = findConversation(view, conv);
271            conversations.add(conv);
272        }
273        AnimatedAdapter adapter = getAnimatedAdapter();
274        if (adapter != null) {
275            adapter.swipeDelete(conversations, listener);
276        }
277    }
278
279    public int findConversation(ConversationItemView view, Conversation conv) {
280        int position = conv.position;
281        long convId = conv.id;
282        try {
283            if (position == INVALID_POSITION) {
284                position = getPositionForView(view);
285            }
286        } catch (Exception e) {
287            position = INVALID_POSITION;
288            LogUtils.w(LOG_TAG, "Exception finding position; using alternate strategy");
289        }
290        if (position == INVALID_POSITION) {
291            // Try the other way!
292            Conversation foundConv;
293            long foundId;
294            for (int i = 0; i < getChildCount(); i++) {
295                View child = getChildAt(i);
296                if (child instanceof SwipeableConversationItemView) {
297                    foundConv = ((SwipeableConversationItemView) child).getSwipeableItemView()
298                            .getConversation();
299                    foundId = foundConv.id;
300                    if (foundId == convId) {
301                        position = i;
302                        break;
303                    }
304                }
305            }
306        }
307        return position;
308    }
309
310    private AnimatedAdapter getAnimatedAdapter() {
311        return (AnimatedAdapter) getAdapter();
312    }
313
314    @Override
315    public boolean performItemClick(View view, int pos, long id) {
316        boolean handled = super.performItemClick(view, pos, id);
317        // Commit any existing destructive actions when the user selects a
318        // conversation to view.
319        commitDestructiveActions(true);
320        return handled;
321    }
322
323    @Override
324    public void onScroll() {
325        commitDestructiveActions(true);
326    }
327}
328