SwipeableListView.java revision 599e7f8bf95d2f21a966cbff1bf72adf77a90a33
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.ContentValues; 21import android.content.Context; 22import android.content.res.Configuration; 23import android.graphics.Rect; 24import android.net.Uri; 25import android.util.AttributeSet; 26import android.widget.AbsListView; 27import android.widget.AbsListView.OnScrollListener; 28import android.view.MotionEvent; 29import android.view.View; 30import android.view.ViewConfiguration; 31import android.widget.ListView; 32 33import com.android.mail.R; 34import com.android.mail.browse.ConversationCursor; 35import com.android.mail.browse.ConversationItemView; 36import com.android.mail.browse.SwipeableConversationItemView; 37import com.android.mail.providers.Account; 38import com.android.mail.providers.Conversation; 39import com.android.mail.providers.Folder; 40import com.android.mail.providers.FolderList; 41import com.android.mail.providers.UIProvider.ConversationListIcon; 42import com.android.mail.ui.SwipeHelper.Callback; 43import com.android.mail.utils.LogTag; 44import com.android.mail.utils.LogUtils; 45import com.android.mail.utils.Utils; 46 47import java.util.ArrayList; 48import java.util.Collection; 49import java.util.HashMap; 50 51public class SwipeableListView extends ListView implements Callback, OnScrollListener { 52 private final SwipeHelper mSwipeHelper; 53 private boolean mEnableSwipe = false; 54 55 public static final String LOG_TAG = LogTag.getLogTag(); 56 57 private ConversationSelectionSet mConvSelectionSet; 58 private int mSwipeAction; 59 private Account mAccount; 60 private Folder mFolder; 61 private ListItemSwipedListener mSwipedListener; 62 private boolean mScrolling; 63 64 private SwipeListener mSwipeListener; 65 66 // Instantiated through view inflation 67 @SuppressWarnings("unused") 68 public SwipeableListView(Context context) { 69 this(context, null); 70 } 71 72 public SwipeableListView(Context context, AttributeSet attrs) { 73 this(context, attrs, -1); 74 } 75 76 public SwipeableListView(Context context, AttributeSet attrs, int defStyle) { 77 super(context, attrs, defStyle); 78 float densityScale = getResources().getDisplayMetrics().density; 79 float pagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop(); 80 mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this, densityScale, 81 pagingTouchSlop); 82 setOnScrollListener(this); 83 } 84 85 @Override 86 protected void onConfigurationChanged(Configuration newConfig) { 87 super.onConfigurationChanged(newConfig); 88 float densityScale = getResources().getDisplayMetrics().density; 89 mSwipeHelper.setDensityScale(densityScale); 90 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); 91 mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); 92 } 93 94 @Override 95 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 96 LogUtils.d(Utils.VIEW_DEBUGGING_TAG, 97 "START CLF-ListView.onFocusChanged layoutRequested=%s root.layoutRequested=%s", 98 isLayoutRequested(), getRootView().isLayoutRequested()); 99 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 100 LogUtils.d(Utils.VIEW_DEBUGGING_TAG, new Error(), 101 "FINISH CLF-ListView.onFocusChanged layoutRequested=%s root.layoutRequested=%s", 102 isLayoutRequested(), getRootView().isLayoutRequested()); 103 } 104 105 /** 106 * Enable swipe gestures. 107 */ 108 public void enableSwipe(boolean enable) { 109 mEnableSwipe = enable; 110 } 111 112 public void setSwipeAction(int action) { 113 mSwipeAction = action; 114 } 115 116 public void setSwipedListener(ListItemSwipedListener listener) { 117 mSwipedListener = listener; 118 } 119 120 public int getSwipeAction() { 121 return mSwipeAction; 122 } 123 124 public void setSelectionSet(ConversationSelectionSet set) { 125 mConvSelectionSet = set; 126 } 127 128 public void setCurrentAccount(Account account) { 129 mAccount = account; 130 } 131 132 public void setCurrentFolder(Folder folder) { 133 mFolder = folder; 134 } 135 136 @Override 137 public ConversationSelectionSet getSelectionSet() { 138 return mConvSelectionSet; 139 } 140 141 @Override 142 public boolean onInterceptTouchEvent(MotionEvent ev) { 143 if (mScrolling || !mEnableSwipe) { 144 return super.onInterceptTouchEvent(ev); 145 } else { 146 return mSwipeHelper.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev); 147 } 148 } 149 150 @Override 151 public boolean onTouchEvent(MotionEvent ev) { 152 if (mEnableSwipe) { 153 return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev); 154 } else { 155 return super.onTouchEvent(ev); 156 } 157 } 158 159 @Override 160 public View getChildAtPosition(MotionEvent ev) { 161 // find the view under the pointer, accounting for GONE views 162 final int count = getChildCount(); 163 int touchY = (int) ev.getY(); 164 int childIdx = 0; 165 View slidingChild; 166 for (; childIdx < count; childIdx++) { 167 slidingChild = getChildAt(childIdx); 168 if (slidingChild.getVisibility() == GONE) { 169 continue; 170 } 171 if (touchY >= slidingChild.getTop() && touchY <= slidingChild.getBottom()) { 172 if (slidingChild instanceof SwipeableConversationItemView) { 173 return ((SwipeableConversationItemView) slidingChild).getSwipeableItemView(); 174 } 175 return slidingChild; 176 } 177 } 178 return null; 179 } 180 181 @Override 182 public boolean canChildBeDismissed(SwipeableItemView v) { 183 return v.canChildBeDismissed(); 184 } 185 186 @Override 187 public void onChildDismissed(SwipeableItemView v) { 188 if (v != null) { 189 v.dismiss(); 190 } 191 } 192 193 // Call this whenever a new action is taken; this forces a commit of any 194 // existing destructive actions. 195 public void commitDestructiveActions(boolean animate) { 196 final AnimatedAdapter adapter = getAnimatedAdapter(); 197 if (adapter != null) { 198 adapter.commitLeaveBehindItems(animate); 199 } 200 } 201 202 public void dismissChild(final ConversationItemView target) { 203 final ToastBarOperation undoOp; 204 205 undoOp = new ToastBarOperation(1, mSwipeAction, ToastBarOperation.UNDO, false /* batch */, 206 mFolder); 207 Conversation conv = target.getConversation(); 208 target.getConversation().position = findConversation(target, conv); 209 final AnimatedAdapter adapter = getAnimatedAdapter(); 210 if (adapter == null) { 211 return; 212 } 213 adapter.setupLeaveBehind(conv, undoOp, conv.position, target.getHeight()); 214 ConversationCursor cc = (ConversationCursor) adapter.getCursor(); 215 Collection<Conversation> convList = Conversation.listOf(conv); 216 ArrayList<Uri> folderUris; 217 ArrayList<Boolean> adds; 218 switch (mSwipeAction) { 219 case R.id.remove_folder: 220 FolderOperation folderOp = new FolderOperation(mFolder, false); 221 HashMap<Uri, Folder> targetFolders = Folder 222 .hashMapForFolders(conv.getRawFolders()); 223 targetFolders.remove(folderOp.mFolder.uri); 224 final FolderList folders = FolderList.copyOf(targetFolders.values()); 225 conv.setRawFolders(folders); 226 final ContentValues values = new ContentValues(); 227 folderUris = new ArrayList<Uri>(); 228 folderUris.add(mFolder.uri); 229 adds = new ArrayList<Boolean>(); 230 adds.add(Boolean.FALSE); 231 ConversationCursor.addFolderUpdates(folderUris, adds, values); 232 ConversationCursor.addTargetFolders(targetFolders.values(), values); 233 cc.mostlyDestructiveUpdate(Conversation.listOf(conv), values); 234 break; 235 case R.id.archive: 236 cc.mostlyArchive(convList); 237 break; 238 case R.id.delete: 239 cc.mostlyDelete(convList); 240 break; 241 } 242 if (mSwipedListener != null) { 243 mSwipedListener.onListItemSwiped(convList); 244 } 245 adapter.notifyDataSetChanged(); 246 if (mConvSelectionSet != null && !mConvSelectionSet.isEmpty() 247 && mConvSelectionSet.contains(conv)) { 248 mConvSelectionSet.toggle(conv); 249 // Don't commit destructive actions if the item we just removed from 250 // the selection set is the item we just destroyed! 251 if (!conv.isMostlyDead() && mConvSelectionSet.isEmpty()) { 252 commitDestructiveActions(true); 253 } 254 } 255 } 256 257 @Override 258 public void onBeginDrag(View v) { 259 // We do this so the underlying ScrollView knows that it won't get 260 // the chance to intercept events anymore 261 requestDisallowInterceptTouchEvent(true); 262 cancelDismissCounter(); 263 264 // Notifies {@link ConversationListView} to disable pull to refresh since once 265 // an item in the list view has been picked up, we don't want any vertical movement 266 // to also trigger refresh. 267 if (mSwipeListener != null) { 268 mSwipeListener.onBeginSwipe(); 269 } 270 } 271 272 @Override 273 public void onDragCancelled(SwipeableItemView v) { 274 final AnimatedAdapter adapter = getAnimatedAdapter(); 275 if (adapter != null) { 276 adapter.startDismissCounter(); 277 adapter.cancelFadeOutLastLeaveBehindItemText(); 278 } 279 } 280 281 /** 282 * Archive items using the swipe away animation before shrinking them away. 283 */ 284 public boolean destroyItems(Collection<Conversation> convs, 285 final ListItemsRemovedListener listener) { 286 if (convs == null) { 287 LogUtils.e(LOG_TAG, "SwipeableListView.destroyItems: null conversations."); 288 return false; 289 } 290 final AnimatedAdapter adapter = getAnimatedAdapter(); 291 if (adapter == null) { 292 LogUtils.e(LOG_TAG, "SwipeableListView.destroyItems: Cannot destroy: adapter is null."); 293 return false; 294 } 295 adapter.swipeDelete(convs, listener); 296 return true; 297 } 298 299 public int findConversation(ConversationItemView view, Conversation conv) { 300 int position = INVALID_POSITION; 301 long convId = conv.id; 302 try { 303 position = getPositionForView(view); 304 } catch (Exception e) { 305 position = INVALID_POSITION; 306 LogUtils.w(LOG_TAG, e, "Exception finding position; using alternate strategy"); 307 } 308 if (position == INVALID_POSITION) { 309 // Try the other way! 310 Conversation foundConv; 311 long foundId; 312 for (int i = 0; i < getChildCount(); i++) { 313 View child = getChildAt(i); 314 if (child instanceof SwipeableConversationItemView) { 315 foundConv = ((SwipeableConversationItemView) child).getSwipeableItemView() 316 .getConversation(); 317 foundId = foundConv.id; 318 if (foundId == convId) { 319 position = i + getFirstVisiblePosition(); 320 break; 321 } 322 } 323 } 324 } 325 return position; 326 } 327 328 private AnimatedAdapter getAnimatedAdapter() { 329 return (AnimatedAdapter) getAdapter(); 330 } 331 332 @Override 333 public boolean performItemClick(View view, int pos, long id) { 334 int previousPosition = getCheckedItemPosition(); 335 boolean selectionSetEmpty = mConvSelectionSet.isEmpty(); 336 337 // Superclass method modifies the selection set 338 boolean handled = super.performItemClick(view, pos, id); 339 340 // If we are in CAB mode with no checkboxes then a click shouldn't 341 // activate the new item, it should only add it to the selection set 342 boolean showSenderImage = mAccount != null 343 && (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE); 344 if (!showSenderImage && !selectionSetEmpty && previousPosition != -1) { 345 setItemChecked(previousPosition, true); 346 } 347 // Commit any existing destructive actions when the user selects a 348 // conversation to view. 349 commitDestructiveActions(true); 350 return handled; 351 } 352 353 @Override 354 public void onScroll() { 355 commitDestructiveActions(true); 356 } 357 358 public interface ListItemsRemovedListener { 359 public void onListItemsRemoved(); 360 } 361 362 public interface ListItemSwipedListener { 363 public void onListItemSwiped(Collection<Conversation> conversations); 364 } 365 366 @Override 367 public void onScroll(AbsListView arg0, int arg1, int arg2, int arg3) { 368 // Do nothing. 369 } 370 371 @Override 372 public void onScrollStateChanged(AbsListView arg0, int scrollState) { 373 switch (scrollState) { 374 case OnScrollListener.SCROLL_STATE_IDLE: 375 mScrolling = false; 376 break; 377 default: 378 mScrolling = true; 379 } 380 } 381 382 @Override 383 public void cancelDismissCounter() { 384 AnimatedAdapter adapter = getAnimatedAdapter(); 385 if (adapter != null) { 386 adapter.cancelDismissCounter(); 387 } 388 } 389 390 @Override 391 public LeaveBehindItem getLastSwipedItem() { 392 AnimatedAdapter adapter = getAnimatedAdapter(); 393 if (adapter != null) { 394 return adapter.getLastLeaveBehindItem(); 395 } 396 return null; 397 } 398 399 public void setSwipeListener(SwipeListener swipeListener) { 400 mSwipeListener = swipeListener; 401 } 402 403 public interface SwipeListener { 404 public void onBeginSwipe(); 405 } 406} 407