SwipeableListView.java revision a59283a9856b9356b058575e89dfe3f17fffa529
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.Context; 21import android.content.res.Configuration; 22import android.graphics.Rect; 23import android.net.Uri; 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.browse.SwipeableConversationItemView; 34import com.android.mail.providers.Conversation; 35import com.android.mail.providers.Folder; 36import com.android.mail.ui.SwipeHelper.Callback; 37import com.android.mail.utils.LogTag; 38import com.android.mail.utils.LogUtils; 39import com.android.mail.utils.Utils; 40 41import java.util.ArrayList; 42import java.util.Collection; 43import java.util.HashMap; 44 45public class SwipeableListView extends ListView implements Callback { 46 private SwipeHelper mSwipeHelper; 47 private boolean mEnableSwipe = false; 48 49 public static final String LOG_TAG = LogTag.getLogTag(); 50 51 private ConversationSelectionSet mConvSelectionSet; 52 private int mSwipeAction; 53 private Folder mFolder; 54 55 public SwipeableListView(Context context) { 56 this(context, null); 57 } 58 59 public SwipeableListView(Context context, AttributeSet attrs) { 60 this(context, attrs, -1); 61 } 62 63 public SwipeableListView(Context context, AttributeSet attrs, int defStyle) { 64 super(context, attrs, defStyle); 65 float densityScale = getResources().getDisplayMetrics().density; 66 float pagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop(); 67 mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this, densityScale, 68 pagingTouchSlop); 69 } 70 71 @Override 72 protected void onConfigurationChanged(Configuration newConfig) { 73 super.onConfigurationChanged(newConfig); 74 float densityScale = getResources().getDisplayMetrics().density; 75 mSwipeHelper.setDensityScale(densityScale); 76 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); 77 mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); 78 } 79 80 @Override 81 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 82 LogUtils.d(Utils.VIEW_DEBUGGING_TAG, 83 "START CLF-ListView.onFocusChanged layoutRequested=%s root.layoutRequested=%s", 84 isLayoutRequested(), getRootView().isLayoutRequested()); 85 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 86 LogUtils.d(Utils.VIEW_DEBUGGING_TAG, new Error(), 87 "FINISH CLF-ListView.onFocusChanged layoutRequested=%s root.layoutRequested=%s", 88 isLayoutRequested(), getRootView().isLayoutRequested()); 89 } 90 91 @Override 92 public void requestLayout() { 93 Utils.checkRequestLayout(this); 94 super.requestLayout(); 95 } 96 97 /** 98 * Enable swipe gestures. 99 */ 100 public void enableSwipe(boolean enable) { 101 mEnableSwipe = enable; 102 } 103 104 public boolean isSwipeEnabled() { 105 return mEnableSwipe; 106 } 107 108 public void setSwipeAction(int action) { 109 mSwipeAction = action; 110 } 111 112 public int getSwipeAction() { 113 return mSwipeAction; 114 } 115 116 public void setSelectionSet(ConversationSelectionSet set) { 117 mConvSelectionSet = set; 118 } 119 120 public void setCurrentFolder(Folder folder) { 121 mFolder = folder; 122 } 123 124 @Override 125 public ConversationSelectionSet getSelectionSet() { 126 return mConvSelectionSet; 127 } 128 129 @Override 130 public boolean onInterceptTouchEvent(MotionEvent ev) { 131 if (mEnableSwipe) { 132 return mSwipeHelper.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev); 133 } else { 134 return super.onInterceptTouchEvent(ev); 135 } 136 } 137 138 @Override 139 public boolean onTouchEvent(MotionEvent ev) { 140 if (mEnableSwipe) { 141 return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev); 142 } else { 143 return super.onTouchEvent(ev); 144 } 145 } 146 147 @Override 148 public View getChildAtPosition(MotionEvent ev) { 149 // find the view under the pointer, accounting for GONE views 150 final int count = getChildCount(); 151 int touchY = (int) ev.getY(); 152 int childIdx = 0; 153 View slidingChild; 154 for (; childIdx < count; childIdx++) { 155 slidingChild = getChildAt(childIdx); 156 if (slidingChild.getVisibility() == GONE) { 157 continue; 158 } 159 if (touchY >= slidingChild.getTop() && touchY <= slidingChild.getBottom()) { 160 if (slidingChild instanceof SwipeableConversationItemView) { 161 return ((SwipeableConversationItemView) slidingChild).getSwipeableItemView(); 162 } 163 return slidingChild; 164 } 165 } 166 return null; 167 } 168 169 @Override 170 public boolean canChildBeDismissed(SwipeableItemView v) { 171 return v.canChildBeDismissed(); 172 } 173 174 @Override 175 public void onChildDismissed(SwipeableItemView v) { 176 if (v != null) { 177 v.dismiss(); 178 } 179 } 180 181 // Call this whenever a new action is taken; this forces a commit of any 182 // existing destructive actions. 183 public void commitDestructiveActions(boolean animate) { 184 final AnimatedAdapter adapter = getAnimatedAdapter(); 185 if (adapter != null) { 186 adapter.commitLeaveBehindItems(animate); 187 } 188 } 189 190 public void dismissChild(final ConversationItemView target) { 191 final Context context = getContext(); 192 final ToastBarOperation undoOp; 193 194 undoOp = new ToastBarOperation(1, mSwipeAction, ToastBarOperation.UNDO, false); 195 Conversation conv = target.getConversation(); 196 target.getConversation().position = findConversation(target, conv); 197 final AnimatedAdapter adapter = getAnimatedAdapter(); 198 if (adapter == null) { 199 return; 200 } 201 adapter.setupLeaveBehind(conv, undoOp, conv.position); 202 ConversationCursor cc = (ConversationCursor) adapter.getCursor(); 203 switch (mSwipeAction) { 204 case R.id.remove_folder: 205 FolderOperation folderOp = new FolderOperation(mFolder, false); 206 HashMap<Uri, Folder> targetFolders = Folder 207 .hashMapForFolders(conv.getRawFolders()); 208 targetFolders.remove(folderOp.mFolder.uri); 209 conv.setRawFolders(Folder.getSerializedFolderString(targetFolders.values())); 210 cc.mostlyDestructiveUpdate(context, Conversation.listOf(conv), 211 Conversation.UPDATE_FOLDER_COLUMN, conv.getRawFoldersString()); 212 break; 213 case R.id.archive: 214 cc.mostlyArchive(context, Conversation.listOf(conv)); 215 break; 216 case R.id.delete: 217 cc.mostlyDelete(context, Conversation.listOf(conv)); 218 break; 219 } 220 adapter.notifyDataSetChanged(); 221 if (mConvSelectionSet != null && !mConvSelectionSet.isEmpty() 222 && mConvSelectionSet.contains(conv)) { 223 mConvSelectionSet.toggle(null, conv); 224 // Don't commit destructive actions if the item we just removed from 225 // the selection set is the item we just destroyed! 226 if (!conv.isMostlyDead() && mConvSelectionSet.isEmpty()) { 227 commitDestructiveActions(true); 228 } 229 } 230 } 231 232 @Override 233 public void onBeginDrag(View v) { 234 // We do this so the underlying ScrollView knows that it won't get 235 // the chance to intercept events anymore 236 requestDisallowInterceptTouchEvent(true); 237 SwipeableConversationItemView view = null; 238 if (v instanceof ConversationItemView) { 239 view = (SwipeableConversationItemView) v.getParent(); 240 } 241 if (view != null) { 242 view.addBackground(getContext()); 243 view.setBackgroundVisibility(View.VISIBLE); 244 } 245 } 246 247 @Override 248 public void onDragCancelled(SwipeableItemView v) { 249 SwipeableConversationItemView view = null; 250 if (v instanceof ConversationItemView) { 251 view = (SwipeableConversationItemView) ((View) v).getParent(); 252 } 253 if (view != null) { 254 view.removeBackground(); 255 } 256 } 257 258 /** 259 * Archive items using the swipe away animation before shrinking them away. 260 */ 261 public void destroyItems(final ArrayList<ConversationItemView> views, 262 final DestructiveAction listener) { 263 if (views == null || views.size() == 0) { 264 return; 265 } 266 // Need to find the items in the LIST! 267 final ArrayList<Conversation> conversations = new ArrayList<Conversation>(); 268 for (ConversationItemView view : views) { 269 Conversation conv = view.getConversation(); 270 conv.position = findConversation(view, conv); 271 conversations.add(conv); 272 } 273 AnimatedAdapter adapter = getAnimatedAdapter(); 274 if (adapter != null) { 275 adapter.swipeDelete(conversations, listener); 276 } 277 } 278 279 public int findConversation(ConversationItemView view, Conversation conv) { 280 int position = conv.position; 281 long convId = conv.id; 282 try { 283 if (position == INVALID_POSITION) { 284 position = getPositionForView(view); 285 } 286 } catch (Exception e) { 287 position = INVALID_POSITION; 288 LogUtils.w(LOG_TAG, "Exception finding position; using alternate strategy"); 289 } 290 if (position == INVALID_POSITION) { 291 // Try the other way! 292 Conversation foundConv; 293 long foundId; 294 for (int i = 0; i < getChildCount(); i++) { 295 View child = getChildAt(i); 296 if (child instanceof SwipeableConversationItemView) { 297 foundConv = ((SwipeableConversationItemView) child).getSwipeableItemView() 298 .getConversation(); 299 foundId = foundConv.id; 300 if (foundId == convId) { 301 position = i; 302 break; 303 } 304 } 305 } 306 } 307 return position; 308 } 309 310 private AnimatedAdapter getAnimatedAdapter() { 311 return (AnimatedAdapter) getAdapter(); 312 } 313 314 @Override 315 public boolean performItemClick(View view, int pos, long id) { 316 boolean handled = super.performItemClick(view, pos, id); 317 // Commit any existing destructive actions when the user selects a 318 // conversation to view. 319 commitDestructiveActions(true); 320 return handled; 321 } 322 323 @Override 324 public void onScroll() { 325 commitDestructiveActions(true); 326 } 327} 328