diff --git a/astrid/plugin-src/com/todoroo/astrid/subtasks/SubtasksListFragment.java b/astrid/plugin-src/com/todoroo/astrid/subtasks/SubtasksListFragment.java index a1c116614..0899afd22 100644 --- a/astrid/plugin-src/com/todoroo/astrid/subtasks/SubtasksListFragment.java +++ b/astrid/plugin-src/com/todoroo/astrid/subtasks/SubtasksListFragment.java @@ -8,7 +8,6 @@ import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; -import com.commonsware.cwac.tlv.TouchListView; import com.commonsware.cwac.tlv.TouchListView.DropListener; import com.commonsware.cwac.tlv.TouchListView.GrabberClickListener; import com.timsu.astrid.R; @@ -17,6 +16,7 @@ import com.todoroo.astrid.activity.TaskListFragment; import com.todoroo.astrid.adapter.TaskAdapter; import com.todoroo.astrid.data.Metadata; import com.todoroo.astrid.data.Task; +import com.todoroo.astrid.ui.DraggableListView; public class SubtasksListFragment extends TaskListFragment { @@ -24,8 +24,8 @@ public class SubtasksListFragment extends TaskListFragment { private final SubtasksUpdater updater = new SubtasksUpdater(); - public TouchListView getTouchListView() { - TouchListView tlv = (TouchListView) getListView(); + public DraggableListView getTouchListView() { + DraggableListView tlv = (DraggableListView) getListView(); return tlv; } @@ -55,12 +55,14 @@ public class SubtasksListFragment extends TaskListFragment { Metadata.TABLE, Task.ID, Metadata.TASK, Metadata.KEY, SubtasksMetadata.METADATA_KEY, query); query = query.replaceAll("ORDER BY .*", ""); - query = query + String.format(" ORDER BY %s ASC, %s ASC", + query = query + String.format(" ORDER BY CAST(%s AS LONG) ASC, %s ASC", SubtasksMetadata.ORDER, Task.ID); filter.sqlQuery = query; super.setUpTaskList(); + + unregisterForContextMenu(getListView()); } private final DropListener dropListener = new DropListener() { @@ -69,12 +71,16 @@ public class SubtasksListFragment extends TaskListFragment { long targetTaskId = taskAdapter.getItemId(from); long destinationTaskId = taskAdapter.getItemId(to); + System.err.println("MOVE " + from + " TO " + to); + System.err.println("BEFORE"); updater.debugPrint(filter, SubtasksMetadata.LIST_ACTIVE_TASKS); + if(to == getListView().getCount() - 1) updater.moveTo(filter, SubtasksMetadata.LIST_ACTIVE_TASKS, targetTaskId, -1); else updater.moveTo(filter, SubtasksMetadata.LIST_ACTIVE_TASKS, targetTaskId, destinationTaskId); + System.err.println("AFTER"); updater.debugPrint(filter, SubtasksMetadata.LIST_ACTIVE_TASKS); diff --git a/astrid/res/layout/task_list_body_subtasks.xml b/astrid/res/layout/task_list_body_subtasks.xml index 33e6d4251..cc0b87a2c 100644 --- a/astrid/res/layout/task_list_body_subtasks.xml +++ b/astrid/res/layout/task_list_body_subtasks.xml @@ -13,7 +13,7 @@ android:text="@string/TLA_no_items" style="@style/TextAppearance.TLA_NoItems"/> - diff --git a/astrid/src/com/todoroo/astrid/ui/DraggableListView.java b/astrid/src/com/todoroo/astrid/ui/DraggableListView.java new file mode 100644 index 000000000..ebdb56f9d --- /dev/null +++ b/astrid/src/com/todoroo/astrid/ui/DraggableListView.java @@ -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"); + } + } + +}