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}