| /* |
| * Copyright (C) 2017 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.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; |
| import com.android.dialer.common.LogUtil; |
| |
| /** 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[] locationOnScreen = new int[2]; |
| private static final long SCROLL_HANDLER_DELAY_MILLIS = 5; |
| private static final int DRAG_SCROLL_PX_UNIT = 25; |
| private static final float DRAG_SHADOW_ALPHA = 0.7f; |
| /** |
| * {@link #topScrollBound} and {@link bottomScrollBound} will be offseted to the top / bottom by |
| * {@link #getHeight} * {@link #BOUND_GAP_RATIO} pixels. |
| */ |
| private static final float BOUND_GAP_RATIO = 0.2f; |
| |
| private float touchSlop; |
| private int topScrollBound; |
| private int bottomScrollBound; |
| private int lastDragY; |
| private Handler scrollHandler; |
| private final Runnable dragScroller = |
| new Runnable() { |
| @Override |
| public void run() { |
| if (lastDragY <= topScrollBound) { |
| smoothScrollBy(-DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS); |
| } else if (lastDragY >= bottomScrollBound) { |
| smoothScrollBy(DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS); |
| } |
| scrollHandler.postDelayed(this, SCROLL_HANDLER_DELAY_MILLIS); |
| } |
| }; |
| private boolean isDragScrollerRunning = false; |
| private int touchDownForDragStartY; |
| private Bitmap dragShadowBitmap; |
| private ImageView dragShadowOverlay; |
| private final AnimatorListenerAdapter dragShadowOverAnimatorListener = |
| new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| if (dragShadowBitmap != null) { |
| dragShadowBitmap.recycle(); |
| dragShadowBitmap = null; |
| } |
| dragShadowOverlay.setVisibility(GONE); |
| dragShadowOverlay.setImageBitmap(null); |
| } |
| }; |
| private View dragShadowParent; |
| private int animationDuration; |
| // 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 touchOffsetToChildLeft; |
| private int touchOffsetToChildTop; |
| private int dragShadowLeft; |
| private int dragShadowTop; |
| private DragDropController dragDropController = new DragDropController(this); |
| |
| public PhoneFavoriteListView(Context context) { |
| this(context, null); |
| } |
| |
| public PhoneFavoriteListView(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public PhoneFavoriteListView(Context context, AttributeSet attrs, int defStyle) { |
| super(context, attrs, defStyle); |
| animationDuration = context.getResources().getInteger(R.integer.fade_duration); |
| touchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop(); |
| dragDropController.addOnDragDropListener(this); |
| } |
| |
| @Override |
| protected void onConfigurationChanged(Configuration newConfig) { |
| super.onConfigurationChanged(newConfig); |
| touchSlop = 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) { |
| touchDownForDragStartY = (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 (!dragDropController.handleDragStarted(this, eX, eY)) { |
| return false; |
| } |
| break; |
| } |
| case DragEvent.ACTION_DRAG_LOCATION: |
| lastDragY = eY; |
| dragDropController.handleDragHovered(this, eX, eY); |
| // Kick off {@link #mScrollHandler} if it's not started yet. |
| if (!isDragScrollerRunning |
| && |
| // And if the distance traveled while dragging exceeds the touch slop |
| (Math.abs(lastDragY - touchDownForDragStartY) >= 4 * touchSlop)) { |
| isDragScrollerRunning = true; |
| ensureScrollHandler(); |
| scrollHandler.postDelayed(dragScroller, SCROLL_HANDLER_DELAY_MILLIS); |
| } |
| break; |
| case DragEvent.ACTION_DRAG_ENTERED: |
| final int boundGap = (int) (getHeight() * BOUND_GAP_RATIO); |
| topScrollBound = (getTop() + boundGap); |
| bottomScrollBound = (getBottom() - boundGap); |
| break; |
| case DragEvent.ACTION_DRAG_EXITED: |
| case DragEvent.ACTION_DRAG_ENDED: |
| case DragEvent.ACTION_DROP: |
| ensureScrollHandler(); |
| scrollHandler.removeCallbacks(dragScroller); |
| isDragScrollerRunning = false; |
| // Either a successful drop or it's ended with out drop. |
| if (action == DragEvent.ACTION_DROP || action == DragEvent.ACTION_DRAG_ENDED) { |
| dragDropController.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) { |
| dragShadowOverlay = overlay; |
| dragShadowParent = (View) dragShadowOverlay.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 (scrollHandler == null) { |
| scrollHandler = getHandler(); |
| } |
| } |
| |
| public DragDropController getDragDropController() { |
| return dragDropController; |
| } |
| |
| @Override |
| public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView tileView) { |
| if (dragShadowOverlay == null) { |
| return; |
| } |
| |
| dragShadowOverlay.clearAnimation(); |
| dragShadowBitmap = createDraggedChildBitmap(tileView); |
| if (dragShadowBitmap == null) { |
| return; |
| } |
| |
| tileView.getLocationOnScreen(locationOnScreen); |
| dragShadowLeft = locationOnScreen[0]; |
| dragShadowTop = locationOnScreen[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. |
| touchOffsetToChildLeft = x - dragShadowLeft; |
| touchOffsetToChildTop = y - dragShadowTop; |
| |
| dragShadowParent.getLocationOnScreen(locationOnScreen); |
| dragShadowLeft -= locationOnScreen[0]; |
| dragShadowTop -= locationOnScreen[1]; |
| |
| dragShadowOverlay.setImageBitmap(dragShadowBitmap); |
| dragShadowOverlay.setVisibility(VISIBLE); |
| dragShadowOverlay.setAlpha(DRAG_SHADOW_ALPHA); |
| |
| dragShadowOverlay.setX(dragShadowLeft); |
| dragShadowOverlay.setY(dragShadowTop); |
| } |
| |
| @Override |
| public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView tileView) { |
| // Update the drag shadow location. |
| dragShadowParent.getLocationOnScreen(locationOnScreen); |
| dragShadowLeft = x - touchOffsetToChildLeft - locationOnScreen[0]; |
| dragShadowTop = y - touchOffsetToChildTop - locationOnScreen[1]; |
| // Draw the drag shadow at its last known location if the drag shadow exists. |
| if (dragShadowOverlay != null) { |
| dragShadowOverlay.setX(dragShadowLeft); |
| dragShadowOverlay.setY(dragShadowTop); |
| } |
| } |
| |
| @Override |
| public void onDragFinished(int x, int y) { |
| if (dragShadowOverlay != null) { |
| dragShadowOverlay.clearAnimation(); |
| dragShadowOverlay |
| .animate() |
| .alpha(0.0f) |
| .setDuration(animationDuration) |
| .setListener(dragShadowOverAnimatorListener) |
| .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) { |
| LogUtil.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(locationOnScreen); |
| // Calculate the X and Y coordinates of the drag event relative to the view |
| final int viewX = x - locationOnScreen[0]; |
| final int viewY = y - locationOnScreen[1]; |
| final View child = getViewAtPosition(viewX, viewY); |
| |
| if (!(child instanceof PhoneFavoriteSquareTileView)) { |
| return null; |
| } |
| |
| return (PhoneFavoriteSquareTileView) child; |
| } |
| } |