Home | History | Annotate | Download | only in list
      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.dialer.list;
     19 
     20 import android.animation.Animator;
     21 import android.animation.AnimatorListenerAdapter;
     22 import android.content.Context;
     23 import android.content.res.Configuration;
     24 import android.graphics.Bitmap;
     25 import android.os.Handler;
     26 import android.util.AttributeSet;
     27 import android.util.Log;
     28 import android.view.DragEvent;
     29 import android.view.MotionEvent;
     30 import android.view.View;
     31 import android.view.ViewConfiguration;
     32 import android.widget.ImageView;
     33 import android.widget.ListView;
     34 
     35 import com.android.dialer.R;
     36 import com.android.dialer.list.PhoneFavoritesTileAdapter.ContactTileRow;
     37 import com.android.dialer.list.SwipeHelper.OnItemGestureListener;
     38 import com.android.dialer.list.SwipeHelper.SwipeHelperCallback;
     39 
     40 /**
     41  * The ListView composed of {@link ContactTileRow}.
     42  * This ListView handles both
     43  * - Swiping, which is borrowed from packages/apps/UnifiedEmail (com.android.mail.ui.Swipeable)
     44  * - Drag and drop
     45  */
     46 public class PhoneFavoriteListView extends ListView implements SwipeHelperCallback,
     47         OnDragDropListener {
     48 
     49     public static final String LOG_TAG = PhoneFavoriteListView.class.getSimpleName();
     50 
     51     private SwipeHelper mSwipeHelper;
     52     private boolean mEnableSwipe = true;
     53 
     54     private OnItemGestureListener mOnItemGestureListener;
     55 
     56     private float mDensityScale;
     57     private float mTouchSlop;
     58 
     59     private int mTopScrollBound;
     60     private int mBottomScrollBound;
     61     private int mLastDragY;
     62 
     63     private Handler mScrollHandler;
     64     private final long SCROLL_HANDLER_DELAY_MILLIS = 5;
     65     private final int DRAG_SCROLL_PX_UNIT = 25;
     66 
     67     private boolean mIsDragScrollerRunning = false;
     68     private int mTouchDownForDragStartX;
     69     private int mTouchDownForDragStartY;
     70 
     71     private Bitmap mDragShadowBitmap;
     72     private ImageView mDragShadowOverlay;
     73     private int mAnimationDuration;
     74 
     75     // X and Y offsets inside the item from where the user grabbed to the
     76     // child's left coordinate. This is used to aid in the drawing of the drag shadow.
     77     private int mTouchOffsetToChildLeft;
     78     private int mTouchOffsetToChildTop;
     79 
     80     private int mDragShadowLeft;
     81     private int mDragShadowTop;
     82 
     83     private DragDropController mDragDropController = new DragDropController();
     84 
     85     private final float DRAG_SHADOW_ALPHA = 0.7f;
     86 
     87     /**
     88      * {@link #mTopScrollBound} and {@link mBottomScrollBound} will be
     89      * offseted to the top / bottom by {@link #getHeight} * {@link #BOUND_GAP_RATIO} pixels.
     90      */
     91     private final float BOUND_GAP_RATIO = 0.2f;
     92 
     93     private final Runnable mDragScroller = new Runnable() {
     94         @Override
     95         public void run() {
     96             if (mLastDragY <= mTopScrollBound) {
     97                 smoothScrollBy(-DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS);
     98             } else if (mLastDragY >= mBottomScrollBound) {
     99                 smoothScrollBy(DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS);
    100             }
    101             mScrollHandler.postDelayed(this, SCROLL_HANDLER_DELAY_MILLIS);
    102         }
    103     };
    104 
    105     private final AnimatorListenerAdapter mDragShadowOverAnimatorListener =
    106             new AnimatorListenerAdapter() {
    107         @Override
    108         public void onAnimationEnd(Animator animation) {
    109             if (mDragShadowBitmap != null) {
    110                 mDragShadowBitmap.recycle();
    111                 mDragShadowBitmap = null;
    112             }
    113             mDragShadowOverlay.setVisibility(GONE);
    114             mDragShadowOverlay.setImageBitmap(null);
    115         }
    116     };
    117 
    118     public PhoneFavoriteListView(Context context) {
    119         this(context, null);
    120     }
    121 
    122     public PhoneFavoriteListView(Context context, AttributeSet attrs) {
    123         this(context, attrs, -1);
    124     }
    125 
    126     public PhoneFavoriteListView(Context context, AttributeSet attrs, int defStyle) {
    127         super(context, attrs, defStyle);
    128         mAnimationDuration = context.getResources().getInteger(R.integer.fade_duration);
    129         mDensityScale = getResources().getDisplayMetrics().density;
    130         mTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
    131         mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this,
    132                 mDensityScale, mTouchSlop);
    133         setItemsCanFocus(true);
    134         mDragDropController.addOnDragDropListener(this);
    135     }
    136 
    137     @Override
    138     protected void onConfigurationChanged(Configuration newConfig) {
    139         super.onConfigurationChanged(newConfig);
    140         mDensityScale= getResources().getDisplayMetrics().density;
    141         mTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
    142         mSwipeHelper.setDensityScale(mDensityScale);
    143         mSwipeHelper.setPagingTouchSlop(mTouchSlop);
    144     }
    145 
    146     /**
    147      * Enable swipe gestures.
    148      */
    149     public void enableSwipe(boolean enable) {
    150         mEnableSwipe = enable;
    151     }
    152 
    153     public boolean isSwipeEnabled() {
    154         return mEnableSwipe && mOnItemGestureListener.isSwipeEnabled();
    155     }
    156 
    157     public void setOnItemSwipeListener(OnItemGestureListener listener) {
    158         mOnItemGestureListener = listener;
    159     }
    160 
    161     /**
    162      * TODO: This is all swipe to remove code (nothing to do with drag to remove). This should
    163      * be cleaned up and removed once drag to remove becomes the only way to remove contacts.
    164      */
    165     @Override
    166     public boolean onInterceptTouchEvent(MotionEvent ev) {
    167         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    168             mTouchDownForDragStartX = (int) ev.getX();
    169             mTouchDownForDragStartY = (int) ev.getY();
    170         }
    171         if (isSwipeEnabled()) {
    172             return mSwipeHelper.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev);
    173         } else {
    174             return super.onInterceptTouchEvent(ev);
    175         }
    176     }
    177 
    178     @Override
    179     public boolean onTouchEvent(MotionEvent ev) {
    180         if (mOnItemGestureListener != null) {
    181             mOnItemGestureListener.onTouch();
    182         }
    183         if (isSwipeEnabled()) {
    184             return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev);
    185         } else {
    186             return super.onTouchEvent(ev);
    187         }
    188     }
    189 
    190     @Override
    191     public View getChildAtPosition(MotionEvent ev) {
    192         final View view = getViewAtPosition((int) ev.getX(), (int) ev.getY());
    193         if (view != null &&
    194                 SwipeHelper.isSwipeable(view) &&
    195                 view.getVisibility() != GONE) {
    196             // If this view is swipable in this listview, then return it. Otherwise
    197             // return a null view, which will simply be ignored by the swipe helper.
    198             return view;
    199         }
    200         return null;
    201     }
    202 
    203     @Override
    204     public View getChildContentView(View view) {
    205         return view.findViewById(R.id.contact_favorite_card);
    206     }
    207 
    208     @Override
    209     public void onScroll() {}
    210 
    211     @Override
    212     public boolean canChildBeDismissed(View v) {
    213         return SwipeHelper.isSwipeable(v);
    214     }
    215 
    216     @Override
    217     public void onChildDismissed(final View v) {
    218         if (v != null) {
    219             if (mOnItemGestureListener != null) {
    220                 mOnItemGestureListener.onSwipe(v);
    221             }
    222         }
    223     }
    224 
    225     @Override
    226     public void onDragCancelled(View v) {}
    227 
    228     @Override
    229     public void onBeginDrag(View v) {
    230         final View tileRow = (View) v.getParent();
    231 
    232         // We do this so the underlying ScrollView knows that it won't get
    233         // the chance to intercept events anymore
    234         requestDisallowInterceptTouchEvent(true);
    235     }
    236 
    237     /**
    238      * End of swipe-to-remove code
    239      */
    240 
    241     @Override
    242     public boolean dispatchDragEvent(DragEvent event) {
    243         final int action = event.getAction();
    244         final int eX = (int) event.getX();
    245         final int eY = (int) event.getY();
    246         switch (action) {
    247             case DragEvent.ACTION_DRAG_STARTED:
    248                 final int[] coordinates = new int[2];
    249                 getLocationOnScreen(coordinates);
    250                 // Calculate the X and Y coordinates of the drag event relative to the view
    251                 final int viewX = eX - coordinates[0];
    252                 final int viewY = eY - coordinates[1];
    253                 final View child = getViewAtPosition(viewX, viewY);
    254 
    255                 if (!(child instanceof ContactTileRow)) {
    256                     // Bail early.
    257                     return false;
    258                 }
    259 
    260                 final ContactTileRow tile = (ContactTileRow) child;
    261 
    262                 // Disable drag and drop if there is a contact that has been swiped and is currently
    263                 // in the pending remove state
    264                 if (tile.getTileAdapter().hasPotentialRemoveEntryIndex()) {
    265                     return false;
    266                 }
    267 
    268                 if (!mDragDropController.handleDragStarted(viewX, viewY, tile)) {
    269                     return false;
    270                 }
    271                 break;
    272             case DragEvent.ACTION_DRAG_LOCATION:
    273                 mLastDragY = eY;
    274                 final View view = getViewAtPosition(eX, eY);
    275                 mDragDropController.handleDragHovered(eX, eY, view);
    276                 // Kick off {@link #mScrollHandler} if it's not started yet.
    277                 if (!mIsDragScrollerRunning &&
    278                         // And if the distance traveled while dragging exceeds the touch slop
    279                         (Math.abs(mLastDragY - mTouchDownForDragStartY) >= 4 * mTouchSlop)) {
    280                     mIsDragScrollerRunning = true;
    281                     ensureScrollHandler();
    282                     mScrollHandler.postDelayed(mDragScroller, SCROLL_HANDLER_DELAY_MILLIS);
    283                 }
    284                 break;
    285             case DragEvent.ACTION_DRAG_ENTERED:
    286                 final int boundGap = (int) (getHeight() * BOUND_GAP_RATIO);
    287                 mTopScrollBound = (getTop() + boundGap);
    288                 mBottomScrollBound = (getBottom() - boundGap);
    289                 break;
    290             case DragEvent.ACTION_DRAG_EXITED:
    291             case DragEvent.ACTION_DRAG_ENDED:
    292             case DragEvent.ACTION_DROP:
    293                 ensureScrollHandler();
    294                 mScrollHandler.removeCallbacks(mDragScroller);
    295                 mIsDragScrollerRunning = false;
    296                 // Either a successful drop or it's ended with out drop.
    297                 if (action == DragEvent.ACTION_DROP || action == DragEvent.ACTION_DRAG_ENDED) {
    298                     mDragDropController.handleDragFinished(eX, eY, false);
    299                 }
    300                 break;
    301             default:
    302                 break;
    303         }
    304         // This ListView will consume the drag events on behalf of its children.
    305         return true;
    306     }
    307 
    308     public void setDragShadowOverlay(ImageView overlay) {
    309         mDragShadowOverlay = overlay;
    310     }
    311 
    312     /**
    313      * Find the view under the pointer.
    314      */
    315     private View getViewAtPosition(int x, int y) {
    316         final int count = getChildCount();
    317         View child;
    318         for (int childIdx = 0; childIdx < count; childIdx++) {
    319             child = getChildAt(childIdx);
    320             if (y >= child.getTop() && y <= child.getBottom()) {
    321                 return child;
    322             }
    323         }
    324         return null;
    325     }
    326 
    327     private void ensureScrollHandler() {
    328         if (mScrollHandler == null) {
    329             mScrollHandler = getHandler();
    330         }
    331     }
    332 
    333     public DragDropController getDragDropController() {
    334         return mDragDropController;
    335     }
    336 
    337     @Override
    338     public void onDragStarted(int itemIndex, int x, int y, PhoneFavoriteTileView tileView) {
    339         if (mDragShadowOverlay == null) {
    340             return;
    341         }
    342 
    343         mDragShadowOverlay.clearAnimation();
    344         mDragShadowBitmap = createDraggedChildBitmap(tileView);
    345         if (mDragShadowBitmap == null) {
    346             return;
    347         }
    348 
    349         if (tileView instanceof PhoneFavoriteRegularRowView) {
    350             mDragShadowLeft = tileView.getParentRow().getLeft();
    351             mDragShadowTop = tileView.getParentRow().getTop();
    352         } else {
    353             // Square tile is relative to the contact tile,
    354             // and contact tile is relative to this list view.
    355             mDragShadowLeft = tileView.getLeft() + tileView.getParentRow().getLeft();
    356             mDragShadowTop = tileView.getTop() + tileView.getParentRow().getTop();
    357         }
    358 
    359         mDragShadowOverlay.setImageBitmap(mDragShadowBitmap);
    360         mDragShadowOverlay.setVisibility(VISIBLE);
    361         mDragShadowOverlay.setAlpha(DRAG_SHADOW_ALPHA);
    362 
    363         mDragShadowOverlay.setX(mDragShadowLeft);
    364         mDragShadowOverlay.setY(mDragShadowTop);
    365 
    366         // x and y passed in are the coordinates of where the user has touched down,
    367         // calculate the offset to the top left coordinate of the dragged child.  This
    368         // will be used for drawing the drag shadow.
    369         mTouchOffsetToChildLeft = x - mDragShadowLeft;
    370         mTouchOffsetToChildTop = y - mDragShadowTop;
    371     }
    372 
    373     @Override
    374     public void onDragHovered(int itemIndex, int x, int y) {
    375         // Update the drag shadow location.
    376         mDragShadowLeft = x - mTouchOffsetToChildLeft;
    377         mDragShadowTop = y - mTouchOffsetToChildTop;
    378         // Draw the drag shadow at its last known location if the drag shadow exists.
    379         if (mDragShadowOverlay != null) {
    380             mDragShadowOverlay.setX(mDragShadowLeft);
    381             mDragShadowOverlay.setY(mDragShadowTop);
    382         }
    383     }
    384 
    385     @Override
    386     public void onDragFinished(int x, int y) {
    387         // Update the drag shadow location.
    388         mDragShadowLeft = x - mTouchOffsetToChildLeft;
    389         mDragShadowTop = y - mTouchOffsetToChildTop;
    390 
    391         if (mDragShadowOverlay != null) {
    392             mDragShadowOverlay.clearAnimation();
    393             mDragShadowOverlay.animate().alpha(0.0f)
    394                     .setDuration(mAnimationDuration)
    395                     .setListener(mDragShadowOverAnimatorListener)
    396                     .start();
    397         }
    398     }
    399 
    400     @Override
    401     public void onDroppedOnRemove() {}
    402 
    403     private Bitmap createDraggedChildBitmap(View view) {
    404         view.setDrawingCacheEnabled(true);
    405         final Bitmap cache = view.getDrawingCache();
    406 
    407         Bitmap bitmap = null;
    408         if (cache != null) {
    409             try {
    410                 bitmap = cache.copy(Bitmap.Config.ARGB_8888, false);
    411             } catch (final OutOfMemoryError e) {
    412                 Log.w(LOG_TAG, "Failed to copy bitmap from Drawing cache", e);
    413                 bitmap = null;
    414             }
    415         }
    416 
    417         view.destroyDrawingCache();
    418         view.setDrawingCacheEnabled(false);
    419 
    420         return bitmap;
    421     }
    422 }
    423