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}