blob: ff867f9c00bb8c71eae4bc12473911c3690a5c55 [file] [log] [blame]
/*
* 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;
}
}