package org.music.player;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.ListView;
/**
* A ListView that supports dragging to reorder its elements.
*
* This implementation has some restrictions:
* Footers are unsupported
* All non-header views must have the same height
* The adapter must implement DragAdapter
*
* Dragging disabled by default. Enable it with
* {@link DragListView#setEditable(boolean)}.
*
* This should really be built-in to Android. This implementation is SUPER-
* HACKY. : /
*/
public class DragListView extends ListView implements Handler.Callback {
/**
* Adapter that implements move and remove operations.
*/
public interface DragAdapter extends ListAdapter {
/**
* Remove the element at position from and insert it at position to.
*/
public void move(int from, int to);
/**
* Remove the element at the given position.
*/
public void remove(int position);
}
/**
* Sent to scroll the list up or down when the dragged view is near the
* top or bottom of the list.
*/
private static final int MSG_SCROLL = 0;
/**
* Height of each row in dip.
*/
public static final int ROW_HEIGHT = 44;
/**
* Padding for each row in dip.
*/
public static final int PADDING = 3;
/**
* Background color of row while it is being dragged.
*/
public static final int DRAG_COLOR = 0xff005500;
/**
* A handler running on the UI thread.
*/
private final Handler mHandler = new Handler(this);
/**
* The system window manager instance.
*/
private WindowManager mWindowManager;
/**
* The adapter that will be called to move/remove rows.
*/
private DragAdapter mAdapter;
/**
* True to allow dragging; false otherwise.
*/
private boolean mEditable;
/**
* Scaled height of each row in pixels.
*/
private final int mRowHeight;
/**
* The view that is actually dragged around during a drag. (The original
* view is hidden).
*/
private ImageView mDragView;
/**
* A copy of the dragged row's scrolling cache that is shown in mDragView.
*/
private Bitmap mDragBitmap;
/**
* Window params for the drag view window. Used to move the window around.
*/
private WindowManager.LayoutParams mWindowParams;
/**
* At which position is the item currently being dragged. Note that this
* takes in to account header items.
*/
private int mDragPos;
/**
* At which position was the item being dragged originally
*/
private int mSrcDragPos;
/**
* At what y offset inside the dragged view did the user grab it.
*/
private int mDragPointY;
/**
* The difference between screen coordinates and coordinates in the drag
* view.
*/
private int mYOffset;
/**
* The y coordinate of the top of the drag view after the last motion
* event.
*/
private int mLastMotionY;
/**
* Default padding for rows.
*/
private final int mPadding;
public DragListView(Context context, AttributeSet attrs)
{
super(context, attrs);
float density = context.getResources().getDisplayMetrics().density;
mPadding = (int)(PADDING * density);
mRowHeight = (int)(ROW_HEIGHT * density);
}
/**
* This should be called instead of
* {@link ListView#setAdapter(android.widget.ListAdapter)}.
* DragListView requires a DragAdapter to handle move/remove callbacks
* from dragging.
*
* @param adapter The adapter to use. Will be passed to
* {@link ListView#setAdapter(android.widget.ListAdapter)}.
*/
public void setAdapter(DragAdapter adapter)
{
super.setAdapter(adapter);
// Keep track of adapter here since getAdapter() will return a wrapper
// when there are headers.
mAdapter = adapter;
}
/**
* Set whether to allow elements to be reordered.
*
* @param editable True to allow reordering.
*/
public void setEditable(boolean editable)
{
mEditable = editable;
if (!editable)
stopDragging();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
if (mEditable) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
stopDragging();
int x = (int)ev.getX();
// The left quarter of the item is the grabber for dragging the item
if (x < getWidth() / 4) {
int item = pointToPosition(x, (int)ev.getY());
if (item != AdapterView.INVALID_POSITION && item >= getHeaderViewsCount()) {
startDragging(item, ev);
return false;
}
}
break;
}
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev)
{
if (!mEditable || mDragView == null)
return super.onTouchEvent(ev);
switch (ev.getAction()) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
stopDragging();
int offset = getHeaderViewsCount();
if (mDragPos >= offset && mDragPos < getCount())
mAdapter.move(mSrcDragPos - offset, mDragPos - offset);
break;
case MotionEvent.ACTION_MOVE:
int y = (int)ev.getY() - mDragPointY;
mLastMotionY = y;
mWindowParams.x = 0;
mWindowParams.y = y + mYOffset;
mWindowManager.updateViewLayout(mDragView, mWindowParams);
computeDragPosition(y);
break;
}
return true;
}
/**
* Restore size and visibility for all list items
*/
private void unExpandViews()
{
int padding = mPadding;
for (int i = 0, count = getChildCount(); i != count; ++i) {
View view = getChildAt(i);
ViewGroup.LayoutParams params = view.getLayoutParams();
params.height = 0;
view.setLayoutParams(params);
view.setVisibility(View.VISIBLE);
view.setPadding(padding, padding, padding, padding);
}
}
/**
* Adjust visibility and size to make it appear as though
* an item is being dragged around and other items are making
* room for it.
*
* If dropping the item would result in it still being in the
* same place, then make the dragged list item's size normal,
* but make the item invisible.
* Otherwise, if the dragged list item is still on screen, make
* it as small as possible and expand the item below the insert
* point.
*/
private void doExpansion()
{
int firstVisibile = getFirstVisiblePosition();
int childNum = mDragPos - firstVisibile;
if (mDragPos > mSrcDragPos)
childNum += 1;
int headerCount = getHeaderViewsCount();
int childCount = getChildCount();
View dragSrcView = getChildAt(mSrcDragPos - firstVisibile);
int start = firstVisibile < headerCount ? headerCount - firstVisibile : 0;
int padding = mPadding;
int rowHeight = mRowHeight;
int nextHeight = rowHeight;
for (int i = start; i != childCount; ++i) {
View view = getChildAt(i);
int height = nextHeight;
nextHeight = rowHeight;
int visibility = View.VISIBLE;
int paddingBottom = padding;
int paddingTop = padding;
if (view == dragSrcView) {
if (mDragPos == mSrcDragPos) {
// hovering over the original location: show empty space
visibility = View.INVISIBLE;
height += 1;
} else {
// not hovering over it: show nothing
// Ideally the item would be completely gone, but neither
// setting its size to 0 nor settings visibility to GONE
// has the desired effect.
height = 1;
}
nextHeight -= 1;
} else if (i == childNum) {
// hovering over this row; expand it to "make room" for the
// dragged item
paddingTop += height;
height *= 2;
} else if (childNum == childCount && i == childCount - 1) {
// hovering over the bottom of the list: we need to "make room"
// at the bottom
paddingBottom += height;
height *= 2;
}
view.setPadding(padding, paddingTop, padding, paddingBottom);
view.setVisibility(visibility);
ViewGroup.LayoutParams params = view.getLayoutParams();
params.height = height;
view.setLayoutParams(params);
}
}
/**
* Computes the drag position based on where the drag view is hovering.
* Expands views and updates scrolling when this position changes.
*
* @param y The y coordinate of the top of the drag view.
* @return The scrolling speed in pixels
*/
private int computeDragPosition(int y)
{
// This assumes uniform height for all non-header rows
int firstVisible = getFirstVisiblePosition();
int topPos = Math.max(getHeaderViewsCount(), firstVisible);
int dragHeight = mRowHeight;
View view = getChildAt(topPos - firstVisible);
int viewMiddle = view.getTop() + dragHeight / 2;
int dragPos = Math.min(getCount() - 1, topPos + Math.max(0, y - viewMiddle + dragHeight) / dragHeight);
if (dragPos != mDragPos) {
mDragPos = dragPos;
doExpansion();
}
int height = getHeight();
int upperBound = height / 4;
int lowerBound = height * 3 / 4;
if (y > lowerBound && (getLastVisiblePosition() < getCount() - 1 || getChildAt(getChildCount() - 1).getBottom() > getBottom()))
return y > (height + lowerBound) / 2 ? 16 : 4;
else if (y < upperBound && (getFirstVisiblePosition() != 0 || getChildAt(0).getTop() < 0))
return y < upperBound / 2 ? -16 : -4;
return 0;
}
/**
* Start a drag operation.
*
* @param row The row number of the item to drag
* @param ev The touch event that started this drag.
*/
private void startDragging(int row, MotionEvent ev)
{
int y = (int)ev.getY();
View item = getChildAt(row - getFirstVisiblePosition());
mDragPointY = y - item.getTop();
mYOffset = (int)ev.getRawY() - y;
mWindowParams = new WindowManager.LayoutParams();
mWindowParams.gravity = Gravity.TOP | Gravity.LEFT;
mWindowParams.x = 0;
mWindowParams.y = y - mDragPointY + mYOffset;
mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
mWindowParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
mWindowParams.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;
mWindowParams.windowAnimations = 0;
int color = item.getDrawingCacheBackgroundColor();
item.setDrawingCacheBackgroundColor(0xff005500);
item.buildDrawingCache();
// 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());
item.setDrawingCacheBackgroundColor(color);
item.destroyDrawingCache();
mDragBitmap = bitmap;
Context context = getContext();
ImageView view = new ImageView(context);
view.setPadding(0, 0, 0, 0);
view.setImageBitmap(bitmap);
mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
mWindowManager.addView(view, mWindowParams);
mDragView = view;
mSrcDragPos = row;
// Force expansion on next motion event
mDragPos = INVALID_POSITION;
mHandler.sendEmptyMessageDelayed(MSG_SCROLL, 50);
}
/**
* Stop a drag operation.
*/
private void stopDragging()
{
if (mDragView != null) {
mDragView.setVisibility(GONE);
mWindowManager.removeView(mDragView);
mDragView.setImageDrawable(null);
mDragView = null;
}
if (mDragBitmap != null) {
mDragBitmap.recycle();
mDragBitmap = null;
}
unExpandViews();
mHandler.removeMessages(MSG_SCROLL);
}
@Override
public boolean handleMessage(Message message)
{
if (message.what == MSG_SCROLL) {
if (mDragPos != INVALID_POSITION) {
int speed = computeDragPosition(mLastMotionY);
if (speed != 0) {
View view = getChildAt(0);
if (view != null) {
int pos = view.getTop();
setSelectionFromTop(getFirstVisiblePosition(), pos - speed);
}
}
}
mHandler.sendEmptyMessageDelayed(MSG_SCROLL, 50);
}
return true;
}
}