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