mirror of https://github.com/tasks/tasks
Add a new class, draggable list view, for dragging and scrolling from same view
parent
57f3a70089
commit
205dd7ac78
@ -0,0 +1,560 @@
|
||||
/*
|
||||
* Copyright (c) 2010 CommonsWare, LLC
|
||||
* Portions Copyright (C) 2008 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.todoroo.astrid.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Vibrator;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.Gravity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
|
||||
import com.commonsware.cwac.tlv.TouchListView.DragListener;
|
||||
import com.commonsware.cwac.tlv.TouchListView.DropListener;
|
||||
import com.commonsware.cwac.tlv.TouchListView.GrabberClickListener;
|
||||
import com.commonsware.cwac.tlv.TouchListView.SwipeListener;
|
||||
import com.timsu.astrid.R;
|
||||
|
||||
public class DraggableListView extends ListView {
|
||||
|
||||
private static final int SWIPE_THRESHOLD = 20;
|
||||
|
||||
private static final int MOVEMENT_THRESHOLD = 10;
|
||||
|
||||
// --- drag status
|
||||
private float mTouchStartX, mTouchCurrentX, mTouchStartY, mTouchCurrentY;
|
||||
private boolean mDragging = false;
|
||||
|
||||
private int mDragPos; // which item is being dragged
|
||||
private int mFirstDragPos; // where was the dragged item originally
|
||||
private int mDragPoint; // at what offset inside the item did the user grab it
|
||||
private int mCoordOffset; // the difference between screen coordinates and coordinates in this view
|
||||
|
||||
// --- drag drawing
|
||||
private ImageView mDragView;
|
||||
private WindowManager mWindowManager;
|
||||
private WindowManager.LayoutParams mWindowParams;
|
||||
private int mUpperBound;
|
||||
private int mLowerBound;
|
||||
private int mHeight;
|
||||
private int mRemoveMode = -1;
|
||||
private final Rect mTempRect = new Rect();
|
||||
private Bitmap mDragBitmap;
|
||||
private final int mTouchSlop;
|
||||
private int dragndropBackgroundColor = 0x00000000;
|
||||
|
||||
// --- listeners
|
||||
private DragListener mDragListener;
|
||||
private DropListener mDropListener;
|
||||
private SwipeListener mSwipeListener;
|
||||
private GrabberClickListener mClickListener;
|
||||
private GestureDetector mGestureDetector;
|
||||
|
||||
public static final int FLING = 0;
|
||||
public static final int SLIDE_RIGHT = 1;
|
||||
public static final int SLIDE_LEFT = 2;
|
||||
|
||||
// --- other instance variables
|
||||
private int mItemHeightNormal = -1;
|
||||
private int mItemHeightExpanded = -1;
|
||||
private Thread dragThread = null;
|
||||
|
||||
// --- constructors
|
||||
|
||||
public DraggableListView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public DraggableListView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray a = getContext().obtainStyledAttributes(attrs,
|
||||
R.styleable.TouchListView, 0, 0);
|
||||
|
||||
mItemHeightNormal = a.getDimensionPixelSize(
|
||||
R.styleable.TouchListView_normal_height, 0);
|
||||
mItemHeightExpanded = a.getDimensionPixelSize(
|
||||
R.styleable.TouchListView_expanded_height,
|
||||
mItemHeightNormal);
|
||||
dragndropBackgroundColor = a.getColor(
|
||||
R.styleable.TouchListView_dragndrop_background, 0x00000000);
|
||||
mRemoveMode = a.getInt(R.styleable.TouchListView_remove_mode, -1);
|
||||
|
||||
a.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isDraggableRow(@SuppressWarnings("unused") View view) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* pointToPosition() doesn't consider invisible views, but we need to, so
|
||||
* implement a slightly different version.
|
||||
*/
|
||||
private int myPointToPosition(int x, int y) {
|
||||
Rect frame = mTempRect;
|
||||
final int count = getChildCount();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
final View child = getChildAt(i);
|
||||
child.getHitRect(frame);
|
||||
if (frame.contains(x, y)) {
|
||||
return getFirstVisiblePosition() + i;
|
||||
}
|
||||
}
|
||||
return INVALID_POSITION;
|
||||
}
|
||||
|
||||
private int getItemForPosition(int y) {
|
||||
int adjustedy = y - mDragPoint - (mItemHeightNormal / 2);
|
||||
int pos = myPointToPosition(0, adjustedy);
|
||||
if (pos >= 0) {
|
||||
if (pos <= mFirstDragPos) {
|
||||
pos += 1;
|
||||
}
|
||||
} else if (adjustedy < 0) {
|
||||
pos = 0;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
private void adjustScrollBounds(int y) {
|
||||
if (y >= mHeight / 3) {
|
||||
mUpperBound = mHeight / 3;
|
||||
}
|
||||
if (y <= mHeight * 2 / 3) {
|
||||
mLowerBound = mHeight * 2 / 3;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Restore size and visibility for all list items
|
||||
*/
|
||||
private void unExpandViews(boolean deletion) {
|
||||
for (int i = 0;; i++) {
|
||||
View v = getChildAt(i);
|
||||
if (v == null) {
|
||||
if (deletion) {
|
||||
// HACK force update of mItemCount
|
||||
int position = getFirstVisiblePosition();
|
||||
int y = getChildAt(0).getTop();
|
||||
setAdapter(getAdapter());
|
||||
setSelectionFromTop(position, y);
|
||||
// end hack
|
||||
}
|
||||
layoutChildren(); // force children to be recreated where needed
|
||||
v = getChildAt(i);
|
||||
if (v == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isDraggableRow(v)) {
|
||||
ViewGroup.LayoutParams params = v.getLayoutParams();
|
||||
params.height = mItemHeightNormal;
|
||||
v.setLayoutParams(params);
|
||||
v.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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 listitem'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. If the dragged item is not on
|
||||
* screen, only expand the item below the current insert point.
|
||||
*/
|
||||
private void doExpansion() {
|
||||
int childnum = mDragPos - getFirstVisiblePosition();
|
||||
if (mDragPos > mFirstDragPos) {
|
||||
childnum++;
|
||||
}
|
||||
|
||||
View first = getChildAt(mFirstDragPos - getFirstVisiblePosition());
|
||||
|
||||
for (int i = 0;; i++) {
|
||||
View vv = getChildAt(i);
|
||||
if (vv == null) {
|
||||
break;
|
||||
}
|
||||
int height = mItemHeightNormal;
|
||||
int visibility = View.VISIBLE;
|
||||
if (vv.equals(first)) {
|
||||
// processing the item that is being dragged
|
||||
if (mDragPos == mFirstDragPos) {
|
||||
// hovering over the original location
|
||||
visibility = View.INVISIBLE;
|
||||
} else {
|
||||
// not hovering over it
|
||||
height = 1;
|
||||
}
|
||||
} else if (i == childnum) {
|
||||
if (mDragPos < getCount() - 1) {
|
||||
height = mItemHeightExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
if (isDraggableRow(vv)) {
|
||||
ViewGroup.LayoutParams params = vv.getLayoutParams();
|
||||
params.height = height;
|
||||
vv.setLayoutParams(params);
|
||||
vv.setVisibility(visibility);
|
||||
}
|
||||
}
|
||||
// Request re-layout since we changed the items layout
|
||||
// and not doing this would cause bogus hitbox calculation
|
||||
// in myPointToPosition
|
||||
layoutChildren();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent ev) {
|
||||
if (mGestureDetector != null)
|
||||
mGestureDetector.onTouchEvent(ev);
|
||||
|
||||
mTouchCurrentX = ev.getX();
|
||||
mTouchCurrentY = ev.getY();
|
||||
|
||||
int action = ev.getAction();
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
if(mDragging)
|
||||
stopDragging();
|
||||
|
||||
else if (dragThread != null && mClickListener != null)
|
||||
mClickListener.onClick(viewAtPosition());
|
||||
|
||||
if(dragThread != null) {
|
||||
dragThread.interrupt();
|
||||
dragThread = null;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
dragThread = new Thread(new DragRunnable(ev));
|
||||
dragThread.start();
|
||||
|
||||
mTouchStartX = ev.getX();
|
||||
mTouchStartY = ev.getY();
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if(mDragging)
|
||||
dragView(ev);
|
||||
|
||||
// detect scrolling
|
||||
if(dragThread != null && (Math.abs(mTouchCurrentX - mTouchStartX) > MOVEMENT_THRESHOLD ||
|
||||
Math.abs(mTouchCurrentY - mTouchStartY) > MOVEMENT_THRESHOLD)) {
|
||||
dragThread.interrupt();
|
||||
dragThread = null;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if(mDragging)
|
||||
return true;
|
||||
|
||||
return super.onTouchEvent(ev);
|
||||
}
|
||||
|
||||
private View viewAtPosition() {
|
||||
int itemNum = pointToPosition((int) mTouchCurrentX, (int) mTouchCurrentY);
|
||||
|
||||
if (itemNum == AdapterView.INVALID_POSITION)
|
||||
return null;
|
||||
|
||||
return (View) getChildAt(itemNum - getFirstVisiblePosition());
|
||||
}
|
||||
|
||||
// --- drag logic
|
||||
|
||||
private class DragRunnable implements Runnable {
|
||||
|
||||
private final MotionEvent ev;
|
||||
|
||||
public DragRunnable(MotionEvent ev) {
|
||||
this.ev = ev;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
System.err.println("<<< commence fire");
|
||||
Thread.sleep(300L);
|
||||
|
||||
System.err.println("<<< initiate drag!");
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
initiateDrag(ev);
|
||||
}
|
||||
});
|
||||
|
||||
Thread.sleep(1000L);
|
||||
System.err.println("<<< me love you long time!");
|
||||
|
||||
post(new Runnable() {
|
||||
public void run() {
|
||||
stopDragging();
|
||||
mClickListener.onLongClick(viewAtPosition());
|
||||
}
|
||||
});
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
// bye!
|
||||
System.err.println("<<< drag interrupted");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @return true if drag was initiated
|
||||
*/
|
||||
protected boolean initiateDrag(MotionEvent ev) {
|
||||
int x = (int) mTouchStartX;
|
||||
int y = (int) mTouchStartY;
|
||||
int itemNum = pointToPosition(x, y);
|
||||
|
||||
if (itemNum == AdapterView.INVALID_POSITION)
|
||||
return false;
|
||||
|
||||
View item = (View) getChildAt(itemNum - getFirstVisiblePosition());
|
||||
|
||||
if(!isDraggableRow(item))
|
||||
return false;
|
||||
|
||||
stopDragging();
|
||||
|
||||
System.err.println("GOT VIEW ITEM " + itemNum + " // " + item);
|
||||
mDragPoint = y - item.getTop();
|
||||
mCoordOffset = ((int) ev.getRawY()) - y;
|
||||
|
||||
item.setDrawingCacheEnabled(true);
|
||||
|
||||
// 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.setDrawingCacheEnabled(false);
|
||||
|
||||
Rect listBounds = new Rect();
|
||||
|
||||
getGlobalVisibleRect(listBounds, null);
|
||||
|
||||
startDragging(bitmap, listBounds.left, y);
|
||||
mDragPos = itemNum;
|
||||
mFirstDragPos = mDragPos;
|
||||
mHeight = getHeight();
|
||||
|
||||
int touchSlop = mTouchSlop;
|
||||
mUpperBound = Math.min(y - touchSlop, mHeight / 3);
|
||||
mLowerBound = Math.max(y + touchSlop, mHeight * 2 / 3);
|
||||
|
||||
Vibrator v = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
|
||||
v.vibrate(100);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void startDragging(Bitmap bm, int x, int y) {
|
||||
mWindowParams = new WindowManager.LayoutParams();
|
||||
mWindowParams.gravity = Gravity.TOP | Gravity.LEFT;
|
||||
mWindowParams.x = x;
|
||||
mWindowParams.y = y - mDragPoint + mCoordOffset;
|
||||
|
||||
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;
|
||||
mWindowParams.format = PixelFormat.TRANSLUCENT;
|
||||
mWindowParams.windowAnimations = 0;
|
||||
|
||||
ImageView v = new ImageView(getContext());
|
||||
v.setBackgroundColor(dragndropBackgroundColor);
|
||||
v.setImageBitmap(bm);
|
||||
mDragBitmap = bm;
|
||||
|
||||
mWindowManager = (WindowManager) getContext().getSystemService(
|
||||
Context.WINDOW_SERVICE);
|
||||
mWindowManager.addView(v, mWindowParams);
|
||||
mDragView = v;
|
||||
mDragging = true;
|
||||
}
|
||||
|
||||
private void dragView(MotionEvent ev) {
|
||||
int x = (int) ev.getX();
|
||||
int y = (int) ev.getY();
|
||||
|
||||
float alpha = 1.0f;
|
||||
int width = mDragView.getWidth();
|
||||
|
||||
if (mRemoveMode == SLIDE_RIGHT) {
|
||||
if (x > width / 2) {
|
||||
alpha = ((float) (width - x)) / (width / 2);
|
||||
}
|
||||
mWindowParams.alpha = alpha;
|
||||
} else if (mRemoveMode == SLIDE_LEFT) {
|
||||
if (x < width / 2) {
|
||||
alpha = ((float) x) / (width / 2);
|
||||
}
|
||||
mWindowParams.alpha = alpha;
|
||||
}
|
||||
mWindowParams.y = y - mDragPoint + mCoordOffset;
|
||||
mWindowManager.updateViewLayout(mDragView, mWindowParams);
|
||||
|
||||
int itemnum = getItemForPosition(y);
|
||||
if (itemnum >= 0) {
|
||||
if (ev.getAction() == MotionEvent.ACTION_DOWN
|
||||
|| itemnum != mDragPos) {
|
||||
if (mDragListener != null) {
|
||||
mDragListener.drag(mDragPos, itemnum);
|
||||
}
|
||||
mDragPos = itemnum;
|
||||
doExpansion();
|
||||
}
|
||||
int speed = 0;
|
||||
adjustScrollBounds(y);
|
||||
if (y > mLowerBound) {
|
||||
// scroll the list up a bit
|
||||
speed = y > (mHeight + mLowerBound) / 2 ? 16 : 4;
|
||||
} else if (y < mUpperBound) {
|
||||
// scroll the list down a bit
|
||||
speed = y < mUpperBound / 2 ? -16 : -4;
|
||||
}
|
||||
if (speed != 0) {
|
||||
int ref = pointToPosition(0, mHeight / 2);
|
||||
if (ref == AdapterView.INVALID_POSITION) {
|
||||
// we hit a divider or an invisible view, check
|
||||
// somewhere else
|
||||
ref = pointToPosition(0, mHeight / 2
|
||||
+ getDividerHeight() + 64);
|
||||
}
|
||||
View v = getChildAt(ref - getFirstVisiblePosition());
|
||||
if (v != null) {
|
||||
int pos = v.getTop();
|
||||
setSelectionFromTop(ref, pos - speed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void stopDragging() {
|
||||
if (mDragBitmap != null) {
|
||||
mDragBitmap.recycle();
|
||||
mDragBitmap = null;
|
||||
}
|
||||
|
||||
unExpandViews(false);
|
||||
|
||||
if (mDragView != null) {
|
||||
WindowManager wm = (WindowManager) getContext().getSystemService(
|
||||
Context.WINDOW_SERVICE);
|
||||
wm.removeView(mDragView);
|
||||
mDragView.setImageDrawable(null);
|
||||
mDragView = null;
|
||||
}
|
||||
|
||||
if(mDragging) {
|
||||
if (mSwipeListener != null && mDragPos == mFirstDragPos) {
|
||||
if (mTouchCurrentX > mTouchStartX + SWIPE_THRESHOLD)
|
||||
mSwipeListener.swipeRight(mFirstDragPos);
|
||||
else if (mTouchStartX < mTouchStartX - SWIPE_THRESHOLD)
|
||||
mSwipeListener.swipeLeft(mFirstDragPos);
|
||||
} else if(mDropListener != null && mDragPos != mFirstDragPos &&
|
||||
mDragPos >= 0 && mDragPos < getCount()) {
|
||||
mDropListener.drop(mFirstDragPos, mDragPos);
|
||||
}
|
||||
}
|
||||
|
||||
mDragging = false;
|
||||
}
|
||||
|
||||
// --- getters and setters
|
||||
|
||||
public void setDragListener(DragListener l) {
|
||||
mDragListener = l;
|
||||
}
|
||||
|
||||
public void setDropListener(DropListener l) {
|
||||
mDropListener = l;
|
||||
}
|
||||
|
||||
public void setSwipeListener(SwipeListener l) {
|
||||
mSwipeListener = l;
|
||||
}
|
||||
|
||||
public void setClickListener(GrabberClickListener listener) {
|
||||
this.mClickListener = listener;
|
||||
}
|
||||
|
||||
public void setDragndropBackgroundColor(int color) {
|
||||
this.dragndropBackgroundColor = color;
|
||||
}
|
||||
|
||||
@SuppressWarnings("nls")
|
||||
@Override
|
||||
final public void addHeaderView(View v, Object data, boolean isSelectable) {
|
||||
throw new RuntimeException(
|
||||
"Headers are not supported with TouchListView");
|
||||
}
|
||||
|
||||
@SuppressWarnings("nls")
|
||||
@Override
|
||||
final public void addHeaderView(View v) {
|
||||
throw new RuntimeException(
|
||||
"Headers are not supported with TouchListView");
|
||||
}
|
||||
|
||||
@SuppressWarnings("nls")
|
||||
@Override
|
||||
final public void addFooterView(View v, Object data, boolean isSelectable) {
|
||||
if (mRemoveMode == SLIDE_LEFT || mRemoveMode == SLIDE_RIGHT) {
|
||||
throw new RuntimeException(
|
||||
"Footers are not supported with TouchListView in conjunction with remove_mode");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("nls")
|
||||
@Override
|
||||
final public void addFooterView(View v) {
|
||||
if (mRemoveMode == SLIDE_LEFT || mRemoveMode == SLIDE_RIGHT) {
|
||||
throw new RuntimeException(
|
||||
"Footers are not supported with TouchListView in conjunction with remove_mode");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue