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