/* * Copyright (C) 2012 Google Inc. * Licensed to The Android Open Source Project. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.dialer.app.list; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.Context; import android.content.res.Configuration; import android.graphics.Bitmap; import android.os.Handler; import android.util.AttributeSet; import android.util.Log; import android.view.DragEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.widget.GridView; import android.widget.ImageView; import com.android.dialer.app.R; import com.android.dialer.app.list.DragDropController.DragItemContainer; /** Viewgroup that presents the user's speed dial contacts in a grid. */ public class PhoneFavoriteListView extends GridView implements OnDragDropListener, DragItemContainer { public static final String LOG_TAG = PhoneFavoriteListView.class.getSimpleName(); final int[] mLocationOnScreen = new int[2]; private final long SCROLL_HANDLER_DELAY_MILLIS = 5; private final int DRAG_SCROLL_PX_UNIT = 25; private final float DRAG_SHADOW_ALPHA = 0.7f; /** * {@link #mTopScrollBound} and {@link mBottomScrollBound} will be offseted to the top / bottom by * {@link #getHeight} * {@link #BOUND_GAP_RATIO} pixels. */ private final float BOUND_GAP_RATIO = 0.2f; private float mTouchSlop; private int mTopScrollBound; private int mBottomScrollBound; private int mLastDragY; private Handler mScrollHandler; private final Runnable mDragScroller = new Runnable() { @Override public void run() { if (mLastDragY <= mTopScrollBound) { smoothScrollBy(-DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS); } else if (mLastDragY >= mBottomScrollBound) { smoothScrollBy(DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS); } mScrollHandler.postDelayed(this, SCROLL_HANDLER_DELAY_MILLIS); } }; private boolean mIsDragScrollerRunning = false; private int mTouchDownForDragStartX; private int mTouchDownForDragStartY; private Bitmap mDragShadowBitmap; private ImageView mDragShadowOverlay; private final AnimatorListenerAdapter mDragShadowOverAnimatorListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (mDragShadowBitmap != null) { mDragShadowBitmap.recycle(); mDragShadowBitmap = null; } mDragShadowOverlay.setVisibility(GONE); mDragShadowOverlay.setImageBitmap(null); } }; private View mDragShadowParent; private int mAnimationDuration; // X and Y offsets inside the item from where the user grabbed to the // child's left coordinate. This is used to aid in the drawing of the drag shadow. private int mTouchOffsetToChildLeft; private int mTouchOffsetToChildTop; private int mDragShadowLeft; private int mDragShadowTop; private DragDropController mDragDropController = new DragDropController(this); public PhoneFavoriteListView(Context context) { this(context, null); } public PhoneFavoriteListView(Context context, AttributeSet attrs) { this(context, attrs, -1); } public PhoneFavoriteListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mAnimationDuration = context.getResources().getInteger(R.integer.fade_duration); mTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop(); mDragDropController.addOnDragDropListener(this); } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); } /** * TODO: This is all swipe to remove code (nothing to do with drag to remove). This should be * cleaned up and removed once drag to remove becomes the only way to remove contacts. */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { mTouchDownForDragStartX = (int) ev.getX(); mTouchDownForDragStartY = (int) ev.getY(); } return super.onInterceptTouchEvent(ev); } @Override public boolean onDragEvent(DragEvent event) { final int action = event.getAction(); final int eX = (int) event.getX(); final int eY = (int) event.getY(); switch (action) { case DragEvent.ACTION_DRAG_STARTED: { if (!PhoneFavoriteTileView.DRAG_PHONE_FAVORITE_TILE.equals(event.getLocalState())) { // Ignore any drag events that were not propagated by long pressing // on a {@link PhoneFavoriteTileView} return false; } if (!mDragDropController.handleDragStarted(this, eX, eY)) { return false; } break; } case DragEvent.ACTION_DRAG_LOCATION: mLastDragY = eY; mDragDropController.handleDragHovered(this, eX, eY); // Kick off {@link #mScrollHandler} if it's not started yet. if (!mIsDragScrollerRunning && // And if the distance traveled while dragging exceeds the touch slop (Math.abs(mLastDragY - mTouchDownForDragStartY) >= 4 * mTouchSlop)) { mIsDragScrollerRunning = true; ensureScrollHandler(); mScrollHandler.postDelayed(mDragScroller, SCROLL_HANDLER_DELAY_MILLIS); } break; case DragEvent.ACTION_DRAG_ENTERED: final int boundGap = (int) (getHeight() * BOUND_GAP_RATIO); mTopScrollBound = (getTop() + boundGap); mBottomScrollBound = (getBottom() - boundGap); break; case DragEvent.ACTION_DRAG_EXITED: case DragEvent.ACTION_DRAG_ENDED: case DragEvent.ACTION_DROP: ensureScrollHandler(); mScrollHandler.removeCallbacks(mDragScroller); mIsDragScrollerRunning = false; // Either a successful drop or it's ended with out drop. if (action == DragEvent.ACTION_DROP || action == DragEvent.ACTION_DRAG_ENDED) { mDragDropController.handleDragFinished(eX, eY, false); } break; default: break; } // This ListView will consume the drag events on behalf of its children. return true; } public void setDragShadowOverlay(ImageView overlay) { mDragShadowOverlay = overlay; mDragShadowParent = (View) mDragShadowOverlay.getParent(); } /** Find the view under the pointer. */ private View getViewAtPosition(int x, int y) { final int count = getChildCount(); View child; for (int childIdx = 0; childIdx < count; childIdx++) { child = getChildAt(childIdx); if (y >= child.getTop() && y <= child.getBottom() && x >= child.getLeft() && x <= child.getRight()) { return child; } } return null; } private void ensureScrollHandler() { if (mScrollHandler == null) { mScrollHandler = getHandler(); } } public DragDropController getDragDropController() { return mDragDropController; } @Override public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView tileView) { if (mDragShadowOverlay == null) { return; } mDragShadowOverlay.clearAnimation(); mDragShadowBitmap = createDraggedChildBitmap(tileView); if (mDragShadowBitmap == null) { return; } tileView.getLocationOnScreen(mLocationOnScreen); mDragShadowLeft = mLocationOnScreen[0]; mDragShadowTop = mLocationOnScreen[1]; // x and y are the coordinates of the on-screen touch event. Using these // and the on-screen location of the tileView, calculate the difference between // the position of the user's finger and the position of the tileView. These will // be used to offset the location of the drag shadow so that it appears that the // tileView is positioned directly under the user's finger. mTouchOffsetToChildLeft = x - mDragShadowLeft; mTouchOffsetToChildTop = y - mDragShadowTop; mDragShadowParent.getLocationOnScreen(mLocationOnScreen); mDragShadowLeft -= mLocationOnScreen[0]; mDragShadowTop -= mLocationOnScreen[1]; mDragShadowOverlay.setImageBitmap(mDragShadowBitmap); mDragShadowOverlay.setVisibility(VISIBLE); mDragShadowOverlay.setAlpha(DRAG_SHADOW_ALPHA); mDragShadowOverlay.setX(mDragShadowLeft); mDragShadowOverlay.setY(mDragShadowTop); } @Override public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView tileView) { // Update the drag shadow location. mDragShadowParent.getLocationOnScreen(mLocationOnScreen); mDragShadowLeft = x - mTouchOffsetToChildLeft - mLocationOnScreen[0]; mDragShadowTop = y - mTouchOffsetToChildTop - mLocationOnScreen[1]; // Draw the drag shadow at its last known location if the drag shadow exists. if (mDragShadowOverlay != null) { mDragShadowOverlay.setX(mDragShadowLeft); mDragShadowOverlay.setY(mDragShadowTop); } } @Override public void onDragFinished(int x, int y) { if (mDragShadowOverlay != null) { mDragShadowOverlay.clearAnimation(); mDragShadowOverlay .animate() .alpha(0.0f) .setDuration(mAnimationDuration) .setListener(mDragShadowOverAnimatorListener) .start(); } } @Override public void onDroppedOnRemove() {} private Bitmap createDraggedChildBitmap(View view) { view.setDrawingCacheEnabled(true); final Bitmap cache = view.getDrawingCache(); Bitmap bitmap = null; if (cache != null) { try { bitmap = cache.copy(Bitmap.Config.ARGB_8888, false); } catch (final OutOfMemoryError e) { Log.w(LOG_TAG, "Failed to copy bitmap from Drawing cache", e); bitmap = null; } } view.destroyDrawingCache(); view.setDrawingCacheEnabled(false); return bitmap; } @Override public PhoneFavoriteSquareTileView getViewForLocation(int x, int y) { getLocationOnScreen(mLocationOnScreen); // Calculate the X and Y coordinates of the drag event relative to the view final int viewX = x - mLocationOnScreen[0]; final int viewY = y - mLocationOnScreen[1]; final View child = getViewAtPosition(viewX, viewY); if (!(child instanceof PhoneFavoriteSquareTileView)) { return null; } return (PhoneFavoriteSquareTileView) child; } }