// Original from Eric Harlow
// http://ericharlow.blogspot.com/2010/10/experience-android-drag-and-drop-list.html
// Modifications Copyright 2011 NPR
//
// 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 org.npr.android.widget;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.ListView;
/**
* Author: Jeremy Wadsack
*/
public class DragNDropListView extends ListView {
private static final String LOG_TAG = DragNDropListView.class.getName();
private boolean mDragMode;
private int mStartPosition;
private int mDragPointOffset; //Used to adjust drag view location
private ImageView mDragView;
private ImageView dropPointView;
private DropListener mDropListener;
private DragListener mDragListener;
public DragNDropListView(Context context, AttributeSet attributes) {
super(context, attributes);
init(context);
}
private void init(Context context) {
// Create the image to use to mark the insertion point
dropPointView = new ImageView(context);
dropPointView.setBackgroundColor(Color.argb(0xFF, 0x33, 0x33, 0x33));
dropPointView.setVisibility(View.GONE);
}
public void setDropListener(DropListener l) {
mDropListener = l;
}
public void setDragListener(DragListener l) {
mDragListener = l;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final int x = (int) ev.getX();
final int y = (int) ev.getY();
// TODO: This should be a parameter of the class
// to determine where the touch target is
if (action == MotionEvent.ACTION_DOWN && x > 3 * this.getWidth() / 4) {
mDragMode = true;
}
if (!mDragMode) {
return super.onTouchEvent(ev);
}
switch (action) {
case MotionEvent.ACTION_DOWN:
mStartPosition = pointToPosition(x, y);
if (mStartPosition != INVALID_POSITION) {
int mItemPosition = mStartPosition - getFirstVisiblePosition();
mDragPointOffset = y - getChildAt(mItemPosition).getTop();
mDragPointOffset -= ((int) ev.getRawY()) - y;
startDrag(mItemPosition, y);
drag(x, y);
}
break;
case MotionEvent.ACTION_MOVE:
drag(x, y);
break;
case MotionEvent.ACTION_CANCEL:
// If the touch event is canceled then some other context consumed
// the event. We need to stop dragging (clean things up) but don't
// fire a drop event -- we want to restore the item to where it was.
mDragMode = false;
stopDrag(mStartPosition - getFirstVisiblePosition());
break;
case MotionEvent.ACTION_UP:
default:
mDragMode = false;
int mEndPosition = calculateDropToPosition(x, y);
stopDrag(mStartPosition - getFirstVisiblePosition());
if (mDropListener != null
&& mStartPosition != INVALID_POSITION
&& mEndPosition != INVALID_POSITION) {
mDropListener.onDrop(mStartPosition, mEndPosition);
}
break;
}
return true;
}
// move the drag view
private void drag(int x, int y) {
if (mDragView == null) {
return;
}
WindowManager.LayoutParams layoutParams =
(WindowManager.LayoutParams) mDragView.getLayoutParams();
// -- because it's a list; never want it to move left or right
layoutParams.x = 0;
layoutParams.y = y - mDragPointOffset;
WindowManager mWindowManager =
(WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
mWindowManager.updateViewLayout(mDragView, layoutParams);
int lineBeforeIndex = calculateDropBeforePosition(x, y);
if (lineBeforeIndex == INVALID_POSITION) {
dropPointView.setVisibility(View.GONE);
} else {
View v = getChildAt(lineBeforeIndex - getFirstVisiblePosition());
if (v != null) {
int[] coordinates = new int[42];
v.getLocationInWindow(coordinates);
WindowManager.LayoutParams dropViewParams =
(WindowManager.LayoutParams)dropPointView.getLayoutParams();
dropViewParams.x = 0;
dropViewParams.y = coordinates[1] - (dropPointView.getHeight() / 2);
dropPointView.setVisibility(View.VISIBLE);
mWindowManager.updateViewLayout(dropPointView, dropViewParams);
} else {
// TODO: capture case where lineBeforeIndex == getLastVisiblePosition
// + 1 and scroll the list?
Log.e(LOG_TAG, "Could not find the view for item " + lineBeforeIndex
+ " so not drawing a drop indicator. Should be between " +
getFirstVisiblePosition() + " and " + getLastVisiblePosition());
dropPointView.setVisibility(View.GONE);
}
}
if (mDragListener != null) {
mDragListener.onDrag(x, y, this);
}
}
// enable the drag view for dragging
private void startDrag(int itemIndex, int y) {
stopDrag(itemIndex);
View item = getChildAt(itemIndex);
if (item == null) {
return;
}
item.setDrawingCacheEnabled(true);
if (mDragListener != null) {
mDragListener.onStartDrag(item);
}
// Create a copy of the drawing cache so that it does not get recycled
// by the framework when the list tries to clean up memory
Bitmap bitmap = Bitmap.createBitmap(item.getDrawingCache());
WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams();
windowParams.gravity = Gravity.TOP;
windowParams.x = 0;
windowParams.y = y - mDragPointOffset;
// Make it translucent to we can see the marker below it
windowParams.alpha = 0xA0;
windowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
windowParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
windowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
windowParams.format = PixelFormat.TRANSLUCENT;
windowParams.windowAnimations = 0;
Context context = getContext();
ImageView v = new ImageView(context);
v.setImageBitmap(bitmap);
WindowManager windowManager =
(WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
windowManager.addView(v, windowParams);
mDragView = v;
WindowManager.LayoutParams dvParam = new WindowManager.LayoutParams();
dvParam.copyFrom(windowParams);
dvParam.width = getMeasuredWidth();
dvParam.height = 2;
windowManager.addView(dropPointView, dvParam);
}
// destroy drag view
private void stopDrag(int itemIndex) {
if (mDragView != null) {
if (mDragListener != null) {
mDragListener.onStopDrag(getChildAt(itemIndex));
}
mDragView.setVisibility(GONE);
WindowManager wm =
(WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
wm.removeView(mDragView);
mDragView.setImageDrawable(null);
mDragView = null;
dropPointView.setVisibility(View.GONE);
wm.removeView(dropPointView);
}
}
/**
* While dragging an item we need to know where in the current list of
* items to draw a divider. This calculates that by making a decision
* half-way through the item being dragged over.
*
* @param x The x position of the MouseEvent
* @param y The y position of the MouseEvent
* @return and index for of the item <em>before</em> which the divider
* should be drawn, or INVALID_POSITION is no divider should be drawn
*/
private int calculateDropBeforePosition(int x, int y) {
int endPosition = pointToPosition(x, y);
if (endPosition == INVALID_POSITION ||
endPosition == mStartPosition) {
return INVALID_POSITION;
}
View child = getChildAt(endPosition + getFirstVisiblePosition());
if (child == null) {
return INVALID_POSITION;
}
int offset = child.getHeight() / 2;
if (mStartPosition > endPosition) {
endPosition = pointToPosition(x, y + offset);
} else {
endPosition = pointToPosition(x, y - offset);
if (endPosition != INVALID_POSITION) {
endPosition++;
}
}
return endPosition == mStartPosition ? INVALID_POSITION : endPosition;
}
/**
* When we are dropping an item we need to tell the drop handler what the
* final rank (order) position the item should end up at. This calculates
* that by making a decision half-way through the item being dragged over.
*
* @param x The x position of the MouseEvent
* @param y The y position of the MouseEvent
* @return and index for the to parameter or INVALID_POSITION
*/
private int calculateDropToPosition(int x, int y) {
int endPosition = pointToPosition(x, y);
Log.d(LOG_TAG, "Drop position " + endPosition + " for " + x + ", " + y);
if (endPosition != INVALID_POSITION) {
View child = getChildAt(endPosition + getFirstVisiblePosition());
if (child == null) {
Log.d(LOG_TAG, "No child at " + endPosition + " for " + x + ", " + y);
return INVALID_POSITION;
}
int offset = child.getHeight() / 2;
if (mStartPosition > endPosition) {
endPosition = pointToPosition(x, y + offset);
} else {
endPosition = pointToPosition(x, y - offset);
}
}
return endPosition;
}
}