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.ListView;
     32 
     33 import com.android.mail.R;
     34 import com.android.mail.analytics.Analytics;
     35 import com.android.mail.browse.ConversationCursor;
     36 import com.android.mail.browse.ConversationItemView;
     37 import com.android.mail.browse.SwipeableConversationItemView;
     38 import com.android.mail.providers.Account;
     39 import com.android.mail.providers.Conversation;
     40 import com.android.mail.providers.Folder;
     41 import com.android.mail.providers.FolderList;
     42 import com.android.mail.ui.SwipeHelper.Callback;
     43 import com.android.mail.utils.LogTag;
     44 import com.android.mail.utils.LogUtils;
     45 import com.android.mail.utils.Utils;
     46 
     47 import java.util.ArrayList;
     48 import java.util.Collection;
     49 import java.util.HashMap;
     50 
     51 public class SwipeableListView extends ListView implements Callback, OnScrollListener {
     52     private static final long INVALID_CONVERSATION_ID = -1;
     53 
     54     private final SwipeHelper mSwipeHelper;
     55     /**
     56      * Are swipes enabled on all items? (Each individual item can still prevent swiping.)<br>
     57      * When swiping is disabled, the UI still reacts to the gesture to acknowledge it.
     58      */
     59     private boolean mEnableSwipe = false;
     60     /**
     61      * When set, we prevent the SwipeHelper from kicking in at all. This
     62      * short-circuits {@link #mEnableSwipe}.
     63      */
     64     private boolean mPreventSwipesEntirely = false;
     65 
     66     public static final String LOG_TAG = LogTag.getLogTag();
     67 
     68     private ConversationCheckedSet mConvCheckedSet;
     69     private int mSwipeAction;
     70     private Account mAccount;
     71     private Folder mFolder;
     72     private ListItemSwipedListener mSwipedListener;
     73     private boolean mScrolling;
     74 
     75     private SwipeListener mSwipeListener;
     76 
     77     private long mSelectedConversationId = INVALID_CONVERSATION_ID;
     78 
     79     // Instantiated through view inflation
     80     @SuppressWarnings("unused")
     81     public SwipeableListView(Context context) {
     82         this(context, null);
     83     }
     84 
     85     public SwipeableListView(Context context, AttributeSet attrs) {
     86         this(context, attrs, -1);
     87     }
     88 
     89     public SwipeableListView(Context context, AttributeSet attrs, int defStyle) {
     90         super(context, attrs, defStyle);
     91         float densityScale = getResources().getDisplayMetrics().density;
     92         float pagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
     93         mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this, densityScale,
     94                 pagingTouchSlop);
     95         mScrolling = false;
     96     }
     97 
     98     @Override
     99     protected void onConfigurationChanged(Configuration newConfig) {
    100         super.onConfigurationChanged(newConfig);
    101         float densityScale = getResources().getDisplayMetrics().density;
    102         mSwipeHelper.setDensityScale(densityScale);
    103         float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
    104         mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
    105     }
    106 
    107     @Override
    108     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
    109         LogUtils.d(Utils.VIEW_DEBUGGING_TAG,
    110                 "START CLF-ListView.onFocusChanged layoutRequested=%s root.layoutRequested=%s",
    111                 isLayoutRequested(), getRootView().isLayoutRequested());
    112         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
    113         LogUtils.d(Utils.VIEW_DEBUGGING_TAG, new Error(),
    114                 "FINISH CLF-ListView.onFocusChanged layoutRequested=%s root.layoutRequested=%s",
    115                 isLayoutRequested(), getRootView().isLayoutRequested());
    116     }
    117 
    118     /**
    119      * Enable swipe gestures.
    120      */
    121     public void enableSwipe(boolean enable) {
    122         mEnableSwipe = enable;
    123     }
    124 
    125     /**
    126      * Completely ignore any horizontal swiping gestures.
    127      */
    128     public void preventSwipesEntirely() {
    129         mPreventSwipesEntirely = true;
    130     }
    131 
    132     /**
    133      * Reverses a prior call to {@link #preventSwipesEntirely()}.
    134      */
    135     public void stopPreventingSwipes() {
    136         mPreventSwipesEntirely = false;
    137     }
    138 
    139     public void setSwipeAction(int action) {
    140         mSwipeAction = action;
    141     }
    142 
    143     public void setListItemSwipedListener(ListItemSwipedListener listener) {
    144         mSwipedListener = listener;
    145     }
    146 
    147     public int getSwipeAction() {
    148         return mSwipeAction;
    149     }
    150 
    151     public void setCheckedSet(ConversationCheckedSet set) {
    152         mConvCheckedSet = set;
    153     }
    154 
    155     public void setCurrentAccount(Account account) {
    156         mAccount = account;
    157     }
    158 
    159     public void setCurrentFolder(Folder folder) {
    160         mFolder = folder;
    161     }
    162 
    163     @Override
    164     public ConversationCheckedSet getCheckedSet() {
    165         return mConvCheckedSet;
    166     }
    167 
    168     @Override
    169     public boolean onInterceptTouchEvent(MotionEvent ev) {
    170         if (mScrolling) {
    171             return super.onInterceptTouchEvent(ev);
    172         } else {
    173             return (!mPreventSwipesEntirely && mSwipeHelper.onInterceptTouchEvent(ev))
    174                     || super.onInterceptTouchEvent(ev);
    175         }
    176     }
    177 
    178     @Override
    179     public boolean onTouchEvent(MotionEvent ev) {
    180         return (!mPreventSwipesEntirely && mSwipeHelper.onTouchEvent(ev)) || super.onTouchEvent(ev);
    181     }
    182 
    183     @Override
    184     public View getChildAtPosition(MotionEvent ev) {
    185         // find the view under the pointer, accounting for GONE views
    186         final int count = getChildCount();
    187         final int touchY = (int) ev.getY();
    188         int childIdx = 0;
    189         View slidingChild;
    190         for (; childIdx < count; childIdx++) {
    191             slidingChild = getChildAt(childIdx);
    192             if (slidingChild.getVisibility() == GONE) {
    193                 continue;
    194             }
    195             if (touchY >= slidingChild.getTop() && touchY <= slidingChild.getBottom()) {
    196                 if (slidingChild instanceof SwipeableConversationItemView) {
    197                     return ((SwipeableConversationItemView) slidingChild).getSwipeableItemView();
    198                 }
    199                 return slidingChild;
    200             }
    201         }
    202         return null;
    203     }
    204 
    205     @Override
    206     public boolean canChildBeDismissed(SwipeableItemView v) {
    207         return mEnableSwipe && v.canChildBeDismissed();
    208     }
    209 
    210     @Override
    211     public void onChildDismissed(SwipeableItemView v) {
    212         if (v != null) {
    213             v.dismiss();
    214         }
    215     }
    216 
    217     // Call this whenever a new action is taken; this forces a commit of any
    218     // existing destructive actions.
    219     public void commitDestructiveActions(boolean animate) {
    220         final AnimatedAdapter adapter = getAnimatedAdapter();
    221         if (adapter != null) {
    222             adapter.commitLeaveBehindItems(animate);
    223         }
    224     }
    225 
    226     public void dismissChild(final ConversationItemView target) {
    227         // Notifies the SwipeListener that a swipe has ended.
    228         if (mSwipeListener != null) {
    229             mSwipeListener.onEndSwipe();
    230         }
    231 
    232         final ToastBarOperation undoOp;
    233 
    234         undoOp = new ToastBarOperation(1, mSwipeAction, ToastBarOperation.UNDO, false /* batch */,
    235                 mFolder);
    236         Conversation conv = target.getConversation();
    237         target.getConversation().position = findConversation(target, conv);
    238         final AnimatedAdapter adapter = getAnimatedAdapter();
    239         if (adapter == null) {
    240             return;
    241         }
    242         adapter.setupLeaveBehind(conv, undoOp, conv.position, target.getHeight());
    243         ConversationCursor cc = (ConversationCursor) adapter.getCursor();
    244         Collection<Conversation> convList = Conversation.listOf(conv);
    245         ArrayList<Uri> folderUris;
    246         ArrayList<Boolean> adds;
    247 
    248         Analytics.getInstance().sendMenuItemEvent("list_swipe", mSwipeAction, null, 0);
    249 
    250         if (mSwipeAction == R.id.remove_folder) {
    251             FolderOperation folderOp = new FolderOperation(mFolder, false);
    252             HashMap<Uri, Folder> targetFolders = Folder
    253                     .hashMapForFolders(conv.getRawFolders());
    254             targetFolders.remove(folderOp.mFolder.folderUri.fullUri);
    255             final FolderList folders = FolderList.copyOf(targetFolders.values());
    256             conv.setRawFolders(folders);
    257             final ContentValues values = new ContentValues();
    258             folderUris = new ArrayList<Uri>();
    259             folderUris.add(mFolder.folderUri.fullUri);
    260             adds = new ArrayList<Boolean>();
    261             adds.add(Boolean.FALSE);
    262             ConversationCursor.addFolderUpdates(folderUris, adds, values);
    263             ConversationCursor.addTargetFolders(targetFolders.values(), values);
    264             cc.mostlyDestructiveUpdate(Conversation.listOf(conv), values);
    265         } else if (mSwipeAction == R.id.archive) {
    266             cc.mostlyArchive(convList);
    267         } else if (mSwipeAction == R.id.delete) {
    268             cc.mostlyDelete(convList);
    269         } else if (mSwipeAction == R.id.discard_outbox) {
    270             cc.moveFailedIntoDrafts(convList);
    271         }
    272         if (mSwipedListener != null) {
    273             mSwipedListener.onListItemSwiped(convList);
    274         }
    275         adapter.notifyDataSetChanged();
    276         if (mConvCheckedSet != null && !mConvCheckedSet.isEmpty()
    277                 && mConvCheckedSet.contains(conv)) {
    278             mConvCheckedSet.toggle(conv);
    279             // Don't commit destructive actions if the item we just removed from
    280             // the selection set is the item we just destroyed!
    281             if (!conv.isMostlyDead() && mConvCheckedSet.isEmpty()) {
    282                 commitDestructiveActions(true);
    283             }
    284         }
    285     }
    286 
    287     @Override
    288     public void onBeginDrag(View v) {
    289         // We do this so the underlying ScrollView knows that it won't get
    290         // the chance to intercept events anymore
    291         requestDisallowInterceptTouchEvent(true);
    292         cancelDismissCounter();
    293 
    294         // Notifies the SwipeListener that a swipe has begun.
    295         if (mSwipeListener != null) {
    296             mSwipeListener.onBeginSwipe();
    297         }
    298     }
    299 
    300     @Override
    301     public void onDragCancelled(SwipeableItemView v) {
    302         final AnimatedAdapter adapter = getAnimatedAdapter();
    303         if (adapter != null) {
    304             adapter.startDismissCounter();
    305             adapter.cancelFadeOutLastLeaveBehindItemText();
    306         }
    307 
    308         // Notifies the SwipeListener that a swipe has ended.
    309         if (mSwipeListener != null) {
    310             mSwipeListener.onEndSwipe();
    311         }
    312     }
    313 
    314     /**
    315      * Archive items using the swipe away animation before shrinking them away.
    316      */
    317     public boolean destroyItems(Collection<Conversation> convs,
    318             final ListItemsRemovedListener listener) {
    319         if (convs == null) {
    320             LogUtils.e(LOG_TAG, "SwipeableListView.destroyItems: null conversations.");
    321             return false;
    322         }
    323         final AnimatedAdapter adapter = getAnimatedAdapter();
    324         if (adapter == null) {
    325             LogUtils.e(LOG_TAG, "SwipeableListView.destroyItems: Cannot destroy: adapter is null.");
    326             return false;
    327         }
    328         adapter.swipeDelete(convs, listener);
    329         return true;
    330     }
    331 
    332     public int findConversation(ConversationItemView view, Conversation conv) {
    333         int position = INVALID_POSITION;
    334         long convId = conv.id;
    335         try {
    336             position = getPositionForView(view);
    337         } catch (Exception e) {
    338             position = INVALID_POSITION;
    339             LogUtils.w(LOG_TAG, e, "Exception finding position; using alternate strategy");
    340         }
    341         if (position == INVALID_POSITION) {
    342             // Try the other way!
    343             Conversation foundConv;
    344             long foundId;
    345             for (int i = 0; i < getChildCount(); i++) {
    346                 View child = getChildAt(i);
    347                 if (child instanceof SwipeableConversationItemView) {
    348                     foundConv = ((SwipeableConversationItemView) child).getSwipeableItemView()
    349                             .getConversation();
    350                     foundId = foundConv.id;
    351                     if (foundId == convId) {
    352                         position = i + getFirstVisiblePosition();
    353                         break;
    354                     }
    355                 }
    356             }
    357         }
    358         return position;
    359     }
    360 
    361     private AnimatedAdapter getAnimatedAdapter() {
    362         return (AnimatedAdapter) getAdapter();
    363     }
    364 
    365     @Override
    366     public boolean performItemClick(View view, int pos, long id) {
    367         // Superclass method modifies the selection set
    368         final boolean handled = super.performItemClick(view, pos, id);
    369 
    370         // Commit any existing destructive actions when the user selects a
    371         // conversation to view.
    372         commitDestructiveActions(true);
    373         return handled;
    374     }
    375 
    376     @Override
    377     public void onScroll() {
    378         commitDestructiveActions(true);
    379     }
    380 
    381     public interface ListItemsRemovedListener {
    382         public void onListItemsRemoved();
    383     }
    384 
    385     public interface ListItemSwipedListener {
    386         public void onListItemSwiped(Collection<Conversation> conversations);
    387     }
    388 
    389     @Override
    390     public final void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
    391             int totalItemCount) {
    392     }
    393 
    394     @Override
    395     public void onScrollStateChanged(final AbsListView view, final int scrollState) {
    396         mScrolling = scrollState != OnScrollListener.SCROLL_STATE_IDLE;
    397 
    398         if (!mScrolling) {
    399             final Context c = getContext();
    400             if (c instanceof ControllableActivity) {
    401                 final ControllableActivity activity = (ControllableActivity) c;
    402                 activity.onAnimationEnd(null /* adapter */);
    403             } else {
    404                 LogUtils.wtf(LOG_TAG, "unexpected context=%s", c);
    405             }
    406         }
    407     }
    408 
    409     public boolean isScrolling() {
    410         return mScrolling;
    411     }
    412 
    413     /**
    414      * Set the currently selected (focused by the list view) position.
    415      */
    416     public void setSelectedConversation(Conversation conv) {
    417         if (conv == null) {
    418             return;
    419         }
    420 
    421         mSelectedConversationId = conv.id;
    422     }
    423 
    424     public boolean isConversationSelected(Conversation conv) {
    425         return mSelectedConversationId != INVALID_CONVERSATION_ID && conv != null
    426                 && mSelectedConversationId == conv.id;
    427     }
    428 
    429     /**
    430      * This is only used for debugging/logging purposes. DO NOT call this function to try to get
    431      * the currently selected position. Use {@link #mSelectedConversationId} instead.
    432      */
    433     public int getSelectedConversationPosDebug() {
    434         for (int i = getFirstVisiblePosition(); i < getLastVisiblePosition(); i++) {
    435             final Object item = getItemAtPosition(i);
    436             if (item instanceof ConversationCursor) {
    437                 final Conversation c = ((ConversationCursor) item).getConversation();
    438                 if (c.id == mSelectedConversationId) {
    439                     return i;
    440                 }
    441             }
    442         }
    443         return ListView.INVALID_POSITION;
    444     }
    445 
    446     @Override
    447     public void onTouchModeChanged(boolean isInTouchMode) {
    448         super.onTouchModeChanged(isInTouchMode);
    449         if (!isInTouchMode) {
    450             // We need to invalidate going from touch mode -> keyboard mode because the currently
    451             // selected item might have changed in touch mode. However, since from the framework's
    452             // perspective the selected position doesn't matter in touch mode, when we enter
    453             // keyboard mode via up/down arrow, the list view will ONLY invalidate the newly
    454             // selected item and not the currently selected item. As a result, we might get an
    455             // inconsistent UI where it looks like both the old and new selected items are focused.
    456             final int index = getSelectedItemPosition();
    457             if (index != ListView.INVALID_POSITION) {
    458                 final View child = getChildAt(index - getFirstVisiblePosition());
    459                 if (child != null) {
    460                     child.invalidate();
    461                 }
    462             }
    463         }
    464     }
    465 
    466     @Override
    467     public void cancelDismissCounter() {
    468         AnimatedAdapter adapter = getAnimatedAdapter();
    469         if (adapter != null) {
    470             adapter.cancelDismissCounter();
    471         }
    472     }
    473 
    474     @Override
    475     public LeaveBehindItem getLastSwipedItem() {
    476         AnimatedAdapter adapter = getAnimatedAdapter();
    477         if (adapter != null) {
    478             return adapter.getLastLeaveBehindItem();
    479         }
    480         return null;
    481     }
    482 
    483     public void setSwipeListener(SwipeListener swipeListener) {
    484         mSwipeListener = swipeListener;
    485     }
    486 
    487     public interface SwipeListener {
    488         public void onBeginSwipe();
    489         public void onEndSwipe();
    490     }
    491 }
    492