From e030d23a6fa64440eaca0b89fc4871e80959b18f Mon Sep 17 00:00:00 2001 From: Tim Su Date: Mon, 4 Oct 2010 16:18:04 -0700 Subject: [PATCH] Started work on draggable task list activity by separating out task list body from task list activity, pulling in cwac TouchListView class (license: APLv2) --- astrid/AndroidManifest.xml | 9 + .../commonsware/cwac/tlv/TouchListView.java | 433 ++++++++++++++++++ astrid/res/layout/gtasks_decoration.xml | 7 - astrid/res/layout/task_list_activity.xml | 27 +- astrid/res/layout/task_list_activity_api3.xml | 101 ---- astrid/res/layout/task_list_body_api3.xml | 23 + .../res/layout/task_list_body_draggable.xml | 37 ++ astrid/res/layout/task_list_body_standard.xml | 28 ++ astrid/res/values/cwac_touchlist_attrs.xml | 15 + .../activity/DraggableTaskListActivity.java | 75 +++ .../astrid/activity/TaskListActivity.java | 34 +- 11 files changed, 644 insertions(+), 145 deletions(-) create mode 100644 astrid/common-src/com/commonsware/cwac/tlv/TouchListView.java delete mode 100644 astrid/res/layout/gtasks_decoration.xml delete mode 100644 astrid/res/layout/task_list_activity_api3.xml create mode 100644 astrid/res/layout/task_list_body_api3.xml create mode 100644 astrid/res/layout/task_list_body_draggable.xml create mode 100644 astrid/res/layout/task_list_body_standard.xml create mode 100644 astrid/res/values/cwac_touchlist_attrs.xml create mode 100644 astrid/src/com/todoroo/astrid/activity/DraggableTaskListActivity.java diff --git a/astrid/AndroidManifest.xml b/astrid/AndroidManifest.xml index eadc62dd8..bc62f4120 100644 --- a/astrid/AndroidManifest.xml +++ b/astrid/AndroidManifest.xml @@ -78,6 +78,15 @@ + + + + + + + diff --git a/astrid/common-src/com/commonsware/cwac/tlv/TouchListView.java b/astrid/common-src/com/commonsware/cwac/tlv/TouchListView.java new file mode 100644 index 000000000..781176ad9 --- /dev/null +++ b/astrid/common-src/com/commonsware/cwac/tlv/TouchListView.java @@ -0,0 +1,433 @@ +/* + * 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.commonsware.cwac.tlv; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.GestureDetector.SimpleOnGestureListener; +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.timsu.astrid.R; + +public class TouchListView extends ListView { + private ImageView mDragView; + private WindowManager mWindowManager; + private WindowManager.LayoutParams mWindowParams; + 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 + private DragListener mDragListener; + private DropListener mDropListener; + private RemoveListener mRemoveListener; + private int mUpperBound; + private int mLowerBound; + private int mHeight; + private GestureDetector mGestureDetector; + public static final int FLING = 0; + public static final int SLIDE_RIGHT = 1; + public static final int SLIDE_LEFT = 2; + private int mRemoveMode = -1; + private final Rect mTempRect = new Rect(); + private Bitmap mDragBitmap; + private final int mTouchSlop; + private int mItemHeightNormal=-1; + private int mItemHeightExpanded=-1; + private int grabberId=-1; + private int dragndropBackgroundColor=0x00000000; + + public TouchListView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TouchListView(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); + grabberId=a.getResourceId(R.styleable.TouchListView_grabber, -1); + dragndropBackgroundColor=a.getColor(R.styleable.TouchListView_dragndrop_background, 0x00000000); + mRemoveMode=a.getInt(R.styleable.TouchListView_remove_mode, -1); + + a.recycle(); + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (mRemoveListener != null && mGestureDetector == null) { + if (mRemoveMode == FLING) { + mGestureDetector = new GestureDetector(getContext(), new SimpleOnGestureListener() { + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + if (mDragView != null) { + if (velocityX > 1000) { + Rect r = mTempRect; + mDragView.getDrawingRect(r); + if ( e2.getX() > r.right * 2 / 3) { + // fast fling right with release near the right edge of the screen + stopDragging(); + mRemoveListener.remove(mFirstDragPos); + unExpandViews(true); + } + } + // flinging while dragging should have no effect + return true; + } + return false; + } + }); + } + } + if (mDragListener != null || mDropListener != null) { + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + int x = (int) ev.getX(); + int y = (int) ev.getY(); + int itemnum = pointToPosition(x, y); + if (itemnum == AdapterView.INVALID_POSITION) { + break; + } + ViewGroup item = (ViewGroup) getChildAt(itemnum - getFirstVisiblePosition()); + mDragPoint = y - item.getTop(); + mCoordOffset = ((int)ev.getRawY()) - y; + View dragger = item.findViewById(grabberId); + Rect r = mTempRect; +// dragger.getDrawingRect(r); + + r.left=dragger.getLeft(); + r.right=dragger.getRight(); + r.top=dragger.getTop(); + r.bottom=dragger.getBottom(); + + if ((r.left= 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 - 32; + 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 listitems + */ + 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; + } + } + 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 listitem 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 insertpoint. + */ + 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; + } + } + ViewGroup.LayoutParams params = vv.getLayoutParams(); + params.height = height; + vv.setLayoutParams(params); + vv.setVisibility(visibility); + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mGestureDetector != null) { + mGestureDetector.onTouchEvent(ev); + } + if ((mDragListener != null || mDropListener != null) && mDragView != null) { + int action = ev.getAction(); + switch (action) { + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + Rect r = mTempRect; + mDragView.getDrawingRect(r); + stopDragging(); + + if (mRemoveMode == SLIDE_RIGHT && ev.getX() > r.left+(r.width()*3/4)) { + if (mRemoveListener != null) { + mRemoveListener.remove(mFirstDragPos); + } + unExpandViews(true); + } else if (mRemoveMode == SLIDE_LEFT && ev.getX() < r.left+(r.width()/4)) { + if (mRemoveListener != null) { + mRemoveListener.remove(mFirstDragPos); + } + unExpandViews(true); + } else { + if (mDropListener != null && mDragPos >= 0 && mDragPos < getCount()) { + mDropListener.drop(mFirstDragPos, mDragPos); + } + unExpandViews(false); + } + break; + + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + int x = (int) ev.getX(); + int y = (int) ev.getY(); + dragView(x, y); + int itemnum = getItemForPosition(y); + if (itemnum >= 0) { + if (action == 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); + } + } + } + break; + } + return true; + } + return super.onTouchEvent(ev); + } + + private void startDragging(Bitmap bm, int y) { + stopDragging(); + + mWindowParams = new WindowManager.LayoutParams(); + mWindowParams.gravity = Gravity.TOP; + mWindowParams.x = 0; + 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()); +// int backGroundColor = getContext().getResources().getColor(R.color.dragndrop_background); + v.setBackgroundColor(dragndropBackgroundColor); + v.setImageBitmap(bm); + mDragBitmap = bm; + + mWindowManager = (WindowManager)getContext().getSystemService("window"); + mWindowManager.addView(v, mWindowParams); + mDragView = v; + } + + private void dragView(int x, int y) { + 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); + } + + private void stopDragging() { + if (mDragView != null) { + WindowManager wm = (WindowManager)getContext().getSystemService("window"); + wm.removeView(mDragView); + mDragView.setImageDrawable(null); + mDragView = null; + } + if (mDragBitmap != null) { + mDragBitmap.recycle(); + mDragBitmap = null; + } + } + + public void setDragListener(DragListener l) { + mDragListener = l; + } + + public void setDropListener(DropListener l) { + mDropListener = l; + } + + public void setRemoveListener(RemoveListener l) { + mRemoveListener = l; + } + + public interface DragListener { + void drag(int from, int to); + } + public interface DropListener { + void drop(int from, int to); + } + public interface RemoveListener { + void remove(int which); + } +} diff --git a/astrid/res/layout/gtasks_decoration.xml b/astrid/res/layout/gtasks_decoration.xml deleted file mode 100644 index 77d6a0f23..000000000 --- a/astrid/res/layout/gtasks_decoration.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/astrid/res/layout/task_list_activity.xml b/astrid/res/layout/task_list_activity.xml index 8dc5c3531..860cafd0f 100644 --- a/astrid/res/layout/task_list_activity.xml +++ b/astrid/res/layout/task_list_activity.xml @@ -38,32 +38,7 @@ - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/astrid/res/layout/task_list_body_api3.xml b/astrid/res/layout/task_list_body_api3.xml new file mode 100644 index 000000000..fffe60e8f --- /dev/null +++ b/astrid/res/layout/task_list_body_api3.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/astrid/res/layout/task_list_body_draggable.xml b/astrid/res/layout/task_list_body_draggable.xml new file mode 100644 index 000000000..096958c76 --- /dev/null +++ b/astrid/res/layout/task_list_body_draggable.xml @@ -0,0 +1,37 @@ + + + + + + + + + + diff --git a/astrid/res/layout/task_list_body_standard.xml b/astrid/res/layout/task_list_body_standard.xml new file mode 100644 index 000000000..b27f20655 --- /dev/null +++ b/astrid/res/layout/task_list_body_standard.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/astrid/res/values/cwac_touchlist_attrs.xml b/astrid/res/values/cwac_touchlist_attrs.xml new file mode 100644 index 000000000..e9eb6afa0 --- /dev/null +++ b/astrid/res/values/cwac_touchlist_attrs.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/astrid/src/com/todoroo/astrid/activity/DraggableTaskListActivity.java b/astrid/src/com/todoroo/astrid/activity/DraggableTaskListActivity.java new file mode 100644 index 000000000..5274a76bd --- /dev/null +++ b/astrid/src/com/todoroo/astrid/activity/DraggableTaskListActivity.java @@ -0,0 +1,75 @@ +package com.todoroo.astrid.activity; + +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; + +import com.commonsware.cwac.tlv.TouchListView; +import com.timsu.astrid.R; +import com.todoroo.andlib.sql.Criterion; +import com.todoroo.andlib.sql.Functions; +import com.todoroo.andlib.sql.Join; +import com.todoroo.andlib.sql.Order; +import com.todoroo.andlib.sql.QueryTemplate; +import com.todoroo.astrid.api.Filter; +import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria; +import com.todoroo.astrid.dao.TaskDao.TaskCriteria; +import com.todoroo.astrid.data.Metadata; +import com.todoroo.astrid.data.Task; + +/** + * Activity for working with draggable task lists, like Google Tasks lists + * + * @author Tim Su + * + */ +public class DraggableTaskListActivity extends TaskListActivity { + + // --- gtasks temp stuff + private final String listId = "17816916813445155620:0:0"; //$NON-NLS-1$ + Filter filter = new Filter("Tim's Tasks", "Tim's Tasks", new QueryTemplate().join( //$NON-NLS-1$ //$NON-NLS-2$ + Join.left(Metadata.TABLE, Task.ID.eq(Metadata.TASK))).where(Criterion.and( + MetadataCriteria.withKey("gtasks"), //$NON-NLS-1$ + TaskCriteria.isVisible(), + TaskCriteria.notDeleted(), + Metadata.VALUE2.eq(listId))).orderBy( + Order.asc(Functions.cast(Metadata.VALUE5, "INTEGER"))), //$NON-NLS-1$ + null); + + // --- end + + @Override + public void onCreate(Bundle icicle) { + getIntent().putExtra(TOKEN_FILTER, filter); + super.onCreate(icicle); + + TouchListView tlv = (TouchListView) getListView(); + tlv.setDropListener(onDrop); + tlv.setRemoveListener(onRemove); + } + + @Override + protected View getListBody(ViewGroup root) { + return getLayoutInflater().inflate(R.layout.task_list_body_draggable, root); + } + + private final TouchListView.DropListener onDrop = new TouchListView.DropListener() { + @Override + public void drop(int from, int to) { + /*String item = adapter.getItem(from); + + adapter.remove(item); + adapter.insert(item, to);*/ + } + }; + + private final TouchListView.RemoveListener onRemove = new TouchListView.RemoveListener() { + @Override + public void remove(int which) { + // new GtasksIndentAction.GtasksIncreaseIndentAction().indent(adapter.getItemId(which)); + // adapter.notifyDataSetChanged(); + // adapter.remove(adapter.getItem(which)); + } + }; + +} diff --git a/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java b/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java index 7f725e46a..86801ece5 100644 --- a/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java +++ b/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java @@ -3,9 +3,9 @@ package com.todoroo.astrid.activity; import java.util.Date; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map.Entry; import java.util.Timer; import java.util.TimerTask; -import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicReference; import android.app.AlertDialog; @@ -27,27 +27,28 @@ import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.view.Window; -import android.view.WindowManager; -import android.view.ContextMenu.ContextMenuInfo; import android.view.View.OnClickListener; import android.view.View.OnKeyListener; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; +import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; -import android.widget.Toast; -import android.widget.AbsListView.OnScrollListener; -import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.TextView.OnEditorActionListener; +import android.widget.Toast; import com.flurry.android.FlurryAgent; import com.timsu.astrid.R; @@ -177,6 +178,18 @@ public class TaskListActivity extends ListActivity implements OnScrollListener, DependencyInjectionService.getInstance().inject(this); } + /** + * @return view to attach to the body of the task list. must contain two + * elements, a view with id android:id/empty and a list view with id + * android:id/list + */ + protected View getListBody(ViewGroup root) { + if(AndroidUtilities.getSdkVersion() > 3) + return getLayoutInflater().inflate(R.layout.task_list_body_standard, root, false); + else + return getLayoutInflater().inflate(R.layout.task_list_body_api3, root, false); + } + /** Called when loading up the activity */ @Override protected void onCreate(Bundle savedInstanceState) { @@ -184,10 +197,9 @@ public class TaskListActivity extends ListActivity implements OnScrollListener, super.onCreate(savedInstanceState); new StartupService().onStartupApplication(this); - if(AndroidUtilities.getSdkVersion() > 3) - setContentView(R.layout.task_list_activity); - else - setContentView(R.layout.task_list_activity_api3); + ViewGroup parent = (ViewGroup) getLayoutInflater().inflate(R.layout.task_list_activity, null); + parent.addView(getListBody(parent), 1); + setContentView(parent); if(database == null) return;