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.app.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.GridView;
     33 import android.widget.ImageView;
     34 import com.android.dialer.app.R;
     35 import com.android.dialer.app.list.DragDropController.DragItemContainer;
     36 
     37 /** Viewgroup that presents the user's speed dial contacts in a grid. */
     38 public class PhoneFavoriteListView extends GridView
     39     implements OnDragDropListener, DragItemContainer {
     40 
     41   public static final String LOG_TAG = PhoneFavoriteListView.class.getSimpleName();
     42   final int[] mLocationOnScreen = new int[2];
     43   private final long SCROLL_HANDLER_DELAY_MILLIS = 5;
     44   private final int DRAG_SCROLL_PX_UNIT = 25;
     45   private final float DRAG_SHADOW_ALPHA = 0.7f;
     46   /**
     47    * {@link #mTopScrollBound} and {@link mBottomScrollBound} will be offseted to the top / bottom by
     48    * {@link #getHeight} * {@link #BOUND_GAP_RATIO} pixels.
     49    */
     50   private final float BOUND_GAP_RATIO = 0.2f;
     51 
     52   private float mTouchSlop;
     53   private int mTopScrollBound;
     54   private int mBottomScrollBound;
     55   private int mLastDragY;
     56   private Handler mScrollHandler;
     57   private final Runnable mDragScroller =
     58       new Runnable() {
     59         @Override
     60         public void run() {
     61           if (mLastDragY <= mTopScrollBound) {
     62             smoothScrollBy(-DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS);
     63           } else if (mLastDragY >= mBottomScrollBound) {
     64             smoothScrollBy(DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS);
     65           }
     66           mScrollHandler.postDelayed(this, SCROLL_HANDLER_DELAY_MILLIS);
     67         }
     68       };
     69   private boolean mIsDragScrollerRunning = false;
     70   private int mTouchDownForDragStartX;
     71   private int mTouchDownForDragStartY;
     72   private Bitmap mDragShadowBitmap;
     73   private ImageView mDragShadowOverlay;
     74   private final AnimatorListenerAdapter mDragShadowOverAnimatorListener =
     75       new AnimatorListenerAdapter() {
     76         @Override
     77         public void onAnimationEnd(Animator animation) {
     78           if (mDragShadowBitmap != null) {
     79             mDragShadowBitmap.recycle();
     80             mDragShadowBitmap = null;
     81           }
     82           mDragShadowOverlay.setVisibility(GONE);
     83           mDragShadowOverlay.setImageBitmap(null);
     84         }
     85       };
     86   private View mDragShadowParent;
     87   private int mAnimationDuration;
     88   // X and Y offsets inside the item from where the user grabbed to the
     89   // child's left coordinate. This is used to aid in the drawing of the drag shadow.
     90   private int mTouchOffsetToChildLeft;
     91   private int mTouchOffsetToChildTop;
     92   private int mDragShadowLeft;
     93   private int mDragShadowTop;
     94   private DragDropController mDragDropController = new DragDropController(this);
     95 
     96   public PhoneFavoriteListView(Context context) {
     97     this(context, null);
     98   }
     99 
    100   public PhoneFavoriteListView(Context context, AttributeSet attrs) {
    101     this(context, attrs, -1);
    102   }
    103 
    104   public PhoneFavoriteListView(Context context, AttributeSet attrs, int defStyle) {
    105     super(context, attrs, defStyle);
    106     mAnimationDuration = context.getResources().getInteger(R.integer.fade_duration);
    107     mTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
    108     mDragDropController.addOnDragDropListener(this);
    109   }
    110 
    111   @Override
    112   protected void onConfigurationChanged(Configuration newConfig) {
    113     super.onConfigurationChanged(newConfig);
    114     mTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
    115   }
    116 
    117   /**
    118    * TODO: This is all swipe to remove code (nothing to do with drag to remove). This should be
    119    * cleaned up and removed once drag to remove becomes the only way to remove contacts.
    120    */
    121   @Override
    122   public boolean onInterceptTouchEvent(MotionEvent ev) {
    123     if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    124       mTouchDownForDragStartX = (int) ev.getX();
    125       mTouchDownForDragStartY = (int) ev.getY();
    126     }
    127 
    128     return super.onInterceptTouchEvent(ev);
    129   }
    130 
    131   @Override
    132   public boolean onDragEvent(DragEvent event) {
    133     final int action = event.getAction();
    134     final int eX = (int) event.getX();
    135     final int eY = (int) event.getY();
    136     switch (action) {
    137       case DragEvent.ACTION_DRAG_STARTED:
    138         {
    139           if (!PhoneFavoriteTileView.DRAG_PHONE_FAVORITE_TILE.equals(event.getLocalState())) {
    140             // Ignore any drag events that were not propagated by long pressing
    141             // on a {@link PhoneFavoriteTileView}
    142             return false;
    143           }
    144           if (!mDragDropController.handleDragStarted(this, eX, eY)) {
    145             return false;
    146           }
    147           break;
    148         }
    149       case DragEvent.ACTION_DRAG_LOCATION:
    150         mLastDragY = eY;
    151         mDragDropController.handleDragHovered(this, eX, eY);
    152         // Kick off {@link #mScrollHandler} if it's not started yet.
    153         if (!mIsDragScrollerRunning
    154             &&
    155             // And if the distance traveled while dragging exceeds the touch slop
    156             (Math.abs(mLastDragY - mTouchDownForDragStartY) >= 4 * mTouchSlop)) {
    157           mIsDragScrollerRunning = true;
    158           ensureScrollHandler();
    159           mScrollHandler.postDelayed(mDragScroller, SCROLL_HANDLER_DELAY_MILLIS);
    160         }
    161         break;
    162       case DragEvent.ACTION_DRAG_ENTERED:
    163         final int boundGap = (int) (getHeight() * BOUND_GAP_RATIO);
    164         mTopScrollBound = (getTop() + boundGap);
    165         mBottomScrollBound = (getBottom() - boundGap);
    166         break;
    167       case DragEvent.ACTION_DRAG_EXITED:
    168       case DragEvent.ACTION_DRAG_ENDED:
    169       case DragEvent.ACTION_DROP:
    170         ensureScrollHandler();
    171         mScrollHandler.removeCallbacks(mDragScroller);
    172         mIsDragScrollerRunning = false;
    173         // Either a successful drop or it's ended with out drop.
    174         if (action == DragEvent.ACTION_DROP || action == DragEvent.ACTION_DRAG_ENDED) {
    175           mDragDropController.handleDragFinished(eX, eY, false);
    176         }
    177         break;
    178       default:
    179         break;
    180     }
    181     // This ListView will consume the drag events on behalf of its children.
    182     return true;
    183   }
    184 
    185   public void setDragShadowOverlay(ImageView overlay) {
    186     mDragShadowOverlay = overlay;
    187     mDragShadowParent = (View) mDragShadowOverlay.getParent();
    188   }
    189 
    190   /** Find the view under the pointer. */
    191   private View getViewAtPosition(int x, int y) {
    192     final int count = getChildCount();
    193     View child;
    194     for (int childIdx = 0; childIdx < count; childIdx++) {
    195       child = getChildAt(childIdx);
    196       if (y >= child.getTop()
    197           && y <= child.getBottom()
    198           && x >= child.getLeft()
    199           && x <= child.getRight()) {
    200         return child;
    201       }
    202     }
    203     return null;
    204   }
    205 
    206   private void ensureScrollHandler() {
    207     if (mScrollHandler == null) {
    208       mScrollHandler = getHandler();
    209     }
    210   }
    211 
    212   public DragDropController getDragDropController() {
    213     return mDragDropController;
    214   }
    215 
    216   @Override
    217   public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView tileView) {
    218     if (mDragShadowOverlay == null) {
    219       return;
    220     }
    221 
    222     mDragShadowOverlay.clearAnimation();
    223     mDragShadowBitmap = createDraggedChildBitmap(tileView);
    224     if (mDragShadowBitmap == null) {
    225       return;
    226     }
    227 
    228     tileView.getLocationOnScreen(mLocationOnScreen);
    229     mDragShadowLeft = mLocationOnScreen[0];
    230     mDragShadowTop = mLocationOnScreen[1];
    231 
    232     // x and y are the coordinates of the on-screen touch event. Using these
    233     // and the on-screen location of the tileView, calculate the difference between
    234     // the position of the user's finger and the position of the tileView. These will
    235     // be used to offset the location of the drag shadow so that it appears that the
    236     // tileView is positioned directly under the user's finger.
    237     mTouchOffsetToChildLeft = x - mDragShadowLeft;
    238     mTouchOffsetToChildTop = y - mDragShadowTop;
    239 
    240     mDragShadowParent.getLocationOnScreen(mLocationOnScreen);
    241     mDragShadowLeft -= mLocationOnScreen[0];
    242     mDragShadowTop -= mLocationOnScreen[1];
    243 
    244     mDragShadowOverlay.setImageBitmap(mDragShadowBitmap);
    245     mDragShadowOverlay.setVisibility(VISIBLE);
    246     mDragShadowOverlay.setAlpha(DRAG_SHADOW_ALPHA);
    247 
    248     mDragShadowOverlay.setX(mDragShadowLeft);
    249     mDragShadowOverlay.setY(mDragShadowTop);
    250   }
    251 
    252   @Override
    253   public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView tileView) {
    254     // Update the drag shadow location.
    255     mDragShadowParent.getLocationOnScreen(mLocationOnScreen);
    256     mDragShadowLeft = x - mTouchOffsetToChildLeft - mLocationOnScreen[0];
    257     mDragShadowTop = y - mTouchOffsetToChildTop - mLocationOnScreen[1];
    258     // Draw the drag shadow at its last known location if the drag shadow exists.
    259     if (mDragShadowOverlay != null) {
    260       mDragShadowOverlay.setX(mDragShadowLeft);
    261       mDragShadowOverlay.setY(mDragShadowTop);
    262     }
    263   }
    264 
    265   @Override
    266   public void onDragFinished(int x, int y) {
    267     if (mDragShadowOverlay != null) {
    268       mDragShadowOverlay.clearAnimation();
    269       mDragShadowOverlay
    270           .animate()
    271           .alpha(0.0f)
    272           .setDuration(mAnimationDuration)
    273           .setListener(mDragShadowOverAnimatorListener)
    274           .start();
    275     }
    276   }
    277 
    278   @Override
    279   public void onDroppedOnRemove() {}
    280 
    281   private Bitmap createDraggedChildBitmap(View view) {
    282     view.setDrawingCacheEnabled(true);
    283     final Bitmap cache = view.getDrawingCache();
    284 
    285     Bitmap bitmap = null;
    286     if (cache != null) {
    287       try {
    288         bitmap = cache.copy(Bitmap.Config.ARGB_8888, false);
    289       } catch (final OutOfMemoryError e) {
    290         Log.w(LOG_TAG, "Failed to copy bitmap from Drawing cache", e);
    291         bitmap = null;
    292       }
    293     }
    294 
    295     view.destroyDrawingCache();
    296     view.setDrawingCacheEnabled(false);
    297 
    298     return bitmap;
    299   }
    300 
    301   @Override
    302   public PhoneFavoriteSquareTileView getViewForLocation(int x, int y) {
    303     getLocationOnScreen(mLocationOnScreen);
    304     // Calculate the X and Y coordinates of the drag event relative to the view
    305     final int viewX = x - mLocationOnScreen[0];
    306     final int viewY = y - mLocationOnScreen[1];
    307     final View child = getViewAtPosition(viewX, viewY);
    308 
    309     if (!(child instanceof PhoneFavoriteSquareTileView)) {
    310       return null;
    311     }
    312 
    313     return (PhoneFavoriteSquareTileView) child;
    314   }
    315 }
    316