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.widget.AbsListView;
     27 import android.widget.AbsListView.OnScrollListener;
     28 import android.view.MotionEvent;
     29 import android.view.View;
     30 import android.view.ViewConfiguration;
     31 import android.widget.ListView;
     32 
     33 import com.android.mail.R;
     34 import com.android.mail.analytics.Analytics;
     35 import com.android.mail.analytics.AnalyticsUtils;
     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      * Set to false to prevent the FLING scroll state from pausing the photo manager loaders.
     59      */
     60     private final static boolean SCROLL_PAUSE_ENABLE = true;
     61 
     62     /**
     63      * Set to true to enable parallax effect for attachment previews as the scroll position varies.
     64      * This effect triggers invalidations on scroll (!) and requires more memory for attachment
     65      * preview bitmaps.
     66      */
     67     public static final boolean ENABLE_ATTACHMENT_PARALLAX = true;
     68 
     69     /**
     70      * Set to true to queue finished decodes in an aggregator so that we display decoded attachment
     71      * previews in an ordered fashion. This artificially delays updating the UI with decoded images,
     72      * since they may have to wait on another image to finish decoding first.
     73      */
     74     public static final boolean ENABLE_ATTACHMENT_DECODE_AGGREGATOR = true;
     75 
     76     /**
     77      * The amount of extra vertical space to decode in attachment previews so we have image data to
     78      * pan within. 1.0 implies no parallax effect.
     79      */
     80     public static final float ATTACHMENT_PARALLAX_MULTIPLIER_NORMAL = 1.5f;
     81     public static final float ATTACHMENT_PARALLAX_MULTIPLIER_ALTERNATIVE = 2.0f;
     82 
     83     private ConversationSelectionSet mConvSelectionSet;
     84     private int mSwipeAction;
     85     private Account mAccount;
     86     private Folder mFolder;
     87     private ListItemSwipedListener mSwipedListener;
     88     private boolean mScrolling;
     89 
     90     private SwipeListener mSwipeListener;
     91 
     92     // Instantiated through view inflation
     93     @SuppressWarnings("unused")
     94     public SwipeableListView(Context context) {
     95         this(context, null);
     96     }
     97 
     98     public SwipeableListView(Context context, AttributeSet attrs) {
     99         this(context, attrs, -1);
    100     }
    101 
    102     public SwipeableListView(Context context, AttributeSet attrs, int defStyle) {
    103         super(context, attrs, defStyle);
    104         setOnScrollListener(this);
    105         float densityScale = getResources().getDisplayMetrics().density;
    106         float pagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
    107         mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this, densityScale,
    108                 pagingTouchSlop);
    109     }
    110 
    111     @Override
    112     protected void onConfigurationChanged(Configuration newConfig) {
    113         super.onConfigurationChanged(newConfig);
    114         float densityScale = getResources().getDisplayMetrics().density;
    115         mSwipeHelper.setDensityScale(densityScale);
    116         float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
    117         mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
    118     }
    119 
    120     @Override
    121     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
    122         LogUtils.d(Utils.VIEW_DEBUGGING_TAG,
    123                 "START CLF-ListView.onFocusChanged layoutRequested=%s root.layoutRequested=%s",
    124                 isLayoutRequested(), getRootView().isLayoutRequested());
    125         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
    126         LogUtils.d(Utils.VIEW_DEBUGGING_TAG, new Error(),
    127                 "FINISH CLF-ListView.onFocusChanged layoutRequested=%s root.layoutRequested=%s",
    128                 isLayoutRequested(), getRootView().isLayoutRequested());
    129     }
    130 
    131     /**
    132      * Enable swipe gestures.
    133      */
    134     public void enableSwipe(boolean enable) {
    135         mEnableSwipe = enable;
    136     }
    137 
    138     public void setSwipeAction(int action) {
    139         mSwipeAction = action;
    140     }
    141 
    142     public void setSwipedListener(ListItemSwipedListener listener) {
    143         mSwipedListener = listener;
    144     }
    145 
    146     public int getSwipeAction() {
    147         return mSwipeAction;
    148     }
    149 
    150     public void setSelectionSet(ConversationSelectionSet set) {
    151         mConvSelectionSet = set;
    152     }
    153 
    154     public void setCurrentAccount(Account account) {
    155         mAccount = account;
    156     }
    157 
    158     public void setCurrentFolder(Folder folder) {
    159         mFolder = folder;
    160     }
    161 
    162     @Override
    163     public ConversationSelectionSet getSelectionSet() {
    164         return mConvSelectionSet;
    165     }
    166 
    167     @Override
    168     public boolean onInterceptTouchEvent(MotionEvent ev) {
    169         if (mScrolling || !mEnableSwipe) {
    170             return super.onInterceptTouchEvent(ev);
    171         } else {
    172             return mSwipeHelper.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev);
    173         }
    174     }
    175 
    176     @Override
    177     public boolean onTouchEvent(MotionEvent ev) {
    178         if (mEnableSwipe) {
    179             return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev);
    180         } else {
    181             return super.onTouchEvent(ev);
    182         }
    183     }
    184 
    185     @Override
    186     public View getChildAtPosition(MotionEvent ev) {
    187         // find the view under the pointer, accounting for GONE views
    188         final int count = getChildCount();
    189         final int touchY = (int) ev.getY();
    190         int childIdx = 0;
    191         View slidingChild;
    192         for (; childIdx < count; childIdx++) {
    193             slidingChild = getChildAt(childIdx);
    194             if (slidingChild.getVisibility() == GONE) {
    195                 continue;
    196             }
    197             if (touchY >= slidingChild.getTop() && touchY <= slidingChild.getBottom()) {
    198                 if (slidingChild instanceof SwipeableConversationItemView) {
    199                     return ((SwipeableConversationItemView) slidingChild).getSwipeableItemView();
    200                 }
    201                 return slidingChild;
    202             }
    203         }
    204         return null;
    205     }
    206 
    207     @Override
    208     public boolean canChildBeDismissed(SwipeableItemView v) {
    209         return v.canChildBeDismissed();
    210     }
    211 
    212     @Override
    213     public void onChildDismissed(SwipeableItemView v) {
    214         if (v != null) {
    215             v.dismiss();
    216         }
    217     }
    218 
    219     // Call this whenever a new action is taken; this forces a commit of any
    220     // existing destructive actions.
    221     public void commitDestructiveActions(boolean animate) {
    222         final AnimatedAdapter adapter = getAnimatedAdapter();
    223         if (adapter != null) {
    224             adapter.commitLeaveBehindItems(animate);
    225         }
    226     }
    227 
    228     public void dismissChild(final ConversationItemView target) {
    229         final ToastBarOperation undoOp;
    230 
    231         undoOp = new ToastBarOperation(1, mSwipeAction, ToastBarOperation.UNDO, false /* batch */,
    232                 mFolder);
    233         Conversation conv = target.getConversation();
    234         target.getConversation().position = findConversation(target, conv);
    235         final AnimatedAdapter adapter = getAnimatedAdapter();
    236         if (adapter == null) {
    237             return;
    238         }
    239         adapter.setupLeaveBehind(conv, undoOp, conv.position, target.getHeight());
    240         ConversationCursor cc = (ConversationCursor) adapter.getCursor();
    241         Collection<Conversation> convList = Conversation.listOf(conv);
    242         ArrayList<Uri> folderUris;
    243         ArrayList<Boolean> adds;
    244 
    245         Analytics.getInstance().sendMenuItemEvent("list_swipe", mSwipeAction, null, 0);
    246 
    247         if (mSwipeAction == R.id.remove_folder) {
    248             FolderOperation folderOp = new FolderOperation(mFolder, false);
    249             HashMap<Uri, Folder> targetFolders = Folder
    250                     .hashMapForFolders(conv.getRawFolders());
    251             targetFolders.remove(folderOp.mFolder.folderUri.fullUri);
    252             final FolderList folders = FolderList.copyOf(targetFolders.values());
    253             conv.setRawFolders(folders);
    254             final ContentValues values = new ContentValues();
    255             folderUris = new ArrayList<Uri>();
    256             folderUris.add(mFolder.folderUri.fullUri);
    257             adds = new ArrayList<Boolean>();
    258             adds.add(Boolean.FALSE);
    259             ConversationCursor.addFolderUpdates(folderUris, adds, values);
    260             ConversationCursor.addTargetFolders(targetFolders.values(), values);
    261             cc.mostlyDestructiveUpdate(Conversation.listOf(conv), values);
    262         } else if (mSwipeAction == R.id.archive) {
    263             cc.mostlyArchive(convList);
    264         } else if (mSwipeAction == R.id.delete) {
    265             cc.mostlyDelete(convList);
    266         }
    267         if (mSwipedListener != null) {
    268             mSwipedListener.onListItemSwiped(convList);
    269         }
    270         adapter.notifyDataSetChanged();
    271         if (mConvSelectionSet != null && !mConvSelectionSet.isEmpty()
    272                 && mConvSelectionSet.contains(conv)) {
    273             mConvSelectionSet.toggle(conv);
    274             // Don't commit destructive actions if the item we just removed from
    275             // the selection set is the item we just destroyed!
    276             if (!conv.isMostlyDead() && mConvSelectionSet.isEmpty()) {
    277                 commitDestructiveActions(true);
    278             }
    279         }
    280     }
    281 
    282     @Override
    283     public void onBeginDrag(View v) {
    284         // We do this so the underlying ScrollView knows that it won't get
    285         // the chance to intercept events anymore
    286         requestDisallowInterceptTouchEvent(true);
    287         cancelDismissCounter();
    288 
    289         // Notifies {@link ConversationListView} to disable pull to refresh since once
    290         // an item in the list view has been picked up, we don't want any vertical movement
    291         // to also trigger refresh.
    292         if (mSwipeListener != null) {
    293             mSwipeListener.onBeginSwipe();
    294         }
    295     }
    296 
    297     @Override
    298     public void onDragCancelled(SwipeableItemView v) {
    299         final AnimatedAdapter adapter = getAnimatedAdapter();
    300         if (adapter != null) {
    301             adapter.startDismissCounter();
    302             adapter.cancelFadeOutLastLeaveBehindItemText();
    303         }
    304     }
    305 
    306     /**
    307      * Archive items using the swipe away animation before shrinking them away.
    308      */
    309     public boolean destroyItems(Collection<Conversation> convs,
    310             final ListItemsRemovedListener listener) {
    311         if (convs == null) {
    312             LogUtils.e(LOG_TAG, "SwipeableListView.destroyItems: null conversations.");
    313             return false;
    314         }
    315         final AnimatedAdapter adapter = getAnimatedAdapter();
    316         if (adapter == null) {
    317             LogUtils.e(LOG_TAG, "SwipeableListView.destroyItems: Cannot destroy: adapter is null.");
    318             return false;
    319         }
    320         adapter.swipeDelete(convs, listener);
    321         return true;
    322     }
    323 
    324     public int findConversation(ConversationItemView view, Conversation conv) {
    325         int position = INVALID_POSITION;
    326         long convId = conv.id;
    327         try {
    328             position = getPositionForView(view);
    329         } catch (Exception e) {
    330             position = INVALID_POSITION;
    331             LogUtils.w(LOG_TAG, e, "Exception finding position; using alternate strategy");
    332         }
    333         if (position == INVALID_POSITION) {
    334             // Try the other way!
    335             Conversation foundConv;
    336             long foundId;
    337             for (int i = 0; i < getChildCount(); i++) {
    338                 View child = getChildAt(i);
    339                 if (child instanceof SwipeableConversationItemView) {
    340                     foundConv = ((SwipeableConversationItemView) child).getSwipeableItemView()
    341                             .getConversation();
    342                     foundId = foundConv.id;
    343                     if (foundId == convId) {
    344                         position = i + getFirstVisiblePosition();
    345                         break;
    346                     }
    347                 }
    348             }
    349         }
    350         return position;
    351     }
    352 
    353     private AnimatedAdapter getAnimatedAdapter() {
    354         return (AnimatedAdapter) getAdapter();
    355     }
    356 
    357     @Override
    358     public boolean performItemClick(View view, int pos, long id) {
    359         final int previousPosition = getCheckedItemPosition();
    360         final boolean selectionSetEmpty = mConvSelectionSet.isEmpty();
    361 
    362         // Superclass method modifies the selection set
    363         final boolean handled = super.performItemClick(view, pos, id);
    364 
    365         // If we are in CAB mode then a click shouldn't
    366         // activate the new item, it should only add it to the selection set
    367         if (!selectionSetEmpty && previousPosition != -1) {
    368             setItemChecked(previousPosition, true);
    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         if (ENABLE_ATTACHMENT_PARALLAX) {
    393             for (int i = 0, len = getChildCount(); i < len; i++) {
    394                 final View child = getChildAt(i);
    395                 if (child instanceof OnScrollListener) {
    396                     ((OnScrollListener) child).onScroll(view, firstVisibleItem, visibleItemCount,
    397                             totalItemCount);
    398                 }
    399             }
    400         }
    401     }
    402 
    403     @Override
    404     public void onScrollStateChanged(final AbsListView view, final int scrollState) {
    405         mScrolling = scrollState != OnScrollListener.SCROLL_STATE_IDLE;
    406 
    407         if (!mScrolling) {
    408             final Context c = getContext();
    409             if (c instanceof ControllableActivity) {
    410                 final ControllableActivity activity = (ControllableActivity) c;
    411                 activity.onAnimationEnd(null /* adapter */);
    412             } else {
    413                 LogUtils.wtf(LOG_TAG, "unexpected context=%s", c);
    414             }
    415         }
    416 
    417         if (SCROLL_PAUSE_ENABLE) {
    418             AnimatedAdapter adapter = getAnimatedAdapter();
    419             if (adapter != null) {
    420                 adapter.onScrollStateChanged(scrollState);
    421             }
    422             ConversationItemView.setScrollStateChanged(scrollState);
    423         }
    424     }
    425 
    426     public boolean isScrolling() {
    427         return mScrolling;
    428     }
    429 
    430     @Override
    431     public void cancelDismissCounter() {
    432         AnimatedAdapter adapter = getAnimatedAdapter();
    433         if (adapter != null) {
    434             adapter.cancelDismissCounter();
    435         }
    436     }
    437 
    438     @Override
    439     public LeaveBehindItem getLastSwipedItem() {
    440         AnimatedAdapter adapter = getAnimatedAdapter();
    441         if (adapter != null) {
    442             return adapter.getLastLeaveBehindItem();
    443         }
    444         return null;
    445     }
    446 
    447     public void setSwipeListener(SwipeListener swipeListener) {
    448         mSwipeListener = swipeListener;
    449     }
    450 
    451     public interface SwipeListener {
    452         public void onBeginSwipe();
    453     }
    454 }
    455