/* * 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.Point; 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; import com.todoroo.astrid.utility.Flags; public class DraggableListView extends ListView { private static final int SWIPE_THRESHOLD = 40; private static final int MOVEMENT_THRESHOLD = 30; // --- 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 Point mDragPoint; // at what offset inside the item did the user grab it private Point 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 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; // --- other instance variables private int mItemHeightNormal = -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); dragndropBackgroundColor = a.getColor( R.styleable.TouchListView_dragndrop_background, 0x00000000); a.recycle(); } setSelector(R.drawable.none); } public void setItemHightNormal(int itemHeightNormal) { this.mItemHeightNormal = itemHeightNormal; } 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.y - (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 = LayoutParams.WRAP_CONTENT; v.setLayoutParams(params); v.setVisibility(View.VISIBLE); v.setPadding(0, 0, 0, 0); } } } /* * 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 = LayoutParams.WRAP_CONTENT; int marginTop = 0; 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) { marginTop = mItemHeightNormal; height = 2 * mItemHeightNormal; } } if (isDraggableRow(vv)) { ViewGroup.LayoutParams params = vv.getLayoutParams(); params.height = height; vv.setLayoutParams(params); vv.setVisibility(visibility); vv.setPadding(0, marginTop, 0, 0); } } // 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) { dragThread.interrupt(); dragThread = null; if (action == MotionEvent.ACTION_UP) mClickListener.onClick(viewAtPosition()); } else if (mSwipeListener != null && Math.abs(mTouchCurrentY - mTouchStartY) < MOVEMENT_THRESHOLD) { int dragPos = pointToPosition((int)mTouchCurrentX, (int)mTouchCurrentY); if (mTouchCurrentX > mTouchStartX + SWIPE_THRESHOLD) mSwipeListener.swipeRight(dragPos); else if (mTouchCurrentX < mTouchStartX - SWIPE_THRESHOLD) mSwipeListener.swipeLeft(dragPos); } } if(dragThread != null) { dragThread.interrupt(); dragThread = null; } break; case MotionEvent.ACTION_DOWN: dragThread = new Thread(new DragRunnable(ev)); dragThread.start(); stopDragging(); 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 { Thread.sleep(300L); post(new Runnable() { @Override public void run() { initiateDrag(ev); } }); Thread.sleep(1000L); post(new Runnable() { public void run() { stopDragging(); dragThread = null; Vibrator v = (Vibrator) getContext().getSystemService( Context.VIBRATOR_SERVICE); v.vibrate(50); mClickListener.onLongClick(viewAtPosition()); } }); } catch (InterruptedException e) { // bye! } } }; /** * @return true if drag was initiated */ protected boolean initiateDrag(MotionEvent ev) { int x = (int) mTouchCurrentX; int y = (int) mTouchCurrentY; int itemNum = pointToPosition(x, y); if (itemNum == AdapterView.INVALID_POSITION) return false; View item = (View) getChildAt(itemNum - getFirstVisiblePosition()); if(!isDraggableRow(item)) return false; mDragPoint = new Point(x - item.getLeft(), y - item.getTop()); mCoordOffset = new Point((int)ev.getRawX() - x, (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(50); 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.y + mCoordOffset.y; 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; Flags.set(Flags.TLFP_NO_INTERCEPT_TOUCH); } private void dragView(MotionEvent ev) { int x = (int) ev.getX(); int y = (int) ev.getY(); mWindowParams.y = y - mDragPoint.y + mCoordOffset.y; if (mDragPos == mFirstDragPos && x > mTouchStartX + SWIPE_THRESHOLD) mDragView.setPadding(30, 1, 0, 1); else if (mDragPos == mFirstDragPos && x < mTouchStartX - SWIPE_THRESHOLD) mDragView.setPadding(-30, 2, 0, 2); else mDragView.setPadding(0, 0, 0, 0); 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 (mTouchCurrentX < mTouchStartX - SWIPE_THRESHOLD) mSwipeListener.swipeLeft(mFirstDragPos); } else if(mDropListener != null && mDragPos != mFirstDragPos && mDragPos >= 0 && mDragPos < getCount()) { if(mFirstDragPos < mDragPos) mDragPos++; mDropListener.drop(mFirstDragPos, mDragPos); } } mDragging = false; Flags.checkAndClear(Flags.TLFP_NO_INTERCEPT_TOUCH); } // --- 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"); } }