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