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