Home | History | Annotate | Download | only in ui
      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