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