From dcbe9bbf98757a512bc468c8ccb364f691ad7e3b Mon Sep 17 00:00:00 2001 From: kd7uiy Date: Sat, 28 Dec 2013 13:30:50 -0500 Subject: [PATCH 01/18] This fixes a bug so the Warp demonstration works (Fast Scrolling) --- .../mobeta/android/dslv/DragSortListView.java | 5994 ++++++++--------- 1 file changed, 2997 insertions(+), 2997 deletions(-) diff --git a/library/src/com/mobeta/android/dslv/DragSortListView.java b/library/src/com/mobeta/android/dslv/DragSortListView.java index ed45637..7f9cc2b 100644 --- a/library/src/com/mobeta/android/dslv/DragSortListView.java +++ b/library/src/com/mobeta/android/dslv/DragSortListView.java @@ -1,2997 +1,2997 @@ -/* - * DragSortListView. - * - * A subclass of the Android ListView component that enables drag - * and drop re-ordering of list items. - * - * Copyright 2012 Carl Bauer - * - * 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.mobeta.android.dslv; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import android.content.Context; -import android.content.res.TypedArray; -import android.database.DataSetObserver; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Point; -import android.graphics.drawable.Drawable; -import android.os.Environment; -import android.os.SystemClock; -import android.util.AttributeSet; -import android.util.Log; -import android.util.SparseBooleanArray; -import android.util.SparseIntArray; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AbsListView; -import android.widget.BaseAdapter; -import android.widget.Checkable; -import android.widget.ListAdapter; -import android.widget.ListView; -import android.widget.WrapperListAdapter; - -/** - * ListView subclass that mediates drag and drop resorting of items. - * - * - * @author heycosmo - * - */ -public class DragSortListView extends ListView { - - - /** - * The View that floats above the ListView and represents - * the dragged item. - */ - private View mFloatView; - - /** - * The float View location. First based on touch location - * and given deltaX and deltaY. Then restricted by callback - * to FloatViewManager.onDragFloatView(). Finally restricted - * by bounds of DSLV. - */ - private Point mFloatLoc = new Point(); - - private Point mTouchLoc = new Point(); - - /** - * The middle (in the y-direction) of the floating View. - */ - private int mFloatViewMid; - - /** - * Flag to make sure float View isn't measured twice - */ - private boolean mFloatViewOnMeasured = false; - - /** - * Watch the Adapter for data changes. Cancel a drag if - * coincident with a change. - */ - private DataSetObserver mObserver; - - /** - * Transparency for the floating View (XML attribute). - */ - private float mFloatAlpha = 1.0f; - private float mCurrFloatAlpha = 1.0f; - - /** - * While drag-sorting, the current position of the floating - * View. If dropped, the dragged item will land in this position. - */ - private int mFloatPos; - - /** - * The first expanded ListView position that helps represent - * the drop slot tracking the floating View. - */ - private int mFirstExpPos; - - /** - * The second expanded ListView position that helps represent - * the drop slot tracking the floating View. This can equal - * mFirstExpPos if there is no slide shuffle occurring; otherwise - * it is equal to mFirstExpPos + 1. - */ - private int mSecondExpPos; - - /** - * Flag set if slide shuffling is enabled. - */ - private boolean mAnimate = false; - - /** - * The user dragged from this position. - */ - private int mSrcPos; - - /** - * Offset (in x) within the dragged item at which the user - * picked it up (or first touched down with the digitalis). - */ - private int mDragDeltaX; - - /** - * Offset (in y) within the dragged item at which the user - * picked it up (or first touched down with the digitalis). - */ - private int mDragDeltaY; - - - /** - * The difference (in x) between screen coordinates and coordinates - * in this view. - */ - //private int mOffsetX; - - /** - * The difference (in y) between screen coordinates and coordinates - * in this view. - */ - //private int mOffsetY; - - /** - * A listener that receives callbacks whenever the floating View - * hovers over a new position. - */ - private DragListener mDragListener; - - /** - * A listener that receives a callback when the floating View - * is dropped. - */ - private DropListener mDropListener; - - /** - * A listener that receives a callback when the floating View - * (or more precisely the originally dragged item) is removed - * by one of the provided gestures. - */ - private RemoveListener mRemoveListener; - - /** - * Enable/Disable item dragging - * - * @attr name dslv:drag_enabled - */ - private boolean mDragEnabled = true; - - /** - * Drag state enum. - */ - private final static int IDLE = 0; - private final static int REMOVING = 1; - private final static int DROPPING = 2; - private final static int STOPPED = 3; - private final static int DRAGGING = 4; - - private int mDragState = IDLE; - - /** - * Height in pixels to which the originally dragged item - * is collapsed during a drag-sort. Currently, this value - * must be greater than zero. - */ - private int mItemHeightCollapsed = 1; - - /** - * Height of the floating View. Stored for the purpose of - * providing the tracking drop slot. - */ - private int mFloatViewHeight; - - /** - * Convenience member. See above. - */ - private int mFloatViewHeightHalf; - - /** - * Save the given width spec for use in measuring children - */ - private int mWidthMeasureSpec = 0; - - /** - * Sample Views ultimately used for calculating the height - * of ListView items that are off-screen. - */ - private View[] mSampleViewTypes = new View[1]; - - /** - * Drag-scroll encapsulator! - */ - private DragScroller mDragScroller; - - /** - * Determines the start of the upward drag-scroll region - * at the top of the ListView. Specified by a fraction - * of the ListView height, thus screen resolution agnostic. - */ - private float mDragUpScrollStartFrac = 1.0f / 3.0f; - - /** - * Determines the start of the downward drag-scroll region - * at the bottom of the ListView. Specified by a fraction - * of the ListView height, thus screen resolution agnostic. - */ - private float mDragDownScrollStartFrac = 1.0f / 3.0f; - - /** - * The following are calculated from the above fracs. - */ - private int mUpScrollStartY; - private int mDownScrollStartY; - private float mDownScrollStartYF; - private float mUpScrollStartYF; - - /** - * Calculated from above above and current ListView height. - */ - private float mDragUpScrollHeight; - - /** - * Calculated from above above and current ListView height. - */ - private float mDragDownScrollHeight; - - /** - * Maximum drag-scroll speed in pixels per ms. Only used with - * default linear drag-scroll profile. - */ - private float mMaxScrollSpeed = 0.5f; - - /** - * Defines the scroll speed during a drag-scroll. User can - * provide their own; this default is a simple linear profile - * where scroll speed increases linearly as the floating View - * nears the top/bottom of the ListView. - */ - private DragScrollProfile mScrollProfile = new DragScrollProfile() { - @Override - public float getSpeed(float w, long t) { - return mMaxScrollSpeed * w; - } - }; - - /** - * Current touch x. - */ - private int mX; - - /** - * Current touch y. - */ - private int mY; - - /** - * Last touch x. - */ - //private int mLastX; - - /** - * Last touch y. - */ - private int mLastY; - - /** - * The touch y-coord at which drag started - */ - //private int mDragStartY; - - /** - * Drag flag bit. Floating View can move in the positive - * x direction. - */ - public final static int DRAG_POS_X = 0x1; - - /** - * Drag flag bit. Floating View can move in the negative - * x direction. - */ - public final static int DRAG_NEG_X = 0x2; - - /** - * Drag flag bit. Floating View can move in the positive - * y direction. This is subtle. What this actually means is - * that, if enabled, the floating View can be dragged below its starting - * position. Remove in favor of upper-bounding item position? - */ - public final static int DRAG_POS_Y = 0x4; - - /** - * Drag flag bit. Floating View can move in the negative - * y direction. This is subtle. What this actually means is - * that the floating View can be dragged above its starting - * position. Remove in favor of lower-bounding item position? - */ - public final static int DRAG_NEG_Y = 0x8; - - /** - * Flags that determine limits on the motion of the - * floating View. See flags above. - */ - private int mDragFlags = 0; - - /** - * Last call to an on*TouchEvent was a call to - * onInterceptTouchEvent. - */ - private boolean mLastCallWasIntercept = false; - - /** - * A touch event is in progress. - */ - private boolean mInTouchEvent = false; - - /** - * Let the user customize the floating View. - */ - private FloatViewManager mFloatViewManager = null; - - /** - * Given to ListView to cancel its action when a drag-sort - * begins. - */ - private MotionEvent mCancelEvent; - - /** - * Enum telling where to cancel the ListView action when a - * drag-sort begins - */ - private static final int NO_CANCEL = 0; - private static final int ON_TOUCH_EVENT = 1; - private static final int ON_INTERCEPT_TOUCH_EVENT = 2; - - /** - * Where to cancel the ListView action when a - * drag-sort begins - */ - private int mCancelMethod = NO_CANCEL; - - /** - * Determines when a slide shuffle animation starts. That is, - * defines how close to the edge of the drop slot the floating - * View must be to initiate the slide. - */ - private float mSlideRegionFrac = 0.25f; - - /** - * Number between 0 and 1 indicating the relative location of - * a sliding item (only used if drag-sort animations - * are turned on). Nearly 1 means the item is - * at the top of the slide region (nearly full blank item - * is directly below). - */ - private float mSlideFrac = 0.0f; - - /** - * Wraps the user-provided ListAdapter. This is used to wrap each - * item View given by the user inside another View (currenly - * a RelativeLayout) which - * expands and collapses to simulate the item shuffling. - */ - private AdapterWrapper mAdapterWrapper; - - /** - * Turn on custom debugger. - */ - private boolean mTrackDragSort = false; - - /** - * Debugging class. - */ - private DragSortTracker mDragSortTracker; - - /** - * Needed for adjusting item heights from within layoutChildren - */ - private boolean mBlockLayoutRequests = false; - - /** - * Set to true when a down event happens during drag sort; - * for example, when drag finish animations are - * playing. - */ - private boolean mIgnoreTouchEvent = false; - - /** - * Caches DragSortItemView child heights. Sometimes DSLV has to - * know the height of an offscreen item. Since ListView virtualizes - * these, DSLV must get the item from the ListAdapter to obtain - * its height. That process can be expensive, but often the same - * offscreen item will be requested many times in a row. Once an - * offscreen item height is calculated, we cache it in this guy. - * Actually, we cache the height of the child of the - * DragSortItemView since the item height changes often during a - * drag-sort. - */ - private static final int sCacheSize = 3; - private HeightCache mChildHeightCache = new HeightCache(sCacheSize); - - private RemoveAnimator mRemoveAnimator; - - private LiftAnimator mLiftAnimator; - - private DropAnimator mDropAnimator; - - private boolean mUseRemoveVelocity; - private float mRemoveVelocityX = 0; - - public DragSortListView(Context context, AttributeSet attrs) { - super(context, attrs); - - int defaultDuration = 150; - int removeAnimDuration = defaultDuration; // ms - int dropAnimDuration = defaultDuration; // ms - - if (attrs != null) { - TypedArray a = getContext().obtainStyledAttributes(attrs, - R.styleable.DragSortListView, 0, 0); - - mItemHeightCollapsed = Math.max(1, a.getDimensionPixelSize( - R.styleable.DragSortListView_collapsed_height, 1)); - - mTrackDragSort = a.getBoolean( - R.styleable.DragSortListView_track_drag_sort, false); - - if (mTrackDragSort) { - mDragSortTracker = new DragSortTracker(); - } - - // alpha between 0 and 255, 0=transparent, 255=opaque - mFloatAlpha = a.getFloat(R.styleable.DragSortListView_float_alpha, mFloatAlpha); - mCurrFloatAlpha = mFloatAlpha; - - mDragEnabled = a.getBoolean(R.styleable.DragSortListView_drag_enabled, mDragEnabled); - - mSlideRegionFrac = Math.max(0.0f, - Math.min(1.0f, 1.0f - a.getFloat( - R.styleable.DragSortListView_slide_shuffle_speed, - 0.75f))); - - mAnimate = mSlideRegionFrac > 0.0f; - - float frac = a.getFloat( - R.styleable.DragSortListView_drag_scroll_start, - mDragUpScrollStartFrac); - - setDragScrollStart(frac); - - mMaxScrollSpeed = a.getFloat( - R.styleable.DragSortListView_max_drag_scroll_speed, - mMaxScrollSpeed); - - removeAnimDuration = a.getInt( - R.styleable.DragSortListView_remove_animation_duration, - removeAnimDuration); - - dropAnimDuration = a.getInt( - R.styleable.DragSortListView_drop_animation_duration, - dropAnimDuration); - - boolean useDefault = a.getBoolean( - R.styleable.DragSortListView_use_default_controller, - true); - - if (useDefault) { - boolean removeEnabled = a.getBoolean( - R.styleable.DragSortListView_remove_enabled, - false); - int removeMode = a.getInt( - R.styleable.DragSortListView_remove_mode, - DragSortController.FLING_REMOVE); - boolean sortEnabled = a.getBoolean( - R.styleable.DragSortListView_sort_enabled, - true); - int dragInitMode = a.getInt( - R.styleable.DragSortListView_drag_start_mode, - DragSortController.ON_DOWN); - int dragHandleId = a.getResourceId( - R.styleable.DragSortListView_drag_handle_id, - 0); - int flingHandleId = a.getResourceId( - R.styleable.DragSortListView_fling_handle_id, - 0); - int clickRemoveId = a.getResourceId( - R.styleable.DragSortListView_click_remove_id, - 0); - int bgColor = a.getColor( - R.styleable.DragSortListView_float_background_color, - Color.BLACK); - - DragSortController controller = new DragSortController( - this, dragHandleId, dragInitMode, removeMode, - clickRemoveId, flingHandleId); - controller.setRemoveEnabled(removeEnabled); - controller.setSortEnabled(sortEnabled); - controller.setBackgroundColor(bgColor); - - mFloatViewManager = controller; - setOnTouchListener(controller); - } - - a.recycle(); - } - - mDragScroller = new DragScroller(); - - float smoothness = 0.5f; - if (removeAnimDuration > 0) { - mRemoveAnimator = new RemoveAnimator(smoothness, removeAnimDuration); - } - // mLiftAnimator = new LiftAnimator(smoothness, 100); - if (dropAnimDuration > 0) { - mDropAnimator = new DropAnimator(smoothness, dropAnimDuration); - } - - mCancelEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0f, 0f, 0, 0f, - 0f, 0, 0); - - // construct the dataset observer - mObserver = new DataSetObserver() { - private void cancel() { - if (mDragState == DRAGGING) { - cancelDrag(); - } - } - - @Override - public void onChanged() { - cancel(); - } - - @Override - public void onInvalidated() { - cancel(); - } - }; - } - - /** - * Usually called from a FloatViewManager. The float alpha - * will be reset to the xml-defined value every time a drag - * is stopped. - */ - public void setFloatAlpha(float alpha) { - mCurrFloatAlpha = alpha; - } - - public float getFloatAlpha() { - return mCurrFloatAlpha; - } - - /** - * Set maximum drag scroll speed in positions/second. Only applies - * if using default ScrollSpeedProfile. - * - * @param max Maximum scroll speed. - */ - public void setMaxScrollSpeed(float max) { - mMaxScrollSpeed = max; - } - - /** - * For each DragSortListView Listener interface implemented by - * adapter, this method calls the appropriate - * set*Listener method with adapter as the argument. - * - * @param adapter The ListAdapter providing data to back - * DragSortListView. - * - * @see android.widget.ListView#setAdapter(android.widget.ListAdapter) - */ - @Override - public void setAdapter(ListAdapter adapter) { - if (adapter != null) { - mAdapterWrapper = new AdapterWrapper(adapter); - adapter.registerDataSetObserver(mObserver); - - if (adapter instanceof DropListener) { - setDropListener((DropListener) adapter); - } - if (adapter instanceof DragListener) { - setDragListener((DragListener) adapter); - } - if (adapter instanceof RemoveListener) { - setRemoveListener((RemoveListener) adapter); - } - } else { - mAdapterWrapper = null; - } - - super.setAdapter(mAdapterWrapper); - } - - /** - * As opposed to {@link ListView#getAdapter()}, which returns - * a heavily wrapped ListAdapter (DragSortListView wraps the - * input ListAdapter {\emph and} ListView wraps the wrapped one). - * - * @return The ListAdapter set as the argument of {@link setAdapter()} - */ - public ListAdapter getInputAdapter() { - if (mAdapterWrapper == null) { - return null; - } else { - return mAdapterWrapper.getWrappedAdapter(); - } - } - - private class AdapterWrapper implements WrapperListAdapter { - private ListAdapter mAdapter; - - public AdapterWrapper(ListAdapter adapter) { - mAdapter = adapter; - } - - @Override - public ListAdapter getWrappedAdapter() { - return mAdapter; - } - - @Override - public long getItemId(int position) { - return mAdapter.getItemId(position); - } - - @Override - public Object getItem(int position) { - return mAdapter.getItem(position); - } - - @Override - public int getCount() { - return mAdapter.getCount(); - } - - @Override - public boolean areAllItemsEnabled() { - return mAdapter.areAllItemsEnabled(); - } - - @Override - public boolean isEnabled(int position) { - return mAdapter.isEnabled(position); - } - - @Override - public int getItemViewType(int position) { - return mAdapter.getItemViewType(position); - } - - @Override - public int getViewTypeCount() { - return mAdapter.getViewTypeCount(); - } - - @Override - public boolean hasStableIds() { - return mAdapter.hasStableIds(); - } - - @Override - public boolean isEmpty() { - return mAdapter.isEmpty(); - } - - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - DragSortItemView v; - View child; - if (convertView != null) { - v = (DragSortItemView) convertView; - View oldChild = v.getChildAt(0); - - child = mAdapter.getView(position, oldChild, DragSortListView.this); - if (child != oldChild) { - // shouldn't get here if user is reusing convertViews - // properly - if (oldChild != null) { - v.removeViewAt(0); - } - v.addView(child); - } - } else { - child = mAdapter.getView(position, null, DragSortListView.this); - if (child instanceof Checkable) { - v = new DragSortItemViewCheckable(getContext()); - } else { - v = new DragSortItemView(getContext()); - } - v.setLayoutParams(new AbsListView.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT)); - v.addView(child); - } - - // Set the correct item height given drag state; passed - // View needs to be measured if measurement is required. - adjustItem(position + getHeaderViewsCount(), v, true); - - return v; - } - - @Override - public void registerDataSetObserver(DataSetObserver observer) { - mAdapter.registerDataSetObserver(observer); - } - - @Override - public void unregisterDataSetObserver(DataSetObserver observer) { - mAdapter.unregisterDataSetObserver(observer); - } - } - - private void drawDivider(int expPosition, Canvas canvas) { - final Drawable divider = getDivider(); - final int dividerHeight = getDividerHeight(); - - if (divider != null && dividerHeight != 0) { - final ViewGroup expItem = (ViewGroup) getChildAt(expPosition - - getFirstVisiblePosition()); - if (expItem != null) { - final int left = getPaddingLeft(); - final int right = getWidth() - getPaddingRight(); - final int top; - final int bottom; - - final int childHeight = expItem.getChildAt(0).getHeight(); - - if (expPosition > mSrcPos) { - top = expItem.getTop() + childHeight; - bottom = top + dividerHeight; - } else { - bottom = expItem.getBottom() - childHeight; - top = bottom - dividerHeight; - } - - // Have to clip to support ColorDrawable on <= Gingerbread - canvas.save(); - canvas.clipRect(left, top, right, bottom); - divider.setBounds(left, top, right, bottom); - divider.draw(canvas); - canvas.restore(); - } - } - } - - @Override - protected void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - - if (mDragState != IDLE) { - // draw the divider over the expanded item - if (mFirstExpPos != mSrcPos) { - drawDivider(mFirstExpPos, canvas); - } - if (mSecondExpPos != mFirstExpPos && mSecondExpPos != mSrcPos) { - drawDivider(mSecondExpPos, canvas); - } - } - - if (mFloatView != null) { - // draw the float view over everything - final int floatViewWidth = mFloatView.getWidth(); - final int floatViewHeight = mFloatView.getHeight(); - - int x = mFloatLoc.x; - - final int listViewWidth = getWidth(); - if (x < 0) - x = -x; - float alphaMod; - if (x < listViewWidth) { - alphaMod = ((float) (listViewWidth - x)) / ((float) listViewWidth); - alphaMod *= alphaMod; - } else { - alphaMod = 0; - } - - final int alpha = (int) (255f * mCurrFloatAlpha * alphaMod); - - canvas.save(); - canvas.translate(mFloatLoc.x, mFloatLoc.y); - canvas.clipRect(0, 0, floatViewWidth, floatViewHeight); - - canvas.saveLayerAlpha(0, 0, floatViewWidth, floatViewHeight, alpha, - Canvas.ALL_SAVE_FLAG); - - mFloatView.draw(canvas); - canvas.restore(); - canvas.restore(); - } - } - - private int getItemHeight(int position) { - View v = getChildAt(position - getFirstVisiblePosition()); - - if (v != null) { - // item is onscreen, just get the height of the View - return v.getHeight(); - } else { - // item is offscreen. get child height and calculate - // item height based on current shuffle state - return calcItemHeight(position, getChildHeight(position)); - } - } - - private class HeightCache { - - private SparseIntArray mMap; - private List mOrder; - private int mMaxSize; - - public HeightCache(int size) { - mMap = new SparseIntArray(size); - mOrder = new ArrayList(size); - mMaxSize = size; - } - - /** - * Add item height at position if doesn't already exist. - */ - public void add(int position, int height) { - int currentHeight = mMap.get(position, -1); - if (currentHeight != height) { - if (currentHeight == -1 && mMap.size() == mMaxSize) { - // remove oldest entry - mMap.delete(mOrder.remove(0)); - } else { - // move position to newest slot - mOrder.remove((Integer) position); - } - mMap.put(position, height); - mOrder.add(position); - } - } - - public int get(int position) { - return mMap.get(position, -1); - } - - public void clear() { - mMap.clear(); - mOrder.clear(); - } - - } - - /** - * Get the shuffle edge for item at position when top of - * item is at y-coord top. Assumes that current item heights - * are consistent with current float view location and - * thus expanded positions and slide fraction. i.e. Should not be - * called between update of expanded positions/slide fraction - * and layoutChildren. - * - * @param position - * @param top - * @param height Height of item at position. If -1, this function - * calculates this height. - * - * @return Shuffle line between position-1 and position (for - * the given view of the list; that is, for when top of item at - * position has y-coord of given `top`). If - * floating View (treated as horizontal line) is dropped - * immediately above this line, it lands in position-1. If - * dropped immediately below this line, it lands in position. - */ - private int getShuffleEdge(int position, int top) { - final int numHeaders = getHeaderViewsCount(); - final int numFooters = getFooterViewsCount(); - - // shuffle edges are defined between items that can be - // dragged; there are N-1 of them if there are N draggable - // items. - - if (position <= numHeaders || (position >= getCount() - numFooters)) { - return top; - } - - int divHeight = getDividerHeight(); - - int edge; - - int maxBlankHeight = mFloatViewHeight - mItemHeightCollapsed; - int childHeight = getChildHeight(position); - int itemHeight = getItemHeight(position); - - // first calculate top of item given that floating View is - // centered over src position - int otop = top; - if (mSecondExpPos <= mSrcPos) { - // items are expanded on and/or above the source position - - if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) { - if (position == mSrcPos) { - otop = top + itemHeight - mFloatViewHeight; - } else { - int blankHeight = itemHeight - childHeight; - otop = top + blankHeight - maxBlankHeight; - } - } else if (position > mSecondExpPos && position <= mSrcPos) { - otop = top - maxBlankHeight; - } - - } else { - // items are expanded on and/or below the source position - - if (position > mSrcPos && position <= mFirstExpPos) { - otop = top + maxBlankHeight; - } else if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) { - int blankHeight = itemHeight - childHeight; - otop = top + blankHeight; - } - } - - // otop is set - if (position <= mSrcPos) { - edge = otop + (mFloatViewHeight - divHeight - getChildHeight(position - 1)) / 2; - } else { - edge = otop + (childHeight - divHeight - mFloatViewHeight) / 2; - } - - return edge; - } - - private boolean updatePositions() { - final int first = getFirstVisiblePosition(); - int startPos = mFirstExpPos; - View startView = getChildAt(startPos - first); - - if (startView == null) { - startPos = first + getChildCount() / 2; - startView = getChildAt(startPos - first); - } - int startTop = startView.getTop(); - - int itemHeight = startView.getHeight(); - - int edge = getShuffleEdge(startPos, startTop); - int lastEdge = edge; - - int divHeight = getDividerHeight(); - - // Log.d("mobeta", "float mid="+mFloatViewMid); - - int itemPos = startPos; - int itemTop = startTop; - if (mFloatViewMid < edge) { - // scanning up for float position - while (itemPos >= 0) { - itemPos--; - itemHeight = getItemHeight(itemPos); - - if (itemPos == 0) { - edge = itemTop - divHeight - itemHeight; - break; - } - - itemTop -= itemHeight + divHeight; - edge = getShuffleEdge(itemPos, itemTop); - - if (mFloatViewMid >= edge) { - break; - } - - lastEdge = edge; - } - } else { - // scanning down for float position - final int count = getCount(); - while (itemPos < count) { - if (itemPos == count - 1) { - edge = itemTop + divHeight + itemHeight; - break; - } - - itemTop += divHeight + itemHeight; - itemHeight = getItemHeight(itemPos + 1); - edge = getShuffleEdge(itemPos + 1, itemTop); - - // test for hit - if (mFloatViewMid < edge) { - break; - } - - lastEdge = edge; - itemPos++; - } - } - - final int numHeaders = getHeaderViewsCount(); - final int numFooters = getFooterViewsCount(); - - boolean updated = false; - - int oldFirstExpPos = mFirstExpPos; - int oldSecondExpPos = mSecondExpPos; - float oldSlideFrac = mSlideFrac; - - if (mAnimate) { - int edgeToEdge = Math.abs(edge - lastEdge); - - int edgeTop, edgeBottom; - if (mFloatViewMid < edge) { - edgeTop = lastEdge; - edgeBottom = edge; - } else { - edgeTop = edge; - edgeBottom = lastEdge; - } - - int slideRgnHeight = (int) (0.5f * mSlideRegionFrac * edgeToEdge); - float slideRgnHeightF = (float) slideRgnHeight; - int slideEdgeTop = edgeTop + slideRgnHeight; - int slideEdgeBottom = edgeBottom - slideRgnHeight; - - // Three regions - if (mFloatViewMid < slideEdgeTop) { - mFirstExpPos = itemPos - 1; - mSecondExpPos = itemPos; - mSlideFrac = 0.5f * ((float) (slideEdgeTop - mFloatViewMid)) / slideRgnHeightF; - } else if (mFloatViewMid < slideEdgeBottom) { - mFirstExpPos = itemPos; - mSecondExpPos = itemPos; - } else { - mFirstExpPos = itemPos; - mSecondExpPos = itemPos + 1; - mSlideFrac = 0.5f * (1.0f + ((float) (edgeBottom - mFloatViewMid)) - / slideRgnHeightF); - } - - } else { - mFirstExpPos = itemPos; - mSecondExpPos = itemPos; - } - - // correct for headers and footers - if (mFirstExpPos < numHeaders) { - itemPos = numHeaders; - mFirstExpPos = itemPos; - mSecondExpPos = itemPos; - } else if (mSecondExpPos >= getCount() - numFooters) { - itemPos = getCount() - numFooters - 1; - mFirstExpPos = itemPos; - mSecondExpPos = itemPos; - } - - if (mFirstExpPos != oldFirstExpPos || mSecondExpPos != oldSecondExpPos - || mSlideFrac != oldSlideFrac) { - updated = true; - } - - if (itemPos != mFloatPos) { - if (mDragListener != null) { - mDragListener.drag(mFloatPos - numHeaders, itemPos - numHeaders); - } - - mFloatPos = itemPos; - updated = true; - } - - return updated; - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - if (mTrackDragSort) { - mDragSortTracker.appendState(); - } - } - - private class SmoothAnimator implements Runnable { - protected long mStartTime; - - private float mDurationF; - - private float mAlpha; - private float mA, mB, mC, mD; - - private boolean mCanceled; - - public SmoothAnimator(float smoothness, int duration) { - mAlpha = smoothness; - mDurationF = (float) duration; - mA = mD = 1f / (2f * mAlpha * (1f - mAlpha)); - mB = mAlpha / (2f * (mAlpha - 1f)); - mC = 1f / (1f - mAlpha); - } - - public float transform(float frac) { - if (frac < mAlpha) { - return mA * frac * frac; - } else if (frac < 1f - mAlpha) { - return mB + mC * frac; - } else { - return 1f - mD * (frac - 1f) * (frac - 1f); - } - } - - public void start() { - mStartTime = SystemClock.uptimeMillis(); - mCanceled = false; - onStart(); - post(this); - } - - public void cancel() { - mCanceled = true; - } - - public void onStart() { - // stub - } - - public void onUpdate(float frac, float smoothFrac) { - // stub - } - - public void onStop() { - // stub - } - - @Override - public void run() { - if (mCanceled) { - return; - } - - float fraction = ((float) (SystemClock.uptimeMillis() - mStartTime)) / mDurationF; - - if (fraction >= 1f) { - onUpdate(1f, 1f); - onStop(); - } else { - onUpdate(fraction, transform(fraction)); - post(this); - } - } - } - - /** - * Centers floating View under touch point. - */ - private class LiftAnimator extends SmoothAnimator { - - private float mInitDragDeltaY; - private float mFinalDragDeltaY; - - public LiftAnimator(float smoothness, int duration) { - super(smoothness, duration); - } - - @Override - public void onStart() { - mInitDragDeltaY = mDragDeltaY; - mFinalDragDeltaY = mFloatViewHeightHalf; - } - - @Override - public void onUpdate(float frac, float smoothFrac) { - if (mDragState != DRAGGING) { - cancel(); - } else { - mDragDeltaY = (int) (smoothFrac * mFinalDragDeltaY + (1f - smoothFrac) - * mInitDragDeltaY); - mFloatLoc.y = mY - mDragDeltaY; - doDragFloatView(true); - } - } - } - - /** - * Centers floating View over drop slot before destroying. - */ - private class DropAnimator extends SmoothAnimator { - - private int mDropPos; - private int srcPos; - private float mInitDeltaY; - private float mInitDeltaX; - - public DropAnimator(float smoothness, int duration) { - super(smoothness, duration); - } - - @Override - public void onStart() { - mDropPos = mFloatPos; - srcPos = mSrcPos; - mDragState = DROPPING; - mInitDeltaY = mFloatLoc.y - getTargetY(); - mInitDeltaX = mFloatLoc.x - getPaddingLeft(); - } - - private int getTargetY() { - final int first = getFirstVisiblePosition(); - final int otherAdjust = (mItemHeightCollapsed + getDividerHeight()) / 2; - View v = getChildAt(mDropPos - first); - int targetY = -1; - if (v != null) { - if (mDropPos == srcPos) { - targetY = v.getTop(); - } else if (mDropPos < srcPos) { - // expanded down - targetY = v.getTop() - otherAdjust; - } else { - // expanded up - targetY = v.getBottom() + otherAdjust - mFloatViewHeight; - } - } else { - // drop position is not on screen?? no animation - cancel(); - } - - return targetY; - } - - @Override - public void onUpdate(float frac, float smoothFrac) { - final int targetY = getTargetY(); - final int targetX = getPaddingLeft(); - final float deltaY = mFloatLoc.y - targetY; - final float deltaX = mFloatLoc.x - targetX; - final float f = 1f - smoothFrac; - if (f < Math.abs(deltaY / mInitDeltaY) || f < Math.abs(deltaX / mInitDeltaX)) { - mFloatLoc.y = targetY + (int) (mInitDeltaY * f); - mFloatLoc.x = getPaddingLeft() + (int) (mInitDeltaX * f); - doDragFloatView(true); - } - } - - @Override - public void onStop() { - dropFloatView(); - } - - } - - /** - * Collapses expanded items. - */ - private class RemoveAnimator extends SmoothAnimator { - - private float mFloatLocX; - private float mFirstStartBlank; - private float mSecondStartBlank; - - private int mFirstChildHeight = -1; - private int mSecondChildHeight = -1; - - private int mFirstPos; - private int mSecondPos; - - public RemoveAnimator(float smoothness, int duration) { - super(smoothness, duration); - } - - @Override - public void onStart() { - mFirstChildHeight = -1; - mSecondChildHeight = -1; - mFirstPos = mFirstExpPos; - mSecondPos = mSecondExpPos; - mDragState = REMOVING; - - mFloatLocX = mFloatLoc.x; - if (mUseRemoveVelocity) { - float minVelocity = 2f * getWidth(); - if (mRemoveVelocityX == 0) { - mRemoveVelocityX = (mFloatLocX < 0 ? -1 : 1) * minVelocity; - } else { - minVelocity *= 2; - if (mRemoveVelocityX < 0 && mRemoveVelocityX > -minVelocity) - mRemoveVelocityX = -minVelocity; - else if (mRemoveVelocityX > 0 && mRemoveVelocityX < minVelocity) - mRemoveVelocityX = minVelocity; - } - } else { - destroyFloatView(); - } - } - - @Override - public void onUpdate(float frac, float smoothFrac) { - float f = 1f - smoothFrac; - - final int firstVis = getFirstVisiblePosition(); - View item = getChildAt(mFirstPos - firstVis); - ViewGroup.LayoutParams lp; - int blank; - - if (mUseRemoveVelocity) { - float dt = (float) (SystemClock.uptimeMillis() - mStartTime) / 1000; - if (dt == 0) - return; - float dx = mRemoveVelocityX * dt; - int w = getWidth(); - mRemoveVelocityX += (mRemoveVelocityX > 0 ? 1 : -1) * dt * w; - mFloatLocX += dx; - mFloatLoc.x = (int) mFloatLocX; - if (mFloatLocX < w && mFloatLocX > -w) { - mStartTime = SystemClock.uptimeMillis(); - doDragFloatView(true); - return; - } - } - - if (item != null) { - if (mFirstChildHeight == -1) { - mFirstChildHeight = getChildHeight(mFirstPos, item, false); - mFirstStartBlank = (float) (item.getHeight() - mFirstChildHeight); - } - blank = Math.max((int) (f * mFirstStartBlank), 1); - lp = item.getLayoutParams(); - lp.height = mFirstChildHeight + blank; - item.setLayoutParams(lp); - } - if (mSecondPos != mFirstPos) { - item = getChildAt(mSecondPos - firstVis); - if (item != null) { - if (mSecondChildHeight == -1) { - mSecondChildHeight = getChildHeight(mSecondPos, item, false); - mSecondStartBlank = (float) (item.getHeight() - mSecondChildHeight); - } - blank = Math.max((int) (f * mSecondStartBlank), 1); - lp = item.getLayoutParams(); - lp.height = mSecondChildHeight + blank; - item.setLayoutParams(lp); - } - } - } - - @Override - public void onStop() { - doRemoveItem(); - } - } - - public void removeItem(int which) { - mUseRemoveVelocity = false; - removeItem(which, 0); - } - - /** - * Removes an item from the list and animates the removal. - * - * @param which Position to remove (NOTE: headers/footers ignored! - * this is a position in your input ListAdapter). - * @param velocityX - */ - public void removeItem(int which, float velocityX) { - if (mDragState == IDLE || mDragState == DRAGGING) { - if (mDragState == IDLE) { - // called from outside drag-sort - mSrcPos = getHeaderViewsCount() + which; - mFirstExpPos = mSrcPos; - mSecondExpPos = mSrcPos; - mFloatPos = mSrcPos; - View v = getChildAt(mSrcPos - getFirstVisiblePosition()); - if (v != null) { - v.setVisibility(View.INVISIBLE); - } - } - - mDragState = REMOVING; - mRemoveVelocityX = velocityX; - - if (mInTouchEvent) { - switch (mCancelMethod) { - case ON_TOUCH_EVENT: - super.onTouchEvent(mCancelEvent); - break; - case ON_INTERCEPT_TOUCH_EVENT: - super.onInterceptTouchEvent(mCancelEvent); - break; - } - } - - if (mRemoveAnimator != null) { - mRemoveAnimator.start(); - } else { - doRemoveItem(which); - } - } - } - - /** - * Move an item, bypassing the drag-sort process. Simply calls - * through to {@link DropListener#drop(int, int)}. - * - * @param from Position to move (NOTE: headers/footers ignored! - * this is a position in your input ListAdapter). - * @param to Target position (NOTE: headers/footers ignored! - * this is a position in your input ListAdapter). - */ - public void moveItem(int from, int to) { - if (mDropListener != null) { - final int count = getInputAdapter().getCount(); - if (from >= 0 && from < count && to >= 0 && to < count) { - mDropListener.drop(from, to); - } - } - } - - /** - * Cancel a drag. Calls {@link #stopDrag(boolean, boolean)} with - * true as the first argument. - */ - public void cancelDrag() { - if (mDragState == DRAGGING) { - mDragScroller.stopScrolling(true); - destroyFloatView(); - clearPositions(); - adjustAllItems(); - - if (mInTouchEvent) { - mDragState = STOPPED; - } else { - mDragState = IDLE; - } - } - } - - private void clearPositions() { - mSrcPos = -1; - mFirstExpPos = -1; - mSecondExpPos = -1; - mFloatPos = -1; - } - - private void dropFloatView() { - // must set to avoid cancelDrag being called from the DataSetObserver - mDragState = DROPPING; - - if (mDropListener != null && mFloatPos >= 0 && mFloatPos < getCount()) { - final int numHeaders = getHeaderViewsCount(); - mDropListener.drop(mSrcPos - numHeaders, mFloatPos - numHeaders); - } - - destroyFloatView(); - - adjustOnReorder(); - clearPositions(); - adjustAllItems(); - - // now the drag is done - if (mInTouchEvent) { - mDragState = STOPPED; - } else { - mDragState = IDLE; - } - } - - private void doRemoveItem() { - doRemoveItem(mSrcPos - getHeaderViewsCount()); - } - - /** - * Removes dragged item from the list. Calls RemoveListener. - */ - private void doRemoveItem(int which) { - // must set to avoid cancelDrag being called from the DataSetObserver - mDragState = REMOVING; - - // end it - if (mRemoveListener != null) { - mRemoveListener.remove(which); - } - - destroyFloatView(); - - adjustOnReorder(); - clearPositions(); - - // now the drag is done - if (mInTouchEvent) { - mDragState = STOPPED; - } else { - mDragState = IDLE; - } - } - - private void adjustOnReorder() { - final int firstPos = getFirstVisiblePosition(); - if (mSrcPos < firstPos) { - // collapsed src item is off screen; - // adjust the scroll after item heights have been fixed - View v = getChildAt(0); - int top = 0; - if (v != null) { - top = v.getTop(); - } - setSelectionFromTop(firstPos - 1, top - getPaddingTop()); - } - } - - /** - * Stop a drag in progress. Pass true if you would - * like to remove the dragged item from the list. - * - * @param remove Remove the dragged item from the list. Calls - * a registered RemoveListener, if one exists. Otherwise, calls - * the DropListener, if one exists. - * - * @return True if the stop was successful. False if there is - * no floating View. - */ - public boolean stopDrag(boolean remove) { - mUseRemoveVelocity = false; - return stopDrag(remove, 0); - } - - public boolean stopDragWithVelocity(boolean remove, float velocityX) { - mUseRemoveVelocity = true; - return stopDrag(remove, velocityX); - } - - public boolean stopDrag(boolean remove, float velocityX) { - if (mFloatView != null) { - mDragScroller.stopScrolling(true); - - if (remove) { - removeItem(mSrcPos - getHeaderViewsCount(), velocityX); - } else { - if (mDropAnimator != null) { - mDropAnimator.start(); - } else { - dropFloatView(); - } - } - - if (mTrackDragSort) { - mDragSortTracker.stopTracking(); - } - - return true; - } else { - // stop failed - return false; - } - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (mIgnoreTouchEvent) { - mIgnoreTouchEvent = false; - return false; - } - - if (!mDragEnabled) { - return super.onTouchEvent(ev); - } - - boolean more = false; - - boolean lastCallWasIntercept = mLastCallWasIntercept; - mLastCallWasIntercept = false; - - if (!lastCallWasIntercept) { - saveTouchCoords(ev); - } - - if (mDragState == DRAGGING) { - onDragTouchEvent(ev); - more = true; // give us more! - } else { - // TODO: what if float view is null because we dropped in middle - // of drag touch event? - - if (mDragState == IDLE) { - if (super.onTouchEvent(ev)) { - more = true; - } - } - - int action = ev.getAction() & MotionEvent.ACTION_MASK; - - switch (action) { - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - doActionUpOrCancel(); - break; - default: - if (more) { - mCancelMethod = ON_TOUCH_EVENT; - } - } - } - - return more; - } - - private void doActionUpOrCancel() { - mCancelMethod = NO_CANCEL; - mInTouchEvent = false; - if (mDragState == STOPPED) { - mDragState = IDLE; - } - mCurrFloatAlpha = mFloatAlpha; - mListViewIntercepted = false; - mChildHeightCache.clear(); - } - - private void saveTouchCoords(MotionEvent ev) { - int action = ev.getAction() & MotionEvent.ACTION_MASK; - if (action != MotionEvent.ACTION_DOWN) { - //mLastX = mX; - mLastY = mY; - } - mX = (int) ev.getX(); - mY = (int) ev.getY(); - if (action == MotionEvent.ACTION_DOWN) { - //mLastX = mX; - mLastY = mY; - } - //mOffsetX = (int) ev.getRawX() - mX; - //mOffsetY = (int) ev.getRawY() - mY; - } - - public boolean listViewIntercepted() { - return mListViewIntercepted; - } - - private boolean mListViewIntercepted = false; - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - if (!mDragEnabled) { - return super.onInterceptTouchEvent(ev); - } - - saveTouchCoords(ev); - mLastCallWasIntercept = true; - - int action = ev.getAction() & MotionEvent.ACTION_MASK; - - if (action == MotionEvent.ACTION_DOWN) { - if (mDragState != IDLE) { - // intercept and ignore - mIgnoreTouchEvent = true; - return true; - } - mInTouchEvent = true; - } - - boolean intercept = false; - - // the following deals with calls to super.onInterceptTouchEvent - if (mFloatView != null) { - // super's touch event cancelled in startDrag - intercept = true; - } else { - if (super.onInterceptTouchEvent(ev)) { - mListViewIntercepted = true; - intercept = true; - } - - switch (action) { - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - doActionUpOrCancel(); - break; - default: - if (intercept) { - mCancelMethod = ON_TOUCH_EVENT; - } else { - mCancelMethod = ON_INTERCEPT_TOUCH_EVENT; - } - } - } - - if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { - mInTouchEvent = false; - } - - return intercept; - } - - /** - * Set the width of each drag scroll region by specifying - * a fraction of the ListView height. - * - * @param heightFraction Fraction of ListView height. Capped at - * 0.5f. - * - */ - public void setDragScrollStart(float heightFraction) { - setDragScrollStarts(heightFraction, heightFraction); - } - - /** - * Set the width of each drag scroll region by specifying - * a fraction of the ListView height. - * - * @param upperFrac Fraction of ListView height for up-scroll bound. - * Capped at 0.5f. - * @param lowerFrac Fraction of ListView height for down-scroll bound. - * Capped at 0.5f. - * - */ - public void setDragScrollStarts(float upperFrac, float lowerFrac) { - if (lowerFrac > 0.5f) { - mDragDownScrollStartFrac = 0.5f; - } else { - mDragDownScrollStartFrac = lowerFrac; - } - - if (upperFrac > 0.5f) { - mDragUpScrollStartFrac = 0.5f; - } else { - mDragUpScrollStartFrac = upperFrac; - } - - if (getHeight() != 0) { - updateScrollStarts(); - } - } - - private void continueDrag(int x, int y) { - // proposed position - mFloatLoc.x = x - mDragDeltaX; - mFloatLoc.y = y - mDragDeltaY; - - doDragFloatView(true); - - int minY = Math.min(y, mFloatViewMid + mFloatViewHeightHalf); - int maxY = Math.max(y, mFloatViewMid - mFloatViewHeightHalf); - - // get the current scroll direction - int currentScrollDir = mDragScroller.getScrollDir(); - - if (minY > mLastY && minY > mDownScrollStartY && currentScrollDir != DragScroller.DOWN) { - // dragged down, it is below the down scroll start and it is not - // scrolling up - - if (currentScrollDir != DragScroller.STOP) { - // moved directly from up scroll to down scroll - mDragScroller.stopScrolling(true); - } - - // start scrolling down - mDragScroller.startScrolling(DragScroller.DOWN); - } else if (maxY < mLastY && maxY < mUpScrollStartY && currentScrollDir != DragScroller.UP) { - // dragged up, it is above the up scroll start and it is not - // scrolling up - - if (currentScrollDir != DragScroller.STOP) { - // moved directly from down scroll to up scroll - mDragScroller.stopScrolling(true); - } - - // start scrolling up - mDragScroller.startScrolling(DragScroller.UP); - } - else if (maxY >= mUpScrollStartY && minY <= mDownScrollStartY - && mDragScroller.isScrolling()) { - // not in the upper nor in the lower drag-scroll regions but it is - // still scrolling - - mDragScroller.stopScrolling(true); - } - } - - private void updateScrollStarts() { - final int padTop = getPaddingTop(); - final int listHeight = getHeight() - padTop - getPaddingBottom(); - float heightF = (float) listHeight; - - mUpScrollStartYF = padTop + mDragUpScrollStartFrac * heightF; - mDownScrollStartYF = padTop + (1.0f - mDragDownScrollStartFrac) * heightF; - - mUpScrollStartY = (int) mUpScrollStartYF; - mDownScrollStartY = (int) mDownScrollStartYF; - - mDragUpScrollHeight = mUpScrollStartYF - padTop; - mDragDownScrollHeight = padTop + listHeight - mDownScrollStartYF; - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - updateScrollStarts(); - } - - private void adjustAllItems() { - final int first = getFirstVisiblePosition(); - final int last = getLastVisiblePosition(); - - int begin = Math.max(0, getHeaderViewsCount() - first); - int end = Math.min(last - first, getCount() - 1 - getFooterViewsCount() - first); - - for (int i = begin; i <= end; ++i) { - View v = getChildAt(i); - if (v != null) { - adjustItem(first + i, v, false); - } - } - } - - /** - * Sets layout param height, gravity, and visibility on - * wrapped item. - */ - private void adjustItem(int position, View v, boolean invalidChildHeight) { - // Adjust item height - ViewGroup.LayoutParams lp = v.getLayoutParams(); - int height; - if (position != mSrcPos && position != mFirstExpPos && position != mSecondExpPos) { - height = ViewGroup.LayoutParams.WRAP_CONTENT; - } else { - height = calcItemHeight(position, v, invalidChildHeight); - } - - if (height != lp.height) { - lp.height = height; - v.setLayoutParams(lp); - } - - // Adjust item gravity - if (position == mFirstExpPos || position == mSecondExpPos) { - if (position < mSrcPos) { - ((DragSortItemView) v).setGravity(Gravity.BOTTOM); - } else if (position > mSrcPos) { - ((DragSortItemView) v).setGravity(Gravity.TOP); - } - } - - // Finally adjust item visibility - int oldVis = v.getVisibility(); - int vis = View.VISIBLE; - - if (position == mSrcPos && mFloatView != null) { - vis = View.INVISIBLE; - } - - if (vis != oldVis) { - v.setVisibility(vis); - } - } - - private int getChildHeight(int position) { - if (position == mSrcPos) { - return 0; - } - - View v = getChildAt(position - getFirstVisiblePosition()); - - if (v != null) { - // item is onscreen, therefore child height is valid, - // hence the "true" - return getChildHeight(position, v, false); - } else { - // item is offscreen - // first check cache for child height at this position - int childHeight = mChildHeightCache.get(position); - if (childHeight != -1) { - return childHeight; - } - - final ListAdapter adapter = getAdapter(); - int type = adapter.getItemViewType(position); - - // There might be a better place for checking for the following - final int typeCount = adapter.getViewTypeCount(); - if (typeCount != mSampleViewTypes.length) { - mSampleViewTypes = new View[typeCount]; - } - - if (type >= 0) { - if (mSampleViewTypes[type] == null) { - v = adapter.getView(position, null, this); - mSampleViewTypes[type] = v; - } else { - v = adapter.getView(position, mSampleViewTypes[type], this); - } - } else { - // type is HEADER_OR_FOOTER or IGNORE - v = adapter.getView(position, null, this); - } - - // current child height is invalid, hence "true" below - childHeight = getChildHeight(position, v, true); - - // cache it because this could have been expensive - mChildHeightCache.add(position, childHeight); - - return childHeight; - } - } - - private int getChildHeight(int position, View item, boolean invalidChildHeight) { - if (position == mSrcPos) { - return 0; - } - - View child; - if (position < getHeaderViewsCount() || position >= getCount() - getFooterViewsCount()) { - child = item; - } else { - child = ((ViewGroup) item).getChildAt(0); - } - - ViewGroup.LayoutParams lp = child.getLayoutParams(); - if (lp != null) { - if (lp.height > 0) { - return lp.height; - } - } - - int childHeight = child.getHeight(); - if (childHeight == 0 || invalidChildHeight) { - measureItem(child); - childHeight = child.getMeasuredHeight(); - } - - return childHeight; - } - - private int calcItemHeight(int position, View item, boolean invalidChildHeight) { - return calcItemHeight(position, getChildHeight(position, item, invalidChildHeight)); - } - - private int calcItemHeight(int position, int childHeight) { - boolean isSliding = mAnimate && mFirstExpPos != mSecondExpPos; - int maxNonSrcBlankHeight = mFloatViewHeight - mItemHeightCollapsed; - int slideHeight = (int) (mSlideFrac * maxNonSrcBlankHeight); - - int height; - - if (position == mSrcPos) { - if (mSrcPos == mFirstExpPos) { - if (isSliding) { - height = slideHeight + mItemHeightCollapsed; - } else { - height = mFloatViewHeight; - } - } else if (mSrcPos == mSecondExpPos) { - // if gets here, we know an item is sliding - height = mFloatViewHeight - slideHeight; - } else { - height = mItemHeightCollapsed; - } - } else if (position == mFirstExpPos) { - if (isSliding) { - height = childHeight + slideHeight; - } else { - height = childHeight + maxNonSrcBlankHeight; - } - } else if (position == mSecondExpPos) { - // we know an item is sliding (b/c 2ndPos != 1stPos) - height = childHeight + maxNonSrcBlankHeight - slideHeight; - } else { - height = childHeight; - } - - return height; - } - - @Override - public void requestLayout() { - if (!mBlockLayoutRequests) { - super.requestLayout(); - } - } - - private int adjustScroll(int movePos, View moveItem, int oldFirstExpPos, int oldSecondExpPos) { - int adjust = 0; - - final int childHeight = getChildHeight(movePos); - - int moveHeightBefore = moveItem.getHeight(); - int moveHeightAfter = calcItemHeight(movePos, childHeight); - - int moveBlankBefore = moveHeightBefore; - int moveBlankAfter = moveHeightAfter; - if (movePos != mSrcPos) { - moveBlankBefore -= childHeight; - moveBlankAfter -= childHeight; - } - - int maxBlank = mFloatViewHeight; - if (mSrcPos != mFirstExpPos && mSrcPos != mSecondExpPos) { - maxBlank -= mItemHeightCollapsed; - } - - if (movePos <= oldFirstExpPos) { - if (movePos > mFirstExpPos) { - adjust += maxBlank - moveBlankAfter; - } - } else if (movePos == oldSecondExpPos) { - if (movePos <= mFirstExpPos) { - adjust += moveBlankBefore - maxBlank; - } else if (movePos == mSecondExpPos) { - adjust += moveHeightBefore - moveHeightAfter; - } else { - adjust += moveBlankBefore; - } - } else { - if (movePos <= mFirstExpPos) { - adjust -= maxBlank; - } else if (movePos == mSecondExpPos) { - adjust -= moveBlankAfter; - } - } - - return adjust; - } - - private void measureItem(View item) { - ViewGroup.LayoutParams lp = item.getLayoutParams(); - if (lp == null) { - lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT); - item.setLayoutParams(lp); - } - - int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft() - + getListPaddingRight(), lp.width); - int hspec; - if (lp.height > 0) { - hspec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); - } else { - hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - } - item.measure(wspec, hspec); - } - - private void measureFloatView() { - if (mFloatView != null) { - measureItem(mFloatView); - mFloatViewHeight = mFloatView.getMeasuredHeight(); - mFloatViewHeightHalf = mFloatViewHeight / 2; - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (mFloatView != null) { - if (mFloatView.isLayoutRequested()) { - measureFloatView(); - } - mFloatViewOnMeasured = true; // set to false after layout - } - mWidthMeasureSpec = widthMeasureSpec; - } - - @Override - protected void layoutChildren() { - super.layoutChildren(); - - if (mFloatView != null) { - if (mFloatView.isLayoutRequested() && !mFloatViewOnMeasured) { - // Have to measure here when usual android measure - // pass is skipped. This happens during a drag-sort - // when layoutChildren is called directly. - measureFloatView(); - } - mFloatView.layout(0, 0, mFloatView.getMeasuredWidth(), mFloatView.getMeasuredHeight()); - mFloatViewOnMeasured = false; - } - } - - protected boolean onDragTouchEvent(MotionEvent ev) { - int action = ev.getAction() & MotionEvent.ACTION_MASK; - switch (action) { - case MotionEvent.ACTION_CANCEL: - if (mDragState == DRAGGING) { - cancelDrag(); - } - doActionUpOrCancel(); - break; - case MotionEvent.ACTION_UP: - if (mDragState == DRAGGING) { - stopDrag(false); - } - doActionUpOrCancel(); - break; - case MotionEvent.ACTION_MOVE: - continueDrag((int) ev.getX(), (int) ev.getY()); - break; - } - - return true; - } - - /** - * Start a drag of item at position using the - * registered FloatViewManager. Calls through - * to {@link #startDrag(int,View,int,int,int)} after obtaining - * the floating View from the FloatViewManager. - * - * @param position Item to drag. - * @param dragFlags Flags that restrict some movements of the - * floating View. For example, set dragFlags |= - * ~{@link #DRAG_NEG_X} to allow dragging the floating - * View in all directions except off the screen to the left. - * @param deltaX Offset in x of the touch coordinate from the - * left edge of the floating View (i.e. touch-x minus float View - * left). - * @param deltaY Offset in y of the touch coordinate from the - * top edge of the floating View (i.e. touch-y minus float View - * top). - * - * @return True if the drag was started, false otherwise. This - * startDrag will fail if we are not currently in - * a touch event, there is no registered FloatViewManager, - * or the FloatViewManager returns a null View. - */ - public boolean startDrag(int position, int dragFlags, int deltaX, int deltaY) { - if (!mInTouchEvent || mFloatViewManager == null) { - return false; - } - - View v = mFloatViewManager.onCreateFloatView(position); - - if (v == null) { - return false; - } else { - return startDrag(position, v, dragFlags, deltaX, deltaY); - } - - } - - /** - * Start a drag of item at position without using - * a FloatViewManager. - * - * @param position Item to drag. - * @param floatView Floating View. - * @param dragFlags Flags that restrict some movements of the - * floating View. For example, set dragFlags |= - * ~{@link #DRAG_NEG_X} to allow dragging the floating - * View in all directions except off the screen to the left. - * @param deltaX Offset in x of the touch coordinate from the - * left edge of the floating View (i.e. touch-x minus float View - * left). - * @param deltaY Offset in y of the touch coordinate from the - * top edge of the floating View (i.e. touch-y minus float View - * top). - * - * @return True if the drag was started, false otherwise. This - * startDrag will fail if we are not currently in - * a touch event, floatView is null, or there is - * a drag in progress. - */ - public boolean startDrag(int position, View floatView, int dragFlags, int deltaX, int deltaY) { - if (mDragState != IDLE || !mInTouchEvent || mFloatView != null || floatView == null - || !mDragEnabled) { - return false; - } - - if (getParent() != null) { - getParent().requestDisallowInterceptTouchEvent(true); - } - - int pos = position + getHeaderViewsCount(); - mFirstExpPos = pos; - mSecondExpPos = pos; - mSrcPos = pos; - mFloatPos = pos; - - mDragState = DRAGGING; - mDragFlags = 0; - mDragFlags |= dragFlags; - - mFloatView = floatView; - measureFloatView(); // sets mFloatViewHeight - - mDragDeltaX = deltaX; - mDragDeltaY = deltaY; - - mFloatLoc.x = mX - mDragDeltaX; - mFloatLoc.y = mY - mDragDeltaY; - - // set src item invisible - final View srcItem = getChildAt(mSrcPos - getFirstVisiblePosition()); - - if (srcItem != null) { - srcItem.setVisibility(View.INVISIBLE); - } - - if (mTrackDragSort) { - mDragSortTracker.startTracking(); - } - - // once float view is created, events are no longer passed - // to ListView - switch (mCancelMethod) { - case ON_TOUCH_EVENT: - super.onTouchEvent(mCancelEvent); - break; - case ON_INTERCEPT_TOUCH_EVENT: - super.onInterceptTouchEvent(mCancelEvent); - break; - } - - requestLayout(); - - if (mLiftAnimator != null) { - mLiftAnimator.start(); - } - - return true; - } - - private void doDragFloatView(boolean forceInvalidate) { - int movePos = getFirstVisiblePosition() + getChildCount() / 2; - View moveItem = getChildAt(getChildCount() / 2); - - if (moveItem == null) { - return; - } - - doDragFloatView(movePos, moveItem, forceInvalidate); - } - - private void doDragFloatView(int movePos, View moveItem, boolean forceInvalidate) { - mBlockLayoutRequests = true; - - updateFloatView(); - - int oldFirstExpPos = mFirstExpPos; - int oldSecondExpPos = mSecondExpPos; - - boolean updated = updatePositions(); - - if (updated) { - adjustAllItems(); - int scroll = adjustScroll(movePos, moveItem, oldFirstExpPos, oldSecondExpPos); - - setSelectionFromTop(movePos, moveItem.getTop() + scroll - getPaddingTop()); - layoutChildren(); - } - - if (updated || forceInvalidate) { - invalidate(); - } - - mBlockLayoutRequests = false; - } - - /** - * Sets float View location based on suggested values and - * constraints set in mDragFlags. - */ - private void updateFloatView() { - - if (mFloatViewManager != null) { - mTouchLoc.set(mX, mY); - mFloatViewManager.onDragFloatView(mFloatView, mFloatLoc, mTouchLoc); - } - - final int floatX = mFloatLoc.x; - final int floatY = mFloatLoc.y; - - // restrict x motion - int padLeft = getPaddingLeft(); - if ((mDragFlags & DRAG_POS_X) == 0 && floatX > padLeft) { - mFloatLoc.x = padLeft; - } else if ((mDragFlags & DRAG_NEG_X) == 0 && floatX < padLeft) { - mFloatLoc.x = padLeft; - } - - // keep floating view from going past bottom of last header view - final int numHeaders = getHeaderViewsCount(); - final int numFooters = getFooterViewsCount(); - final int firstPos = getFirstVisiblePosition(); - final int lastPos = getLastVisiblePosition(); - - int topLimit = getPaddingTop(); - if (firstPos < numHeaders) { - topLimit = getChildAt(numHeaders - firstPos - 1).getBottom(); - } - if ((mDragFlags & DRAG_NEG_Y) == 0) { - if (firstPos <= mSrcPos) { - topLimit = Math.max(getChildAt(mSrcPos - firstPos).getTop(), topLimit); - } - } - // bottom limit is top of first footer View or - // bottom of last item in list - int bottomLimit = getHeight() - getPaddingBottom(); - if (lastPos >= getCount() - numFooters - 1) { - bottomLimit = getChildAt(getCount() - numFooters - 1 - firstPos).getBottom(); - } - if ((mDragFlags & DRAG_POS_Y) == 0) { - if (lastPos >= mSrcPos) { - bottomLimit = Math.min(getChildAt(mSrcPos - firstPos).getBottom(), bottomLimit); - } - } - - if (floatY < topLimit) { - mFloatLoc.y = topLimit; - } else if (floatY + mFloatViewHeight > bottomLimit) { - mFloatLoc.y = bottomLimit - mFloatViewHeight; - } - - // get y-midpoint of floating view (constrained to ListView bounds) - mFloatViewMid = mFloatLoc.y + mFloatViewHeightHalf; - } - - private void destroyFloatView() { - if (mFloatView != null) { - mFloatView.setVisibility(GONE); - if (mFloatViewManager != null) { - mFloatViewManager.onDestroyFloatView(mFloatView); - } - mFloatView = null; - invalidate(); - } - } - - /** - * Interface for customization of the floating View appearance - * and dragging behavior. Implement - * your own and pass it to {@link #setFloatViewManager}. If - * your own is not passed, the default {@link SimpleFloatViewManager} - * implementation is used. - */ - public interface FloatViewManager { - /** - * Return the floating View for item at position. - * DragSortListView will measure and layout this View for you, - * so feel free to just inflate it. You can help DSLV by - * setting some {@link ViewGroup.LayoutParams} on this View; - * otherwise it will set some for you (with a width of FILL_PARENT - * and a height of WRAP_CONTENT). - * - * @param position Position of item to drag (NOTE: - * position excludes header Views; thus, if you - * want to call {@link ListView#getChildAt(int)}, you will need - * to add {@link ListView#getHeaderViewsCount()} to the index). - * - * @return The View you wish to display as the floating View. - */ - public View onCreateFloatView(int position); - - /** - * Called whenever the floating View is dragged. Float View - * properties can be changed here. Also, the upcoming location - * of the float View can be altered by setting - * location.x and location.y. - * - * @param floatView The floating View. - * @param location The location (top-left; relative to DSLV - * top-left) at which the float - * View would like to appear, given the current touch location - * and the offset provided in {@link DragSortListView#startDrag}. - * @param touch The current touch location (relative to DSLV - * top-left). - * @param pendingScroll - */ - public void onDragFloatView(View floatView, Point location, Point touch); - - /** - * Called when the float View is dropped; lets you perform - * any necessary cleanup. The internal DSLV floating View - * reference is set to null immediately after this is called. - * - * @param floatView The floating View passed to - * {@link #onCreateFloatView(int)}. - */ - public void onDestroyFloatView(View floatView); - } - - public void setFloatViewManager(FloatViewManager manager) { - mFloatViewManager = manager; - } - - public void setDragListener(DragListener l) { - mDragListener = l; - } - - /** - * Allows for easy toggling between a DragSortListView - * and a regular old ListView. If enabled, items are - * draggable, where the drag init mode determines how - * items are lifted (see {@link setDragInitMode(int)}). - * If disabled, items cannot be dragged. - * - * @param enabled Set true to enable list - * item dragging - */ - public void setDragEnabled(boolean enabled) { - mDragEnabled = enabled; - } - - public boolean isDragEnabled() { - return mDragEnabled; - } - - /** - * This better reorder your ListAdapter! DragSortListView does not do this - * for you; doesn't make sense to. Make sure - * {@link BaseAdapter#notifyDataSetChanged()} or something like it is called - * in your implementation. Furthermore, if you have a choiceMode other than - * none and the ListAdapter does not return true for - * {@link ListAdapter#hasStableIds()}, you will need to call - * {@link #moveCheckState(int, int)} to move the check boxes along with the - * list items. - * - * @param l - */ - public void setDropListener(DropListener l) { - mDropListener = l; - } - - /** - * Probably a no-brainer, but make sure that your remove listener - * calls {@link BaseAdapter#notifyDataSetChanged()} or something like it. - * When an item removal occurs, DragSortListView - * relies on a redraw of all the items to recover invisible views - * and such. Strictly speaking, if you remove something, your dataset - * has changed... - * - * @param l - */ - public void setRemoveListener(RemoveListener l) { - mRemoveListener = l; - } - - public interface DragListener { - public void drag(int from, int to); - } - - /** - * Your implementation of this has to reorder your ListAdapter! - * Make sure to call - * {@link BaseAdapter#notifyDataSetChanged()} or something like it - * in your implementation. - * - * @author heycosmo - * - */ - public interface DropListener { - public void drop(int from, int to); - } - - /** - * Make sure to call - * {@link BaseAdapter#notifyDataSetChanged()} or something like it - * in your implementation. - * - * @author heycosmo - * - */ - public interface RemoveListener { - public void remove(int which); - } - - public interface DragSortListener extends DropListener, DragListener, RemoveListener { - } - - public void setDragSortListener(DragSortListener listener) { - setDropListener(listener); - setDragListener(listener); - setRemoveListener(listener); - } - - /** - * Completely custom scroll speed profile. Default increases linearly - * with position and is constant in time. Create your own by implementing - * {@link DragSortListView.DragScrollProfile}. - * - * @param scrollProfile - */ - public void setDragScrollProfile(DragScrollProfile scrollProfile) { - if (scrollProfile != null) { - mScrollProfile = scrollProfile; - } - } - - /** - * Use this to move the check state of an item from one position to another - * in a drop operation. If you have a choiceMode which is not none, this - * method must be called when the order of items changes in an underlying - * adapter which does not have stable IDs (see - * {@link ListAdapter#hasStableIds()}). This is because without IDs, the - * ListView has no way of knowing which items have moved where, and cannot - * update the check state accordingly. - *

- * A word of warning about a "feature" in Android that you may run into when - * dealing with movable list items: for an adapter that does have - * stable IDs, ListView will attempt to locate each item based on its ID and - * move the check state from the item's old position to the new position — - * which is all fine and good (and removes the need for calling this - * function), except for the half-baked approach. Apparently to save time in - * the naive algorithm used, ListView will only search for an ID in the - * close neighborhood of the old position. If the user moves an item too far - * (specifically, more than 20 rows away), ListView will give up and just - * force the item to be unchecked. So if there is a reasonable chance that - * the user will move items more than 20 rows away from the original - * position, you may wish to use an adapter with unstable IDs and call this - * method manually instead. - * - * @param from - * @param to - */ - public void moveCheckState(int from, int to) { - // This method runs in O(n log n) time (n being the number of list - // items). The bottleneck is the call to AbsListView.setItemChecked, - // which is O(log n) because of the binary search involved in calling - // SparseBooleanArray.put(). - // - // To improve on the average time, we minimize the number of calls to - // setItemChecked by only calling it for items that actually have a - // changed state. This is achieved by building a list containing the - // start and end of the "runs" of checked items, and then moving the - // runs. Note that moving an item from A to B is essentially a rotation - // of the range of items in [A, B]. Let's say we have - // . . U V X Y Z . . - // and move U after Z. This is equivalent to a rotation one step to the - // left within the range you are moving across: - // . . V X Y Z U . . - // - // So, to perform the move we enumerate all the runs within the move - // range, then rotate each run one step to the left or right (depending - // on move direction). For example, in the list: - // X X . X X X . X - // we have two runs. One begins at the last item of the list and wraps - // around to the beginning, ending at position 1. The second begins at - // position 3 and ends at position 5. To rotate a run, regardless of - // length, we only need to set a check mark at one end of the run, and - // clear a check mark at the other end: - // X . X X X . X X - SparseBooleanArray cip = getCheckedItemPositions(); - int rangeStart = from; - int rangeEnd = to; - if (to < from) { - rangeStart = to; - rangeEnd = from; - } - rangeEnd += 1; - - int[] runStart = new int[cip.size()]; - int[] runEnd = new int[cip.size()]; - int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd); - if (runCount == 1 && (runStart[0] == runEnd[0])) { - // Special case where all items are checked, we can never set any - // item to false like we do below. - return; - } - - if (from < to) { - for (int i = 0; i != runCount; i++) { - setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd), true); - setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd), false); - } - - } else { - for (int i = 0; i != runCount; i++) { - setItemChecked(runStart[i], false); - setItemChecked(runEnd[i], true); - } - } - } - - /** - * Use this when an item has been deleted, to move the check state of all - * following items up one step. If you have a choiceMode which is not none, - * this method must be called when the order of items changes in an - * underlying adapter which does not have stable IDs (see - * {@link ListAdapter#hasStableIds()}). This is because without IDs, the - * ListView has no way of knowing which items have moved where, and cannot - * update the check state accordingly. - * - * See also further comments on {@link #moveCheckState(int, int)}. - * - * @param position - */ - public void removeCheckState(int position) { - SparseBooleanArray cip = getCheckedItemPositions(); - - if (cip.size() == 0) - return; - int[] runStart = new int[cip.size()]; - int[] runEnd = new int[cip.size()]; - int rangeStart = position; - int rangeEnd = cip.keyAt(cip.size() - 1) + 1; - int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd); - for (int i = 0; i != runCount; i++) { - if (!(runStart[i] == position || (runEnd[i] < runStart[i] && runEnd[i] > position))) { - // Only set a new check mark in front of this run if it does - // not contain the deleted position. If it does, we only need - // to make it one check mark shorter at the end. - setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd), true); - } - setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd), false); - } - } - - private static int buildRunList(SparseBooleanArray cip, int rangeStart, int rangeEnd, - int[] runStart, int[] runEnd) { - - int runCount = 0; - - int i = findFirstSetIndex(cip, rangeStart, rangeEnd); - if (i == -1) - return 0; - - int position = cip.keyAt(i); - int currentRunStart = position; - int currentRunEnd = currentRunStart + 1; - for (i++; i < cip.size() && (position = cip.keyAt(i)) < rangeEnd; i++) { - if (!cip.valueAt(i)) // not checked => not interesting - continue; - if (position == currentRunEnd) { - currentRunEnd++; - } else { - runStart[runCount] = currentRunStart; - runEnd[runCount] = currentRunEnd; - runCount++; - currentRunStart = position; - currentRunEnd = position + 1; - } - } - - if (currentRunEnd == rangeEnd) { - // rangeStart and rangeEnd are equivalent positions so to be - // consistent we translate them to the same integer value. That way - // we can check whether a run covers the entire range by just - // checking if the start equals the end position. - currentRunEnd = rangeStart; - } - runStart[runCount] = currentRunStart; - runEnd[runCount] = currentRunEnd; - runCount++; - - if (runCount > 1) { - if (runStart[0] == rangeStart && runEnd[runCount - 1] == rangeStart) { - // The last run ends at the end of the range, and the first run - // starts at the beginning of the range. So they are actually - // part of the same run, except they wrap around the end of the - // range. To avoid adjacent runs, we need to merge them. - runStart[0] = runStart[runCount - 1]; - runCount--; - } - } - return runCount; - } - - private static int rotate(int value, int offset, int lowerBound, int upperBound) { - int windowSize = upperBound - lowerBound; - - value += offset; - if (value < lowerBound) { - value += windowSize; - } else if (value >= upperBound) { - value -= windowSize; - } - return value; - } - - private static int findFirstSetIndex(SparseBooleanArray sba, int rangeStart, int rangeEnd) { - int size = sba.size(); - int i = insertionIndexForKey(sba, rangeStart); - while (i < size && sba.keyAt(i) < rangeEnd && !sba.valueAt(i)) - i++; - if (i == size || sba.keyAt(i) >= rangeEnd) - return -1; - return i; - } - - private static int insertionIndexForKey(SparseBooleanArray sba, int key) { - int low = 0; - int high = sba.size(); - while (high - low > 0) { - int middle = (low + high) >> 1; - if (sba.keyAt(middle) < key) - low = middle + 1; - else - high = middle; - } - return low; - } - - /** - * Interface for controlling - * scroll speed as a function of touch position and time. Use - * {@link DragSortListView#setDragScrollProfile(DragScrollProfile)} to - * set custom profile. - * - * @author heycosmo - * - */ - public interface DragScrollProfile { - /** - * Return a scroll speed in pixels/millisecond. Always return a - * positive number. - * - * @param w Normalized position in scroll region (i.e. w \in [0,1]). - * Small w typically means slow scrolling. - * @param t Time (in milliseconds) since start of scroll (handy if you - * want scroll acceleration). - * @return Scroll speed at position w and time t in pixels/ms. - */ - float getSpeed(float w, long t); - } - - private class DragScroller implements Runnable { - - private boolean mAbort; - - private long mPrevTime; - private long mCurrTime; - - private int dy; - private float dt; - private long tStart; - private int scrollDir; - - public final static int STOP = -1; - public final static int UP = 0; - public final static int DOWN = 1; - - private float mScrollSpeed; // pixels per ms - - private boolean mScrolling = false; - - public boolean isScrolling() { - return mScrolling; - } - - public int getScrollDir() { - return mScrolling ? scrollDir : STOP; - } - - public DragScroller() { - } - - public void startScrolling(int dir) { - if (!mScrolling) { - mAbort = false; - mScrolling = true; - tStart = SystemClock.uptimeMillis(); - mPrevTime = tStart; - scrollDir = dir; - post(this); - } - } - - public void stopScrolling(boolean now) { - if (now) { - DragSortListView.this.removeCallbacks(this); - mScrolling = false; - } else { - mAbort = true; - } - } - - @Override - public void run() { - if (mAbort) { - mScrolling = false; - return; - } - - final int first = getFirstVisiblePosition(); - final int last = getLastVisiblePosition(); - final int count = getCount(); - final int padTop = getPaddingTop(); - final int listHeight = getHeight() - padTop - getPaddingBottom(); - - int minY = Math.min(mY, mFloatViewMid + mFloatViewHeightHalf); - int maxY = Math.max(mY, mFloatViewMid - mFloatViewHeightHalf); - - if (scrollDir == UP) { - View v = getChildAt(0); - if (v == null) { - mScrolling = false; - return; - } else { - if (first == 0 && v.getTop() == padTop) { - mScrolling = false; - return; - } - } - mScrollSpeed = mScrollProfile.getSpeed((mUpScrollStartYF - maxY) - / mDragUpScrollHeight, mPrevTime); - } else { - View v = getChildAt(last - first); - if (v == null) { - mScrolling = false; - return; - } else { - if (last == count - 1 && v.getBottom() <= listHeight + padTop) { - mScrolling = false; - return; - } - } - mScrollSpeed = -mScrollProfile.getSpeed((minY - mDownScrollStartYF) - / mDragDownScrollHeight, mPrevTime); - } - - mCurrTime = SystemClock.uptimeMillis(); - dt = (float) (mCurrTime - mPrevTime); - - // dy is change in View position of a list item; i.e. positive dy - // means user is scrolling up (list item moves down the screen, - // remember - // y=0 is at top of View). - dy = (int) Math.round(mScrollSpeed * dt); - - int movePos; - if (dy >= 0) { - dy = Math.min(listHeight, dy); - movePos = first; - } else { - dy = Math.max(-listHeight, dy); - movePos = last; - } - - final View moveItem = getChildAt(movePos - first); - int top = moveItem.getTop() + dy; - - if (movePos == 0 && top > padTop) { - top = padTop; - } - - // always do scroll - mBlockLayoutRequests = true; - - setSelectionFromTop(movePos, top - padTop); - DragSortListView.this.layoutChildren(); - invalidate(); - - mBlockLayoutRequests = false; - - // scroll means relative float View movement - doDragFloatView(movePos, moveItem, false); - - mPrevTime = mCurrTime; - - post(this); - } - } - - // TODO: Bluuurgh... switch to SharedPreferences - private class DragSortTracker { - StringBuilder mBuilder = new StringBuilder(); - - File mFile; - - private int mNumInBuffer = 0; - private int mNumFlushes = 0; - - private boolean mTracking = false; - - public DragSortTracker() { - File root = Environment.getExternalStorageDirectory(); - mFile = new File(root, "dslv_state.txt"); - - if (!mFile.exists()) { - try { - mFile.createNewFile(); - Log.d("mobeta", "file created"); - } catch (IOException e) { - Log.w("mobeta", "Could not create dslv_state.txt"); - Log.d("mobeta", e.getMessage()); - } - } - - } - - public void startTracking() { - mBuilder.append("\n"); - mNumFlushes = 0; - mTracking = true; - } - - public void appendState() { - if (!mTracking) { - return; - } - - mBuilder.append("\n"); - final int children = getChildCount(); - final int first = getFirstVisiblePosition(); - mBuilder.append(" "); - for (int i = 0; i < children; ++i) { - mBuilder.append(first + i).append(","); - } - mBuilder.append("\n"); - - mBuilder.append(" "); - for (int i = 0; i < children; ++i) { - mBuilder.append(getChildAt(i).getTop()).append(","); - } - mBuilder.append("\n"); - mBuilder.append(" "); - for (int i = 0; i < children; ++i) { - mBuilder.append(getChildAt(i).getBottom()).append(","); - } - mBuilder.append("\n"); - - mBuilder.append(" ").append(mFirstExpPos).append("\n"); - mBuilder.append(" ") - .append(getItemHeight(mFirstExpPos) - getChildHeight(mFirstExpPos)) - .append("\n"); - mBuilder.append(" ").append(mSecondExpPos).append("\n"); - mBuilder.append(" ") - .append(getItemHeight(mSecondExpPos) - getChildHeight(mSecondExpPos)) - .append("\n"); - mBuilder.append(" ").append(mSrcPos).append("\n"); - mBuilder.append(" ").append(mFloatViewHeight + getDividerHeight()) - .append("\n"); - mBuilder.append(" ").append(getHeight()).append("\n"); - mBuilder.append(" ").append(mLastY).append("\n"); - mBuilder.append(" ").append(mFloatViewMid).append("\n"); - mBuilder.append(" "); - for (int i = 0; i < children; ++i) { - mBuilder.append(getShuffleEdge(first + i, getChildAt(i).getTop())).append(","); - } - mBuilder.append("\n"); - - mBuilder.append("\n"); - mNumInBuffer++; - - if (mNumInBuffer > 1000) { - flush(); - mNumInBuffer = 0; - } - } - - public void flush() { - if (!mTracking) { - return; - } - - // save to file on sdcard - try { - boolean append = true; - if (mNumFlushes == 0) { - append = false; - } - FileWriter writer = new FileWriter(mFile, append); - - writer.write(mBuilder.toString()); - mBuilder.delete(0, mBuilder.length()); - - writer.flush(); - writer.close(); - - mNumFlushes++; - } catch (IOException e) { - // do nothing - } - } - - public void stopTracking() { - if (mTracking) { - mBuilder.append("\n"); - flush(); - mTracking = false; - } - } - - } - -} +/* + * DragSortListView. + * + * A subclass of the Android ListView component that enables drag + * and drop re-ordering of list items. + * + * Copyright 2012 Carl Bauer + * + * 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.mobeta.android.dslv; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import android.content.Context; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Point; +import android.graphics.drawable.Drawable; +import android.os.Environment; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseBooleanArray; +import android.util.SparseIntArray; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.BaseAdapter; +import android.widget.Checkable; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.WrapperListAdapter; + +/** + * ListView subclass that mediates drag and drop resorting of items. + * + * + * @author heycosmo + * + */ +public class DragSortListView extends ListView { + + + /** + * The View that floats above the ListView and represents + * the dragged item. + */ + private View mFloatView; + + /** + * The float View location. First based on touch location + * and given deltaX and deltaY. Then restricted by callback + * to FloatViewManager.onDragFloatView(). Finally restricted + * by bounds of DSLV. + */ + private Point mFloatLoc = new Point(); + + private Point mTouchLoc = new Point(); + + /** + * The middle (in the y-direction) of the floating View. + */ + private int mFloatViewMid; + + /** + * Flag to make sure float View isn't measured twice + */ + private boolean mFloatViewOnMeasured = false; + + /** + * Watch the Adapter for data changes. Cancel a drag if + * coincident with a change. + */ + private DataSetObserver mObserver; + + /** + * Transparency for the floating View (XML attribute). + */ + private float mFloatAlpha = 1.0f; + private float mCurrFloatAlpha = 1.0f; + + /** + * While drag-sorting, the current position of the floating + * View. If dropped, the dragged item will land in this position. + */ + private int mFloatPos; + + /** + * The first expanded ListView position that helps represent + * the drop slot tracking the floating View. + */ + private int mFirstExpPos; + + /** + * The second expanded ListView position that helps represent + * the drop slot tracking the floating View. This can equal + * mFirstExpPos if there is no slide shuffle occurring; otherwise + * it is equal to mFirstExpPos + 1. + */ + private int mSecondExpPos; + + /** + * Flag set if slide shuffling is enabled. + */ + private boolean mAnimate = false; + + /** + * The user dragged from this position. + */ + private int mSrcPos; + + /** + * Offset (in x) within the dragged item at which the user + * picked it up (or first touched down with the digitalis). + */ + private int mDragDeltaX; + + /** + * Offset (in y) within the dragged item at which the user + * picked it up (or first touched down with the digitalis). + */ + private int mDragDeltaY; + + + /** + * The difference (in x) between screen coordinates and coordinates + * in this view. + */ + //private int mOffsetX; + + /** + * The difference (in y) between screen coordinates and coordinates + * in this view. + */ + //private int mOffsetY; + + /** + * A listener that receives callbacks whenever the floating View + * hovers over a new position. + */ + private DragListener mDragListener; + + /** + * A listener that receives a callback when the floating View + * is dropped. + */ + private DropListener mDropListener; + + /** + * A listener that receives a callback when the floating View + * (or more precisely the originally dragged item) is removed + * by one of the provided gestures. + */ + private RemoveListener mRemoveListener; + + /** + * Enable/Disable item dragging + * + * @attr name dslv:drag_enabled + */ + private boolean mDragEnabled = true; + + /** + * Drag state enum. + */ + private final static int IDLE = 0; + private final static int REMOVING = 1; + private final static int DROPPING = 2; + private final static int STOPPED = 3; + private final static int DRAGGING = 4; + + private int mDragState = IDLE; + + /** + * Height in pixels to which the originally dragged item + * is collapsed during a drag-sort. Currently, this value + * must be greater than zero. + */ + private int mItemHeightCollapsed = 1; + + /** + * Height of the floating View. Stored for the purpose of + * providing the tracking drop slot. + */ + private int mFloatViewHeight; + + /** + * Convenience member. See above. + */ + private int mFloatViewHeightHalf; + + /** + * Save the given width spec for use in measuring children + */ + private int mWidthMeasureSpec = 0; + + /** + * Sample Views ultimately used for calculating the height + * of ListView items that are off-screen. + */ + private View[] mSampleViewTypes = new View[1]; + + /** + * Drag-scroll encapsulator! + */ + private DragScroller mDragScroller; + + /** + * Determines the start of the upward drag-scroll region + * at the top of the ListView. Specified by a fraction + * of the ListView height, thus screen resolution agnostic. + */ + private float mDragUpScrollStartFrac = 1.0f / 3.0f; + + /** + * Determines the start of the downward drag-scroll region + * at the bottom of the ListView. Specified by a fraction + * of the ListView height, thus screen resolution agnostic. + */ + private float mDragDownScrollStartFrac = 1.0f / 3.0f; + + /** + * The following are calculated from the above fracs. + */ + private int mUpScrollStartY; + private int mDownScrollStartY; + private float mDownScrollStartYF; + private float mUpScrollStartYF; + + /** + * Calculated from above above and current ListView height. + */ + private float mDragUpScrollHeight; + + /** + * Calculated from above above and current ListView height. + */ + private float mDragDownScrollHeight; + + /** + * Maximum drag-scroll speed in pixels per ms. Only used with + * default linear drag-scroll profile. + */ + private float mMaxScrollSpeed = 0.5f; + + /** + * Defines the scroll speed during a drag-scroll. User can + * provide their own; this default is a simple linear profile + * where scroll speed increases linearly as the floating View + * nears the top/bottom of the ListView. + */ + private DragScrollProfile mScrollProfile = new DragScrollProfile() { + @Override + public float getSpeed(float w, long t) { + return mMaxScrollSpeed * w; + } + }; + + /** + * Current touch x. + */ + private int mX; + + /** + * Current touch y. + */ + private int mY; + + /** + * Last touch x. + */ + //private int mLastX; + + /** + * Last touch y. + */ + private int mLastY; + + /** + * The touch y-coord at which drag started + */ + //private int mDragStartY; + + /** + * Drag flag bit. Floating View can move in the positive + * x direction. + */ + public final static int DRAG_POS_X = 0x1; + + /** + * Drag flag bit. Floating View can move in the negative + * x direction. + */ + public final static int DRAG_NEG_X = 0x2; + + /** + * Drag flag bit. Floating View can move in the positive + * y direction. This is subtle. What this actually means is + * that, if enabled, the floating View can be dragged below its starting + * position. Remove in favor of upper-bounding item position? + */ + public final static int DRAG_POS_Y = 0x4; + + /** + * Drag flag bit. Floating View can move in the negative + * y direction. This is subtle. What this actually means is + * that the floating View can be dragged above its starting + * position. Remove in favor of lower-bounding item position? + */ + public final static int DRAG_NEG_Y = 0x8; + + /** + * Flags that determine limits on the motion of the + * floating View. See flags above. + */ + private int mDragFlags = 0; + + /** + * Last call to an on*TouchEvent was a call to + * onInterceptTouchEvent. + */ + private boolean mLastCallWasIntercept = false; + + /** + * A touch event is in progress. + */ + private boolean mInTouchEvent = false; + + /** + * Let the user customize the floating View. + */ + private FloatViewManager mFloatViewManager = null; + + /** + * Given to ListView to cancel its action when a drag-sort + * begins. + */ + private MotionEvent mCancelEvent; + + /** + * Enum telling where to cancel the ListView action when a + * drag-sort begins + */ + private static final int NO_CANCEL = 0; + private static final int ON_TOUCH_EVENT = 1; + private static final int ON_INTERCEPT_TOUCH_EVENT = 2; + + /** + * Where to cancel the ListView action when a + * drag-sort begins + */ + private int mCancelMethod = NO_CANCEL; + + /** + * Determines when a slide shuffle animation starts. That is, + * defines how close to the edge of the drop slot the floating + * View must be to initiate the slide. + */ + private float mSlideRegionFrac = 0.25f; + + /** + * Number between 0 and 1 indicating the relative location of + * a sliding item (only used if drag-sort animations + * are turned on). Nearly 1 means the item is + * at the top of the slide region (nearly full blank item + * is directly below). + */ + private float mSlideFrac = 0.0f; + + /** + * Wraps the user-provided ListAdapter. This is used to wrap each + * item View given by the user inside another View (currenly + * a RelativeLayout) which + * expands and collapses to simulate the item shuffling. + */ + private AdapterWrapper mAdapterWrapper; + + /** + * Turn on custom debugger. + */ + private boolean mTrackDragSort = false; + + /** + * Debugging class. + */ + private DragSortTracker mDragSortTracker; + + /** + * Needed for adjusting item heights from within layoutChildren + */ + private boolean mBlockLayoutRequests = false; + + /** + * Set to true when a down event happens during drag sort; + * for example, when drag finish animations are + * playing. + */ + private boolean mIgnoreTouchEvent = false; + + /** + * Caches DragSortItemView child heights. Sometimes DSLV has to + * know the height of an offscreen item. Since ListView virtualizes + * these, DSLV must get the item from the ListAdapter to obtain + * its height. That process can be expensive, but often the same + * offscreen item will be requested many times in a row. Once an + * offscreen item height is calculated, we cache it in this guy. + * Actually, we cache the height of the child of the + * DragSortItemView since the item height changes often during a + * drag-sort. + */ + private static final int sCacheSize = 3; + private HeightCache mChildHeightCache = new HeightCache(sCacheSize); + + private RemoveAnimator mRemoveAnimator; + + private LiftAnimator mLiftAnimator; + + private DropAnimator mDropAnimator; + + private boolean mUseRemoveVelocity; + private float mRemoveVelocityX = 0; + + public DragSortListView(Context context, AttributeSet attrs) { + super(context, attrs); + + int defaultDuration = 150; + int removeAnimDuration = defaultDuration; // ms + int dropAnimDuration = defaultDuration; // ms + + if (attrs != null) { + TypedArray a = getContext().obtainStyledAttributes(attrs, + R.styleable.DragSortListView, 0, 0); + + mItemHeightCollapsed = Math.max(1, a.getDimensionPixelSize( + R.styleable.DragSortListView_collapsed_height, 1)); + + mTrackDragSort = a.getBoolean( + R.styleable.DragSortListView_track_drag_sort, false); + + if (mTrackDragSort) { + mDragSortTracker = new DragSortTracker(); + } + + // alpha between 0 and 255, 0=transparent, 255=opaque + mFloatAlpha = a.getFloat(R.styleable.DragSortListView_float_alpha, mFloatAlpha); + mCurrFloatAlpha = mFloatAlpha; + + mDragEnabled = a.getBoolean(R.styleable.DragSortListView_drag_enabled, mDragEnabled); + + mSlideRegionFrac = Math.max(0.0f, + Math.min(1.0f, 1.0f - a.getFloat( + R.styleable.DragSortListView_slide_shuffle_speed, + 0.75f))); + + mAnimate = mSlideRegionFrac > 0.0f; + + float frac = a.getFloat( + R.styleable.DragSortListView_drag_scroll_start, + mDragUpScrollStartFrac); + + setDragScrollStart(frac); + + mMaxScrollSpeed = a.getFloat( + R.styleable.DragSortListView_max_drag_scroll_speed, + mMaxScrollSpeed); + + removeAnimDuration = a.getInt( + R.styleable.DragSortListView_remove_animation_duration, + removeAnimDuration); + + dropAnimDuration = a.getInt( + R.styleable.DragSortListView_drop_animation_duration, + dropAnimDuration); + + boolean useDefault = a.getBoolean( + R.styleable.DragSortListView_use_default_controller, + true); + + if (useDefault) { + boolean removeEnabled = a.getBoolean( + R.styleable.DragSortListView_remove_enabled, + false); + int removeMode = a.getInt( + R.styleable.DragSortListView_remove_mode, + DragSortController.FLING_REMOVE); + boolean sortEnabled = a.getBoolean( + R.styleable.DragSortListView_sort_enabled, + true); + int dragInitMode = a.getInt( + R.styleable.DragSortListView_drag_start_mode, + DragSortController.ON_DOWN); + int dragHandleId = a.getResourceId( + R.styleable.DragSortListView_drag_handle_id, + 0); + int flingHandleId = a.getResourceId( + R.styleable.DragSortListView_fling_handle_id, + 0); + int clickRemoveId = a.getResourceId( + R.styleable.DragSortListView_click_remove_id, + 0); + int bgColor = a.getColor( + R.styleable.DragSortListView_float_background_color, + Color.BLACK); + + DragSortController controller = new DragSortController( + this, dragHandleId, dragInitMode, removeMode, + clickRemoveId, flingHandleId); + controller.setRemoveEnabled(removeEnabled); + controller.setSortEnabled(sortEnabled); + controller.setBackgroundColor(bgColor); + + mFloatViewManager = controller; + setOnTouchListener(controller); + } + + a.recycle(); + } + + mDragScroller = new DragScroller(); + + float smoothness = 0.5f; + if (removeAnimDuration > 0) { + mRemoveAnimator = new RemoveAnimator(smoothness, removeAnimDuration); + } + // mLiftAnimator = new LiftAnimator(smoothness, 100); + if (dropAnimDuration > 0) { + mDropAnimator = new DropAnimator(smoothness, dropAnimDuration); + } + + mCancelEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0f, 0f, 0, 0f, + 0f, 0, 0); + + // construct the dataset observer + mObserver = new DataSetObserver() { + private void cancel() { + if (mDragState == DRAGGING) { + cancelDrag(); + } + } + + @Override + public void onChanged() { + cancel(); + } + + @Override + public void onInvalidated() { + cancel(); + } + }; + } + + /** + * Usually called from a FloatViewManager. The float alpha + * will be reset to the xml-defined value every time a drag + * is stopped. + */ + public void setFloatAlpha(float alpha) { + mCurrFloatAlpha = alpha; + } + + public float getFloatAlpha() { + return mCurrFloatAlpha; + } + + /** + * Set maximum drag scroll speed in positions/second. Only applies + * if using default ScrollSpeedProfile. + * + * @param max Maximum scroll speed. + */ + public void setMaxScrollSpeed(float max) { + mMaxScrollSpeed = max; + } + + /** + * For each DragSortListView Listener interface implemented by + * adapter, this method calls the appropriate + * set*Listener method with adapter as the argument. + * + * @param adapter The ListAdapter providing data to back + * DragSortListView. + * + * @see android.widget.ListView#setAdapter(android.widget.ListAdapter) + */ + @Override + public void setAdapter(ListAdapter adapter) { + if (adapter != null) { + mAdapterWrapper = new AdapterWrapper(adapter); + adapter.registerDataSetObserver(mObserver); + + if (adapter instanceof DropListener) { + setDropListener((DropListener) adapter); + } + if (adapter instanceof DragListener) { + setDragListener((DragListener) adapter); + } + if (adapter instanceof RemoveListener) { + setRemoveListener((RemoveListener) adapter); + } + } else { + mAdapterWrapper = null; + } + + super.setAdapter(mAdapterWrapper); + } + + /** + * As opposed to {@link ListView#getAdapter()}, which returns + * a heavily wrapped ListAdapter (DragSortListView wraps the + * input ListAdapter {\emph and} ListView wraps the wrapped one). + * + * @return The ListAdapter set as the argument of {@link setAdapter()} + */ + public ListAdapter getInputAdapter() { + if (mAdapterWrapper == null) { + return null; + } else { + return mAdapterWrapper.getWrappedAdapter(); + } + } + + private class AdapterWrapper extends BaseAdapter implements WrapperListAdapter { + private ListAdapter mAdapter; + + public AdapterWrapper(ListAdapter adapter) { + mAdapter = adapter; + } + + @Override + public ListAdapter getWrappedAdapter() { + return mAdapter; + } + + @Override + public long getItemId(int position) { + return mAdapter.getItemId(position); + } + + @Override + public Object getItem(int position) { + return mAdapter.getItem(position); + } + + @Override + public int getCount() { + return mAdapter.getCount(); + } + + @Override + public boolean areAllItemsEnabled() { + return mAdapter.areAllItemsEnabled(); + } + + @Override + public boolean isEnabled(int position) { + return mAdapter.isEnabled(position); + } + + @Override + public int getItemViewType(int position) { + return mAdapter.getItemViewType(position); + } + + @Override + public int getViewTypeCount() { + return mAdapter.getViewTypeCount(); + } + + @Override + public boolean hasStableIds() { + return mAdapter.hasStableIds(); + } + + @Override + public boolean isEmpty() { + return mAdapter.isEmpty(); + } + + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + DragSortItemView v; + View child; + if (convertView != null) { + v = (DragSortItemView) convertView; + View oldChild = v.getChildAt(0); + + child = mAdapter.getView(position, oldChild, DragSortListView.this); + if (child != oldChild) { + // shouldn't get here if user is reusing convertViews + // properly + if (oldChild != null) { + v.removeViewAt(0); + } + v.addView(child); + } + } else { + child = mAdapter.getView(position, null, DragSortListView.this); + if (child instanceof Checkable) { + v = new DragSortItemViewCheckable(getContext()); + } else { + v = new DragSortItemView(getContext()); + } + v.setLayoutParams(new AbsListView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + v.addView(child); + } + + // Set the correct item height given drag state; passed + // View needs to be measured if measurement is required. + adjustItem(position + getHeaderViewsCount(), v, true); + + return v; + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + mAdapter.registerDataSetObserver(observer); + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + mAdapter.unregisterDataSetObserver(observer); + } + } + + private void drawDivider(int expPosition, Canvas canvas) { + final Drawable divider = getDivider(); + final int dividerHeight = getDividerHeight(); + + if (divider != null && dividerHeight != 0) { + final ViewGroup expItem = (ViewGroup) getChildAt(expPosition + - getFirstVisiblePosition()); + if (expItem != null) { + final int left = getPaddingLeft(); + final int right = getWidth() - getPaddingRight(); + final int top; + final int bottom; + + final int childHeight = expItem.getChildAt(0).getHeight(); + + if (expPosition > mSrcPos) { + top = expItem.getTop() + childHeight; + bottom = top + dividerHeight; + } else { + bottom = expItem.getBottom() - childHeight; + top = bottom - dividerHeight; + } + + // Have to clip to support ColorDrawable on <= Gingerbread + canvas.save(); + canvas.clipRect(left, top, right, bottom); + divider.setBounds(left, top, right, bottom); + divider.draw(canvas); + canvas.restore(); + } + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (mDragState != IDLE) { + // draw the divider over the expanded item + if (mFirstExpPos != mSrcPos) { + drawDivider(mFirstExpPos, canvas); + } + if (mSecondExpPos != mFirstExpPos && mSecondExpPos != mSrcPos) { + drawDivider(mSecondExpPos, canvas); + } + } + + if (mFloatView != null) { + // draw the float view over everything + final int floatViewWidth = mFloatView.getWidth(); + final int floatViewHeight = mFloatView.getHeight(); + + int x = mFloatLoc.x; + + final int listViewWidth = getWidth(); + if (x < 0) + x = -x; + float alphaMod; + if (x < listViewWidth) { + alphaMod = ((float) (listViewWidth - x)) / ((float) listViewWidth); + alphaMod *= alphaMod; + } else { + alphaMod = 0; + } + + final int alpha = (int) (255f * mCurrFloatAlpha * alphaMod); + + canvas.save(); + canvas.translate(mFloatLoc.x, mFloatLoc.y); + canvas.clipRect(0, 0, floatViewWidth, floatViewHeight); + + canvas.saveLayerAlpha(0, 0, floatViewWidth, floatViewHeight, alpha, + Canvas.ALL_SAVE_FLAG); + + mFloatView.draw(canvas); + canvas.restore(); + canvas.restore(); + } + } + + private int getItemHeight(int position) { + View v = getChildAt(position - getFirstVisiblePosition()); + + if (v != null) { + // item is onscreen, just get the height of the View + return v.getHeight(); + } else { + // item is offscreen. get child height and calculate + // item height based on current shuffle state + return calcItemHeight(position, getChildHeight(position)); + } + } + + private class HeightCache { + + private SparseIntArray mMap; + private List mOrder; + private int mMaxSize; + + public HeightCache(int size) { + mMap = new SparseIntArray(size); + mOrder = new ArrayList(size); + mMaxSize = size; + } + + /** + * Add item height at position if doesn't already exist. + */ + public void add(int position, int height) { + int currentHeight = mMap.get(position, -1); + if (currentHeight != height) { + if (currentHeight == -1 && mMap.size() == mMaxSize) { + // remove oldest entry + mMap.delete(mOrder.remove(0)); + } else { + // move position to newest slot + mOrder.remove((Integer) position); + } + mMap.put(position, height); + mOrder.add(position); + } + } + + public int get(int position) { + return mMap.get(position, -1); + } + + public void clear() { + mMap.clear(); + mOrder.clear(); + } + + } + + /** + * Get the shuffle edge for item at position when top of + * item is at y-coord top. Assumes that current item heights + * are consistent with current float view location and + * thus expanded positions and slide fraction. i.e. Should not be + * called between update of expanded positions/slide fraction + * and layoutChildren. + * + * @param position + * @param top + * @param height Height of item at position. If -1, this function + * calculates this height. + * + * @return Shuffle line between position-1 and position (for + * the given view of the list; that is, for when top of item at + * position has y-coord of given `top`). If + * floating View (treated as horizontal line) is dropped + * immediately above this line, it lands in position-1. If + * dropped immediately below this line, it lands in position. + */ + private int getShuffleEdge(int position, int top) { + final int numHeaders = getHeaderViewsCount(); + final int numFooters = getFooterViewsCount(); + + // shuffle edges are defined between items that can be + // dragged; there are N-1 of them if there are N draggable + // items. + + if (position <= numHeaders || (position >= getCount() - numFooters)) { + return top; + } + + int divHeight = getDividerHeight(); + + int edge; + + int maxBlankHeight = mFloatViewHeight - mItemHeightCollapsed; + int childHeight = getChildHeight(position); + int itemHeight = getItemHeight(position); + + // first calculate top of item given that floating View is + // centered over src position + int otop = top; + if (mSecondExpPos <= mSrcPos) { + // items are expanded on and/or above the source position + + if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) { + if (position == mSrcPos) { + otop = top + itemHeight - mFloatViewHeight; + } else { + int blankHeight = itemHeight - childHeight; + otop = top + blankHeight - maxBlankHeight; + } + } else if (position > mSecondExpPos && position <= mSrcPos) { + otop = top - maxBlankHeight; + } + + } else { + // items are expanded on and/or below the source position + + if (position > mSrcPos && position <= mFirstExpPos) { + otop = top + maxBlankHeight; + } else if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) { + int blankHeight = itemHeight - childHeight; + otop = top + blankHeight; + } + } + + // otop is set + if (position <= mSrcPos) { + edge = otop + (mFloatViewHeight - divHeight - getChildHeight(position - 1)) / 2; + } else { + edge = otop + (childHeight - divHeight - mFloatViewHeight) / 2; + } + + return edge; + } + + private boolean updatePositions() { + final int first = getFirstVisiblePosition(); + int startPos = mFirstExpPos; + View startView = getChildAt(startPos - first); + + if (startView == null) { + startPos = first + getChildCount() / 2; + startView = getChildAt(startPos - first); + } + int startTop = startView.getTop(); + + int itemHeight = startView.getHeight(); + + int edge = getShuffleEdge(startPos, startTop); + int lastEdge = edge; + + int divHeight = getDividerHeight(); + + // Log.d("mobeta", "float mid="+mFloatViewMid); + + int itemPos = startPos; + int itemTop = startTop; + if (mFloatViewMid < edge) { + // scanning up for float position + while (itemPos >= 0) { + itemPos--; + itemHeight = getItemHeight(itemPos); + + if (itemPos == 0) { + edge = itemTop - divHeight - itemHeight; + break; + } + + itemTop -= itemHeight + divHeight; + edge = getShuffleEdge(itemPos, itemTop); + + if (mFloatViewMid >= edge) { + break; + } + + lastEdge = edge; + } + } else { + // scanning down for float position + final int count = getCount(); + while (itemPos < count) { + if (itemPos == count - 1) { + edge = itemTop + divHeight + itemHeight; + break; + } + + itemTop += divHeight + itemHeight; + itemHeight = getItemHeight(itemPos + 1); + edge = getShuffleEdge(itemPos + 1, itemTop); + + // test for hit + if (mFloatViewMid < edge) { + break; + } + + lastEdge = edge; + itemPos++; + } + } + + final int numHeaders = getHeaderViewsCount(); + final int numFooters = getFooterViewsCount(); + + boolean updated = false; + + int oldFirstExpPos = mFirstExpPos; + int oldSecondExpPos = mSecondExpPos; + float oldSlideFrac = mSlideFrac; + + if (mAnimate) { + int edgeToEdge = Math.abs(edge - lastEdge); + + int edgeTop, edgeBottom; + if (mFloatViewMid < edge) { + edgeTop = lastEdge; + edgeBottom = edge; + } else { + edgeTop = edge; + edgeBottom = lastEdge; + } + + int slideRgnHeight = (int) (0.5f * mSlideRegionFrac * edgeToEdge); + float slideRgnHeightF = (float) slideRgnHeight; + int slideEdgeTop = edgeTop + slideRgnHeight; + int slideEdgeBottom = edgeBottom - slideRgnHeight; + + // Three regions + if (mFloatViewMid < slideEdgeTop) { + mFirstExpPos = itemPos - 1; + mSecondExpPos = itemPos; + mSlideFrac = 0.5f * ((float) (slideEdgeTop - mFloatViewMid)) / slideRgnHeightF; + } else if (mFloatViewMid < slideEdgeBottom) { + mFirstExpPos = itemPos; + mSecondExpPos = itemPos; + } else { + mFirstExpPos = itemPos; + mSecondExpPos = itemPos + 1; + mSlideFrac = 0.5f * (1.0f + ((float) (edgeBottom - mFloatViewMid)) + / slideRgnHeightF); + } + + } else { + mFirstExpPos = itemPos; + mSecondExpPos = itemPos; + } + + // correct for headers and footers + if (mFirstExpPos < numHeaders) { + itemPos = numHeaders; + mFirstExpPos = itemPos; + mSecondExpPos = itemPos; + } else if (mSecondExpPos >= getCount() - numFooters) { + itemPos = getCount() - numFooters - 1; + mFirstExpPos = itemPos; + mSecondExpPos = itemPos; + } + + if (mFirstExpPos != oldFirstExpPos || mSecondExpPos != oldSecondExpPos + || mSlideFrac != oldSlideFrac) { + updated = true; + } + + if (itemPos != mFloatPos) { + if (mDragListener != null) { + mDragListener.drag(mFloatPos - numHeaders, itemPos - numHeaders); + } + + mFloatPos = itemPos; + updated = true; + } + + return updated; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mTrackDragSort) { + mDragSortTracker.appendState(); + } + } + + private class SmoothAnimator implements Runnable { + protected long mStartTime; + + private float mDurationF; + + private float mAlpha; + private float mA, mB, mC, mD; + + private boolean mCanceled; + + public SmoothAnimator(float smoothness, int duration) { + mAlpha = smoothness; + mDurationF = (float) duration; + mA = mD = 1f / (2f * mAlpha * (1f - mAlpha)); + mB = mAlpha / (2f * (mAlpha - 1f)); + mC = 1f / (1f - mAlpha); + } + + public float transform(float frac) { + if (frac < mAlpha) { + return mA * frac * frac; + } else if (frac < 1f - mAlpha) { + return mB + mC * frac; + } else { + return 1f - mD * (frac - 1f) * (frac - 1f); + } + } + + public void start() { + mStartTime = SystemClock.uptimeMillis(); + mCanceled = false; + onStart(); + post(this); + } + + public void cancel() { + mCanceled = true; + } + + public void onStart() { + // stub + } + + public void onUpdate(float frac, float smoothFrac) { + // stub + } + + public void onStop() { + // stub + } + + @Override + public void run() { + if (mCanceled) { + return; + } + + float fraction = ((float) (SystemClock.uptimeMillis() - mStartTime)) / mDurationF; + + if (fraction >= 1f) { + onUpdate(1f, 1f); + onStop(); + } else { + onUpdate(fraction, transform(fraction)); + post(this); + } + } + } + + /** + * Centers floating View under touch point. + */ + private class LiftAnimator extends SmoothAnimator { + + private float mInitDragDeltaY; + private float mFinalDragDeltaY; + + public LiftAnimator(float smoothness, int duration) { + super(smoothness, duration); + } + + @Override + public void onStart() { + mInitDragDeltaY = mDragDeltaY; + mFinalDragDeltaY = mFloatViewHeightHalf; + } + + @Override + public void onUpdate(float frac, float smoothFrac) { + if (mDragState != DRAGGING) { + cancel(); + } else { + mDragDeltaY = (int) (smoothFrac * mFinalDragDeltaY + (1f - smoothFrac) + * mInitDragDeltaY); + mFloatLoc.y = mY - mDragDeltaY; + doDragFloatView(true); + } + } + } + + /** + * Centers floating View over drop slot before destroying. + */ + private class DropAnimator extends SmoothAnimator { + + private int mDropPos; + private int srcPos; + private float mInitDeltaY; + private float mInitDeltaX; + + public DropAnimator(float smoothness, int duration) { + super(smoothness, duration); + } + + @Override + public void onStart() { + mDropPos = mFloatPos; + srcPos = mSrcPos; + mDragState = DROPPING; + mInitDeltaY = mFloatLoc.y - getTargetY(); + mInitDeltaX = mFloatLoc.x - getPaddingLeft(); + } + + private int getTargetY() { + final int first = getFirstVisiblePosition(); + final int otherAdjust = (mItemHeightCollapsed + getDividerHeight()) / 2; + View v = getChildAt(mDropPos - first); + int targetY = -1; + if (v != null) { + if (mDropPos == srcPos) { + targetY = v.getTop(); + } else if (mDropPos < srcPos) { + // expanded down + targetY = v.getTop() - otherAdjust; + } else { + // expanded up + targetY = v.getBottom() + otherAdjust - mFloatViewHeight; + } + } else { + // drop position is not on screen?? no animation + cancel(); + } + + return targetY; + } + + @Override + public void onUpdate(float frac, float smoothFrac) { + final int targetY = getTargetY(); + final int targetX = getPaddingLeft(); + final float deltaY = mFloatLoc.y - targetY; + final float deltaX = mFloatLoc.x - targetX; + final float f = 1f - smoothFrac; + if (f < Math.abs(deltaY / mInitDeltaY) || f < Math.abs(deltaX / mInitDeltaX)) { + mFloatLoc.y = targetY + (int) (mInitDeltaY * f); + mFloatLoc.x = getPaddingLeft() + (int) (mInitDeltaX * f); + doDragFloatView(true); + } + } + + @Override + public void onStop() { + dropFloatView(); + } + + } + + /** + * Collapses expanded items. + */ + private class RemoveAnimator extends SmoothAnimator { + + private float mFloatLocX; + private float mFirstStartBlank; + private float mSecondStartBlank; + + private int mFirstChildHeight = -1; + private int mSecondChildHeight = -1; + + private int mFirstPos; + private int mSecondPos; + + public RemoveAnimator(float smoothness, int duration) { + super(smoothness, duration); + } + + @Override + public void onStart() { + mFirstChildHeight = -1; + mSecondChildHeight = -1; + mFirstPos = mFirstExpPos; + mSecondPos = mSecondExpPos; + mDragState = REMOVING; + + mFloatLocX = mFloatLoc.x; + if (mUseRemoveVelocity) { + float minVelocity = 2f * getWidth(); + if (mRemoveVelocityX == 0) { + mRemoveVelocityX = (mFloatLocX < 0 ? -1 : 1) * minVelocity; + } else { + minVelocity *= 2; + if (mRemoveVelocityX < 0 && mRemoveVelocityX > -minVelocity) + mRemoveVelocityX = -minVelocity; + else if (mRemoveVelocityX > 0 && mRemoveVelocityX < minVelocity) + mRemoveVelocityX = minVelocity; + } + } else { + destroyFloatView(); + } + } + + @Override + public void onUpdate(float frac, float smoothFrac) { + float f = 1f - smoothFrac; + + final int firstVis = getFirstVisiblePosition(); + View item = getChildAt(mFirstPos - firstVis); + ViewGroup.LayoutParams lp; + int blank; + + if (mUseRemoveVelocity) { + float dt = (float) (SystemClock.uptimeMillis() - mStartTime) / 1000; + if (dt == 0) + return; + float dx = mRemoveVelocityX * dt; + int w = getWidth(); + mRemoveVelocityX += (mRemoveVelocityX > 0 ? 1 : -1) * dt * w; + mFloatLocX += dx; + mFloatLoc.x = (int) mFloatLocX; + if (mFloatLocX < w && mFloatLocX > -w) { + mStartTime = SystemClock.uptimeMillis(); + doDragFloatView(true); + return; + } + } + + if (item != null) { + if (mFirstChildHeight == -1) { + mFirstChildHeight = getChildHeight(mFirstPos, item, false); + mFirstStartBlank = (float) (item.getHeight() - mFirstChildHeight); + } + blank = Math.max((int) (f * mFirstStartBlank), 1); + lp = item.getLayoutParams(); + lp.height = mFirstChildHeight + blank; + item.setLayoutParams(lp); + } + if (mSecondPos != mFirstPos) { + item = getChildAt(mSecondPos - firstVis); + if (item != null) { + if (mSecondChildHeight == -1) { + mSecondChildHeight = getChildHeight(mSecondPos, item, false); + mSecondStartBlank = (float) (item.getHeight() - mSecondChildHeight); + } + blank = Math.max((int) (f * mSecondStartBlank), 1); + lp = item.getLayoutParams(); + lp.height = mSecondChildHeight + blank; + item.setLayoutParams(lp); + } + } + } + + @Override + public void onStop() { + doRemoveItem(); + } + } + + public void removeItem(int which) { + mUseRemoveVelocity = false; + removeItem(which, 0); + } + + /** + * Removes an item from the list and animates the removal. + * + * @param which Position to remove (NOTE: headers/footers ignored! + * this is a position in your input ListAdapter). + * @param velocityX + */ + public void removeItem(int which, float velocityX) { + if (mDragState == IDLE || mDragState == DRAGGING) { + if (mDragState == IDLE) { + // called from outside drag-sort + mSrcPos = getHeaderViewsCount() + which; + mFirstExpPos = mSrcPos; + mSecondExpPos = mSrcPos; + mFloatPos = mSrcPos; + View v = getChildAt(mSrcPos - getFirstVisiblePosition()); + if (v != null) { + v.setVisibility(View.INVISIBLE); + } + } + + mDragState = REMOVING; + mRemoveVelocityX = velocityX; + + if (mInTouchEvent) { + switch (mCancelMethod) { + case ON_TOUCH_EVENT: + super.onTouchEvent(mCancelEvent); + break; + case ON_INTERCEPT_TOUCH_EVENT: + super.onInterceptTouchEvent(mCancelEvent); + break; + } + } + + if (mRemoveAnimator != null) { + mRemoveAnimator.start(); + } else { + doRemoveItem(which); + } + } + } + + /** + * Move an item, bypassing the drag-sort process. Simply calls + * through to {@link DropListener#drop(int, int)}. + * + * @param from Position to move (NOTE: headers/footers ignored! + * this is a position in your input ListAdapter). + * @param to Target position (NOTE: headers/footers ignored! + * this is a position in your input ListAdapter). + */ + public void moveItem(int from, int to) { + if (mDropListener != null) { + final int count = getInputAdapter().getCount(); + if (from >= 0 && from < count && to >= 0 && to < count) { + mDropListener.drop(from, to); + } + } + } + + /** + * Cancel a drag. Calls {@link #stopDrag(boolean, boolean)} with + * true as the first argument. + */ + public void cancelDrag() { + if (mDragState == DRAGGING) { + mDragScroller.stopScrolling(true); + destroyFloatView(); + clearPositions(); + adjustAllItems(); + + if (mInTouchEvent) { + mDragState = STOPPED; + } else { + mDragState = IDLE; + } + } + } + + private void clearPositions() { + mSrcPos = -1; + mFirstExpPos = -1; + mSecondExpPos = -1; + mFloatPos = -1; + } + + private void dropFloatView() { + // must set to avoid cancelDrag being called from the DataSetObserver + mDragState = DROPPING; + + if (mDropListener != null && mFloatPos >= 0 && mFloatPos < getCount()) { + final int numHeaders = getHeaderViewsCount(); + mDropListener.drop(mSrcPos - numHeaders, mFloatPos - numHeaders); + } + + destroyFloatView(); + + adjustOnReorder(); + clearPositions(); + adjustAllItems(); + + // now the drag is done + if (mInTouchEvent) { + mDragState = STOPPED; + } else { + mDragState = IDLE; + } + } + + private void doRemoveItem() { + doRemoveItem(mSrcPos - getHeaderViewsCount()); + } + + /** + * Removes dragged item from the list. Calls RemoveListener. + */ + private void doRemoveItem(int which) { + // must set to avoid cancelDrag being called from the DataSetObserver + mDragState = REMOVING; + + // end it + if (mRemoveListener != null) { + mRemoveListener.remove(which); + } + + destroyFloatView(); + + adjustOnReorder(); + clearPositions(); + + // now the drag is done + if (mInTouchEvent) { + mDragState = STOPPED; + } else { + mDragState = IDLE; + } + } + + private void adjustOnReorder() { + final int firstPos = getFirstVisiblePosition(); + if (mSrcPos < firstPos) { + // collapsed src item is off screen; + // adjust the scroll after item heights have been fixed + View v = getChildAt(0); + int top = 0; + if (v != null) { + top = v.getTop(); + } + setSelectionFromTop(firstPos - 1, top - getPaddingTop()); + } + } + + /** + * Stop a drag in progress. Pass true if you would + * like to remove the dragged item from the list. + * + * @param remove Remove the dragged item from the list. Calls + * a registered RemoveListener, if one exists. Otherwise, calls + * the DropListener, if one exists. + * + * @return True if the stop was successful. False if there is + * no floating View. + */ + public boolean stopDrag(boolean remove) { + mUseRemoveVelocity = false; + return stopDrag(remove, 0); + } + + public boolean stopDragWithVelocity(boolean remove, float velocityX) { + mUseRemoveVelocity = true; + return stopDrag(remove, velocityX); + } + + public boolean stopDrag(boolean remove, float velocityX) { + if (mFloatView != null) { + mDragScroller.stopScrolling(true); + + if (remove) { + removeItem(mSrcPos - getHeaderViewsCount(), velocityX); + } else { + if (mDropAnimator != null) { + mDropAnimator.start(); + } else { + dropFloatView(); + } + } + + if (mTrackDragSort) { + mDragSortTracker.stopTracking(); + } + + return true; + } else { + // stop failed + return false; + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mIgnoreTouchEvent) { + mIgnoreTouchEvent = false; + return false; + } + + if (!mDragEnabled) { + return super.onTouchEvent(ev); + } + + boolean more = false; + + boolean lastCallWasIntercept = mLastCallWasIntercept; + mLastCallWasIntercept = false; + + if (!lastCallWasIntercept) { + saveTouchCoords(ev); + } + + if (mDragState == DRAGGING) { + onDragTouchEvent(ev); + more = true; // give us more! + } else { + // TODO: what if float view is null because we dropped in middle + // of drag touch event? + + if (mDragState == IDLE) { + if (super.onTouchEvent(ev)) { + more = true; + } + } + + int action = ev.getAction() & MotionEvent.ACTION_MASK; + + switch (action) { + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + doActionUpOrCancel(); + break; + default: + if (more) { + mCancelMethod = ON_TOUCH_EVENT; + } + } + } + + return more; + } + + private void doActionUpOrCancel() { + mCancelMethod = NO_CANCEL; + mInTouchEvent = false; + if (mDragState == STOPPED) { + mDragState = IDLE; + } + mCurrFloatAlpha = mFloatAlpha; + mListViewIntercepted = false; + mChildHeightCache.clear(); + } + + private void saveTouchCoords(MotionEvent ev) { + int action = ev.getAction() & MotionEvent.ACTION_MASK; + if (action != MotionEvent.ACTION_DOWN) { + //mLastX = mX; + mLastY = mY; + } + mX = (int) ev.getX(); + mY = (int) ev.getY(); + if (action == MotionEvent.ACTION_DOWN) { + //mLastX = mX; + mLastY = mY; + } + //mOffsetX = (int) ev.getRawX() - mX; + //mOffsetY = (int) ev.getRawY() - mY; + } + + public boolean listViewIntercepted() { + return mListViewIntercepted; + } + + private boolean mListViewIntercepted = false; + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (!mDragEnabled) { + return super.onInterceptTouchEvent(ev); + } + + saveTouchCoords(ev); + mLastCallWasIntercept = true; + + int action = ev.getAction() & MotionEvent.ACTION_MASK; + + if (action == MotionEvent.ACTION_DOWN) { + if (mDragState != IDLE) { + // intercept and ignore + mIgnoreTouchEvent = true; + return true; + } + mInTouchEvent = true; + } + + boolean intercept = false; + + // the following deals with calls to super.onInterceptTouchEvent + if (mFloatView != null) { + // super's touch event cancelled in startDrag + intercept = true; + } else { + if (super.onInterceptTouchEvent(ev)) { + mListViewIntercepted = true; + intercept = true; + } + + switch (action) { + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + doActionUpOrCancel(); + break; + default: + if (intercept) { + mCancelMethod = ON_TOUCH_EVENT; + } else { + mCancelMethod = ON_INTERCEPT_TOUCH_EVENT; + } + } + } + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + mInTouchEvent = false; + } + + return intercept; + } + + /** + * Set the width of each drag scroll region by specifying + * a fraction of the ListView height. + * + * @param heightFraction Fraction of ListView height. Capped at + * 0.5f. + * + */ + public void setDragScrollStart(float heightFraction) { + setDragScrollStarts(heightFraction, heightFraction); + } + + /** + * Set the width of each drag scroll region by specifying + * a fraction of the ListView height. + * + * @param upperFrac Fraction of ListView height for up-scroll bound. + * Capped at 0.5f. + * @param lowerFrac Fraction of ListView height for down-scroll bound. + * Capped at 0.5f. + * + */ + public void setDragScrollStarts(float upperFrac, float lowerFrac) { + if (lowerFrac > 0.5f) { + mDragDownScrollStartFrac = 0.5f; + } else { + mDragDownScrollStartFrac = lowerFrac; + } + + if (upperFrac > 0.5f) { + mDragUpScrollStartFrac = 0.5f; + } else { + mDragUpScrollStartFrac = upperFrac; + } + + if (getHeight() != 0) { + updateScrollStarts(); + } + } + + private void continueDrag(int x, int y) { + // proposed position + mFloatLoc.x = x - mDragDeltaX; + mFloatLoc.y = y - mDragDeltaY; + + doDragFloatView(true); + + int minY = Math.min(y, mFloatViewMid + mFloatViewHeightHalf); + int maxY = Math.max(y, mFloatViewMid - mFloatViewHeightHalf); + + // get the current scroll direction + int currentScrollDir = mDragScroller.getScrollDir(); + + if (minY > mLastY && minY > mDownScrollStartY && currentScrollDir != DragScroller.DOWN) { + // dragged down, it is below the down scroll start and it is not + // scrolling up + + if (currentScrollDir != DragScroller.STOP) { + // moved directly from up scroll to down scroll + mDragScroller.stopScrolling(true); + } + + // start scrolling down + mDragScroller.startScrolling(DragScroller.DOWN); + } else if (maxY < mLastY && maxY < mUpScrollStartY && currentScrollDir != DragScroller.UP) { + // dragged up, it is above the up scroll start and it is not + // scrolling up + + if (currentScrollDir != DragScroller.STOP) { + // moved directly from down scroll to up scroll + mDragScroller.stopScrolling(true); + } + + // start scrolling up + mDragScroller.startScrolling(DragScroller.UP); + } + else if (maxY >= mUpScrollStartY && minY <= mDownScrollStartY + && mDragScroller.isScrolling()) { + // not in the upper nor in the lower drag-scroll regions but it is + // still scrolling + + mDragScroller.stopScrolling(true); + } + } + + private void updateScrollStarts() { + final int padTop = getPaddingTop(); + final int listHeight = getHeight() - padTop - getPaddingBottom(); + float heightF = (float) listHeight; + + mUpScrollStartYF = padTop + mDragUpScrollStartFrac * heightF; + mDownScrollStartYF = padTop + (1.0f - mDragDownScrollStartFrac) * heightF; + + mUpScrollStartY = (int) mUpScrollStartYF; + mDownScrollStartY = (int) mDownScrollStartYF; + + mDragUpScrollHeight = mUpScrollStartYF - padTop; + mDragDownScrollHeight = padTop + listHeight - mDownScrollStartYF; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + updateScrollStarts(); + } + + private void adjustAllItems() { + final int first = getFirstVisiblePosition(); + final int last = getLastVisiblePosition(); + + int begin = Math.max(0, getHeaderViewsCount() - first); + int end = Math.min(last - first, getCount() - 1 - getFooterViewsCount() - first); + + for (int i = begin; i <= end; ++i) { + View v = getChildAt(i); + if (v != null) { + adjustItem(first + i, v, false); + } + } + } + + /** + * Sets layout param height, gravity, and visibility on + * wrapped item. + */ + private void adjustItem(int position, View v, boolean invalidChildHeight) { + // Adjust item height + ViewGroup.LayoutParams lp = v.getLayoutParams(); + int height; + if (position != mSrcPos && position != mFirstExpPos && position != mSecondExpPos) { + height = ViewGroup.LayoutParams.WRAP_CONTENT; + } else { + height = calcItemHeight(position, v, invalidChildHeight); + } + + if (height != lp.height) { + lp.height = height; + v.setLayoutParams(lp); + } + + // Adjust item gravity + if (position == mFirstExpPos || position == mSecondExpPos) { + if (position < mSrcPos) { + ((DragSortItemView) v).setGravity(Gravity.BOTTOM); + } else if (position > mSrcPos) { + ((DragSortItemView) v).setGravity(Gravity.TOP); + } + } + + // Finally adjust item visibility + int oldVis = v.getVisibility(); + int vis = View.VISIBLE; + + if (position == mSrcPos && mFloatView != null) { + vis = View.INVISIBLE; + } + + if (vis != oldVis) { + v.setVisibility(vis); + } + } + + private int getChildHeight(int position) { + if (position == mSrcPos) { + return 0; + } + + View v = getChildAt(position - getFirstVisiblePosition()); + + if (v != null) { + // item is onscreen, therefore child height is valid, + // hence the "true" + return getChildHeight(position, v, false); + } else { + // item is offscreen + // first check cache for child height at this position + int childHeight = mChildHeightCache.get(position); + if (childHeight != -1) { + return childHeight; + } + + final ListAdapter adapter = getAdapter(); + int type = adapter.getItemViewType(position); + + // There might be a better place for checking for the following + final int typeCount = adapter.getViewTypeCount(); + if (typeCount != mSampleViewTypes.length) { + mSampleViewTypes = new View[typeCount]; + } + + if (type >= 0) { + if (mSampleViewTypes[type] == null) { + v = adapter.getView(position, null, this); + mSampleViewTypes[type] = v; + } else { + v = adapter.getView(position, mSampleViewTypes[type], this); + } + } else { + // type is HEADER_OR_FOOTER or IGNORE + v = adapter.getView(position, null, this); + } + + // current child height is invalid, hence "true" below + childHeight = getChildHeight(position, v, true); + + // cache it because this could have been expensive + mChildHeightCache.add(position, childHeight); + + return childHeight; + } + } + + private int getChildHeight(int position, View item, boolean invalidChildHeight) { + if (position == mSrcPos) { + return 0; + } + + View child; + if (position < getHeaderViewsCount() || position >= getCount() - getFooterViewsCount()) { + child = item; + } else { + child = ((ViewGroup) item).getChildAt(0); + } + + ViewGroup.LayoutParams lp = child.getLayoutParams(); + if (lp != null) { + if (lp.height > 0) { + return lp.height; + } + } + + int childHeight = child.getHeight(); + if (childHeight == 0 || invalidChildHeight) { + measureItem(child); + childHeight = child.getMeasuredHeight(); + } + + return childHeight; + } + + private int calcItemHeight(int position, View item, boolean invalidChildHeight) { + return calcItemHeight(position, getChildHeight(position, item, invalidChildHeight)); + } + + private int calcItemHeight(int position, int childHeight) { + boolean isSliding = mAnimate && mFirstExpPos != mSecondExpPos; + int maxNonSrcBlankHeight = mFloatViewHeight - mItemHeightCollapsed; + int slideHeight = (int) (mSlideFrac * maxNonSrcBlankHeight); + + int height; + + if (position == mSrcPos) { + if (mSrcPos == mFirstExpPos) { + if (isSliding) { + height = slideHeight + mItemHeightCollapsed; + } else { + height = mFloatViewHeight; + } + } else if (mSrcPos == mSecondExpPos) { + // if gets here, we know an item is sliding + height = mFloatViewHeight - slideHeight; + } else { + height = mItemHeightCollapsed; + } + } else if (position == mFirstExpPos) { + if (isSliding) { + height = childHeight + slideHeight; + } else { + height = childHeight + maxNonSrcBlankHeight; + } + } else if (position == mSecondExpPos) { + // we know an item is sliding (b/c 2ndPos != 1stPos) + height = childHeight + maxNonSrcBlankHeight - slideHeight; + } else { + height = childHeight; + } + + return height; + } + + @Override + public void requestLayout() { + if (!mBlockLayoutRequests) { + super.requestLayout(); + } + } + + private int adjustScroll(int movePos, View moveItem, int oldFirstExpPos, int oldSecondExpPos) { + int adjust = 0; + + final int childHeight = getChildHeight(movePos); + + int moveHeightBefore = moveItem.getHeight(); + int moveHeightAfter = calcItemHeight(movePos, childHeight); + + int moveBlankBefore = moveHeightBefore; + int moveBlankAfter = moveHeightAfter; + if (movePos != mSrcPos) { + moveBlankBefore -= childHeight; + moveBlankAfter -= childHeight; + } + + int maxBlank = mFloatViewHeight; + if (mSrcPos != mFirstExpPos && mSrcPos != mSecondExpPos) { + maxBlank -= mItemHeightCollapsed; + } + + if (movePos <= oldFirstExpPos) { + if (movePos > mFirstExpPos) { + adjust += maxBlank - moveBlankAfter; + } + } else if (movePos == oldSecondExpPos) { + if (movePos <= mFirstExpPos) { + adjust += moveBlankBefore - maxBlank; + } else if (movePos == mSecondExpPos) { + adjust += moveHeightBefore - moveHeightAfter; + } else { + adjust += moveBlankBefore; + } + } else { + if (movePos <= mFirstExpPos) { + adjust -= maxBlank; + } else if (movePos == mSecondExpPos) { + adjust -= moveBlankAfter; + } + } + + return adjust; + } + + private void measureItem(View item) { + ViewGroup.LayoutParams lp = item.getLayoutParams(); + if (lp == null) { + lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + item.setLayoutParams(lp); + } + + int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft() + + getListPaddingRight(), lp.width); + int hspec; + if (lp.height > 0) { + hspec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); + } else { + hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + item.measure(wspec, hspec); + } + + private void measureFloatView() { + if (mFloatView != null) { + measureItem(mFloatView); + mFloatViewHeight = mFloatView.getMeasuredHeight(); + mFloatViewHeightHalf = mFloatViewHeight / 2; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (mFloatView != null) { + if (mFloatView.isLayoutRequested()) { + measureFloatView(); + } + mFloatViewOnMeasured = true; // set to false after layout + } + mWidthMeasureSpec = widthMeasureSpec; + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + + if (mFloatView != null) { + if (mFloatView.isLayoutRequested() && !mFloatViewOnMeasured) { + // Have to measure here when usual android measure + // pass is skipped. This happens during a drag-sort + // when layoutChildren is called directly. + measureFloatView(); + } + mFloatView.layout(0, 0, mFloatView.getMeasuredWidth(), mFloatView.getMeasuredHeight()); + mFloatViewOnMeasured = false; + } + } + + protected boolean onDragTouchEvent(MotionEvent ev) { + int action = ev.getAction() & MotionEvent.ACTION_MASK; + switch (action) { + case MotionEvent.ACTION_CANCEL: + if (mDragState == DRAGGING) { + cancelDrag(); + } + doActionUpOrCancel(); + break; + case MotionEvent.ACTION_UP: + if (mDragState == DRAGGING) { + stopDrag(false); + } + doActionUpOrCancel(); + break; + case MotionEvent.ACTION_MOVE: + continueDrag((int) ev.getX(), (int) ev.getY()); + break; + } + + return true; + } + + /** + * Start a drag of item at position using the + * registered FloatViewManager. Calls through + * to {@link #startDrag(int,View,int,int,int)} after obtaining + * the floating View from the FloatViewManager. + * + * @param position Item to drag. + * @param dragFlags Flags that restrict some movements of the + * floating View. For example, set dragFlags |= + * ~{@link #DRAG_NEG_X} to allow dragging the floating + * View in all directions except off the screen to the left. + * @param deltaX Offset in x of the touch coordinate from the + * left edge of the floating View (i.e. touch-x minus float View + * left). + * @param deltaY Offset in y of the touch coordinate from the + * top edge of the floating View (i.e. touch-y minus float View + * top). + * + * @return True if the drag was started, false otherwise. This + * startDrag will fail if we are not currently in + * a touch event, there is no registered FloatViewManager, + * or the FloatViewManager returns a null View. + */ + public boolean startDrag(int position, int dragFlags, int deltaX, int deltaY) { + if (!mInTouchEvent || mFloatViewManager == null) { + return false; + } + + View v = mFloatViewManager.onCreateFloatView(position); + + if (v == null) { + return false; + } else { + return startDrag(position, v, dragFlags, deltaX, deltaY); + } + + } + + /** + * Start a drag of item at position without using + * a FloatViewManager. + * + * @param position Item to drag. + * @param floatView Floating View. + * @param dragFlags Flags that restrict some movements of the + * floating View. For example, set dragFlags |= + * ~{@link #DRAG_NEG_X} to allow dragging the floating + * View in all directions except off the screen to the left. + * @param deltaX Offset in x of the touch coordinate from the + * left edge of the floating View (i.e. touch-x minus float View + * left). + * @param deltaY Offset in y of the touch coordinate from the + * top edge of the floating View (i.e. touch-y minus float View + * top). + * + * @return True if the drag was started, false otherwise. This + * startDrag will fail if we are not currently in + * a touch event, floatView is null, or there is + * a drag in progress. + */ + public boolean startDrag(int position, View floatView, int dragFlags, int deltaX, int deltaY) { + if (mDragState != IDLE || !mInTouchEvent || mFloatView != null || floatView == null + || !mDragEnabled) { + return false; + } + + if (getParent() != null) { + getParent().requestDisallowInterceptTouchEvent(true); + } + + int pos = position + getHeaderViewsCount(); + mFirstExpPos = pos; + mSecondExpPos = pos; + mSrcPos = pos; + mFloatPos = pos; + + mDragState = DRAGGING; + mDragFlags = 0; + mDragFlags |= dragFlags; + + mFloatView = floatView; + measureFloatView(); // sets mFloatViewHeight + + mDragDeltaX = deltaX; + mDragDeltaY = deltaY; + + mFloatLoc.x = mX - mDragDeltaX; + mFloatLoc.y = mY - mDragDeltaY; + + // set src item invisible + final View srcItem = getChildAt(mSrcPos - getFirstVisiblePosition()); + + if (srcItem != null) { + srcItem.setVisibility(View.INVISIBLE); + } + + if (mTrackDragSort) { + mDragSortTracker.startTracking(); + } + + // once float view is created, events are no longer passed + // to ListView + switch (mCancelMethod) { + case ON_TOUCH_EVENT: + super.onTouchEvent(mCancelEvent); + break; + case ON_INTERCEPT_TOUCH_EVENT: + super.onInterceptTouchEvent(mCancelEvent); + break; + } + + requestLayout(); + + if (mLiftAnimator != null) { + mLiftAnimator.start(); + } + + return true; + } + + private void doDragFloatView(boolean forceInvalidate) { + int movePos = getFirstVisiblePosition() + getChildCount() / 2; + View moveItem = getChildAt(getChildCount() / 2); + + if (moveItem == null) { + return; + } + + doDragFloatView(movePos, moveItem, forceInvalidate); + } + + private void doDragFloatView(int movePos, View moveItem, boolean forceInvalidate) { + mBlockLayoutRequests = true; + + updateFloatView(); + + int oldFirstExpPos = mFirstExpPos; + int oldSecondExpPos = mSecondExpPos; + + boolean updated = updatePositions(); + + if (updated) { + adjustAllItems(); + int scroll = adjustScroll(movePos, moveItem, oldFirstExpPos, oldSecondExpPos); + + setSelectionFromTop(movePos, moveItem.getTop() + scroll - getPaddingTop()); + layoutChildren(); + } + + if (updated || forceInvalidate) { + invalidate(); + } + + mBlockLayoutRequests = false; + } + + /** + * Sets float View location based on suggested values and + * constraints set in mDragFlags. + */ + private void updateFloatView() { + + if (mFloatViewManager != null) { + mTouchLoc.set(mX, mY); + mFloatViewManager.onDragFloatView(mFloatView, mFloatLoc, mTouchLoc); + } + + final int floatX = mFloatLoc.x; + final int floatY = mFloatLoc.y; + + // restrict x motion + int padLeft = getPaddingLeft(); + if ((mDragFlags & DRAG_POS_X) == 0 && floatX > padLeft) { + mFloatLoc.x = padLeft; + } else if ((mDragFlags & DRAG_NEG_X) == 0 && floatX < padLeft) { + mFloatLoc.x = padLeft; + } + + // keep floating view from going past bottom of last header view + final int numHeaders = getHeaderViewsCount(); + final int numFooters = getFooterViewsCount(); + final int firstPos = getFirstVisiblePosition(); + final int lastPos = getLastVisiblePosition(); + + int topLimit = getPaddingTop(); + if (firstPos < numHeaders) { + topLimit = getChildAt(numHeaders - firstPos - 1).getBottom(); + } + if ((mDragFlags & DRAG_NEG_Y) == 0) { + if (firstPos <= mSrcPos) { + topLimit = Math.max(getChildAt(mSrcPos - firstPos).getTop(), topLimit); + } + } + // bottom limit is top of first footer View or + // bottom of last item in list + int bottomLimit = getHeight() - getPaddingBottom(); + if (lastPos >= getCount() - numFooters - 1) { + bottomLimit = getChildAt(getCount() - numFooters - 1 - firstPos).getBottom(); + } + if ((mDragFlags & DRAG_POS_Y) == 0) { + if (lastPos >= mSrcPos) { + bottomLimit = Math.min(getChildAt(mSrcPos - firstPos).getBottom(), bottomLimit); + } + } + + if (floatY < topLimit) { + mFloatLoc.y = topLimit; + } else if (floatY + mFloatViewHeight > bottomLimit) { + mFloatLoc.y = bottomLimit - mFloatViewHeight; + } + + // get y-midpoint of floating view (constrained to ListView bounds) + mFloatViewMid = mFloatLoc.y + mFloatViewHeightHalf; + } + + private void destroyFloatView() { + if (mFloatView != null) { + mFloatView.setVisibility(GONE); + if (mFloatViewManager != null) { + mFloatViewManager.onDestroyFloatView(mFloatView); + } + mFloatView = null; + invalidate(); + } + } + + /** + * Interface for customization of the floating View appearance + * and dragging behavior. Implement + * your own and pass it to {@link #setFloatViewManager}. If + * your own is not passed, the default {@link SimpleFloatViewManager} + * implementation is used. + */ + public interface FloatViewManager { + /** + * Return the floating View for item at position. + * DragSortListView will measure and layout this View for you, + * so feel free to just inflate it. You can help DSLV by + * setting some {@link ViewGroup.LayoutParams} on this View; + * otherwise it will set some for you (with a width of FILL_PARENT + * and a height of WRAP_CONTENT). + * + * @param position Position of item to drag (NOTE: + * position excludes header Views; thus, if you + * want to call {@link ListView#getChildAt(int)}, you will need + * to add {@link ListView#getHeaderViewsCount()} to the index). + * + * @return The View you wish to display as the floating View. + */ + public View onCreateFloatView(int position); + + /** + * Called whenever the floating View is dragged. Float View + * properties can be changed here. Also, the upcoming location + * of the float View can be altered by setting + * location.x and location.y. + * + * @param floatView The floating View. + * @param location The location (top-left; relative to DSLV + * top-left) at which the float + * View would like to appear, given the current touch location + * and the offset provided in {@link DragSortListView#startDrag}. + * @param touch The current touch location (relative to DSLV + * top-left). + * @param pendingScroll + */ + public void onDragFloatView(View floatView, Point location, Point touch); + + /** + * Called when the float View is dropped; lets you perform + * any necessary cleanup. The internal DSLV floating View + * reference is set to null immediately after this is called. + * + * @param floatView The floating View passed to + * {@link #onCreateFloatView(int)}. + */ + public void onDestroyFloatView(View floatView); + } + + public void setFloatViewManager(FloatViewManager manager) { + mFloatViewManager = manager; + } + + public void setDragListener(DragListener l) { + mDragListener = l; + } + + /** + * Allows for easy toggling between a DragSortListView + * and a regular old ListView. If enabled, items are + * draggable, where the drag init mode determines how + * items are lifted (see {@link setDragInitMode(int)}). + * If disabled, items cannot be dragged. + * + * @param enabled Set true to enable list + * item dragging + */ + public void setDragEnabled(boolean enabled) { + mDragEnabled = enabled; + } + + public boolean isDragEnabled() { + return mDragEnabled; + } + + /** + * This better reorder your ListAdapter! DragSortListView does not do this + * for you; doesn't make sense to. Make sure + * {@link BaseAdapter#notifyDataSetChanged()} or something like it is called + * in your implementation. Furthermore, if you have a choiceMode other than + * none and the ListAdapter does not return true for + * {@link ListAdapter#hasStableIds()}, you will need to call + * {@link #moveCheckState(int, int)} to move the check boxes along with the + * list items. + * + * @param l + */ + public void setDropListener(DropListener l) { + mDropListener = l; + } + + /** + * Probably a no-brainer, but make sure that your remove listener + * calls {@link BaseAdapter#notifyDataSetChanged()} or something like it. + * When an item removal occurs, DragSortListView + * relies on a redraw of all the items to recover invisible views + * and such. Strictly speaking, if you remove something, your dataset + * has changed... + * + * @param l + */ + public void setRemoveListener(RemoveListener l) { + mRemoveListener = l; + } + + public interface DragListener { + public void drag(int from, int to); + } + + /** + * Your implementation of this has to reorder your ListAdapter! + * Make sure to call + * {@link BaseAdapter#notifyDataSetChanged()} or something like it + * in your implementation. + * + * @author heycosmo + * + */ + public interface DropListener { + public void drop(int from, int to); + } + + /** + * Make sure to call + * {@link BaseAdapter#notifyDataSetChanged()} or something like it + * in your implementation. + * + * @author heycosmo + * + */ + public interface RemoveListener { + public void remove(int which); + } + + public interface DragSortListener extends DropListener, DragListener, RemoveListener { + } + + public void setDragSortListener(DragSortListener listener) { + setDropListener(listener); + setDragListener(listener); + setRemoveListener(listener); + } + + /** + * Completely custom scroll speed profile. Default increases linearly + * with position and is constant in time. Create your own by implementing + * {@link DragSortListView.DragScrollProfile}. + * + * @param scrollProfile + */ + public void setDragScrollProfile(DragScrollProfile scrollProfile) { + if (scrollProfile != null) { + mScrollProfile = scrollProfile; + } + } + + /** + * Use this to move the check state of an item from one position to another + * in a drop operation. If you have a choiceMode which is not none, this + * method must be called when the order of items changes in an underlying + * adapter which does not have stable IDs (see + * {@link ListAdapter#hasStableIds()}). This is because without IDs, the + * ListView has no way of knowing which items have moved where, and cannot + * update the check state accordingly. + *

+ * A word of warning about a "feature" in Android that you may run into when + * dealing with movable list items: for an adapter that does have + * stable IDs, ListView will attempt to locate each item based on its ID and + * move the check state from the item's old position to the new position — + * which is all fine and good (and removes the need for calling this + * function), except for the half-baked approach. Apparently to save time in + * the naive algorithm used, ListView will only search for an ID in the + * close neighborhood of the old position. If the user moves an item too far + * (specifically, more than 20 rows away), ListView will give up and just + * force the item to be unchecked. So if there is a reasonable chance that + * the user will move items more than 20 rows away from the original + * position, you may wish to use an adapter with unstable IDs and call this + * method manually instead. + * + * @param from + * @param to + */ + public void moveCheckState(int from, int to) { + // This method runs in O(n log n) time (n being the number of list + // items). The bottleneck is the call to AbsListView.setItemChecked, + // which is O(log n) because of the binary search involved in calling + // SparseBooleanArray.put(). + // + // To improve on the average time, we minimize the number of calls to + // setItemChecked by only calling it for items that actually have a + // changed state. This is achieved by building a list containing the + // start and end of the "runs" of checked items, and then moving the + // runs. Note that moving an item from A to B is essentially a rotation + // of the range of items in [A, B]. Let's say we have + // . . U V X Y Z . . + // and move U after Z. This is equivalent to a rotation one step to the + // left within the range you are moving across: + // . . V X Y Z U . . + // + // So, to perform the move we enumerate all the runs within the move + // range, then rotate each run one step to the left or right (depending + // on move direction). For example, in the list: + // X X . X X X . X + // we have two runs. One begins at the last item of the list and wraps + // around to the beginning, ending at position 1. The second begins at + // position 3 and ends at position 5. To rotate a run, regardless of + // length, we only need to set a check mark at one end of the run, and + // clear a check mark at the other end: + // X . X X X . X X + SparseBooleanArray cip = getCheckedItemPositions(); + int rangeStart = from; + int rangeEnd = to; + if (to < from) { + rangeStart = to; + rangeEnd = from; + } + rangeEnd += 1; + + int[] runStart = new int[cip.size()]; + int[] runEnd = new int[cip.size()]; + int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd); + if (runCount == 1 && (runStart[0] == runEnd[0])) { + // Special case where all items are checked, we can never set any + // item to false like we do below. + return; + } + + if (from < to) { + for (int i = 0; i != runCount; i++) { + setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd), true); + setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd), false); + } + + } else { + for (int i = 0; i != runCount; i++) { + setItemChecked(runStart[i], false); + setItemChecked(runEnd[i], true); + } + } + } + + /** + * Use this when an item has been deleted, to move the check state of all + * following items up one step. If you have a choiceMode which is not none, + * this method must be called when the order of items changes in an + * underlying adapter which does not have stable IDs (see + * {@link ListAdapter#hasStableIds()}). This is because without IDs, the + * ListView has no way of knowing which items have moved where, and cannot + * update the check state accordingly. + * + * See also further comments on {@link #moveCheckState(int, int)}. + * + * @param position + */ + public void removeCheckState(int position) { + SparseBooleanArray cip = getCheckedItemPositions(); + + if (cip.size() == 0) + return; + int[] runStart = new int[cip.size()]; + int[] runEnd = new int[cip.size()]; + int rangeStart = position; + int rangeEnd = cip.keyAt(cip.size() - 1) + 1; + int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd); + for (int i = 0; i != runCount; i++) { + if (!(runStart[i] == position || (runEnd[i] < runStart[i] && runEnd[i] > position))) { + // Only set a new check mark in front of this run if it does + // not contain the deleted position. If it does, we only need + // to make it one check mark shorter at the end. + setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd), true); + } + setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd), false); + } + } + + private static int buildRunList(SparseBooleanArray cip, int rangeStart, int rangeEnd, + int[] runStart, int[] runEnd) { + + int runCount = 0; + + int i = findFirstSetIndex(cip, rangeStart, rangeEnd); + if (i == -1) + return 0; + + int position = cip.keyAt(i); + int currentRunStart = position; + int currentRunEnd = currentRunStart + 1; + for (i++; i < cip.size() && (position = cip.keyAt(i)) < rangeEnd; i++) { + if (!cip.valueAt(i)) // not checked => not interesting + continue; + if (position == currentRunEnd) { + currentRunEnd++; + } else { + runStart[runCount] = currentRunStart; + runEnd[runCount] = currentRunEnd; + runCount++; + currentRunStart = position; + currentRunEnd = position + 1; + } + } + + if (currentRunEnd == rangeEnd) { + // rangeStart and rangeEnd are equivalent positions so to be + // consistent we translate them to the same integer value. That way + // we can check whether a run covers the entire range by just + // checking if the start equals the end position. + currentRunEnd = rangeStart; + } + runStart[runCount] = currentRunStart; + runEnd[runCount] = currentRunEnd; + runCount++; + + if (runCount > 1) { + if (runStart[0] == rangeStart && runEnd[runCount - 1] == rangeStart) { + // The last run ends at the end of the range, and the first run + // starts at the beginning of the range. So they are actually + // part of the same run, except they wrap around the end of the + // range. To avoid adjacent runs, we need to merge them. + runStart[0] = runStart[runCount - 1]; + runCount--; + } + } + return runCount; + } + + private static int rotate(int value, int offset, int lowerBound, int upperBound) { + int windowSize = upperBound - lowerBound; + + value += offset; + if (value < lowerBound) { + value += windowSize; + } else if (value >= upperBound) { + value -= windowSize; + } + return value; + } + + private static int findFirstSetIndex(SparseBooleanArray sba, int rangeStart, int rangeEnd) { + int size = sba.size(); + int i = insertionIndexForKey(sba, rangeStart); + while (i < size && sba.keyAt(i) < rangeEnd && !sba.valueAt(i)) + i++; + if (i == size || sba.keyAt(i) >= rangeEnd) + return -1; + return i; + } + + private static int insertionIndexForKey(SparseBooleanArray sba, int key) { + int low = 0; + int high = sba.size(); + while (high - low > 0) { + int middle = (low + high) >> 1; + if (sba.keyAt(middle) < key) + low = middle + 1; + else + high = middle; + } + return low; + } + + /** + * Interface for controlling + * scroll speed as a function of touch position and time. Use + * {@link DragSortListView#setDragScrollProfile(DragScrollProfile)} to + * set custom profile. + * + * @author heycosmo + * + */ + public interface DragScrollProfile { + /** + * Return a scroll speed in pixels/millisecond. Always return a + * positive number. + * + * @param w Normalized position in scroll region (i.e. w \in [0,1]). + * Small w typically means slow scrolling. + * @param t Time (in milliseconds) since start of scroll (handy if you + * want scroll acceleration). + * @return Scroll speed at position w and time t in pixels/ms. + */ + float getSpeed(float w, long t); + } + + private class DragScroller implements Runnable { + + private boolean mAbort; + + private long mPrevTime; + private long mCurrTime; + + private int dy; + private float dt; + private long tStart; + private int scrollDir; + + public final static int STOP = -1; + public final static int UP = 0; + public final static int DOWN = 1; + + private float mScrollSpeed; // pixels per ms + + private boolean mScrolling = false; + + public boolean isScrolling() { + return mScrolling; + } + + public int getScrollDir() { + return mScrolling ? scrollDir : STOP; + } + + public DragScroller() { + } + + public void startScrolling(int dir) { + if (!mScrolling) { + mAbort = false; + mScrolling = true; + tStart = SystemClock.uptimeMillis(); + mPrevTime = tStart; + scrollDir = dir; + post(this); + } + } + + public void stopScrolling(boolean now) { + if (now) { + DragSortListView.this.removeCallbacks(this); + mScrolling = false; + } else { + mAbort = true; + } + } + + @Override + public void run() { + if (mAbort) { + mScrolling = false; + return; + } + + final int first = getFirstVisiblePosition(); + final int last = getLastVisiblePosition(); + final int count = getCount(); + final int padTop = getPaddingTop(); + final int listHeight = getHeight() - padTop - getPaddingBottom(); + + int minY = Math.min(mY, mFloatViewMid + mFloatViewHeightHalf); + int maxY = Math.max(mY, mFloatViewMid - mFloatViewHeightHalf); + + if (scrollDir == UP) { + View v = getChildAt(0); + if (v == null) { + mScrolling = false; + return; + } else { + if (first == 0 && v.getTop() == padTop) { + mScrolling = false; + return; + } + } + mScrollSpeed = mScrollProfile.getSpeed((mUpScrollStartYF - maxY) + / mDragUpScrollHeight, mPrevTime); + } else { + View v = getChildAt(last - first); + if (v == null) { + mScrolling = false; + return; + } else { + if (last == count - 1 && v.getBottom() <= listHeight + padTop) { + mScrolling = false; + return; + } + } + mScrollSpeed = -mScrollProfile.getSpeed((minY - mDownScrollStartYF) + / mDragDownScrollHeight, mPrevTime); + } + + mCurrTime = SystemClock.uptimeMillis(); + dt = (float) (mCurrTime - mPrevTime); + + // dy is change in View position of a list item; i.e. positive dy + // means user is scrolling up (list item moves down the screen, + // remember + // y=0 is at top of View). + dy = (int) Math.round(mScrollSpeed * dt); + + int movePos; + if (dy >= 0) { + dy = Math.min(listHeight, dy); + movePos = first; + } else { + dy = Math.max(-listHeight, dy); + movePos = last; + } + + final View moveItem = getChildAt(movePos - first); + int top = moveItem.getTop() + dy; + + if (movePos == 0 && top > padTop) { + top = padTop; + } + + // always do scroll + mBlockLayoutRequests = true; + + setSelectionFromTop(movePos, top - padTop); + DragSortListView.this.layoutChildren(); + invalidate(); + + mBlockLayoutRequests = false; + + // scroll means relative float View movement + doDragFloatView(movePos, moveItem, false); + + mPrevTime = mCurrTime; + + post(this); + } + } + + // TODO: Bluuurgh... switch to SharedPreferences + private class DragSortTracker { + StringBuilder mBuilder = new StringBuilder(); + + File mFile; + + private int mNumInBuffer = 0; + private int mNumFlushes = 0; + + private boolean mTracking = false; + + public DragSortTracker() { + File root = Environment.getExternalStorageDirectory(); + mFile = new File(root, "dslv_state.txt"); + + if (!mFile.exists()) { + try { + mFile.createNewFile(); + Log.d("mobeta", "file created"); + } catch (IOException e) { + Log.w("mobeta", "Could not create dslv_state.txt"); + Log.d("mobeta", e.getMessage()); + } + } + + } + + public void startTracking() { + mBuilder.append("\n"); + mNumFlushes = 0; + mTracking = true; + } + + public void appendState() { + if (!mTracking) { + return; + } + + mBuilder.append("\n"); + final int children = getChildCount(); + final int first = getFirstVisiblePosition(); + mBuilder.append(" "); + for (int i = 0; i < children; ++i) { + mBuilder.append(first + i).append(","); + } + mBuilder.append("\n"); + + mBuilder.append(" "); + for (int i = 0; i < children; ++i) { + mBuilder.append(getChildAt(i).getTop()).append(","); + } + mBuilder.append("\n"); + mBuilder.append(" "); + for (int i = 0; i < children; ++i) { + mBuilder.append(getChildAt(i).getBottom()).append(","); + } + mBuilder.append("\n"); + + mBuilder.append(" ").append(mFirstExpPos).append("\n"); + mBuilder.append(" ") + .append(getItemHeight(mFirstExpPos) - getChildHeight(mFirstExpPos)) + .append("\n"); + mBuilder.append(" ").append(mSecondExpPos).append("\n"); + mBuilder.append(" ") + .append(getItemHeight(mSecondExpPos) - getChildHeight(mSecondExpPos)) + .append("\n"); + mBuilder.append(" ").append(mSrcPos).append("\n"); + mBuilder.append(" ").append(mFloatViewHeight + getDividerHeight()) + .append("\n"); + mBuilder.append(" ").append(getHeight()).append("\n"); + mBuilder.append(" ").append(mLastY).append("\n"); + mBuilder.append(" ").append(mFloatViewMid).append("\n"); + mBuilder.append(" "); + for (int i = 0; i < children; ++i) { + mBuilder.append(getShuffleEdge(first + i, getChildAt(i).getTop())).append(","); + } + mBuilder.append("\n"); + + mBuilder.append("\n"); + mNumInBuffer++; + + if (mNumInBuffer > 1000) { + flush(); + mNumInBuffer = 0; + } + } + + public void flush() { + if (!mTracking) { + return; + } + + // save to file on sdcard + try { + boolean append = true; + if (mNumFlushes == 0) { + append = false; + } + FileWriter writer = new FileWriter(mFile, append); + + writer.write(mBuilder.toString()); + mBuilder.delete(0, mBuilder.length()); + + writer.flush(); + writer.close(); + + mNumFlushes++; + } catch (IOException e) { + // do nothing + } + } + + public void stopTracking() { + if (mTracking) { + mBuilder.append("\n"); + flush(); + mTracking = false; + } + } + + } + +} From 565bfc1cb9994b899e2dc731428f1548424590cc Mon Sep 17 00:00:00 2001 From: kd7uiy Date: Sun, 29 Dec 2013 07:50:06 -0500 Subject: [PATCH 02/18] This is the start of a SortableListPreference class. As it stands right now, the items are displayed correctly, and are sorted, but not saved or loaded correctly. --- demo/AndroidManifest.xml | 101 +-- .../res/layout/list_item_simple_checkable.xml | 22 + .../sort_list_array_dialog_preference.xml | 15 + demo/res/menu/activity_menu.xml | 6 + demo/res/values/preferences_strings.xml | 20 + demo/res/values/strings.xml | 609 +++++++++--------- demo/res/xml/pref_headers.xml | 9 + demo/res/xml/pref_name.xml | 9 + .../com/mobeta/android/demodslv/Launcher.java | 187 +++--- .../demodslv/MainSettingsActivity.java | 227 +++++++ .../demodslv/SortableListPreference.java | 196 ++++++ 11 files changed, 967 insertions(+), 434 deletions(-) create mode 100644 demo/res/layout/list_item_simple_checkable.xml create mode 100644 demo/res/layout/sort_list_array_dialog_preference.xml create mode 100644 demo/res/menu/activity_menu.xml create mode 100644 demo/res/values/preferences_strings.xml create mode 100644 demo/res/xml/pref_headers.xml create mode 100644 demo/res/xml/pref_name.xml create mode 100644 demo/src/com/mobeta/android/demodslv/MainSettingsActivity.java create mode 100644 demo/src/com/mobeta/android/demodslv/SortableListPreference.java diff --git a/demo/AndroidManifest.xml b/demo/AndroidManifest.xml index 14b68e1..65a53b4 100644 --- a/demo/AndroidManifest.xml +++ b/demo/AndroidManifest.xml @@ -1,50 +1,51 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/res/layout/list_item_simple_checkable.xml b/demo/res/layout/list_item_simple_checkable.xml new file mode 100644 index 0000000..4d08069 --- /dev/null +++ b/demo/res/layout/list_item_simple_checkable.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/demo/res/layout/sort_list_array_dialog_preference.xml b/demo/res/layout/sort_list_array_dialog_preference.xml new file mode 100644 index 0000000..8b4f702 --- /dev/null +++ b/demo/res/layout/sort_list_array_dialog_preference.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/demo/res/menu/activity_menu.xml b/demo/res/menu/activity_menu.xml new file mode 100644 index 0000000..c9d1644 --- /dev/null +++ b/demo/res/menu/activity_menu.xml @@ -0,0 +1,6 @@ + +

+ + + + diff --git a/demo/res/values/preferences_strings.xml b/demo/res/values/preferences_strings.xml new file mode 100644 index 0000000..5b8f032 --- /dev/null +++ b/demo/res/values/preferences_strings.xml @@ -0,0 +1,20 @@ + + + + Value 1 + Value 2 + Value 3 + + + Bob + George + Fred + + + Fred + Bob + George + + Name Order Preference + Test + diff --git a/demo/res/values/strings.xml b/demo/res/values/strings.xml index 533fc7c..30243d4 100644 --- a/demo/res/values/strings.xml +++ b/demo/res/values/strings.xml @@ -1,304 +1,305 @@ - - - Drag-Sort Demos - - Basic usage playground - Heteroheight - Warp - Background handle - Sections - CursorAdapter - Multiple-choice mode - Single-choice mode - - - - Use the menu to adjust settings related to item - removal and drag initiation. These options are - provided by the DragSortController class. - - - Demonstrates (tests) drag-sorting when list items - have varying heights. - - - Demonstrates drag-scroll customization; long lists - need fast scrolling. - - - Simple DragSortController customization. In this example, - the drag handle is a background drawable; therefore, the - drag handle hit detection mechanism needs modification. - Also, the floating View is given some flavor. - - - Demonstrates floating View control from a custom - FloatViewManager. Restrict item drags to list sections. - - - Demonstrates usage of the DragSortCursorAdapter class, - which abstracts drag-sorts with a remapping of Cursor - positions to ListView positions. - - - Uses Checkable list items in multiple-choice mode. - - - Uses Checkable list items to allow for selectable radio - buttons in single-choice mode. - - - OK - Cancel - Remove modes - Start-drag modes - Enable/Disable - Add header - Add footer - - Click remove - Fling remove - - - On down - On drag - On long press - - - Enable drag - Enable sort - Enable remove - - - Brad Mehldau - Joshua Redman - Chick Corea - Kurt Rosenwinkel - Miles Davis - Wayne Shorter - Michael Brecker - Herbie Hancock - Joe Zawinul - Brian Blade - Jeff Ballard - Larry Grenadier - Keith Jarrett - McCoy Tyner - Stephon Harris - Mark Turner - - - Largo, Art of the Trio 1-4, Highway Rider, Songs - Elastic, Momentum, Mood Swing, Back East - Light as a Feather, Akoustic Band, My Spanish Heart - Deep Song, Heartcore, Our Secret World, Reflections, The Remedy: Live at the Village Vanguard, The Enemies of Energy, The Next Step - - - - Afghanistan - Albania - Algeria - American Samoa - Andorra - Angola - Anguilla - Antigua and Barbuda - Argentina - Armenia - Australia - Austria - Azerbajan - Bahamas - Bahrain - Bangladesh - Barbados - Belarus - Belgium - Belize - Benin - Bermuda - Bhutan - Bolivia - Bosnia and Herzegovina - Botswana - Brazil - Brunei Darussalam - Bulgaria - Burkina Faso - Burundi - Cambodia - Cameroon - Canada - Chile - China - Colombia - Costa Rica - Cuba - Cyprus - Czech Republic - Democratic Republic Congo - Denmark - Djibouti - Dominican Republic - East Timor - Ecuador - Egypt - El Salvador - England - Eritrea - Estonia - Ethiopia - Faroe Islands - Fiji - Finland - France - French Polynesia - Gambia - Georgia (Sakartvelo) - Germany - Gabon - Ghana - Greece - Greenland - Kalaallit Nunaat - Grenada - Gouadeloupe - Guam - Guatemala - Guernsey - Guyana - Guyane - Haiti - Honduras - Hong Kong - Hrvatska (Croatia) - Hungary - Iceland - India - Indonesia - Iran - Iraq - Ireland - Israel - Italy - Jamaica - Japan - Jordan - Kazakhstan - Kenya - Korea Republic - Kosovo - Kurdistan - Kuwait - Kyrgyzstan - Laos - Latvia - Lebanon - Lesotho - Liberia - Libyan Arab Jamahiriya - Liechtenstein - Lithuania - Luxembourg - Macau - Macedonia - Malawi - Malaysia - Mali - Malta - Marshall Islands - Mauritania - Martinique - Mauritius - Mexico - Micronesia - Moldova - Monaco - Mongolia - Morocco - Mozambique - Namibia - Nepal - Netherlands - Netherlands Antilles - New Caledonia - New Zealand (Aotearoa) - Nicaragua - Nigeria - Niue - Norfolk Island - Northern Ireland - Northern Mariana Islands - Norway - Oman - Pakistan - Palau - Palestina - Panama - Papua New Guinea - Paraguay - Peru - Philippines - Portugal - Puerto Rico - Qatar - Reunion - Romania - Russian Federation (AsianPart) - Russian Federation (European Part) - Rwanda - Saint Kitts and Nevis - Saint Vincent and the Grenadines - Samoa (American Samoa) - Samoa (Western Samoa) - San Marino - Saudi Arabia - Scotland - Senegal - Seychelles - Sierra Leone - Singapore - Slovakia - Slovenia - Solomon Islands - Somalia - Somaliland - South Africa - Spain - Sri Lanka - Sudan - Suriname - Svalbard and Jan Mayen - Swaziland - Sweden - Switzerland - Syrian Arab Republic - Taiwan - Tanzania - Thailand - Tibet - Togo - Tonga - Trinidad and Tobago - Tunisia - Turkey - Turkmenistan - Turks and Caicos Islands - Uganda - Ukraine - United Arab Emirates - United Kingdom - Uruguay - USA - Uzbekistan - Vatican City State - Holy See - Venezuela - Viet Nam - Virgin Islands (British) - Virgin Islands (U.S.) - Wales - Yemen - Yugoslavia - Zambia - Zimbabwe - - + + + Drag-Sort Demos + + Basic usage playground + Heteroheight + Warp + Background handle + Sections + CursorAdapter + Multiple-choice mode + Single-choice mode + + + + Use the menu to adjust settings related to item + removal and drag initiation. These options are + provided by the DragSortController class. + + + Demonstrates (tests) drag-sorting when list items + have varying heights. + + + Demonstrates drag-scroll customization; long lists + need fast scrolling. + + + Simple DragSortController customization. In this example, + the drag handle is a background drawable; therefore, the + drag handle hit detection mechanism needs modification. + Also, the floating View is given some flavor. + + + Demonstrates floating View control from a custom + FloatViewManager. Restrict item drags to list sections. + + + Demonstrates usage of the DragSortCursorAdapter class, + which abstracts drag-sorts with a remapping of Cursor + positions to ListView positions. + + + Uses Checkable list items in multiple-choice mode. + + + Uses Checkable list items to allow for selectable radio + buttons in single-choice mode. + + + OK + Cancel + Remove modes + Start-drag modes + Enable/Disable + Add header + Add footer + + Click remove + Fling remove + + + On down + On drag + On long press + + + Enable drag + Enable sort + Enable remove + + + Brad Mehldau + Joshua Redman + Chick Corea + Kurt Rosenwinkel + Miles Davis + Wayne Shorter + Michael Brecker + Herbie Hancock + Joe Zawinul + Brian Blade + Jeff Ballard + Larry Grenadier + Keith Jarrett + McCoy Tyner + Stephon Harris + Mark Turner + + + Largo, Art of the Trio 1-4, Highway Rider, Songs + Elastic, Momentum, Mood Swing, Back East + Light as a Feather, Akoustic Band, My Spanish Heart + Deep Song, Heartcore, Our Secret World, Reflections, The Remedy: Live at the Village Vanguard, The Enemies of Energy, The Next Step + + + + Afghanistan + Albania + Algeria + American Samoa + Andorra + Angola + Anguilla + Antigua and Barbuda + Argentina + Armenia + Australia + Austria + Azerbajan + Bahamas + Bahrain + Bangladesh + Barbados + Belarus + Belgium + Belize + Benin + Bermuda + Bhutan + Bolivia + Bosnia and Herzegovina + Botswana + Brazil + Brunei Darussalam + Bulgaria + Burkina Faso + Burundi + Cambodia + Cameroon + Canada + Chile + China + Colombia + Costa Rica + Cuba + Cyprus + Czech Republic + Democratic Republic Congo + Denmark + Djibouti + Dominican Republic + East Timor + Ecuador + Egypt + El Salvador + England + Eritrea + Estonia + Ethiopia + Faroe Islands + Fiji + Finland + France + French Polynesia + Gambia + Georgia (Sakartvelo) + Germany + Gabon + Ghana + Greece + Greenland - Kalaallit Nunaat + Grenada + Gouadeloupe + Guam + Guatemala + Guernsey + Guyana + Guyane + Haiti + Honduras + Hong Kong + Hrvatska (Croatia) + Hungary + Iceland + India + Indonesia + Iran + Iraq + Ireland + Israel + Italy + Jamaica + Japan + Jordan + Kazakhstan + Kenya + Korea Republic + Kosovo + Kurdistan + Kuwait + Kyrgyzstan + Laos + Latvia + Lebanon + Lesotho + Liberia + Libyan Arab Jamahiriya + Liechtenstein + Lithuania + Luxembourg + Macau + Macedonia + Malawi + Malaysia + Mali + Malta + Marshall Islands + Mauritania + Martinique + Mauritius + Mexico + Micronesia + Moldova + Monaco + Mongolia + Morocco + Mozambique + Namibia + Nepal + Netherlands + Netherlands Antilles + New Caledonia + New Zealand (Aotearoa) + Nicaragua + Nigeria + Niue + Norfolk Island + Northern Ireland + Northern Mariana Islands + Norway + Oman + Pakistan + Palau + Palestina + Panama + Papua New Guinea + Paraguay + Peru + Philippines + Portugal + Puerto Rico + Qatar + Reunion + Romania + Russian Federation (AsianPart) + Russian Federation (European Part) + Rwanda + Saint Kitts and Nevis + Saint Vincent and the Grenadines + Samoa (American Samoa) + Samoa (Western Samoa) + San Marino + Saudi Arabia + Scotland + Senegal + Seychelles + Sierra Leone + Singapore + Slovakia + Slovenia + Solomon Islands + Somalia + Somaliland + South Africa + Spain + Sri Lanka + Sudan + Suriname + Svalbard and Jan Mayen + Swaziland + Sweden + Switzerland + Syrian Arab Republic + Taiwan + Tanzania + Thailand + Tibet + Togo + Tonga + Trinidad and Tobago + Tunisia + Turkey + Turkmenistan + Turks and Caicos Islands + Uganda + Ukraine + United Arab Emirates + United Kingdom + Uruguay + USA + Uzbekistan + Vatican City State - Holy See + Venezuela + Viet Nam + Virgin Islands (British) + Virgin Islands (U.S.) + Wales + Yemen + Yugoslavia + Zambia + Zimbabwe + + Settings + diff --git a/demo/res/xml/pref_headers.xml b/demo/res/xml/pref_headers.xml new file mode 100644 index 0000000..7746d7f --- /dev/null +++ b/demo/res/xml/pref_headers.xml @@ -0,0 +1,9 @@ + + + + +
+ diff --git a/demo/res/xml/pref_name.xml b/demo/res/xml/pref_name.xml new file mode 100644 index 0000000..e34f2c6 --- /dev/null +++ b/demo/res/xml/pref_name.xml @@ -0,0 +1,9 @@ + + + diff --git a/demo/src/com/mobeta/android/demodslv/Launcher.java b/demo/src/com/mobeta/android/demodslv/Launcher.java index fe0ef0d..8516a6b 100644 --- a/demo/src/com/mobeta/android/demodslv/Launcher.java +++ b/demo/src/com/mobeta/android/demodslv/Launcher.java @@ -1,80 +1,107 @@ -package com.mobeta.android.demodslv; - -import android.app.ListActivity; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; - -import java.util.ArrayList; -import java.util.Arrays; - - -public class Launcher extends ListActivity { - - private ArrayList mActivities = null; - - private String[] mActTitles; - private String[] mActDescs; - - /** Called when the activity is first created. */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.launcher); - - try { - PackageInfo pi = getPackageManager().getPackageInfo( - "com.mobeta.android.demodslv", PackageManager.GET_ACTIVITIES); - - mActivities = new ArrayList(Arrays.asList(pi.activities)); - String ourName = getClass().getName(); - for (int i = 0; i < mActivities.size(); ++i) { - if (ourName.equals(mActivities.get(i).name)) { - mActivities.remove(i); - break; - } - } - } catch (PackageManager.NameNotFoundException e) { - // Do nothing. Adapter will be empty. - } - - mActTitles = getResources().getStringArray(R.array.activity_titles); - mActDescs = getResources().getStringArray(R.array.activity_descs); - - setListAdapter(new MyAdapter()); - } - - @Override - protected void onListItemClick(ListView l, View v, int position, long id) { - Intent intent = new Intent(); - intent.setClassName(this, mActivities.get(position).name); - startActivity(intent); - } - - private class MyAdapter extends ArrayAdapter { - MyAdapter() { - super(Launcher.this, R.layout.launcher_item, R.id.activity_title, mActivities); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View v = super.getView(position, convertView, parent); - - TextView title = (TextView) v.findViewById(R.id.activity_title); - TextView desc = (TextView) v.findViewById(R.id.activity_desc); - - title.setText(mActTitles[position]); - desc.setText(mActDescs[position]); - return v; - } - - } - -} +package com.mobeta.android.demodslv; + +import android.app.ListActivity; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Arrays; + +public class Launcher extends ListActivity { + + private ArrayList mActivities = null; + + private String[] mActTitles; + private String[] mActDescs; + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.launcher); + + try { + PackageInfo pi = getPackageManager().getPackageInfo( + "com.mobeta.android.demodslv", PackageManager.GET_ACTIVITIES); + + mActivities = new ArrayList(Arrays.asList(pi.activities)); + String[] excludeList = new String[]{getClass().getName(), + MainSettingsActivity.class.getName()}; + for (int i = 0; i < mActivities.size(); ++i) { + for (String name: excludeList) + { + if (name.equals(mActivities.get(i).name)) { + mActivities.remove(i); + break; + } + } + } + } catch (PackageManager.NameNotFoundException e) { + // Do nothing. Adapter will be empty. + } + + mActTitles = getResources().getStringArray(R.array.activity_titles); + mActDescs = getResources().getStringArray(R.array.activity_descs); + + setListAdapter(new MyAdapter()); + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + Intent intent = new Intent(); + intent.setClassName(this, mActivities.get(position).name); + startActivity(intent); + } + + private class MyAdapter extends ArrayAdapter { + MyAdapter() { + super(Launcher.this, R.layout.launcher_item, R.id.activity_title, mActivities); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View v = super.getView(position, convertView, parent); + + TextView title = (TextView) v.findViewById(R.id.activity_title); + TextView desc = (TextView) v.findViewById(R.id.activity_desc); + + title.setText(mActTitles[position]); + desc.setText(mActDescs[position]); + return v; + } + + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.activity_menu, menu); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + Intent intent; + switch (item.getItemId()) { + case R.id.menu_settings: + intent =new Intent(this,MainSettingsActivity.class); + startActivity(intent); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + +} diff --git a/demo/src/com/mobeta/android/demodslv/MainSettingsActivity.java b/demo/src/com/mobeta/android/demodslv/MainSettingsActivity.java new file mode 100644 index 0000000..15b5b5f --- /dev/null +++ b/demo/src/com/mobeta/android/demodslv/MainSettingsActivity.java @@ -0,0 +1,227 @@ +package com.mobeta.android.demodslv; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; +import android.support.v4.app.NavUtils; +import android.view.MenuItem; + +import java.util.List; + + +/** + * A {@link PreferenceActivity} that presents a set of application settings. On + * handset devices, settings are presented as a single list. On tablets, + * settings are split by category, with category headers shown to the left of + * the list of settings. + *

+ * See + * Android Design: Settings for design guidelines and the Settings + * API Guide for more information on developing a Settings UI. + */ +public class MainSettingsActivity extends PreferenceActivity{ + private static final String TAG="Settings"; + /** + * Determines whether to always show the simplified settings UI, where + * settings are presented in a single list. When false, settings are shown + * as a master/detail two-pane view on tablets. When true, a single pane is + * shown on tablets. + */ + private static final boolean ALWAYS_SIMPLE_PREFS = false; + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + setupSimplePreferencesScreen(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) + getActionBar().setDisplayHomeAsUpEnabled(true); + } + + /** + * Shows the simplified settings UI if the device configuration if the + * device configuration dictates that a simplified, single-pane UI should be + * shown. + */ + @SuppressWarnings("deprecation") + private void setupSimplePreferencesScreen() { + if (!isSimplePreferences(this)) { + return; + } + + // In the simplified UI, fragments are not used at all and we instead + // use the older PreferenceActivity APIs. + + // Add 'general' preferences. + addPreferencesFromResource(R.xml.pref_name); + + + // Bind the summaries of EditText/List/Dialog/Ringtone preferences to + // their values. When their values change, their summaries are updated + // to reflect the new value, per the Android Design guidelines. + bindPreferenceSummaryToValue(findPreference("name_order")); + + } + + /** {@inheritDoc} */ + @Override + public boolean onIsMultiPane() { + return isLargeTablet(this) && !isSimplePreferences(this); + } + + /** + * Helper method to determine if the device has an extra-large screen. For + * example, 10" tablets are extra-large. + */ + private static boolean isLargeTablet(Context context) { + return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE; + } + + /** + * Determines whether the simplified settings UI should be shown. This is + * true if this is forced via {@link #ALWAYS_SIMPLE_PREFS}, or the device + * doesn't have newer APIs like {@link PreferenceFragment}, or the device + * doesn't have an extra-large screen. In these cases, a single-pane + * "simplified" settings UI should be shown. + */ + private static boolean isSimplePreferences(Context context) { + return ALWAYS_SIMPLE_PREFS + || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB; + } + + /** {@inheritDoc} */ + @Override + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void onBuildHeaders(List

target) { + if (!isSimplePreferences(this)) { + loadHeadersFromResource(R.xml.pref_headers, target); + } + } + + /** + * A preference value change listener that updates the preference's summary + * to reflect its new value. + */ + private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object value) { + String stringValue = value.toString(); + + if (preference instanceof ListPreference) { + // For list preferences, look up the correct display value in + // the preference's 'entries' list. + ListPreference listPreference = (ListPreference) preference; + int index = listPreference.findIndexOfValue(stringValue); + + // Set the summary to reflect the new value. + preference + .setSummary(index >= 0 ? listPreference.getEntries()[index] + : null); + } else { + // For all other preferences, set the summary to the value's + // simple string representation. + preference.setSummary(stringValue); + } + return true; + } + }; + + /** + * Binds a preference's summary to its value. More specifically, when the + * preference's value is changed, its summary (line of text below the + * preference title) is updated to reflect the value. The summary is also + * immediately updated upon calling this method. The exact display format is + * dependent on the type of preference. + * + * @see #sBindPreferenceSummaryToValueListener + */ + private static void bindPreferenceSummaryToValue(Preference preference) { + // Set the listener to watch for value changes. + preference + .setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); + + if (preference instanceof CheckBoxPreference) + { + // Trigger the listener immediately with the preference's + // current value. + sBindPreferenceSummaryToValueListener.onPreferenceChange( + preference, + PreferenceManager.getDefaultSharedPreferences( + preference.getContext()).getBoolean(preference.getKey(),false)); + } + else if (preference instanceof EditTextPreference) + { + + // Trigger the listener immediately with the preference's + // current value. + sBindPreferenceSummaryToValueListener.onPreferenceChange( + preference, + PreferenceManager.getDefaultSharedPreferences( + preference.getContext()).getString(preference.getKey(),"")); + } + else if (preference instanceof SortableListPreference) + { + // Trigger the listener immediately with the preference's + // current value. + sBindPreferenceSummaryToValueListener.onPreferenceChange( + preference, + PreferenceManager.getDefaultSharedPreferences( + preference.getContext()).getString(preference.getKey(),"")); + } + else + { + throw new IllegalArgumentException("Attempting to bind to unknown type of preference!"); + } + } + + /** + * This fragment shows location update preferences only. It is used when the + * activity is showing a two-pane settings UI. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static class NameSelectionPreferenceFragment extends + PreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.pref_name); + + // Bind the summaries of EditText/List/Dialog/Ringtone preferences + // to their values. When their values change, their summaries are + // updated to reflect the new value, per the Android Design + // guidelines. + + bindPreferenceSummaryToValue(findPreference("name_order")); + } + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + protected boolean isValidFragment (String fragmentName) + { + if(MainSettingsActivity.class.getName().equals(fragmentName) || + NameSelectionPreferenceFragment.class.getName().equals(fragmentName)) + return true; + return false; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + default: + return super.onOptionsItemSelected(item); + } + } +} diff --git a/demo/src/com/mobeta/android/demodslv/SortableListPreference.java b/demo/src/com/mobeta/android/demodslv/SortableListPreference.java new file mode 100644 index 0000000..9a46504 --- /dev/null +++ b/demo/src/com/mobeta/android/demodslv/SortableListPreference.java @@ -0,0 +1,196 @@ +package com.mobeta.android.demodslv; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import com.mobeta.android.dslv.DragSortListView; +import com.mobeta.android.dslv.DragSortListView.DropListener; + +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.DialogInterface.OnMultiChoiceClickListener; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.preference.ListPreference; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; + +public class SortableListPreference extends ListPreference { + private static final String TAG=SortableListPreference.class.getName(); + + DragSortListView mListView; + ArrayAdapter mAdapter; + + private static final String DEFAULT_SEPARATOR = "\u0001\u0007\u001D\u0007\u0001"; + private boolean[] entryChecked = new boolean[getEntries().length];; + String separator; + + public static CharSequence[] decodeValue(String input) + { + return decodeValue(input,DEFAULT_SEPARATOR); + } + + public static CharSequence[] decodeValue(String input,String separator) + { + if (input==null) + return null; + return input.split(separator); + } + + public SortableListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + separator = DEFAULT_SEPARATOR; + setPersistent(false); + setDialogLayoutResource(R.layout.sort_list_array_dialog_preference); + } + + @Override + protected void onBindDialogView(View view) + { + super.onBindDialogView(view); + ViewGroup group=(ViewGroup)view; + //Update the view with the values of the preference + SharedPreferences prefs = getSharedPreferences(); + mListView= (DragSortListView) view.findViewById(android.R.id.list); + String value=prefs.getString(getKey(),null); + CharSequence[] order=decodeValue(value,separator); + if (order==null) + { + mAdapter =new ArrayAdapter(mListView.getContext(),android.R.layout.simple_list_item_1); + } + else + { + mAdapter =new ArrayAdapter(mListView.getContext(),android.R.layout.simple_list_item_1,order); + } + Log.v(TAG,"Setting adapter"); + mListView.setAdapter(mAdapter); + } + + @Override + public void onDialogClosed(boolean positiveResult) + { + super.onDialogClosed(positiveResult); + } + + private void setValueAndEvent(String value) { + if (callChangeListener(decodeValue(value,separator))) { + persistString(value); + } + } + + @Override + protected Object onGetDefaultValue(TypedArray typedArray, int index) { + return typedArray.getTextArray(index); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, + Object rawDefaultValue) { + String value = null; + CharSequence[] defaultValue; + if (rawDefaultValue == null) { + defaultValue = new CharSequence[0]; + } else { + defaultValue = (CharSequence[]) rawDefaultValue; + } + List joined = Arrays.asList(defaultValue); + String joinedDefaultValue = join(joined, separator); + if (restoreValue) { + value = getPersistedString(joinedDefaultValue); + } else { + value = joinedDefaultValue; + } + + setSummary(prepareSummary(Arrays.asList(decodeValue(value,separator)))); + setValueAndEvent(value); + } + + private String prepareSummary(List joined) { + List titles = new ArrayList(); + CharSequence[] entryTitle = getEntries(); + CharSequence[] entryValues = getEntryValues(); + int ix = 0; + for (CharSequence value : entryValues) { + if (joined.contains(value)) { + titles.add((String) entryTitle[ix]); + } + ix += 1; + } + return join(titles, ", "); + } + + @Override + protected void onPrepareDialogBuilder(Builder builder) { + CharSequence[] entries = getEntries(); + CharSequence[] entryValues = getEntryValues(); + if (entries == null || entryValues == null + || entries.length != entryValues.length) { + throw new IllegalStateException( + "SortableListPreference requires an entries array and an entryValues " + + "array which are both the same length"); + } + + restoreEntries(); + + for (CharSequence entry:entries) + mAdapter.add(entry); + mListView.setDropListener(new DropListener() + { + @Override + public void drop(int from, int to) { + + CharSequence item = mAdapter.getItem(from); +// Log.v(TAG,"Moving item "+item+" from "+from+" to "+to); + + mAdapter.remove(item); + mAdapter.insert(item, to); + mAdapter.notifyDataSetChanged(); + } + + }); + } + + private void restoreEntries() { + CharSequence[] entryValues = getEntryValues(); + + // Explode the string read in sharedpreferences + CharSequence[] vals = decodeValue(getValue(),separator); + + if (vals != null) { + List valuesList = Arrays.asList(vals); + for (int i = 0; i < entryValues.length; i++) { + CharSequence entry = entryValues[i]; + entryChecked[i] = valuesList.contains(entry); + } + } + } + + /** + * Joins array of object to single string by separator + * + * Credits to kurellajunior on this post + * http://snippets.dzone.com/posts/show/91 + * + * @param iterable + * any kind of iterable ex.: ["a", "b", "c"] + * @param separator + * separetes entries ex.: "," + * @return joined string ex.: "a,b,c" + */ + protected static String join(Iterable iterable, String separator) { + Iterator oIter; + if (iterable == null || (!(oIter = iterable.iterator()).hasNext())) + return ""; + StringBuilder oBuilder = new StringBuilder(String.valueOf(oIter.next())); + while (oIter.hasNext()) + oBuilder.append(separator).append(oIter.next()); + return oBuilder.toString(); + } +} From 3d3a9f85222bb11202811d26ed0cf5c4b06a1227 Mon Sep 17 00:00:00 2001 From: kd7uiy Date: Sun, 29 Dec 2013 09:16:15 -0500 Subject: [PATCH 03/18] Preferences now are being preserved correctly. Changes the names to be more descriptive. --- demo/res/values/preferences_strings.xml | 14 +- .../demodslv/SortableListPreference.java | 134 +++++++++++++----- 2 files changed, 106 insertions(+), 42 deletions(-) diff --git a/demo/res/values/preferences_strings.xml b/demo/res/values/preferences_strings.xml index 5b8f032..3776d1b 100644 --- a/demo/res/values/preferences_strings.xml +++ b/demo/res/values/preferences_strings.xml @@ -1,19 +1,19 @@ - Value 1 - Value 2 - Value 3 - - Bob - George Fred + George + + + Bob Menwaldo + Fred Frankfurd + George Stephanopolis + George Fred Bob - George Name Order Preference Test diff --git a/demo/src/com/mobeta/android/demodslv/SortableListPreference.java b/demo/src/com/mobeta/android/demodslv/SortableListPreference.java index 9a46504..3028d4e 100644 --- a/demo/src/com/mobeta/android/demodslv/SortableListPreference.java +++ b/demo/src/com/mobeta/android/demodslv/SortableListPreference.java @@ -1,3 +1,34 @@ +/* + * Original Source: https://github.com/kd7uiy/drag-sort-listview + * + * The MIT License (MIT) + * + * Copyright (c) 2013 The Making of a Ham, http://www.kd7uiy.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Code snippets copied from the following sources: + * https://gist.github.com/cardil/4754571 + * + * + */ + package com.mobeta.android.demodslv; import java.util.ArrayList; @@ -10,16 +41,12 @@ import android.app.AlertDialog.Builder; import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.content.DialogInterface.OnMultiChoiceClickListener; import android.content.SharedPreferences; import android.content.res.TypedArray; import android.preference.ListPreference; import android.util.AttributeSet; import android.util.Log; import android.view.View; -import android.view.ViewGroup; import android.widget.ArrayAdapter; public class SortableListPreference extends ListPreference { @@ -29,7 +56,6 @@ public class SortableListPreference extends ListPreference { ArrayAdapter mAdapter; private static final String DEFAULT_SEPARATOR = "\u0001\u0007\u001D\u0007\u0001"; - private boolean[] entryChecked = new boolean[getEntries().length];; String separator; public static CharSequence[] decodeValue(String input) @@ -55,7 +81,6 @@ public SortableListPreference(Context context, AttributeSet attrs) { protected void onBindDialogView(View view) { super.onBindDialogView(view); - ViewGroup group=(ViewGroup)view; //Update the view with the values of the preference SharedPreferences prefs = getSharedPreferences(); mListView= (DragSortListView) view.findViewById(android.R.id.list); @@ -69,19 +94,31 @@ protected void onBindDialogView(View view) { mAdapter =new ArrayAdapter(mListView.getContext(),android.R.layout.simple_list_item_1,order); } - Log.v(TAG,"Setting adapter"); mListView.setAdapter(mAdapter); } @Override - public void onDialogClosed(boolean positiveResult) - { - super.onDialogClosed(positiveResult); + protected void onDialogClosed(boolean positiveResult) { + List values = new ArrayList(); + + CharSequence[] entryValues = getEntryValues(); + if (positiveResult && entryValues != null) { + for (int i = 0; i < entryValues.length; i++) { + String val = (String) mAdapter.getItem(i); + + values.add(entryValues[getValueTitleIndex(val)]); + } + + String value = join(values, separator); + Log.v(TAG,"Closing: Value="+value); + setSummary(prepareSummary(values)); + setValueAndEvent(value); + } } private void setValueAndEvent(String value) { if (callChangeListener(decodeValue(value,separator))) { - persistString(value); + setValue(value); } } @@ -108,6 +145,7 @@ protected void onSetInitialValue(boolean restoreValue, value = joinedDefaultValue; } + Log.v(TAG,"Initial Value="+value); setSummary(prepareSummary(Arrays.asList(decodeValue(value,separator)))); setValueAndEvent(value); } @@ -115,15 +153,49 @@ protected void onSetInitialValue(boolean restoreValue, private String prepareSummary(List joined) { List titles = new ArrayList(); CharSequence[] entryTitle = getEntries(); + for (CharSequence item : joined) { + int ix=getValueIndex(item); + titles.add((String) entryTitle[ix]); + } + return join(titles, ", "); + } + + public int getValueIndex(CharSequence item) + { CharSequence[] entryValues = getEntryValues(); - int ix = 0; - for (CharSequence value : entryValues) { - if (joined.contains(value)) { - titles.add((String) entryTitle[ix]); + int ix=0; + boolean found=false; + for (CharSequence value:entryValues) + { + if (value.equals(item)) + { + found=true; + break; } - ix += 1; + ix+=1; } - return join(titles, ", "); + if (!found) + throw new IllegalArgumentException(item+" not found in value list"); + return ix; + } + + public int getValueTitleIndex(CharSequence item) + { + CharSequence[] entries = getEntries(); + int ix=0; + boolean found=false; + for (CharSequence value:entries) + { + if (value.equals(item)) + { + found=true; + break; + } + ix+=1; + } + if (!found) + throw new IllegalArgumentException(item+" not found in value title list"); + return ix; } @Override @@ -137,39 +209,31 @@ protected void onPrepareDialogBuilder(Builder builder) { + "array which are both the same length"); } - restoreEntries(); - - for (CharSequence entry:entries) - mAdapter.add(entry); + CharSequence[] restoredValues=restoreEntries(); + Log.v(TAG,"restoredValue="+restoredValues); + for (CharSequence value:restoredValues) + { + mAdapter.add(entries[getValueIndex(value)]); + } mListView.setDropListener(new DropListener() { @Override public void drop(int from, int to) { - CharSequence item = mAdapter.getItem(from); // Log.v(TAG,"Moving item "+item+" from "+from+" to "+to); mAdapter.remove(item); mAdapter.insert(item, to); mAdapter.notifyDataSetChanged(); + } }); } - private void restoreEntries() { - CharSequence[] entryValues = getEntryValues(); - - // Explode the string read in sharedpreferences - CharSequence[] vals = decodeValue(getValue(),separator); - - if (vals != null) { - List valuesList = Arrays.asList(vals); - for (int i = 0; i < entryValues.length; i++) { - CharSequence entry = entryValues[i]; - entryChecked[i] = valuesList.contains(entry); - } - } + private CharSequence[] restoreEntries() { + CharSequence[] orderedList=decodeValue(getValue(),separator); + return orderedList; } /** From bd70b3adc873885cfd5b70076727a751ac10ab69 Mon Sep 17 00:00:00 2001 From: kd7uiy Date: Sun, 29 Dec 2013 09:17:48 -0500 Subject: [PATCH 04/18] Removed debug code. --- .../mobeta/android/demodslv/SortableListPreference.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/demo/src/com/mobeta/android/demodslv/SortableListPreference.java b/demo/src/com/mobeta/android/demodslv/SortableListPreference.java index 3028d4e..f7dade9 100644 --- a/demo/src/com/mobeta/android/demodslv/SortableListPreference.java +++ b/demo/src/com/mobeta/android/demodslv/SortableListPreference.java @@ -45,7 +45,6 @@ import android.content.res.TypedArray; import android.preference.ListPreference; import android.util.AttributeSet; -import android.util.Log; import android.view.View; import android.widget.ArrayAdapter; @@ -110,7 +109,6 @@ protected void onDialogClosed(boolean positiveResult) { } String value = join(values, separator); - Log.v(TAG,"Closing: Value="+value); setSummary(prepareSummary(values)); setValueAndEvent(value); } @@ -145,7 +143,6 @@ protected void onSetInitialValue(boolean restoreValue, value = joinedDefaultValue; } - Log.v(TAG,"Initial Value="+value); setSummary(prepareSummary(Arrays.asList(decodeValue(value,separator)))); setValueAndEvent(value); } @@ -210,7 +207,6 @@ protected void onPrepareDialogBuilder(Builder builder) { } CharSequence[] restoredValues=restoreEntries(); - Log.v(TAG,"restoredValue="+restoredValues); for (CharSequence value:restoredValues) { mAdapter.add(entries[getValueIndex(value)]); @@ -220,8 +216,7 @@ protected void onPrepareDialogBuilder(Builder builder) { @Override public void drop(int from, int to) { CharSequence item = mAdapter.getItem(from); -// Log.v(TAG,"Moving item "+item+" from "+from+" to "+to); - + mAdapter.remove(item); mAdapter.insert(item, to); mAdapter.notifyDataSetChanged(); From 51f6b66a749b273d920284f9c6654689727a12c3 Mon Sep 17 00:00:00 2001 From: kd7uiy Date: Sun, 29 Dec 2013 17:19:59 -0500 Subject: [PATCH 05/18] Now supports checkable preference lists. The values are not put into the preference if not checked. --- demo/res/layout/list_item_click_remove.xml | 56 +++---- .../res/layout/list_item_simple_checkable.xml | 1 + .../sort_list_array_dialog_preference.xml | 3 + demo/res/values/strings.xml | 2 + .../demodslv/MultipleChoiceListView.java | 130 ++++++++-------- .../demodslv/SortableListPreference.java | 147 ++++++++++++------ 6 files changed, 199 insertions(+), 140 deletions(-) diff --git a/demo/res/layout/list_item_click_remove.xml b/demo/res/layout/list_item_click_remove.xml index ef381b2..6223538 100644 --- a/demo/res/layout/list_item_click_remove.xml +++ b/demo/res/layout/list_item_click_remove.xml @@ -1,27 +1,29 @@ - - - - - - + + + + + + diff --git a/demo/res/layout/list_item_simple_checkable.xml b/demo/res/layout/list_item_simple_checkable.xml index 4d08069..5220bc4 100644 --- a/demo/res/layout/list_item_simple_checkable.xml +++ b/demo/res/layout/list_item_simple_checkable.xml @@ -9,6 +9,7 @@ android:background="@drawable/drag" android:layout_width="wrap_content" android:layout_height="@dimen/item_height" + android:contentDescription="@string/drag_item" android:layout_weight="0" /> diff --git a/demo/res/values/strings.xml b/demo/res/values/strings.xml index 30243d4..6cb6f30 100644 --- a/demo/res/values/strings.xml +++ b/demo/res/values/strings.xml @@ -302,4 +302,6 @@ Zimbabwe Settings + Drag Item + Remove Item diff --git a/demo/src/com/mobeta/android/demodslv/MultipleChoiceListView.java b/demo/src/com/mobeta/android/demodslv/MultipleChoiceListView.java index afdad49..75e506c 100644 --- a/demo/src/com/mobeta/android/demodslv/MultipleChoiceListView.java +++ b/demo/src/com/mobeta/android/demodslv/MultipleChoiceListView.java @@ -1,65 +1,65 @@ -package com.mobeta.android.demodslv; - -import android.app.ListActivity; -import android.os.Bundle; -import android.widget.ArrayAdapter; - -import com.mobeta.android.dslv.DragSortListView; -import com.mobeta.android.dslv.DragSortListView.RemoveListener; - -import java.util.ArrayList; -import java.util.Arrays; - - -public class MultipleChoiceListView extends ListActivity { - - private ArrayAdapter adapter; - - private final DragSortListView.DropListener mDropListener = - new DragSortListView.DropListener() { - @Override - public void drop(int from, int to) { - if (from != to) { - DragSortListView list = getListView(); - String item = adapter.getItem(from); - adapter.remove(item); - adapter.insert(item, to); - list.moveCheckState(from, to); - } - } - }; - - private final RemoveListener mRemoveListener = - new DragSortListView.RemoveListener() { - @Override - public void remove(int which) { - DragSortListView list = getListView(); - String item = adapter.getItem(which); - adapter.remove(item); - list.removeCheckState(which); - } - }; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.checkable_main); - - String[] array = getResources().getStringArray(R.array.jazz_artist_names); - ArrayList arrayList = new ArrayList(Arrays.asList(array)); - - adapter = new ArrayAdapter(this, R.layout.list_item_checkable, R.id.text, arrayList); - - setListAdapter(adapter); - - DragSortListView list = getListView(); - list.setDropListener(mDropListener); - list.setRemoveListener(mRemoveListener); - } - - @Override - public DragSortListView getListView() { - return (DragSortListView) super.getListView(); - } - -} +package com.mobeta.android.demodslv; + +import android.app.ListActivity; +import android.os.Bundle; +import android.widget.ArrayAdapter; + +import com.mobeta.android.dslv.DragSortListView; +import com.mobeta.android.dslv.DragSortListView.RemoveListener; + +import java.util.ArrayList; +import java.util.Arrays; + + +public class MultipleChoiceListView extends ListActivity { + + private ArrayAdapter adapter; + + private final DragSortListView.DropListener mDropListener = + new DragSortListView.DropListener() { + @Override + public void drop(int from, int to) { + if (from != to) { + DragSortListView list = getListView(); + String item = adapter.getItem(from); + adapter.remove(item); + adapter.insert(item, to); + list.moveCheckState(from, to); + } + } + }; + + private final RemoveListener mRemoveListener = + new DragSortListView.RemoveListener() { + @Override + public void remove(int which) { + DragSortListView list = getListView(); + String item = adapter.getItem(which); + adapter.remove(item); + list.removeCheckState(which); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.checkable_main); + + String[] array = getResources().getStringArray(R.array.jazz_artist_names); + ArrayList arrayList = new ArrayList(Arrays.asList(array)); + + adapter = new ArrayAdapter(this, R.layout.list_item_checkable, R.id.text, arrayList); + + setListAdapter(adapter); + + DragSortListView list = getListView(); + list.setDropListener(mDropListener); + list.setRemoveListener(mRemoveListener); + } + + @Override + public DragSortListView getListView() { + return (DragSortListView) super.getListView(); + } + +} diff --git a/demo/src/com/mobeta/android/demodslv/SortableListPreference.java b/demo/src/com/mobeta/android/demodslv/SortableListPreference.java index f7dade9..868128f 100644 --- a/demo/src/com/mobeta/android/demodslv/SortableListPreference.java +++ b/demo/src/com/mobeta/android/demodslv/SortableListPreference.java @@ -1,4 +1,17 @@ /* + * Sortable Preference ListView. Allows for sorting items in a view, + * and selecting which ones to use. + * + * Example Usage (In a preference file) + * + * + * * Original Source: https://github.com/kd7uiy/drag-sort-listview * * The MIT License (MIT) @@ -33,6 +46,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -41,12 +55,18 @@ import android.app.AlertDialog.Builder; import android.content.Context; -import android.content.SharedPreferences; +import android.content.DialogInterface; +import android.content.DialogInterface.OnMultiChoiceClickListener; import android.content.res.TypedArray; import android.preference.ListPreference; import android.util.AttributeSet; +import android.util.Log; import android.view.View; +import android.view.View.OnClickListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; +import android.widget.CheckedTextView; public class SortableListPreference extends ListPreference { private static final String TAG=SortableListPreference.class.getName(); @@ -56,7 +76,17 @@ public class SortableListPreference extends ListPreference { private static final String DEFAULT_SEPARATOR = "\u0001\u0007\u001D\u0007\u0001"; String separator; + + private HashMap entryChecked; + public SortableListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + separator = DEFAULT_SEPARATOR; + setPersistent(false); + setDialogLayoutResource(R.layout.sort_list_array_dialog_preference); + entryChecked = new HashMap(); + } + public static CharSequence[] decodeValue(String input) { return decodeValue(input,DEFAULT_SEPARATOR); @@ -69,31 +99,46 @@ public static CharSequence[] decodeValue(String input,String separator) return input.split(separator); } - public SortableListPreference(Context context, AttributeSet attrs) { - super(context, attrs); - separator = DEFAULT_SEPARATOR; - setPersistent(false); - setDialogLayoutResource(R.layout.sort_list_array_dialog_preference); - } - @Override protected void onBindDialogView(View view) { super.onBindDialogView(view); - //Update the view with the values of the preference - SharedPreferences prefs = getSharedPreferences(); mListView= (DragSortListView) view.findViewById(android.R.id.list); - String value=prefs.getString(getKey(),null); - CharSequence[] order=decodeValue(value,separator); - if (order==null) + mAdapter =new ArrayAdapter(mListView.getContext(),R.layout.list_item_simple_checkable,R.id.text); + mListView.setAdapter(mAdapter); + //This will drop the item in the new location + mListView.setDropListener(new DropListener() { - mAdapter =new ArrayAdapter(mListView.getContext(),android.R.layout.simple_list_item_1); + @Override + public void drop(int from, int to) { + CharSequence item = mAdapter.getItem(from); + mAdapter.remove(item); + mAdapter.insert(item, to); + mAdapter.notifyDataSetChanged(); + //Updates checked states + mListView.moveCheckState(from,to); + } + }); + // Setting the default values happens in onPrepareDialogBuilder + } + + @Override + protected void onPrepareDialogBuilder(Builder builder) { + CharSequence[] entries = getEntries(); + CharSequence[] entryValues = getEntryValues(); + if (entries == null || entryValues == null + || entries.length != entryValues.length) { + throw new IllegalStateException( + "SortableListPreference requires an entries array and an entryValues " + + "array which are both the same length"); } - else + + CharSequence[] restoredValues=restoreEntries(); + for (CharSequence value:restoredValues) { - mAdapter =new ArrayAdapter(mListView.getContext(),android.R.layout.simple_list_item_1,order); + mAdapter.add(entries[getValueIndex(value)]); } - mListView.setAdapter(mAdapter); + } @Override @@ -104,8 +149,12 @@ protected void onDialogClosed(boolean positiveResult) { if (positiveResult && entryValues != null) { for (int i = 0; i < entryValues.length; i++) { String val = (String) mAdapter.getItem(i); - - values.add(entryValues[getValueTitleIndex(val)]); + boolean isChecked=mListView.isItemChecked(i); + Log.v(TAG,"Item "+val+" ischecked="+isChecked); + if (isChecked) + { + values.add(entryValues[getValueTitleIndex(val)]); + } } String value = join(values, separator); @@ -195,40 +244,42 @@ public int getValueTitleIndex(CharSequence item) return ix; } - @Override - protected void onPrepareDialogBuilder(Builder builder) { - CharSequence[] entries = getEntries(); - CharSequence[] entryValues = getEntryValues(); - if (entries == null || entryValues == null - || entries.length != entryValues.length) { - throw new IllegalStateException( - "SortableListPreference requires an entries array and an entryValues " - + "array which are both the same length"); + private CharSequence[] restoreEntries() { + + ArrayList orderedList=new ArrayList(); + + Log.v(TAG,"Initial Value="+getValue()); + + int ix=0; + //Initially populated with all of the values in the determined list. + for (CharSequence value:decodeValue(getValue(),separator)) + { + orderedList.add(value); + mListView.setItemChecked(ix,true); + ix++; } - - CharSequence[] restoredValues=restoreEntries(); - for (CharSequence value:restoredValues) + + //This loop sets the default states, and adds to the name list if not on the list. + for (CharSequence value:getEntryValues()) { - mAdapter.add(entries[getValueIndex(value)]); + entryChecked.put(value,false); + if (!orderedList.contains(value)) + { + orderedList.add(value); + } } - mListView.setDropListener(new DropListener() + for (CharSequence value:orderedList) { - @Override - public void drop(int from, int to) { - CharSequence item = mAdapter.getItem(from); - - mAdapter.remove(item); - mAdapter.insert(item, to); - mAdapter.notifyDataSetChanged(); - + if (entryChecked.containsKey(value)) + { + entryChecked.put(value,true); } - - }); - } - - private CharSequence[] restoreEntries() { - CharSequence[] orderedList=decodeValue(getValue(),separator); - return orderedList; + else + { + throw new IllegalArgumentException("Invalid value "+value+" in key list"); + } + } + return orderedList.toArray(new CharSequence[0]); } /** From c214e3b46953fa46d2c57fc152f326dd9cb7f1d5 Mon Sep 17 00:00:00 2001 From: kd7uiy Date: Sun, 29 Dec 2013 17:23:42 -0500 Subject: [PATCH 06/18] Cleaned up --- .../android/demodslv/SortableListPreference.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/demo/src/com/mobeta/android/demodslv/SortableListPreference.java b/demo/src/com/mobeta/android/demodslv/SortableListPreference.java index 868128f..f266266 100644 --- a/demo/src/com/mobeta/android/demodslv/SortableListPreference.java +++ b/demo/src/com/mobeta/android/demodslv/SortableListPreference.java @@ -55,18 +55,11 @@ import android.app.AlertDialog.Builder; import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnMultiChoiceClickListener; import android.content.res.TypedArray; import android.preference.ListPreference; import android.util.AttributeSet; -import android.util.Log; import android.view.View; -import android.view.View.OnClickListener; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; -import android.widget.CheckedTextView; public class SortableListPreference extends ListPreference { private static final String TAG=SortableListPreference.class.getName(); @@ -150,7 +143,6 @@ protected void onDialogClosed(boolean positiveResult) { for (int i = 0; i < entryValues.length; i++) { String val = (String) mAdapter.getItem(i); boolean isChecked=mListView.isItemChecked(i); - Log.v(TAG,"Item "+val+" ischecked="+isChecked); if (isChecked) { values.add(entryValues[getValueTitleIndex(val)]); @@ -247,9 +239,7 @@ public int getValueTitleIndex(CharSequence item) private CharSequence[] restoreEntries() { ArrayList orderedList=new ArrayList(); - - Log.v(TAG,"Initial Value="+getValue()); - + int ix=0; //Initially populated with all of the values in the determined list. for (CharSequence value:decodeValue(getValue(),separator)) From cbdbbc5bd51c68d22cacf8bb2e4120a24244b11c Mon Sep 17 00:00:00 2001 From: kd7uiy Date: Sun, 29 Dec 2013 18:29:42 -0500 Subject: [PATCH 07/18] Removing setPersistant as it causes undesired behavior. --- demo/src/com/mobeta/android/demodslv/SortableListPreference.java | 1 - 1 file changed, 1 deletion(-) diff --git a/demo/src/com/mobeta/android/demodslv/SortableListPreference.java b/demo/src/com/mobeta/android/demodslv/SortableListPreference.java index f266266..c2fdb5c 100644 --- a/demo/src/com/mobeta/android/demodslv/SortableListPreference.java +++ b/demo/src/com/mobeta/android/demodslv/SortableListPreference.java @@ -75,7 +75,6 @@ public class SortableListPreference extends ListPreference { public SortableListPreference(Context context, AttributeSet attrs) { super(context, attrs); separator = DEFAULT_SEPARATOR; - setPersistent(false); setDialogLayoutResource(R.layout.sort_list_array_dialog_preference); entryChecked = new HashMap(); } From dc84525dfcb488037769fec2fa80cb1eb605ca66 Mon Sep 17 00:00:00 2001 From: kd7uiy Date: Mon, 30 Dec 2013 13:32:14 -0500 Subject: [PATCH 08/18] Converted to Unix formatting --- .../mobeta/android/dslv/DragSortListView.java | 5994 ++++++++--------- 1 file changed, 2997 insertions(+), 2997 deletions(-) diff --git a/library/src/com/mobeta/android/dslv/DragSortListView.java b/library/src/com/mobeta/android/dslv/DragSortListView.java index 7f9cc2b..a90786e 100644 --- a/library/src/com/mobeta/android/dslv/DragSortListView.java +++ b/library/src/com/mobeta/android/dslv/DragSortListView.java @@ -1,2997 +1,2997 @@ -/* - * DragSortListView. - * - * A subclass of the Android ListView component that enables drag - * and drop re-ordering of list items. - * - * Copyright 2012 Carl Bauer - * - * 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.mobeta.android.dslv; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import android.content.Context; -import android.content.res.TypedArray; -import android.database.DataSetObserver; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Point; -import android.graphics.drawable.Drawable; -import android.os.Environment; -import android.os.SystemClock; -import android.util.AttributeSet; -import android.util.Log; -import android.util.SparseBooleanArray; -import android.util.SparseIntArray; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AbsListView; -import android.widget.BaseAdapter; -import android.widget.Checkable; -import android.widget.ListAdapter; -import android.widget.ListView; -import android.widget.WrapperListAdapter; - -/** - * ListView subclass that mediates drag and drop resorting of items. - * - * - * @author heycosmo - * - */ -public class DragSortListView extends ListView { - - - /** - * The View that floats above the ListView and represents - * the dragged item. - */ - private View mFloatView; - - /** - * The float View location. First based on touch location - * and given deltaX and deltaY. Then restricted by callback - * to FloatViewManager.onDragFloatView(). Finally restricted - * by bounds of DSLV. - */ - private Point mFloatLoc = new Point(); - - private Point mTouchLoc = new Point(); - - /** - * The middle (in the y-direction) of the floating View. - */ - private int mFloatViewMid; - - /** - * Flag to make sure float View isn't measured twice - */ - private boolean mFloatViewOnMeasured = false; - - /** - * Watch the Adapter for data changes. Cancel a drag if - * coincident with a change. - */ - private DataSetObserver mObserver; - - /** - * Transparency for the floating View (XML attribute). - */ - private float mFloatAlpha = 1.0f; - private float mCurrFloatAlpha = 1.0f; - - /** - * While drag-sorting, the current position of the floating - * View. If dropped, the dragged item will land in this position. - */ - private int mFloatPos; - - /** - * The first expanded ListView position that helps represent - * the drop slot tracking the floating View. - */ - private int mFirstExpPos; - - /** - * The second expanded ListView position that helps represent - * the drop slot tracking the floating View. This can equal - * mFirstExpPos if there is no slide shuffle occurring; otherwise - * it is equal to mFirstExpPos + 1. - */ - private int mSecondExpPos; - - /** - * Flag set if slide shuffling is enabled. - */ - private boolean mAnimate = false; - - /** - * The user dragged from this position. - */ - private int mSrcPos; - - /** - * Offset (in x) within the dragged item at which the user - * picked it up (or first touched down with the digitalis). - */ - private int mDragDeltaX; - - /** - * Offset (in y) within the dragged item at which the user - * picked it up (or first touched down with the digitalis). - */ - private int mDragDeltaY; - - - /** - * The difference (in x) between screen coordinates and coordinates - * in this view. - */ - //private int mOffsetX; - - /** - * The difference (in y) between screen coordinates and coordinates - * in this view. - */ - //private int mOffsetY; - - /** - * A listener that receives callbacks whenever the floating View - * hovers over a new position. - */ - private DragListener mDragListener; - - /** - * A listener that receives a callback when the floating View - * is dropped. - */ - private DropListener mDropListener; - - /** - * A listener that receives a callback when the floating View - * (or more precisely the originally dragged item) is removed - * by one of the provided gestures. - */ - private RemoveListener mRemoveListener; - - /** - * Enable/Disable item dragging - * - * @attr name dslv:drag_enabled - */ - private boolean mDragEnabled = true; - - /** - * Drag state enum. - */ - private final static int IDLE = 0; - private final static int REMOVING = 1; - private final static int DROPPING = 2; - private final static int STOPPED = 3; - private final static int DRAGGING = 4; - - private int mDragState = IDLE; - - /** - * Height in pixels to which the originally dragged item - * is collapsed during a drag-sort. Currently, this value - * must be greater than zero. - */ - private int mItemHeightCollapsed = 1; - - /** - * Height of the floating View. Stored for the purpose of - * providing the tracking drop slot. - */ - private int mFloatViewHeight; - - /** - * Convenience member. See above. - */ - private int mFloatViewHeightHalf; - - /** - * Save the given width spec for use in measuring children - */ - private int mWidthMeasureSpec = 0; - - /** - * Sample Views ultimately used for calculating the height - * of ListView items that are off-screen. - */ - private View[] mSampleViewTypes = new View[1]; - - /** - * Drag-scroll encapsulator! - */ - private DragScroller mDragScroller; - - /** - * Determines the start of the upward drag-scroll region - * at the top of the ListView. Specified by a fraction - * of the ListView height, thus screen resolution agnostic. - */ - private float mDragUpScrollStartFrac = 1.0f / 3.0f; - - /** - * Determines the start of the downward drag-scroll region - * at the bottom of the ListView. Specified by a fraction - * of the ListView height, thus screen resolution agnostic. - */ - private float mDragDownScrollStartFrac = 1.0f / 3.0f; - - /** - * The following are calculated from the above fracs. - */ - private int mUpScrollStartY; - private int mDownScrollStartY; - private float mDownScrollStartYF; - private float mUpScrollStartYF; - - /** - * Calculated from above above and current ListView height. - */ - private float mDragUpScrollHeight; - - /** - * Calculated from above above and current ListView height. - */ - private float mDragDownScrollHeight; - - /** - * Maximum drag-scroll speed in pixels per ms. Only used with - * default linear drag-scroll profile. - */ - private float mMaxScrollSpeed = 0.5f; - - /** - * Defines the scroll speed during a drag-scroll. User can - * provide their own; this default is a simple linear profile - * where scroll speed increases linearly as the floating View - * nears the top/bottom of the ListView. - */ - private DragScrollProfile mScrollProfile = new DragScrollProfile() { - @Override - public float getSpeed(float w, long t) { - return mMaxScrollSpeed * w; - } - }; - - /** - * Current touch x. - */ - private int mX; - - /** - * Current touch y. - */ - private int mY; - - /** - * Last touch x. - */ - //private int mLastX; - - /** - * Last touch y. - */ - private int mLastY; - - /** - * The touch y-coord at which drag started - */ - //private int mDragStartY; - - /** - * Drag flag bit. Floating View can move in the positive - * x direction. - */ - public final static int DRAG_POS_X = 0x1; - - /** - * Drag flag bit. Floating View can move in the negative - * x direction. - */ - public final static int DRAG_NEG_X = 0x2; - - /** - * Drag flag bit. Floating View can move in the positive - * y direction. This is subtle. What this actually means is - * that, if enabled, the floating View can be dragged below its starting - * position. Remove in favor of upper-bounding item position? - */ - public final static int DRAG_POS_Y = 0x4; - - /** - * Drag flag bit. Floating View can move in the negative - * y direction. This is subtle. What this actually means is - * that the floating View can be dragged above its starting - * position. Remove in favor of lower-bounding item position? - */ - public final static int DRAG_NEG_Y = 0x8; - - /** - * Flags that determine limits on the motion of the - * floating View. See flags above. - */ - private int mDragFlags = 0; - - /** - * Last call to an on*TouchEvent was a call to - * onInterceptTouchEvent. - */ - private boolean mLastCallWasIntercept = false; - - /** - * A touch event is in progress. - */ - private boolean mInTouchEvent = false; - - /** - * Let the user customize the floating View. - */ - private FloatViewManager mFloatViewManager = null; - - /** - * Given to ListView to cancel its action when a drag-sort - * begins. - */ - private MotionEvent mCancelEvent; - - /** - * Enum telling where to cancel the ListView action when a - * drag-sort begins - */ - private static final int NO_CANCEL = 0; - private static final int ON_TOUCH_EVENT = 1; - private static final int ON_INTERCEPT_TOUCH_EVENT = 2; - - /** - * Where to cancel the ListView action when a - * drag-sort begins - */ - private int mCancelMethod = NO_CANCEL; - - /** - * Determines when a slide shuffle animation starts. That is, - * defines how close to the edge of the drop slot the floating - * View must be to initiate the slide. - */ - private float mSlideRegionFrac = 0.25f; - - /** - * Number between 0 and 1 indicating the relative location of - * a sliding item (only used if drag-sort animations - * are turned on). Nearly 1 means the item is - * at the top of the slide region (nearly full blank item - * is directly below). - */ - private float mSlideFrac = 0.0f; - - /** - * Wraps the user-provided ListAdapter. This is used to wrap each - * item View given by the user inside another View (currenly - * a RelativeLayout) which - * expands and collapses to simulate the item shuffling. - */ - private AdapterWrapper mAdapterWrapper; - - /** - * Turn on custom debugger. - */ - private boolean mTrackDragSort = false; - - /** - * Debugging class. - */ - private DragSortTracker mDragSortTracker; - - /** - * Needed for adjusting item heights from within layoutChildren - */ - private boolean mBlockLayoutRequests = false; - - /** - * Set to true when a down event happens during drag sort; - * for example, when drag finish animations are - * playing. - */ - private boolean mIgnoreTouchEvent = false; - - /** - * Caches DragSortItemView child heights. Sometimes DSLV has to - * know the height of an offscreen item. Since ListView virtualizes - * these, DSLV must get the item from the ListAdapter to obtain - * its height. That process can be expensive, but often the same - * offscreen item will be requested many times in a row. Once an - * offscreen item height is calculated, we cache it in this guy. - * Actually, we cache the height of the child of the - * DragSortItemView since the item height changes often during a - * drag-sort. - */ - private static final int sCacheSize = 3; - private HeightCache mChildHeightCache = new HeightCache(sCacheSize); - - private RemoveAnimator mRemoveAnimator; - - private LiftAnimator mLiftAnimator; - - private DropAnimator mDropAnimator; - - private boolean mUseRemoveVelocity; - private float mRemoveVelocityX = 0; - - public DragSortListView(Context context, AttributeSet attrs) { - super(context, attrs); - - int defaultDuration = 150; - int removeAnimDuration = defaultDuration; // ms - int dropAnimDuration = defaultDuration; // ms - - if (attrs != null) { - TypedArray a = getContext().obtainStyledAttributes(attrs, - R.styleable.DragSortListView, 0, 0); - - mItemHeightCollapsed = Math.max(1, a.getDimensionPixelSize( - R.styleable.DragSortListView_collapsed_height, 1)); - - mTrackDragSort = a.getBoolean( - R.styleable.DragSortListView_track_drag_sort, false); - - if (mTrackDragSort) { - mDragSortTracker = new DragSortTracker(); - } - - // alpha between 0 and 255, 0=transparent, 255=opaque - mFloatAlpha = a.getFloat(R.styleable.DragSortListView_float_alpha, mFloatAlpha); - mCurrFloatAlpha = mFloatAlpha; - - mDragEnabled = a.getBoolean(R.styleable.DragSortListView_drag_enabled, mDragEnabled); - - mSlideRegionFrac = Math.max(0.0f, - Math.min(1.0f, 1.0f - a.getFloat( - R.styleable.DragSortListView_slide_shuffle_speed, - 0.75f))); - - mAnimate = mSlideRegionFrac > 0.0f; - - float frac = a.getFloat( - R.styleable.DragSortListView_drag_scroll_start, - mDragUpScrollStartFrac); - - setDragScrollStart(frac); - - mMaxScrollSpeed = a.getFloat( - R.styleable.DragSortListView_max_drag_scroll_speed, - mMaxScrollSpeed); - - removeAnimDuration = a.getInt( - R.styleable.DragSortListView_remove_animation_duration, - removeAnimDuration); - - dropAnimDuration = a.getInt( - R.styleable.DragSortListView_drop_animation_duration, - dropAnimDuration); - - boolean useDefault = a.getBoolean( - R.styleable.DragSortListView_use_default_controller, - true); - - if (useDefault) { - boolean removeEnabled = a.getBoolean( - R.styleable.DragSortListView_remove_enabled, - false); - int removeMode = a.getInt( - R.styleable.DragSortListView_remove_mode, - DragSortController.FLING_REMOVE); - boolean sortEnabled = a.getBoolean( - R.styleable.DragSortListView_sort_enabled, - true); - int dragInitMode = a.getInt( - R.styleable.DragSortListView_drag_start_mode, - DragSortController.ON_DOWN); - int dragHandleId = a.getResourceId( - R.styleable.DragSortListView_drag_handle_id, - 0); - int flingHandleId = a.getResourceId( - R.styleable.DragSortListView_fling_handle_id, - 0); - int clickRemoveId = a.getResourceId( - R.styleable.DragSortListView_click_remove_id, - 0); - int bgColor = a.getColor( - R.styleable.DragSortListView_float_background_color, - Color.BLACK); - - DragSortController controller = new DragSortController( - this, dragHandleId, dragInitMode, removeMode, - clickRemoveId, flingHandleId); - controller.setRemoveEnabled(removeEnabled); - controller.setSortEnabled(sortEnabled); - controller.setBackgroundColor(bgColor); - - mFloatViewManager = controller; - setOnTouchListener(controller); - } - - a.recycle(); - } - - mDragScroller = new DragScroller(); - - float smoothness = 0.5f; - if (removeAnimDuration > 0) { - mRemoveAnimator = new RemoveAnimator(smoothness, removeAnimDuration); - } - // mLiftAnimator = new LiftAnimator(smoothness, 100); - if (dropAnimDuration > 0) { - mDropAnimator = new DropAnimator(smoothness, dropAnimDuration); - } - - mCancelEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0f, 0f, 0, 0f, - 0f, 0, 0); - - // construct the dataset observer - mObserver = new DataSetObserver() { - private void cancel() { - if (mDragState == DRAGGING) { - cancelDrag(); - } - } - - @Override - public void onChanged() { - cancel(); - } - - @Override - public void onInvalidated() { - cancel(); - } - }; - } - - /** - * Usually called from a FloatViewManager. The float alpha - * will be reset to the xml-defined value every time a drag - * is stopped. - */ - public void setFloatAlpha(float alpha) { - mCurrFloatAlpha = alpha; - } - - public float getFloatAlpha() { - return mCurrFloatAlpha; - } - - /** - * Set maximum drag scroll speed in positions/second. Only applies - * if using default ScrollSpeedProfile. - * - * @param max Maximum scroll speed. - */ - public void setMaxScrollSpeed(float max) { - mMaxScrollSpeed = max; - } - - /** - * For each DragSortListView Listener interface implemented by - * adapter, this method calls the appropriate - * set*Listener method with adapter as the argument. - * - * @param adapter The ListAdapter providing data to back - * DragSortListView. - * - * @see android.widget.ListView#setAdapter(android.widget.ListAdapter) - */ - @Override - public void setAdapter(ListAdapter adapter) { - if (adapter != null) { - mAdapterWrapper = new AdapterWrapper(adapter); - adapter.registerDataSetObserver(mObserver); - - if (adapter instanceof DropListener) { - setDropListener((DropListener) adapter); - } - if (adapter instanceof DragListener) { - setDragListener((DragListener) adapter); - } - if (adapter instanceof RemoveListener) { - setRemoveListener((RemoveListener) adapter); - } - } else { - mAdapterWrapper = null; - } - - super.setAdapter(mAdapterWrapper); - } - - /** - * As opposed to {@link ListView#getAdapter()}, which returns - * a heavily wrapped ListAdapter (DragSortListView wraps the - * input ListAdapter {\emph and} ListView wraps the wrapped one). - * - * @return The ListAdapter set as the argument of {@link setAdapter()} - */ - public ListAdapter getInputAdapter() { - if (mAdapterWrapper == null) { - return null; - } else { - return mAdapterWrapper.getWrappedAdapter(); - } - } - - private class AdapterWrapper extends BaseAdapter implements WrapperListAdapter { - private ListAdapter mAdapter; - - public AdapterWrapper(ListAdapter adapter) { - mAdapter = adapter; - } - - @Override - public ListAdapter getWrappedAdapter() { - return mAdapter; - } - - @Override - public long getItemId(int position) { - return mAdapter.getItemId(position); - } - - @Override - public Object getItem(int position) { - return mAdapter.getItem(position); - } - - @Override - public int getCount() { - return mAdapter.getCount(); - } - - @Override - public boolean areAllItemsEnabled() { - return mAdapter.areAllItemsEnabled(); - } - - @Override - public boolean isEnabled(int position) { - return mAdapter.isEnabled(position); - } - - @Override - public int getItemViewType(int position) { - return mAdapter.getItemViewType(position); - } - - @Override - public int getViewTypeCount() { - return mAdapter.getViewTypeCount(); - } - - @Override - public boolean hasStableIds() { - return mAdapter.hasStableIds(); - } - - @Override - public boolean isEmpty() { - return mAdapter.isEmpty(); - } - - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - DragSortItemView v; - View child; - if (convertView != null) { - v = (DragSortItemView) convertView; - View oldChild = v.getChildAt(0); - - child = mAdapter.getView(position, oldChild, DragSortListView.this); - if (child != oldChild) { - // shouldn't get here if user is reusing convertViews - // properly - if (oldChild != null) { - v.removeViewAt(0); - } - v.addView(child); - } - } else { - child = mAdapter.getView(position, null, DragSortListView.this); - if (child instanceof Checkable) { - v = new DragSortItemViewCheckable(getContext()); - } else { - v = new DragSortItemView(getContext()); - } - v.setLayoutParams(new AbsListView.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT)); - v.addView(child); - } - - // Set the correct item height given drag state; passed - // View needs to be measured if measurement is required. - adjustItem(position + getHeaderViewsCount(), v, true); - - return v; - } - - @Override - public void registerDataSetObserver(DataSetObserver observer) { - mAdapter.registerDataSetObserver(observer); - } - - @Override - public void unregisterDataSetObserver(DataSetObserver observer) { - mAdapter.unregisterDataSetObserver(observer); - } - } - - private void drawDivider(int expPosition, Canvas canvas) { - final Drawable divider = getDivider(); - final int dividerHeight = getDividerHeight(); - - if (divider != null && dividerHeight != 0) { - final ViewGroup expItem = (ViewGroup) getChildAt(expPosition - - getFirstVisiblePosition()); - if (expItem != null) { - final int left = getPaddingLeft(); - final int right = getWidth() - getPaddingRight(); - final int top; - final int bottom; - - final int childHeight = expItem.getChildAt(0).getHeight(); - - if (expPosition > mSrcPos) { - top = expItem.getTop() + childHeight; - bottom = top + dividerHeight; - } else { - bottom = expItem.getBottom() - childHeight; - top = bottom - dividerHeight; - } - - // Have to clip to support ColorDrawable on <= Gingerbread - canvas.save(); - canvas.clipRect(left, top, right, bottom); - divider.setBounds(left, top, right, bottom); - divider.draw(canvas); - canvas.restore(); - } - } - } - - @Override - protected void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - - if (mDragState != IDLE) { - // draw the divider over the expanded item - if (mFirstExpPos != mSrcPos) { - drawDivider(mFirstExpPos, canvas); - } - if (mSecondExpPos != mFirstExpPos && mSecondExpPos != mSrcPos) { - drawDivider(mSecondExpPos, canvas); - } - } - - if (mFloatView != null) { - // draw the float view over everything - final int floatViewWidth = mFloatView.getWidth(); - final int floatViewHeight = mFloatView.getHeight(); - - int x = mFloatLoc.x; - - final int listViewWidth = getWidth(); - if (x < 0) - x = -x; - float alphaMod; - if (x < listViewWidth) { - alphaMod = ((float) (listViewWidth - x)) / ((float) listViewWidth); - alphaMod *= alphaMod; - } else { - alphaMod = 0; - } - - final int alpha = (int) (255f * mCurrFloatAlpha * alphaMod); - - canvas.save(); - canvas.translate(mFloatLoc.x, mFloatLoc.y); - canvas.clipRect(0, 0, floatViewWidth, floatViewHeight); - - canvas.saveLayerAlpha(0, 0, floatViewWidth, floatViewHeight, alpha, - Canvas.ALL_SAVE_FLAG); - - mFloatView.draw(canvas); - canvas.restore(); - canvas.restore(); - } - } - - private int getItemHeight(int position) { - View v = getChildAt(position - getFirstVisiblePosition()); - - if (v != null) { - // item is onscreen, just get the height of the View - return v.getHeight(); - } else { - // item is offscreen. get child height and calculate - // item height based on current shuffle state - return calcItemHeight(position, getChildHeight(position)); - } - } - - private class HeightCache { - - private SparseIntArray mMap; - private List mOrder; - private int mMaxSize; - - public HeightCache(int size) { - mMap = new SparseIntArray(size); - mOrder = new ArrayList(size); - mMaxSize = size; - } - - /** - * Add item height at position if doesn't already exist. - */ - public void add(int position, int height) { - int currentHeight = mMap.get(position, -1); - if (currentHeight != height) { - if (currentHeight == -1 && mMap.size() == mMaxSize) { - // remove oldest entry - mMap.delete(mOrder.remove(0)); - } else { - // move position to newest slot - mOrder.remove((Integer) position); - } - mMap.put(position, height); - mOrder.add(position); - } - } - - public int get(int position) { - return mMap.get(position, -1); - } - - public void clear() { - mMap.clear(); - mOrder.clear(); - } - - } - - /** - * Get the shuffle edge for item at position when top of - * item is at y-coord top. Assumes that current item heights - * are consistent with current float view location and - * thus expanded positions and slide fraction. i.e. Should not be - * called between update of expanded positions/slide fraction - * and layoutChildren. - * - * @param position - * @param top - * @param height Height of item at position. If -1, this function - * calculates this height. - * - * @return Shuffle line between position-1 and position (for - * the given view of the list; that is, for when top of item at - * position has y-coord of given `top`). If - * floating View (treated as horizontal line) is dropped - * immediately above this line, it lands in position-1. If - * dropped immediately below this line, it lands in position. - */ - private int getShuffleEdge(int position, int top) { - final int numHeaders = getHeaderViewsCount(); - final int numFooters = getFooterViewsCount(); - - // shuffle edges are defined between items that can be - // dragged; there are N-1 of them if there are N draggable - // items. - - if (position <= numHeaders || (position >= getCount() - numFooters)) { - return top; - } - - int divHeight = getDividerHeight(); - - int edge; - - int maxBlankHeight = mFloatViewHeight - mItemHeightCollapsed; - int childHeight = getChildHeight(position); - int itemHeight = getItemHeight(position); - - // first calculate top of item given that floating View is - // centered over src position - int otop = top; - if (mSecondExpPos <= mSrcPos) { - // items are expanded on and/or above the source position - - if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) { - if (position == mSrcPos) { - otop = top + itemHeight - mFloatViewHeight; - } else { - int blankHeight = itemHeight - childHeight; - otop = top + blankHeight - maxBlankHeight; - } - } else if (position > mSecondExpPos && position <= mSrcPos) { - otop = top - maxBlankHeight; - } - - } else { - // items are expanded on and/or below the source position - - if (position > mSrcPos && position <= mFirstExpPos) { - otop = top + maxBlankHeight; - } else if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) { - int blankHeight = itemHeight - childHeight; - otop = top + blankHeight; - } - } - - // otop is set - if (position <= mSrcPos) { - edge = otop + (mFloatViewHeight - divHeight - getChildHeight(position - 1)) / 2; - } else { - edge = otop + (childHeight - divHeight - mFloatViewHeight) / 2; - } - - return edge; - } - - private boolean updatePositions() { - final int first = getFirstVisiblePosition(); - int startPos = mFirstExpPos; - View startView = getChildAt(startPos - first); - - if (startView == null) { - startPos = first + getChildCount() / 2; - startView = getChildAt(startPos - first); - } - int startTop = startView.getTop(); - - int itemHeight = startView.getHeight(); - - int edge = getShuffleEdge(startPos, startTop); - int lastEdge = edge; - - int divHeight = getDividerHeight(); - - // Log.d("mobeta", "float mid="+mFloatViewMid); - - int itemPos = startPos; - int itemTop = startTop; - if (mFloatViewMid < edge) { - // scanning up for float position - while (itemPos >= 0) { - itemPos--; - itemHeight = getItemHeight(itemPos); - - if (itemPos == 0) { - edge = itemTop - divHeight - itemHeight; - break; - } - - itemTop -= itemHeight + divHeight; - edge = getShuffleEdge(itemPos, itemTop); - - if (mFloatViewMid >= edge) { - break; - } - - lastEdge = edge; - } - } else { - // scanning down for float position - final int count = getCount(); - while (itemPos < count) { - if (itemPos == count - 1) { - edge = itemTop + divHeight + itemHeight; - break; - } - - itemTop += divHeight + itemHeight; - itemHeight = getItemHeight(itemPos + 1); - edge = getShuffleEdge(itemPos + 1, itemTop); - - // test for hit - if (mFloatViewMid < edge) { - break; - } - - lastEdge = edge; - itemPos++; - } - } - - final int numHeaders = getHeaderViewsCount(); - final int numFooters = getFooterViewsCount(); - - boolean updated = false; - - int oldFirstExpPos = mFirstExpPos; - int oldSecondExpPos = mSecondExpPos; - float oldSlideFrac = mSlideFrac; - - if (mAnimate) { - int edgeToEdge = Math.abs(edge - lastEdge); - - int edgeTop, edgeBottom; - if (mFloatViewMid < edge) { - edgeTop = lastEdge; - edgeBottom = edge; - } else { - edgeTop = edge; - edgeBottom = lastEdge; - } - - int slideRgnHeight = (int) (0.5f * mSlideRegionFrac * edgeToEdge); - float slideRgnHeightF = (float) slideRgnHeight; - int slideEdgeTop = edgeTop + slideRgnHeight; - int slideEdgeBottom = edgeBottom - slideRgnHeight; - - // Three regions - if (mFloatViewMid < slideEdgeTop) { - mFirstExpPos = itemPos - 1; - mSecondExpPos = itemPos; - mSlideFrac = 0.5f * ((float) (slideEdgeTop - mFloatViewMid)) / slideRgnHeightF; - } else if (mFloatViewMid < slideEdgeBottom) { - mFirstExpPos = itemPos; - mSecondExpPos = itemPos; - } else { - mFirstExpPos = itemPos; - mSecondExpPos = itemPos + 1; - mSlideFrac = 0.5f * (1.0f + ((float) (edgeBottom - mFloatViewMid)) - / slideRgnHeightF); - } - - } else { - mFirstExpPos = itemPos; - mSecondExpPos = itemPos; - } - - // correct for headers and footers - if (mFirstExpPos < numHeaders) { - itemPos = numHeaders; - mFirstExpPos = itemPos; - mSecondExpPos = itemPos; - } else if (mSecondExpPos >= getCount() - numFooters) { - itemPos = getCount() - numFooters - 1; - mFirstExpPos = itemPos; - mSecondExpPos = itemPos; - } - - if (mFirstExpPos != oldFirstExpPos || mSecondExpPos != oldSecondExpPos - || mSlideFrac != oldSlideFrac) { - updated = true; - } - - if (itemPos != mFloatPos) { - if (mDragListener != null) { - mDragListener.drag(mFloatPos - numHeaders, itemPos - numHeaders); - } - - mFloatPos = itemPos; - updated = true; - } - - return updated; - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - if (mTrackDragSort) { - mDragSortTracker.appendState(); - } - } - - private class SmoothAnimator implements Runnable { - protected long mStartTime; - - private float mDurationF; - - private float mAlpha; - private float mA, mB, mC, mD; - - private boolean mCanceled; - - public SmoothAnimator(float smoothness, int duration) { - mAlpha = smoothness; - mDurationF = (float) duration; - mA = mD = 1f / (2f * mAlpha * (1f - mAlpha)); - mB = mAlpha / (2f * (mAlpha - 1f)); - mC = 1f / (1f - mAlpha); - } - - public float transform(float frac) { - if (frac < mAlpha) { - return mA * frac * frac; - } else if (frac < 1f - mAlpha) { - return mB + mC * frac; - } else { - return 1f - mD * (frac - 1f) * (frac - 1f); - } - } - - public void start() { - mStartTime = SystemClock.uptimeMillis(); - mCanceled = false; - onStart(); - post(this); - } - - public void cancel() { - mCanceled = true; - } - - public void onStart() { - // stub - } - - public void onUpdate(float frac, float smoothFrac) { - // stub - } - - public void onStop() { - // stub - } - - @Override - public void run() { - if (mCanceled) { - return; - } - - float fraction = ((float) (SystemClock.uptimeMillis() - mStartTime)) / mDurationF; - - if (fraction >= 1f) { - onUpdate(1f, 1f); - onStop(); - } else { - onUpdate(fraction, transform(fraction)); - post(this); - } - } - } - - /** - * Centers floating View under touch point. - */ - private class LiftAnimator extends SmoothAnimator { - - private float mInitDragDeltaY; - private float mFinalDragDeltaY; - - public LiftAnimator(float smoothness, int duration) { - super(smoothness, duration); - } - - @Override - public void onStart() { - mInitDragDeltaY = mDragDeltaY; - mFinalDragDeltaY = mFloatViewHeightHalf; - } - - @Override - public void onUpdate(float frac, float smoothFrac) { - if (mDragState != DRAGGING) { - cancel(); - } else { - mDragDeltaY = (int) (smoothFrac * mFinalDragDeltaY + (1f - smoothFrac) - * mInitDragDeltaY); - mFloatLoc.y = mY - mDragDeltaY; - doDragFloatView(true); - } - } - } - - /** - * Centers floating View over drop slot before destroying. - */ - private class DropAnimator extends SmoothAnimator { - - private int mDropPos; - private int srcPos; - private float mInitDeltaY; - private float mInitDeltaX; - - public DropAnimator(float smoothness, int duration) { - super(smoothness, duration); - } - - @Override - public void onStart() { - mDropPos = mFloatPos; - srcPos = mSrcPos; - mDragState = DROPPING; - mInitDeltaY = mFloatLoc.y - getTargetY(); - mInitDeltaX = mFloatLoc.x - getPaddingLeft(); - } - - private int getTargetY() { - final int first = getFirstVisiblePosition(); - final int otherAdjust = (mItemHeightCollapsed + getDividerHeight()) / 2; - View v = getChildAt(mDropPos - first); - int targetY = -1; - if (v != null) { - if (mDropPos == srcPos) { - targetY = v.getTop(); - } else if (mDropPos < srcPos) { - // expanded down - targetY = v.getTop() - otherAdjust; - } else { - // expanded up - targetY = v.getBottom() + otherAdjust - mFloatViewHeight; - } - } else { - // drop position is not on screen?? no animation - cancel(); - } - - return targetY; - } - - @Override - public void onUpdate(float frac, float smoothFrac) { - final int targetY = getTargetY(); - final int targetX = getPaddingLeft(); - final float deltaY = mFloatLoc.y - targetY; - final float deltaX = mFloatLoc.x - targetX; - final float f = 1f - smoothFrac; - if (f < Math.abs(deltaY / mInitDeltaY) || f < Math.abs(deltaX / mInitDeltaX)) { - mFloatLoc.y = targetY + (int) (mInitDeltaY * f); - mFloatLoc.x = getPaddingLeft() + (int) (mInitDeltaX * f); - doDragFloatView(true); - } - } - - @Override - public void onStop() { - dropFloatView(); - } - - } - - /** - * Collapses expanded items. - */ - private class RemoveAnimator extends SmoothAnimator { - - private float mFloatLocX; - private float mFirstStartBlank; - private float mSecondStartBlank; - - private int mFirstChildHeight = -1; - private int mSecondChildHeight = -1; - - private int mFirstPos; - private int mSecondPos; - - public RemoveAnimator(float smoothness, int duration) { - super(smoothness, duration); - } - - @Override - public void onStart() { - mFirstChildHeight = -1; - mSecondChildHeight = -1; - mFirstPos = mFirstExpPos; - mSecondPos = mSecondExpPos; - mDragState = REMOVING; - - mFloatLocX = mFloatLoc.x; - if (mUseRemoveVelocity) { - float minVelocity = 2f * getWidth(); - if (mRemoveVelocityX == 0) { - mRemoveVelocityX = (mFloatLocX < 0 ? -1 : 1) * minVelocity; - } else { - minVelocity *= 2; - if (mRemoveVelocityX < 0 && mRemoveVelocityX > -minVelocity) - mRemoveVelocityX = -minVelocity; - else if (mRemoveVelocityX > 0 && mRemoveVelocityX < minVelocity) - mRemoveVelocityX = minVelocity; - } - } else { - destroyFloatView(); - } - } - - @Override - public void onUpdate(float frac, float smoothFrac) { - float f = 1f - smoothFrac; - - final int firstVis = getFirstVisiblePosition(); - View item = getChildAt(mFirstPos - firstVis); - ViewGroup.LayoutParams lp; - int blank; - - if (mUseRemoveVelocity) { - float dt = (float) (SystemClock.uptimeMillis() - mStartTime) / 1000; - if (dt == 0) - return; - float dx = mRemoveVelocityX * dt; - int w = getWidth(); - mRemoveVelocityX += (mRemoveVelocityX > 0 ? 1 : -1) * dt * w; - mFloatLocX += dx; - mFloatLoc.x = (int) mFloatLocX; - if (mFloatLocX < w && mFloatLocX > -w) { - mStartTime = SystemClock.uptimeMillis(); - doDragFloatView(true); - return; - } - } - - if (item != null) { - if (mFirstChildHeight == -1) { - mFirstChildHeight = getChildHeight(mFirstPos, item, false); - mFirstStartBlank = (float) (item.getHeight() - mFirstChildHeight); - } - blank = Math.max((int) (f * mFirstStartBlank), 1); - lp = item.getLayoutParams(); - lp.height = mFirstChildHeight + blank; - item.setLayoutParams(lp); - } - if (mSecondPos != mFirstPos) { - item = getChildAt(mSecondPos - firstVis); - if (item != null) { - if (mSecondChildHeight == -1) { - mSecondChildHeight = getChildHeight(mSecondPos, item, false); - mSecondStartBlank = (float) (item.getHeight() - mSecondChildHeight); - } - blank = Math.max((int) (f * mSecondStartBlank), 1); - lp = item.getLayoutParams(); - lp.height = mSecondChildHeight + blank; - item.setLayoutParams(lp); - } - } - } - - @Override - public void onStop() { - doRemoveItem(); - } - } - - public void removeItem(int which) { - mUseRemoveVelocity = false; - removeItem(which, 0); - } - - /** - * Removes an item from the list and animates the removal. - * - * @param which Position to remove (NOTE: headers/footers ignored! - * this is a position in your input ListAdapter). - * @param velocityX - */ - public void removeItem(int which, float velocityX) { - if (mDragState == IDLE || mDragState == DRAGGING) { - if (mDragState == IDLE) { - // called from outside drag-sort - mSrcPos = getHeaderViewsCount() + which; - mFirstExpPos = mSrcPos; - mSecondExpPos = mSrcPos; - mFloatPos = mSrcPos; - View v = getChildAt(mSrcPos - getFirstVisiblePosition()); - if (v != null) { - v.setVisibility(View.INVISIBLE); - } - } - - mDragState = REMOVING; - mRemoveVelocityX = velocityX; - - if (mInTouchEvent) { - switch (mCancelMethod) { - case ON_TOUCH_EVENT: - super.onTouchEvent(mCancelEvent); - break; - case ON_INTERCEPT_TOUCH_EVENT: - super.onInterceptTouchEvent(mCancelEvent); - break; - } - } - - if (mRemoveAnimator != null) { - mRemoveAnimator.start(); - } else { - doRemoveItem(which); - } - } - } - - /** - * Move an item, bypassing the drag-sort process. Simply calls - * through to {@link DropListener#drop(int, int)}. - * - * @param from Position to move (NOTE: headers/footers ignored! - * this is a position in your input ListAdapter). - * @param to Target position (NOTE: headers/footers ignored! - * this is a position in your input ListAdapter). - */ - public void moveItem(int from, int to) { - if (mDropListener != null) { - final int count = getInputAdapter().getCount(); - if (from >= 0 && from < count && to >= 0 && to < count) { - mDropListener.drop(from, to); - } - } - } - - /** - * Cancel a drag. Calls {@link #stopDrag(boolean, boolean)} with - * true as the first argument. - */ - public void cancelDrag() { - if (mDragState == DRAGGING) { - mDragScroller.stopScrolling(true); - destroyFloatView(); - clearPositions(); - adjustAllItems(); - - if (mInTouchEvent) { - mDragState = STOPPED; - } else { - mDragState = IDLE; - } - } - } - - private void clearPositions() { - mSrcPos = -1; - mFirstExpPos = -1; - mSecondExpPos = -1; - mFloatPos = -1; - } - - private void dropFloatView() { - // must set to avoid cancelDrag being called from the DataSetObserver - mDragState = DROPPING; - - if (mDropListener != null && mFloatPos >= 0 && mFloatPos < getCount()) { - final int numHeaders = getHeaderViewsCount(); - mDropListener.drop(mSrcPos - numHeaders, mFloatPos - numHeaders); - } - - destroyFloatView(); - - adjustOnReorder(); - clearPositions(); - adjustAllItems(); - - // now the drag is done - if (mInTouchEvent) { - mDragState = STOPPED; - } else { - mDragState = IDLE; - } - } - - private void doRemoveItem() { - doRemoveItem(mSrcPos - getHeaderViewsCount()); - } - - /** - * Removes dragged item from the list. Calls RemoveListener. - */ - private void doRemoveItem(int which) { - // must set to avoid cancelDrag being called from the DataSetObserver - mDragState = REMOVING; - - // end it - if (mRemoveListener != null) { - mRemoveListener.remove(which); - } - - destroyFloatView(); - - adjustOnReorder(); - clearPositions(); - - // now the drag is done - if (mInTouchEvent) { - mDragState = STOPPED; - } else { - mDragState = IDLE; - } - } - - private void adjustOnReorder() { - final int firstPos = getFirstVisiblePosition(); - if (mSrcPos < firstPos) { - // collapsed src item is off screen; - // adjust the scroll after item heights have been fixed - View v = getChildAt(0); - int top = 0; - if (v != null) { - top = v.getTop(); - } - setSelectionFromTop(firstPos - 1, top - getPaddingTop()); - } - } - - /** - * Stop a drag in progress. Pass true if you would - * like to remove the dragged item from the list. - * - * @param remove Remove the dragged item from the list. Calls - * a registered RemoveListener, if one exists. Otherwise, calls - * the DropListener, if one exists. - * - * @return True if the stop was successful. False if there is - * no floating View. - */ - public boolean stopDrag(boolean remove) { - mUseRemoveVelocity = false; - return stopDrag(remove, 0); - } - - public boolean stopDragWithVelocity(boolean remove, float velocityX) { - mUseRemoveVelocity = true; - return stopDrag(remove, velocityX); - } - - public boolean stopDrag(boolean remove, float velocityX) { - if (mFloatView != null) { - mDragScroller.stopScrolling(true); - - if (remove) { - removeItem(mSrcPos - getHeaderViewsCount(), velocityX); - } else { - if (mDropAnimator != null) { - mDropAnimator.start(); - } else { - dropFloatView(); - } - } - - if (mTrackDragSort) { - mDragSortTracker.stopTracking(); - } - - return true; - } else { - // stop failed - return false; - } - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (mIgnoreTouchEvent) { - mIgnoreTouchEvent = false; - return false; - } - - if (!mDragEnabled) { - return super.onTouchEvent(ev); - } - - boolean more = false; - - boolean lastCallWasIntercept = mLastCallWasIntercept; - mLastCallWasIntercept = false; - - if (!lastCallWasIntercept) { - saveTouchCoords(ev); - } - - if (mDragState == DRAGGING) { - onDragTouchEvent(ev); - more = true; // give us more! - } else { - // TODO: what if float view is null because we dropped in middle - // of drag touch event? - - if (mDragState == IDLE) { - if (super.onTouchEvent(ev)) { - more = true; - } - } - - int action = ev.getAction() & MotionEvent.ACTION_MASK; - - switch (action) { - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - doActionUpOrCancel(); - break; - default: - if (more) { - mCancelMethod = ON_TOUCH_EVENT; - } - } - } - - return more; - } - - private void doActionUpOrCancel() { - mCancelMethod = NO_CANCEL; - mInTouchEvent = false; - if (mDragState == STOPPED) { - mDragState = IDLE; - } - mCurrFloatAlpha = mFloatAlpha; - mListViewIntercepted = false; - mChildHeightCache.clear(); - } - - private void saveTouchCoords(MotionEvent ev) { - int action = ev.getAction() & MotionEvent.ACTION_MASK; - if (action != MotionEvent.ACTION_DOWN) { - //mLastX = mX; - mLastY = mY; - } - mX = (int) ev.getX(); - mY = (int) ev.getY(); - if (action == MotionEvent.ACTION_DOWN) { - //mLastX = mX; - mLastY = mY; - } - //mOffsetX = (int) ev.getRawX() - mX; - //mOffsetY = (int) ev.getRawY() - mY; - } - - public boolean listViewIntercepted() { - return mListViewIntercepted; - } - - private boolean mListViewIntercepted = false; - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - if (!mDragEnabled) { - return super.onInterceptTouchEvent(ev); - } - - saveTouchCoords(ev); - mLastCallWasIntercept = true; - - int action = ev.getAction() & MotionEvent.ACTION_MASK; - - if (action == MotionEvent.ACTION_DOWN) { - if (mDragState != IDLE) { - // intercept and ignore - mIgnoreTouchEvent = true; - return true; - } - mInTouchEvent = true; - } - - boolean intercept = false; - - // the following deals with calls to super.onInterceptTouchEvent - if (mFloatView != null) { - // super's touch event cancelled in startDrag - intercept = true; - } else { - if (super.onInterceptTouchEvent(ev)) { - mListViewIntercepted = true; - intercept = true; - } - - switch (action) { - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - doActionUpOrCancel(); - break; - default: - if (intercept) { - mCancelMethod = ON_TOUCH_EVENT; - } else { - mCancelMethod = ON_INTERCEPT_TOUCH_EVENT; - } - } - } - - if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { - mInTouchEvent = false; - } - - return intercept; - } - - /** - * Set the width of each drag scroll region by specifying - * a fraction of the ListView height. - * - * @param heightFraction Fraction of ListView height. Capped at - * 0.5f. - * - */ - public void setDragScrollStart(float heightFraction) { - setDragScrollStarts(heightFraction, heightFraction); - } - - /** - * Set the width of each drag scroll region by specifying - * a fraction of the ListView height. - * - * @param upperFrac Fraction of ListView height for up-scroll bound. - * Capped at 0.5f. - * @param lowerFrac Fraction of ListView height for down-scroll bound. - * Capped at 0.5f. - * - */ - public void setDragScrollStarts(float upperFrac, float lowerFrac) { - if (lowerFrac > 0.5f) { - mDragDownScrollStartFrac = 0.5f; - } else { - mDragDownScrollStartFrac = lowerFrac; - } - - if (upperFrac > 0.5f) { - mDragUpScrollStartFrac = 0.5f; - } else { - mDragUpScrollStartFrac = upperFrac; - } - - if (getHeight() != 0) { - updateScrollStarts(); - } - } - - private void continueDrag(int x, int y) { - // proposed position - mFloatLoc.x = x - mDragDeltaX; - mFloatLoc.y = y - mDragDeltaY; - - doDragFloatView(true); - - int minY = Math.min(y, mFloatViewMid + mFloatViewHeightHalf); - int maxY = Math.max(y, mFloatViewMid - mFloatViewHeightHalf); - - // get the current scroll direction - int currentScrollDir = mDragScroller.getScrollDir(); - - if (minY > mLastY && minY > mDownScrollStartY && currentScrollDir != DragScroller.DOWN) { - // dragged down, it is below the down scroll start and it is not - // scrolling up - - if (currentScrollDir != DragScroller.STOP) { - // moved directly from up scroll to down scroll - mDragScroller.stopScrolling(true); - } - - // start scrolling down - mDragScroller.startScrolling(DragScroller.DOWN); - } else if (maxY < mLastY && maxY < mUpScrollStartY && currentScrollDir != DragScroller.UP) { - // dragged up, it is above the up scroll start and it is not - // scrolling up - - if (currentScrollDir != DragScroller.STOP) { - // moved directly from down scroll to up scroll - mDragScroller.stopScrolling(true); - } - - // start scrolling up - mDragScroller.startScrolling(DragScroller.UP); - } - else if (maxY >= mUpScrollStartY && minY <= mDownScrollStartY - && mDragScroller.isScrolling()) { - // not in the upper nor in the lower drag-scroll regions but it is - // still scrolling - - mDragScroller.stopScrolling(true); - } - } - - private void updateScrollStarts() { - final int padTop = getPaddingTop(); - final int listHeight = getHeight() - padTop - getPaddingBottom(); - float heightF = (float) listHeight; - - mUpScrollStartYF = padTop + mDragUpScrollStartFrac * heightF; - mDownScrollStartYF = padTop + (1.0f - mDragDownScrollStartFrac) * heightF; - - mUpScrollStartY = (int) mUpScrollStartYF; - mDownScrollStartY = (int) mDownScrollStartYF; - - mDragUpScrollHeight = mUpScrollStartYF - padTop; - mDragDownScrollHeight = padTop + listHeight - mDownScrollStartYF; - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - updateScrollStarts(); - } - - private void adjustAllItems() { - final int first = getFirstVisiblePosition(); - final int last = getLastVisiblePosition(); - - int begin = Math.max(0, getHeaderViewsCount() - first); - int end = Math.min(last - first, getCount() - 1 - getFooterViewsCount() - first); - - for (int i = begin; i <= end; ++i) { - View v = getChildAt(i); - if (v != null) { - adjustItem(first + i, v, false); - } - } - } - - /** - * Sets layout param height, gravity, and visibility on - * wrapped item. - */ - private void adjustItem(int position, View v, boolean invalidChildHeight) { - // Adjust item height - ViewGroup.LayoutParams lp = v.getLayoutParams(); - int height; - if (position != mSrcPos && position != mFirstExpPos && position != mSecondExpPos) { - height = ViewGroup.LayoutParams.WRAP_CONTENT; - } else { - height = calcItemHeight(position, v, invalidChildHeight); - } - - if (height != lp.height) { - lp.height = height; - v.setLayoutParams(lp); - } - - // Adjust item gravity - if (position == mFirstExpPos || position == mSecondExpPos) { - if (position < mSrcPos) { - ((DragSortItemView) v).setGravity(Gravity.BOTTOM); - } else if (position > mSrcPos) { - ((DragSortItemView) v).setGravity(Gravity.TOP); - } - } - - // Finally adjust item visibility - int oldVis = v.getVisibility(); - int vis = View.VISIBLE; - - if (position == mSrcPos && mFloatView != null) { - vis = View.INVISIBLE; - } - - if (vis != oldVis) { - v.setVisibility(vis); - } - } - - private int getChildHeight(int position) { - if (position == mSrcPos) { - return 0; - } - - View v = getChildAt(position - getFirstVisiblePosition()); - - if (v != null) { - // item is onscreen, therefore child height is valid, - // hence the "true" - return getChildHeight(position, v, false); - } else { - // item is offscreen - // first check cache for child height at this position - int childHeight = mChildHeightCache.get(position); - if (childHeight != -1) { - return childHeight; - } - - final ListAdapter adapter = getAdapter(); - int type = adapter.getItemViewType(position); - - // There might be a better place for checking for the following - final int typeCount = adapter.getViewTypeCount(); - if (typeCount != mSampleViewTypes.length) { - mSampleViewTypes = new View[typeCount]; - } - - if (type >= 0) { - if (mSampleViewTypes[type] == null) { - v = adapter.getView(position, null, this); - mSampleViewTypes[type] = v; - } else { - v = adapter.getView(position, mSampleViewTypes[type], this); - } - } else { - // type is HEADER_OR_FOOTER or IGNORE - v = adapter.getView(position, null, this); - } - - // current child height is invalid, hence "true" below - childHeight = getChildHeight(position, v, true); - - // cache it because this could have been expensive - mChildHeightCache.add(position, childHeight); - - return childHeight; - } - } - - private int getChildHeight(int position, View item, boolean invalidChildHeight) { - if (position == mSrcPos) { - return 0; - } - - View child; - if (position < getHeaderViewsCount() || position >= getCount() - getFooterViewsCount()) { - child = item; - } else { - child = ((ViewGroup) item).getChildAt(0); - } - - ViewGroup.LayoutParams lp = child.getLayoutParams(); - if (lp != null) { - if (lp.height > 0) { - return lp.height; - } - } - - int childHeight = child.getHeight(); - if (childHeight == 0 || invalidChildHeight) { - measureItem(child); - childHeight = child.getMeasuredHeight(); - } - - return childHeight; - } - - private int calcItemHeight(int position, View item, boolean invalidChildHeight) { - return calcItemHeight(position, getChildHeight(position, item, invalidChildHeight)); - } - - private int calcItemHeight(int position, int childHeight) { - boolean isSliding = mAnimate && mFirstExpPos != mSecondExpPos; - int maxNonSrcBlankHeight = mFloatViewHeight - mItemHeightCollapsed; - int slideHeight = (int) (mSlideFrac * maxNonSrcBlankHeight); - - int height; - - if (position == mSrcPos) { - if (mSrcPos == mFirstExpPos) { - if (isSliding) { - height = slideHeight + mItemHeightCollapsed; - } else { - height = mFloatViewHeight; - } - } else if (mSrcPos == mSecondExpPos) { - // if gets here, we know an item is sliding - height = mFloatViewHeight - slideHeight; - } else { - height = mItemHeightCollapsed; - } - } else if (position == mFirstExpPos) { - if (isSliding) { - height = childHeight + slideHeight; - } else { - height = childHeight + maxNonSrcBlankHeight; - } - } else if (position == mSecondExpPos) { - // we know an item is sliding (b/c 2ndPos != 1stPos) - height = childHeight + maxNonSrcBlankHeight - slideHeight; - } else { - height = childHeight; - } - - return height; - } - - @Override - public void requestLayout() { - if (!mBlockLayoutRequests) { - super.requestLayout(); - } - } - - private int adjustScroll(int movePos, View moveItem, int oldFirstExpPos, int oldSecondExpPos) { - int adjust = 0; - - final int childHeight = getChildHeight(movePos); - - int moveHeightBefore = moveItem.getHeight(); - int moveHeightAfter = calcItemHeight(movePos, childHeight); - - int moveBlankBefore = moveHeightBefore; - int moveBlankAfter = moveHeightAfter; - if (movePos != mSrcPos) { - moveBlankBefore -= childHeight; - moveBlankAfter -= childHeight; - } - - int maxBlank = mFloatViewHeight; - if (mSrcPos != mFirstExpPos && mSrcPos != mSecondExpPos) { - maxBlank -= mItemHeightCollapsed; - } - - if (movePos <= oldFirstExpPos) { - if (movePos > mFirstExpPos) { - adjust += maxBlank - moveBlankAfter; - } - } else if (movePos == oldSecondExpPos) { - if (movePos <= mFirstExpPos) { - adjust += moveBlankBefore - maxBlank; - } else if (movePos == mSecondExpPos) { - adjust += moveHeightBefore - moveHeightAfter; - } else { - adjust += moveBlankBefore; - } - } else { - if (movePos <= mFirstExpPos) { - adjust -= maxBlank; - } else if (movePos == mSecondExpPos) { - adjust -= moveBlankAfter; - } - } - - return adjust; - } - - private void measureItem(View item) { - ViewGroup.LayoutParams lp = item.getLayoutParams(); - if (lp == null) { - lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT); - item.setLayoutParams(lp); - } - - int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft() - + getListPaddingRight(), lp.width); - int hspec; - if (lp.height > 0) { - hspec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); - } else { - hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - } - item.measure(wspec, hspec); - } - - private void measureFloatView() { - if (mFloatView != null) { - measureItem(mFloatView); - mFloatViewHeight = mFloatView.getMeasuredHeight(); - mFloatViewHeightHalf = mFloatViewHeight / 2; - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (mFloatView != null) { - if (mFloatView.isLayoutRequested()) { - measureFloatView(); - } - mFloatViewOnMeasured = true; // set to false after layout - } - mWidthMeasureSpec = widthMeasureSpec; - } - - @Override - protected void layoutChildren() { - super.layoutChildren(); - - if (mFloatView != null) { - if (mFloatView.isLayoutRequested() && !mFloatViewOnMeasured) { - // Have to measure here when usual android measure - // pass is skipped. This happens during a drag-sort - // when layoutChildren is called directly. - measureFloatView(); - } - mFloatView.layout(0, 0, mFloatView.getMeasuredWidth(), mFloatView.getMeasuredHeight()); - mFloatViewOnMeasured = false; - } - } - - protected boolean onDragTouchEvent(MotionEvent ev) { - int action = ev.getAction() & MotionEvent.ACTION_MASK; - switch (action) { - case MotionEvent.ACTION_CANCEL: - if (mDragState == DRAGGING) { - cancelDrag(); - } - doActionUpOrCancel(); - break; - case MotionEvent.ACTION_UP: - if (mDragState == DRAGGING) { - stopDrag(false); - } - doActionUpOrCancel(); - break; - case MotionEvent.ACTION_MOVE: - continueDrag((int) ev.getX(), (int) ev.getY()); - break; - } - - return true; - } - - /** - * Start a drag of item at position using the - * registered FloatViewManager. Calls through - * to {@link #startDrag(int,View,int,int,int)} after obtaining - * the floating View from the FloatViewManager. - * - * @param position Item to drag. - * @param dragFlags Flags that restrict some movements of the - * floating View. For example, set dragFlags |= - * ~{@link #DRAG_NEG_X} to allow dragging the floating - * View in all directions except off the screen to the left. - * @param deltaX Offset in x of the touch coordinate from the - * left edge of the floating View (i.e. touch-x minus float View - * left). - * @param deltaY Offset in y of the touch coordinate from the - * top edge of the floating View (i.e. touch-y minus float View - * top). - * - * @return True if the drag was started, false otherwise. This - * startDrag will fail if we are not currently in - * a touch event, there is no registered FloatViewManager, - * or the FloatViewManager returns a null View. - */ - public boolean startDrag(int position, int dragFlags, int deltaX, int deltaY) { - if (!mInTouchEvent || mFloatViewManager == null) { - return false; - } - - View v = mFloatViewManager.onCreateFloatView(position); - - if (v == null) { - return false; - } else { - return startDrag(position, v, dragFlags, deltaX, deltaY); - } - - } - - /** - * Start a drag of item at position without using - * a FloatViewManager. - * - * @param position Item to drag. - * @param floatView Floating View. - * @param dragFlags Flags that restrict some movements of the - * floating View. For example, set dragFlags |= - * ~{@link #DRAG_NEG_X} to allow dragging the floating - * View in all directions except off the screen to the left. - * @param deltaX Offset in x of the touch coordinate from the - * left edge of the floating View (i.e. touch-x minus float View - * left). - * @param deltaY Offset in y of the touch coordinate from the - * top edge of the floating View (i.e. touch-y minus float View - * top). - * - * @return True if the drag was started, false otherwise. This - * startDrag will fail if we are not currently in - * a touch event, floatView is null, or there is - * a drag in progress. - */ - public boolean startDrag(int position, View floatView, int dragFlags, int deltaX, int deltaY) { - if (mDragState != IDLE || !mInTouchEvent || mFloatView != null || floatView == null - || !mDragEnabled) { - return false; - } - - if (getParent() != null) { - getParent().requestDisallowInterceptTouchEvent(true); - } - - int pos = position + getHeaderViewsCount(); - mFirstExpPos = pos; - mSecondExpPos = pos; - mSrcPos = pos; - mFloatPos = pos; - - mDragState = DRAGGING; - mDragFlags = 0; - mDragFlags |= dragFlags; - - mFloatView = floatView; - measureFloatView(); // sets mFloatViewHeight - - mDragDeltaX = deltaX; - mDragDeltaY = deltaY; - - mFloatLoc.x = mX - mDragDeltaX; - mFloatLoc.y = mY - mDragDeltaY; - - // set src item invisible - final View srcItem = getChildAt(mSrcPos - getFirstVisiblePosition()); - - if (srcItem != null) { - srcItem.setVisibility(View.INVISIBLE); - } - - if (mTrackDragSort) { - mDragSortTracker.startTracking(); - } - - // once float view is created, events are no longer passed - // to ListView - switch (mCancelMethod) { - case ON_TOUCH_EVENT: - super.onTouchEvent(mCancelEvent); - break; - case ON_INTERCEPT_TOUCH_EVENT: - super.onInterceptTouchEvent(mCancelEvent); - break; - } - - requestLayout(); - - if (mLiftAnimator != null) { - mLiftAnimator.start(); - } - - return true; - } - - private void doDragFloatView(boolean forceInvalidate) { - int movePos = getFirstVisiblePosition() + getChildCount() / 2; - View moveItem = getChildAt(getChildCount() / 2); - - if (moveItem == null) { - return; - } - - doDragFloatView(movePos, moveItem, forceInvalidate); - } - - private void doDragFloatView(int movePos, View moveItem, boolean forceInvalidate) { - mBlockLayoutRequests = true; - - updateFloatView(); - - int oldFirstExpPos = mFirstExpPos; - int oldSecondExpPos = mSecondExpPos; - - boolean updated = updatePositions(); - - if (updated) { - adjustAllItems(); - int scroll = adjustScroll(movePos, moveItem, oldFirstExpPos, oldSecondExpPos); - - setSelectionFromTop(movePos, moveItem.getTop() + scroll - getPaddingTop()); - layoutChildren(); - } - - if (updated || forceInvalidate) { - invalidate(); - } - - mBlockLayoutRequests = false; - } - - /** - * Sets float View location based on suggested values and - * constraints set in mDragFlags. - */ - private void updateFloatView() { - - if (mFloatViewManager != null) { - mTouchLoc.set(mX, mY); - mFloatViewManager.onDragFloatView(mFloatView, mFloatLoc, mTouchLoc); - } - - final int floatX = mFloatLoc.x; - final int floatY = mFloatLoc.y; - - // restrict x motion - int padLeft = getPaddingLeft(); - if ((mDragFlags & DRAG_POS_X) == 0 && floatX > padLeft) { - mFloatLoc.x = padLeft; - } else if ((mDragFlags & DRAG_NEG_X) == 0 && floatX < padLeft) { - mFloatLoc.x = padLeft; - } - - // keep floating view from going past bottom of last header view - final int numHeaders = getHeaderViewsCount(); - final int numFooters = getFooterViewsCount(); - final int firstPos = getFirstVisiblePosition(); - final int lastPos = getLastVisiblePosition(); - - int topLimit = getPaddingTop(); - if (firstPos < numHeaders) { - topLimit = getChildAt(numHeaders - firstPos - 1).getBottom(); - } - if ((mDragFlags & DRAG_NEG_Y) == 0) { - if (firstPos <= mSrcPos) { - topLimit = Math.max(getChildAt(mSrcPos - firstPos).getTop(), topLimit); - } - } - // bottom limit is top of first footer View or - // bottom of last item in list - int bottomLimit = getHeight() - getPaddingBottom(); - if (lastPos >= getCount() - numFooters - 1) { - bottomLimit = getChildAt(getCount() - numFooters - 1 - firstPos).getBottom(); - } - if ((mDragFlags & DRAG_POS_Y) == 0) { - if (lastPos >= mSrcPos) { - bottomLimit = Math.min(getChildAt(mSrcPos - firstPos).getBottom(), bottomLimit); - } - } - - if (floatY < topLimit) { - mFloatLoc.y = topLimit; - } else if (floatY + mFloatViewHeight > bottomLimit) { - mFloatLoc.y = bottomLimit - mFloatViewHeight; - } - - // get y-midpoint of floating view (constrained to ListView bounds) - mFloatViewMid = mFloatLoc.y + mFloatViewHeightHalf; - } - - private void destroyFloatView() { - if (mFloatView != null) { - mFloatView.setVisibility(GONE); - if (mFloatViewManager != null) { - mFloatViewManager.onDestroyFloatView(mFloatView); - } - mFloatView = null; - invalidate(); - } - } - - /** - * Interface for customization of the floating View appearance - * and dragging behavior. Implement - * your own and pass it to {@link #setFloatViewManager}. If - * your own is not passed, the default {@link SimpleFloatViewManager} - * implementation is used. - */ - public interface FloatViewManager { - /** - * Return the floating View for item at position. - * DragSortListView will measure and layout this View for you, - * so feel free to just inflate it. You can help DSLV by - * setting some {@link ViewGroup.LayoutParams} on this View; - * otherwise it will set some for you (with a width of FILL_PARENT - * and a height of WRAP_CONTENT). - * - * @param position Position of item to drag (NOTE: - * position excludes header Views; thus, if you - * want to call {@link ListView#getChildAt(int)}, you will need - * to add {@link ListView#getHeaderViewsCount()} to the index). - * - * @return The View you wish to display as the floating View. - */ - public View onCreateFloatView(int position); - - /** - * Called whenever the floating View is dragged. Float View - * properties can be changed here. Also, the upcoming location - * of the float View can be altered by setting - * location.x and location.y. - * - * @param floatView The floating View. - * @param location The location (top-left; relative to DSLV - * top-left) at which the float - * View would like to appear, given the current touch location - * and the offset provided in {@link DragSortListView#startDrag}. - * @param touch The current touch location (relative to DSLV - * top-left). - * @param pendingScroll - */ - public void onDragFloatView(View floatView, Point location, Point touch); - - /** - * Called when the float View is dropped; lets you perform - * any necessary cleanup. The internal DSLV floating View - * reference is set to null immediately after this is called. - * - * @param floatView The floating View passed to - * {@link #onCreateFloatView(int)}. - */ - public void onDestroyFloatView(View floatView); - } - - public void setFloatViewManager(FloatViewManager manager) { - mFloatViewManager = manager; - } - - public void setDragListener(DragListener l) { - mDragListener = l; - } - - /** - * Allows for easy toggling between a DragSortListView - * and a regular old ListView. If enabled, items are - * draggable, where the drag init mode determines how - * items are lifted (see {@link setDragInitMode(int)}). - * If disabled, items cannot be dragged. - * - * @param enabled Set true to enable list - * item dragging - */ - public void setDragEnabled(boolean enabled) { - mDragEnabled = enabled; - } - - public boolean isDragEnabled() { - return mDragEnabled; - } - - /** - * This better reorder your ListAdapter! DragSortListView does not do this - * for you; doesn't make sense to. Make sure - * {@link BaseAdapter#notifyDataSetChanged()} or something like it is called - * in your implementation. Furthermore, if you have a choiceMode other than - * none and the ListAdapter does not return true for - * {@link ListAdapter#hasStableIds()}, you will need to call - * {@link #moveCheckState(int, int)} to move the check boxes along with the - * list items. - * - * @param l - */ - public void setDropListener(DropListener l) { - mDropListener = l; - } - - /** - * Probably a no-brainer, but make sure that your remove listener - * calls {@link BaseAdapter#notifyDataSetChanged()} or something like it. - * When an item removal occurs, DragSortListView - * relies on a redraw of all the items to recover invisible views - * and such. Strictly speaking, if you remove something, your dataset - * has changed... - * - * @param l - */ - public void setRemoveListener(RemoveListener l) { - mRemoveListener = l; - } - - public interface DragListener { - public void drag(int from, int to); - } - - /** - * Your implementation of this has to reorder your ListAdapter! - * Make sure to call - * {@link BaseAdapter#notifyDataSetChanged()} or something like it - * in your implementation. - * - * @author heycosmo - * - */ - public interface DropListener { - public void drop(int from, int to); - } - - /** - * Make sure to call - * {@link BaseAdapter#notifyDataSetChanged()} or something like it - * in your implementation. - * - * @author heycosmo - * - */ - public interface RemoveListener { - public void remove(int which); - } - - public interface DragSortListener extends DropListener, DragListener, RemoveListener { - } - - public void setDragSortListener(DragSortListener listener) { - setDropListener(listener); - setDragListener(listener); - setRemoveListener(listener); - } - - /** - * Completely custom scroll speed profile. Default increases linearly - * with position and is constant in time. Create your own by implementing - * {@link DragSortListView.DragScrollProfile}. - * - * @param scrollProfile - */ - public void setDragScrollProfile(DragScrollProfile scrollProfile) { - if (scrollProfile != null) { - mScrollProfile = scrollProfile; - } - } - - /** - * Use this to move the check state of an item from one position to another - * in a drop operation. If you have a choiceMode which is not none, this - * method must be called when the order of items changes in an underlying - * adapter which does not have stable IDs (see - * {@link ListAdapter#hasStableIds()}). This is because without IDs, the - * ListView has no way of knowing which items have moved where, and cannot - * update the check state accordingly. - *

- * A word of warning about a "feature" in Android that you may run into when - * dealing with movable list items: for an adapter that does have - * stable IDs, ListView will attempt to locate each item based on its ID and - * move the check state from the item's old position to the new position — - * which is all fine and good (and removes the need for calling this - * function), except for the half-baked approach. Apparently to save time in - * the naive algorithm used, ListView will only search for an ID in the - * close neighborhood of the old position. If the user moves an item too far - * (specifically, more than 20 rows away), ListView will give up and just - * force the item to be unchecked. So if there is a reasonable chance that - * the user will move items more than 20 rows away from the original - * position, you may wish to use an adapter with unstable IDs and call this - * method manually instead. - * - * @param from - * @param to - */ - public void moveCheckState(int from, int to) { - // This method runs in O(n log n) time (n being the number of list - // items). The bottleneck is the call to AbsListView.setItemChecked, - // which is O(log n) because of the binary search involved in calling - // SparseBooleanArray.put(). - // - // To improve on the average time, we minimize the number of calls to - // setItemChecked by only calling it for items that actually have a - // changed state. This is achieved by building a list containing the - // start and end of the "runs" of checked items, and then moving the - // runs. Note that moving an item from A to B is essentially a rotation - // of the range of items in [A, B]. Let's say we have - // . . U V X Y Z . . - // and move U after Z. This is equivalent to a rotation one step to the - // left within the range you are moving across: - // . . V X Y Z U . . - // - // So, to perform the move we enumerate all the runs within the move - // range, then rotate each run one step to the left or right (depending - // on move direction). For example, in the list: - // X X . X X X . X - // we have two runs. One begins at the last item of the list and wraps - // around to the beginning, ending at position 1. The second begins at - // position 3 and ends at position 5. To rotate a run, regardless of - // length, we only need to set a check mark at one end of the run, and - // clear a check mark at the other end: - // X . X X X . X X - SparseBooleanArray cip = getCheckedItemPositions(); - int rangeStart = from; - int rangeEnd = to; - if (to < from) { - rangeStart = to; - rangeEnd = from; - } - rangeEnd += 1; - - int[] runStart = new int[cip.size()]; - int[] runEnd = new int[cip.size()]; - int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd); - if (runCount == 1 && (runStart[0] == runEnd[0])) { - // Special case where all items are checked, we can never set any - // item to false like we do below. - return; - } - - if (from < to) { - for (int i = 0; i != runCount; i++) { - setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd), true); - setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd), false); - } - - } else { - for (int i = 0; i != runCount; i++) { - setItemChecked(runStart[i], false); - setItemChecked(runEnd[i], true); - } - } - } - - /** - * Use this when an item has been deleted, to move the check state of all - * following items up one step. If you have a choiceMode which is not none, - * this method must be called when the order of items changes in an - * underlying adapter which does not have stable IDs (see - * {@link ListAdapter#hasStableIds()}). This is because without IDs, the - * ListView has no way of knowing which items have moved where, and cannot - * update the check state accordingly. - * - * See also further comments on {@link #moveCheckState(int, int)}. - * - * @param position - */ - public void removeCheckState(int position) { - SparseBooleanArray cip = getCheckedItemPositions(); - - if (cip.size() == 0) - return; - int[] runStart = new int[cip.size()]; - int[] runEnd = new int[cip.size()]; - int rangeStart = position; - int rangeEnd = cip.keyAt(cip.size() - 1) + 1; - int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd); - for (int i = 0; i != runCount; i++) { - if (!(runStart[i] == position || (runEnd[i] < runStart[i] && runEnd[i] > position))) { - // Only set a new check mark in front of this run if it does - // not contain the deleted position. If it does, we only need - // to make it one check mark shorter at the end. - setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd), true); - } - setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd), false); - } - } - - private static int buildRunList(SparseBooleanArray cip, int rangeStart, int rangeEnd, - int[] runStart, int[] runEnd) { - - int runCount = 0; - - int i = findFirstSetIndex(cip, rangeStart, rangeEnd); - if (i == -1) - return 0; - - int position = cip.keyAt(i); - int currentRunStart = position; - int currentRunEnd = currentRunStart + 1; - for (i++; i < cip.size() && (position = cip.keyAt(i)) < rangeEnd; i++) { - if (!cip.valueAt(i)) // not checked => not interesting - continue; - if (position == currentRunEnd) { - currentRunEnd++; - } else { - runStart[runCount] = currentRunStart; - runEnd[runCount] = currentRunEnd; - runCount++; - currentRunStart = position; - currentRunEnd = position + 1; - } - } - - if (currentRunEnd == rangeEnd) { - // rangeStart and rangeEnd are equivalent positions so to be - // consistent we translate them to the same integer value. That way - // we can check whether a run covers the entire range by just - // checking if the start equals the end position. - currentRunEnd = rangeStart; - } - runStart[runCount] = currentRunStart; - runEnd[runCount] = currentRunEnd; - runCount++; - - if (runCount > 1) { - if (runStart[0] == rangeStart && runEnd[runCount - 1] == rangeStart) { - // The last run ends at the end of the range, and the first run - // starts at the beginning of the range. So they are actually - // part of the same run, except they wrap around the end of the - // range. To avoid adjacent runs, we need to merge them. - runStart[0] = runStart[runCount - 1]; - runCount--; - } - } - return runCount; - } - - private static int rotate(int value, int offset, int lowerBound, int upperBound) { - int windowSize = upperBound - lowerBound; - - value += offset; - if (value < lowerBound) { - value += windowSize; - } else if (value >= upperBound) { - value -= windowSize; - } - return value; - } - - private static int findFirstSetIndex(SparseBooleanArray sba, int rangeStart, int rangeEnd) { - int size = sba.size(); - int i = insertionIndexForKey(sba, rangeStart); - while (i < size && sba.keyAt(i) < rangeEnd && !sba.valueAt(i)) - i++; - if (i == size || sba.keyAt(i) >= rangeEnd) - return -1; - return i; - } - - private static int insertionIndexForKey(SparseBooleanArray sba, int key) { - int low = 0; - int high = sba.size(); - while (high - low > 0) { - int middle = (low + high) >> 1; - if (sba.keyAt(middle) < key) - low = middle + 1; - else - high = middle; - } - return low; - } - - /** - * Interface for controlling - * scroll speed as a function of touch position and time. Use - * {@link DragSortListView#setDragScrollProfile(DragScrollProfile)} to - * set custom profile. - * - * @author heycosmo - * - */ - public interface DragScrollProfile { - /** - * Return a scroll speed in pixels/millisecond. Always return a - * positive number. - * - * @param w Normalized position in scroll region (i.e. w \in [0,1]). - * Small w typically means slow scrolling. - * @param t Time (in milliseconds) since start of scroll (handy if you - * want scroll acceleration). - * @return Scroll speed at position w and time t in pixels/ms. - */ - float getSpeed(float w, long t); - } - - private class DragScroller implements Runnable { - - private boolean mAbort; - - private long mPrevTime; - private long mCurrTime; - - private int dy; - private float dt; - private long tStart; - private int scrollDir; - - public final static int STOP = -1; - public final static int UP = 0; - public final static int DOWN = 1; - - private float mScrollSpeed; // pixels per ms - - private boolean mScrolling = false; - - public boolean isScrolling() { - return mScrolling; - } - - public int getScrollDir() { - return mScrolling ? scrollDir : STOP; - } - - public DragScroller() { - } - - public void startScrolling(int dir) { - if (!mScrolling) { - mAbort = false; - mScrolling = true; - tStart = SystemClock.uptimeMillis(); - mPrevTime = tStart; - scrollDir = dir; - post(this); - } - } - - public void stopScrolling(boolean now) { - if (now) { - DragSortListView.this.removeCallbacks(this); - mScrolling = false; - } else { - mAbort = true; - } - } - - @Override - public void run() { - if (mAbort) { - mScrolling = false; - return; - } - - final int first = getFirstVisiblePosition(); - final int last = getLastVisiblePosition(); - final int count = getCount(); - final int padTop = getPaddingTop(); - final int listHeight = getHeight() - padTop - getPaddingBottom(); - - int minY = Math.min(mY, mFloatViewMid + mFloatViewHeightHalf); - int maxY = Math.max(mY, mFloatViewMid - mFloatViewHeightHalf); - - if (scrollDir == UP) { - View v = getChildAt(0); - if (v == null) { - mScrolling = false; - return; - } else { - if (first == 0 && v.getTop() == padTop) { - mScrolling = false; - return; - } - } - mScrollSpeed = mScrollProfile.getSpeed((mUpScrollStartYF - maxY) - / mDragUpScrollHeight, mPrevTime); - } else { - View v = getChildAt(last - first); - if (v == null) { - mScrolling = false; - return; - } else { - if (last == count - 1 && v.getBottom() <= listHeight + padTop) { - mScrolling = false; - return; - } - } - mScrollSpeed = -mScrollProfile.getSpeed((minY - mDownScrollStartYF) - / mDragDownScrollHeight, mPrevTime); - } - - mCurrTime = SystemClock.uptimeMillis(); - dt = (float) (mCurrTime - mPrevTime); - - // dy is change in View position of a list item; i.e. positive dy - // means user is scrolling up (list item moves down the screen, - // remember - // y=0 is at top of View). - dy = (int) Math.round(mScrollSpeed * dt); - - int movePos; - if (dy >= 0) { - dy = Math.min(listHeight, dy); - movePos = first; - } else { - dy = Math.max(-listHeight, dy); - movePos = last; - } - - final View moveItem = getChildAt(movePos - first); - int top = moveItem.getTop() + dy; - - if (movePos == 0 && top > padTop) { - top = padTop; - } - - // always do scroll - mBlockLayoutRequests = true; - - setSelectionFromTop(movePos, top - padTop); - DragSortListView.this.layoutChildren(); - invalidate(); - - mBlockLayoutRequests = false; - - // scroll means relative float View movement - doDragFloatView(movePos, moveItem, false); - - mPrevTime = mCurrTime; - - post(this); - } - } - - // TODO: Bluuurgh... switch to SharedPreferences - private class DragSortTracker { - StringBuilder mBuilder = new StringBuilder(); - - File mFile; - - private int mNumInBuffer = 0; - private int mNumFlushes = 0; - - private boolean mTracking = false; - - public DragSortTracker() { - File root = Environment.getExternalStorageDirectory(); - mFile = new File(root, "dslv_state.txt"); - - if (!mFile.exists()) { - try { - mFile.createNewFile(); - Log.d("mobeta", "file created"); - } catch (IOException e) { - Log.w("mobeta", "Could not create dslv_state.txt"); - Log.d("mobeta", e.getMessage()); - } - } - - } - - public void startTracking() { - mBuilder.append("\n"); - mNumFlushes = 0; - mTracking = true; - } - - public void appendState() { - if (!mTracking) { - return; - } - - mBuilder.append("\n"); - final int children = getChildCount(); - final int first = getFirstVisiblePosition(); - mBuilder.append(" "); - for (int i = 0; i < children; ++i) { - mBuilder.append(first + i).append(","); - } - mBuilder.append("\n"); - - mBuilder.append(" "); - for (int i = 0; i < children; ++i) { - mBuilder.append(getChildAt(i).getTop()).append(","); - } - mBuilder.append("\n"); - mBuilder.append(" "); - for (int i = 0; i < children; ++i) { - mBuilder.append(getChildAt(i).getBottom()).append(","); - } - mBuilder.append("\n"); - - mBuilder.append(" ").append(mFirstExpPos).append("\n"); - mBuilder.append(" ") - .append(getItemHeight(mFirstExpPos) - getChildHeight(mFirstExpPos)) - .append("\n"); - mBuilder.append(" ").append(mSecondExpPos).append("\n"); - mBuilder.append(" ") - .append(getItemHeight(mSecondExpPos) - getChildHeight(mSecondExpPos)) - .append("\n"); - mBuilder.append(" ").append(mSrcPos).append("\n"); - mBuilder.append(" ").append(mFloatViewHeight + getDividerHeight()) - .append("\n"); - mBuilder.append(" ").append(getHeight()).append("\n"); - mBuilder.append(" ").append(mLastY).append("\n"); - mBuilder.append(" ").append(mFloatViewMid).append("\n"); - mBuilder.append(" "); - for (int i = 0; i < children; ++i) { - mBuilder.append(getShuffleEdge(first + i, getChildAt(i).getTop())).append(","); - } - mBuilder.append("\n"); - - mBuilder.append("\n"); - mNumInBuffer++; - - if (mNumInBuffer > 1000) { - flush(); - mNumInBuffer = 0; - } - } - - public void flush() { - if (!mTracking) { - return; - } - - // save to file on sdcard - try { - boolean append = true; - if (mNumFlushes == 0) { - append = false; - } - FileWriter writer = new FileWriter(mFile, append); - - writer.write(mBuilder.toString()); - mBuilder.delete(0, mBuilder.length()); - - writer.flush(); - writer.close(); - - mNumFlushes++; - } catch (IOException e) { - // do nothing - } - } - - public void stopTracking() { - if (mTracking) { - mBuilder.append("\n"); - flush(); - mTracking = false; - } - } - - } - -} +/* + * DragSortListView. + * + * A subclass of the Android ListView component that enables drag + * and drop re-ordering of list items. + * + * Copyright 2012 Carl Bauer + * + * 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.mobeta.android.dslv; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import android.content.Context; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Point; +import android.graphics.drawable.Drawable; +import android.os.Environment; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseBooleanArray; +import android.util.SparseIntArray; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.BaseAdapter; +import android.widget.Checkable; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.WrapperListAdapter; + +/** + * ListView subclass that mediates drag and drop resorting of items. + * + * + * @author heycosmo + * + */ +public class DragSortListView extends ListView { + + + /** + * The View that floats above the ListView and represents + * the dragged item. + */ + private View mFloatView; + + /** + * The float View location. First based on touch location + * and given deltaX and deltaY. Then restricted by callback + * to FloatViewManager.onDragFloatView(). Finally restricted + * by bounds of DSLV. + */ + private Point mFloatLoc = new Point(); + + private Point mTouchLoc = new Point(); + + /** + * The middle (in the y-direction) of the floating View. + */ + private int mFloatViewMid; + + /** + * Flag to make sure float View isn't measured twice + */ + private boolean mFloatViewOnMeasured = false; + + /** + * Watch the Adapter for data changes. Cancel a drag if + * coincident with a change. + */ + private DataSetObserver mObserver; + + /** + * Transparency for the floating View (XML attribute). + */ + private float mFloatAlpha = 1.0f; + private float mCurrFloatAlpha = 1.0f; + + /** + * While drag-sorting, the current position of the floating + * View. If dropped, the dragged item will land in this position. + */ + private int mFloatPos; + + /** + * The first expanded ListView position that helps represent + * the drop slot tracking the floating View. + */ + private int mFirstExpPos; + + /** + * The second expanded ListView position that helps represent + * the drop slot tracking the floating View. This can equal + * mFirstExpPos if there is no slide shuffle occurring; otherwise + * it is equal to mFirstExpPos + 1. + */ + private int mSecondExpPos; + + /** + * Flag set if slide shuffling is enabled. + */ + private boolean mAnimate = false; + + /** + * The user dragged from this position. + */ + private int mSrcPos; + + /** + * Offset (in x) within the dragged item at which the user + * picked it up (or first touched down with the digitalis). + */ + private int mDragDeltaX; + + /** + * Offset (in y) within the dragged item at which the user + * picked it up (or first touched down with the digitalis). + */ + private int mDragDeltaY; + + + /** + * The difference (in x) between screen coordinates and coordinates + * in this view. + */ + //private int mOffsetX; + + /** + * The difference (in y) between screen coordinates and coordinates + * in this view. + */ + //private int mOffsetY; + + /** + * A listener that receives callbacks whenever the floating View + * hovers over a new position. + */ + private DragListener mDragListener; + + /** + * A listener that receives a callback when the floating View + * is dropped. + */ + private DropListener mDropListener; + + /** + * A listener that receives a callback when the floating View + * (or more precisely the originally dragged item) is removed + * by one of the provided gestures. + */ + private RemoveListener mRemoveListener; + + /** + * Enable/Disable item dragging + * + * @attr name dslv:drag_enabled + */ + private boolean mDragEnabled = true; + + /** + * Drag state enum. + */ + private final static int IDLE = 0; + private final static int REMOVING = 1; + private final static int DROPPING = 2; + private final static int STOPPED = 3; + private final static int DRAGGING = 4; + + private int mDragState = IDLE; + + /** + * Height in pixels to which the originally dragged item + * is collapsed during a drag-sort. Currently, this value + * must be greater than zero. + */ + private int mItemHeightCollapsed = 1; + + /** + * Height of the floating View. Stored for the purpose of + * providing the tracking drop slot. + */ + private int mFloatViewHeight; + + /** + * Convenience member. See above. + */ + private int mFloatViewHeightHalf; + + /** + * Save the given width spec for use in measuring children + */ + private int mWidthMeasureSpec = 0; + + /** + * Sample Views ultimately used for calculating the height + * of ListView items that are off-screen. + */ + private View[] mSampleViewTypes = new View[1]; + + /** + * Drag-scroll encapsulator! + */ + private DragScroller mDragScroller; + + /** + * Determines the start of the upward drag-scroll region + * at the top of the ListView. Specified by a fraction + * of the ListView height, thus screen resolution agnostic. + */ + private float mDragUpScrollStartFrac = 1.0f / 3.0f; + + /** + * Determines the start of the downward drag-scroll region + * at the bottom of the ListView. Specified by a fraction + * of the ListView height, thus screen resolution agnostic. + */ + private float mDragDownScrollStartFrac = 1.0f / 3.0f; + + /** + * The following are calculated from the above fracs. + */ + private int mUpScrollStartY; + private int mDownScrollStartY; + private float mDownScrollStartYF; + private float mUpScrollStartYF; + + /** + * Calculated from above above and current ListView height. + */ + private float mDragUpScrollHeight; + + /** + * Calculated from above above and current ListView height. + */ + private float mDragDownScrollHeight; + + /** + * Maximum drag-scroll speed in pixels per ms. Only used with + * default linear drag-scroll profile. + */ + private float mMaxScrollSpeed = 0.5f; + + /** + * Defines the scroll speed during a drag-scroll. User can + * provide their own; this default is a simple linear profile + * where scroll speed increases linearly as the floating View + * nears the top/bottom of the ListView. + */ + private DragScrollProfile mScrollProfile = new DragScrollProfile() { + @Override + public float getSpeed(float w, long t) { + return mMaxScrollSpeed * w; + } + }; + + /** + * Current touch x. + */ + private int mX; + + /** + * Current touch y. + */ + private int mY; + + /** + * Last touch x. + */ + //private int mLastX; + + /** + * Last touch y. + */ + private int mLastY; + + /** + * The touch y-coord at which drag started + */ + //private int mDragStartY; + + /** + * Drag flag bit. Floating View can move in the positive + * x direction. + */ + public final static int DRAG_POS_X = 0x1; + + /** + * Drag flag bit. Floating View can move in the negative + * x direction. + */ + public final static int DRAG_NEG_X = 0x2; + + /** + * Drag flag bit. Floating View can move in the positive + * y direction. This is subtle. What this actually means is + * that, if enabled, the floating View can be dragged below its starting + * position. Remove in favor of upper-bounding item position? + */ + public final static int DRAG_POS_Y = 0x4; + + /** + * Drag flag bit. Floating View can move in the negative + * y direction. This is subtle. What this actually means is + * that the floating View can be dragged above its starting + * position. Remove in favor of lower-bounding item position? + */ + public final static int DRAG_NEG_Y = 0x8; + + /** + * Flags that determine limits on the motion of the + * floating View. See flags above. + */ + private int mDragFlags = 0; + + /** + * Last call to an on*TouchEvent was a call to + * onInterceptTouchEvent. + */ + private boolean mLastCallWasIntercept = false; + + /** + * A touch event is in progress. + */ + private boolean mInTouchEvent = false; + + /** + * Let the user customize the floating View. + */ + private FloatViewManager mFloatViewManager = null; + + /** + * Given to ListView to cancel its action when a drag-sort + * begins. + */ + private MotionEvent mCancelEvent; + + /** + * Enum telling where to cancel the ListView action when a + * drag-sort begins + */ + private static final int NO_CANCEL = 0; + private static final int ON_TOUCH_EVENT = 1; + private static final int ON_INTERCEPT_TOUCH_EVENT = 2; + + /** + * Where to cancel the ListView action when a + * drag-sort begins + */ + private int mCancelMethod = NO_CANCEL; + + /** + * Determines when a slide shuffle animation starts. That is, + * defines how close to the edge of the drop slot the floating + * View must be to initiate the slide. + */ + private float mSlideRegionFrac = 0.25f; + + /** + * Number between 0 and 1 indicating the relative location of + * a sliding item (only used if drag-sort animations + * are turned on). Nearly 1 means the item is + * at the top of the slide region (nearly full blank item + * is directly below). + */ + private float mSlideFrac = 0.0f; + + /** + * Wraps the user-provided ListAdapter. This is used to wrap each + * item View given by the user inside another View (currenly + * a RelativeLayout) which + * expands and collapses to simulate the item shuffling. + */ + private AdapterWrapper mAdapterWrapper; + + /** + * Turn on custom debugger. + */ + private boolean mTrackDragSort = false; + + /** + * Debugging class. + */ + private DragSortTracker mDragSortTracker; + + /** + * Needed for adjusting item heights from within layoutChildren + */ + private boolean mBlockLayoutRequests = false; + + /** + * Set to true when a down event happens during drag sort; + * for example, when drag finish animations are + * playing. + */ + private boolean mIgnoreTouchEvent = false; + + /** + * Caches DragSortItemView child heights. Sometimes DSLV has to + * know the height of an offscreen item. Since ListView virtualizes + * these, DSLV must get the item from the ListAdapter to obtain + * its height. That process can be expensive, but often the same + * offscreen item will be requested many times in a row. Once an + * offscreen item height is calculated, we cache it in this guy. + * Actually, we cache the height of the child of the + * DragSortItemView since the item height changes often during a + * drag-sort. + */ + private static final int sCacheSize = 3; + private HeightCache mChildHeightCache = new HeightCache(sCacheSize); + + private RemoveAnimator mRemoveAnimator; + + private LiftAnimator mLiftAnimator; + + private DropAnimator mDropAnimator; + + private boolean mUseRemoveVelocity; + private float mRemoveVelocityX = 0; + + public DragSortListView(Context context, AttributeSet attrs) { + super(context, attrs); + + int defaultDuration = 150; + int removeAnimDuration = defaultDuration; // ms + int dropAnimDuration = defaultDuration; // ms + + if (attrs != null) { + TypedArray a = getContext().obtainStyledAttributes(attrs, + R.styleable.DragSortListView, 0, 0); + + mItemHeightCollapsed = Math.max(1, a.getDimensionPixelSize( + R.styleable.DragSortListView_collapsed_height, 1)); + + mTrackDragSort = a.getBoolean( + R.styleable.DragSortListView_track_drag_sort, false); + + if (mTrackDragSort) { + mDragSortTracker = new DragSortTracker(); + } + + // alpha between 0 and 255, 0=transparent, 255=opaque + mFloatAlpha = a.getFloat(R.styleable.DragSortListView_float_alpha, mFloatAlpha); + mCurrFloatAlpha = mFloatAlpha; + + mDragEnabled = a.getBoolean(R.styleable.DragSortListView_drag_enabled, mDragEnabled); + + mSlideRegionFrac = Math.max(0.0f, + Math.min(1.0f, 1.0f - a.getFloat( + R.styleable.DragSortListView_slide_shuffle_speed, + 0.75f))); + + mAnimate = mSlideRegionFrac > 0.0f; + + float frac = a.getFloat( + R.styleable.DragSortListView_drag_scroll_start, + mDragUpScrollStartFrac); + + setDragScrollStart(frac); + + mMaxScrollSpeed = a.getFloat( + R.styleable.DragSortListView_max_drag_scroll_speed, + mMaxScrollSpeed); + + removeAnimDuration = a.getInt( + R.styleable.DragSortListView_remove_animation_duration, + removeAnimDuration); + + dropAnimDuration = a.getInt( + R.styleable.DragSortListView_drop_animation_duration, + dropAnimDuration); + + boolean useDefault = a.getBoolean( + R.styleable.DragSortListView_use_default_controller, + true); + + if (useDefault) { + boolean removeEnabled = a.getBoolean( + R.styleable.DragSortListView_remove_enabled, + false); + int removeMode = a.getInt( + R.styleable.DragSortListView_remove_mode, + DragSortController.FLING_REMOVE); + boolean sortEnabled = a.getBoolean( + R.styleable.DragSortListView_sort_enabled, + true); + int dragInitMode = a.getInt( + R.styleable.DragSortListView_drag_start_mode, + DragSortController.ON_DOWN); + int dragHandleId = a.getResourceId( + R.styleable.DragSortListView_drag_handle_id, + 0); + int flingHandleId = a.getResourceId( + R.styleable.DragSortListView_fling_handle_id, + 0); + int clickRemoveId = a.getResourceId( + R.styleable.DragSortListView_click_remove_id, + 0); + int bgColor = a.getColor( + R.styleable.DragSortListView_float_background_color, + Color.BLACK); + + DragSortController controller = new DragSortController( + this, dragHandleId, dragInitMode, removeMode, + clickRemoveId, flingHandleId); + controller.setRemoveEnabled(removeEnabled); + controller.setSortEnabled(sortEnabled); + controller.setBackgroundColor(bgColor); + + mFloatViewManager = controller; + setOnTouchListener(controller); + } + + a.recycle(); + } + + mDragScroller = new DragScroller(); + + float smoothness = 0.5f; + if (removeAnimDuration > 0) { + mRemoveAnimator = new RemoveAnimator(smoothness, removeAnimDuration); + } + // mLiftAnimator = new LiftAnimator(smoothness, 100); + if (dropAnimDuration > 0) { + mDropAnimator = new DropAnimator(smoothness, dropAnimDuration); + } + + mCancelEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0f, 0f, 0, 0f, + 0f, 0, 0); + + // construct the dataset observer + mObserver = new DataSetObserver() { + private void cancel() { + if (mDragState == DRAGGING) { + cancelDrag(); + } + } + + @Override + public void onChanged() { + cancel(); + } + + @Override + public void onInvalidated() { + cancel(); + } + }; + } + + /** + * Usually called from a FloatViewManager. The float alpha + * will be reset to the xml-defined value every time a drag + * is stopped. + */ + public void setFloatAlpha(float alpha) { + mCurrFloatAlpha = alpha; + } + + public float getFloatAlpha() { + return mCurrFloatAlpha; + } + + /** + * Set maximum drag scroll speed in positions/second. Only applies + * if using default ScrollSpeedProfile. + * + * @param max Maximum scroll speed. + */ + public void setMaxScrollSpeed(float max) { + mMaxScrollSpeed = max; + } + + /** + * For each DragSortListView Listener interface implemented by + * adapter, this method calls the appropriate + * set*Listener method with adapter as the argument. + * + * @param adapter The ListAdapter providing data to back + * DragSortListView. + * + * @see android.widget.ListView#setAdapter(android.widget.ListAdapter) + */ + @Override + public void setAdapter(ListAdapter adapter) { + if (adapter != null) { + mAdapterWrapper = new AdapterWrapper(adapter); + adapter.registerDataSetObserver(mObserver); + + if (adapter instanceof DropListener) { + setDropListener((DropListener) adapter); + } + if (adapter instanceof DragListener) { + setDragListener((DragListener) adapter); + } + if (adapter instanceof RemoveListener) { + setRemoveListener((RemoveListener) adapter); + } + } else { + mAdapterWrapper = null; + } + + super.setAdapter(mAdapterWrapper); + } + + /** + * As opposed to {@link ListView#getAdapter()}, which returns + * a heavily wrapped ListAdapter (DragSortListView wraps the + * input ListAdapter {\emph and} ListView wraps the wrapped one). + * + * @return The ListAdapter set as the argument of {@link setAdapter()} + */ + public ListAdapter getInputAdapter() { + if (mAdapterWrapper == null) { + return null; + } else { + return mAdapterWrapper.getWrappedAdapter(); + } + } + + private class AdapterWrapper extends BaseAdapter implements WrapperListAdapter { + private ListAdapter mAdapter; + + public AdapterWrapper(ListAdapter adapter) { + mAdapter = adapter; + } + + @Override + public ListAdapter getWrappedAdapter() { + return mAdapter; + } + + @Override + public long getItemId(int position) { + return mAdapter.getItemId(position); + } + + @Override + public Object getItem(int position) { + return mAdapter.getItem(position); + } + + @Override + public int getCount() { + return mAdapter.getCount(); + } + + @Override + public boolean areAllItemsEnabled() { + return mAdapter.areAllItemsEnabled(); + } + + @Override + public boolean isEnabled(int position) { + return mAdapter.isEnabled(position); + } + + @Override + public int getItemViewType(int position) { + return mAdapter.getItemViewType(position); + } + + @Override + public int getViewTypeCount() { + return mAdapter.getViewTypeCount(); + } + + @Override + public boolean hasStableIds() { + return mAdapter.hasStableIds(); + } + + @Override + public boolean isEmpty() { + return mAdapter.isEmpty(); + } + + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + DragSortItemView v; + View child; + if (convertView != null) { + v = (DragSortItemView) convertView; + View oldChild = v.getChildAt(0); + + child = mAdapter.getView(position, oldChild, DragSortListView.this); + if (child != oldChild) { + // shouldn't get here if user is reusing convertViews + // properly + if (oldChild != null) { + v.removeViewAt(0); + } + v.addView(child); + } + } else { + child = mAdapter.getView(position, null, DragSortListView.this); + if (child instanceof Checkable) { + v = new DragSortItemViewCheckable(getContext()); + } else { + v = new DragSortItemView(getContext()); + } + v.setLayoutParams(new AbsListView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + v.addView(child); + } + + // Set the correct item height given drag state; passed + // View needs to be measured if measurement is required. + adjustItem(position + getHeaderViewsCount(), v, true); + + return v; + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + mAdapter.registerDataSetObserver(observer); + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + mAdapter.unregisterDataSetObserver(observer); + } + } + + private void drawDivider(int expPosition, Canvas canvas) { + final Drawable divider = getDivider(); + final int dividerHeight = getDividerHeight(); + + if (divider != null && dividerHeight != 0) { + final ViewGroup expItem = (ViewGroup) getChildAt(expPosition + - getFirstVisiblePosition()); + if (expItem != null) { + final int left = getPaddingLeft(); + final int right = getWidth() - getPaddingRight(); + final int top; + final int bottom; + + final int childHeight = expItem.getChildAt(0).getHeight(); + + if (expPosition > mSrcPos) { + top = expItem.getTop() + childHeight; + bottom = top + dividerHeight; + } else { + bottom = expItem.getBottom() - childHeight; + top = bottom - dividerHeight; + } + + // Have to clip to support ColorDrawable on <= Gingerbread + canvas.save(); + canvas.clipRect(left, top, right, bottom); + divider.setBounds(left, top, right, bottom); + divider.draw(canvas); + canvas.restore(); + } + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (mDragState != IDLE) { + // draw the divider over the expanded item + if (mFirstExpPos != mSrcPos) { + drawDivider(mFirstExpPos, canvas); + } + if (mSecondExpPos != mFirstExpPos && mSecondExpPos != mSrcPos) { + drawDivider(mSecondExpPos, canvas); + } + } + + if (mFloatView != null) { + // draw the float view over everything + final int floatViewWidth = mFloatView.getWidth(); + final int floatViewHeight = mFloatView.getHeight(); + + int x = mFloatLoc.x; + + final int listViewWidth = getWidth(); + if (x < 0) + x = -x; + float alphaMod; + if (x < listViewWidth) { + alphaMod = ((float) (listViewWidth - x)) / ((float) listViewWidth); + alphaMod *= alphaMod; + } else { + alphaMod = 0; + } + + final int alpha = (int) (255f * mCurrFloatAlpha * alphaMod); + + canvas.save(); + canvas.translate(mFloatLoc.x, mFloatLoc.y); + canvas.clipRect(0, 0, floatViewWidth, floatViewHeight); + + canvas.saveLayerAlpha(0, 0, floatViewWidth, floatViewHeight, alpha, + Canvas.ALL_SAVE_FLAG); + + mFloatView.draw(canvas); + canvas.restore(); + canvas.restore(); + } + } + + private int getItemHeight(int position) { + View v = getChildAt(position - getFirstVisiblePosition()); + + if (v != null) { + // item is onscreen, just get the height of the View + return v.getHeight(); + } else { + // item is offscreen. get child height and calculate + // item height based on current shuffle state + return calcItemHeight(position, getChildHeight(position)); + } + } + + private class HeightCache { + + private SparseIntArray mMap; + private List mOrder; + private int mMaxSize; + + public HeightCache(int size) { + mMap = new SparseIntArray(size); + mOrder = new ArrayList(size); + mMaxSize = size; + } + + /** + * Add item height at position if doesn't already exist. + */ + public void add(int position, int height) { + int currentHeight = mMap.get(position, -1); + if (currentHeight != height) { + if (currentHeight == -1 && mMap.size() == mMaxSize) { + // remove oldest entry + mMap.delete(mOrder.remove(0)); + } else { + // move position to newest slot + mOrder.remove((Integer) position); + } + mMap.put(position, height); + mOrder.add(position); + } + } + + public int get(int position) { + return mMap.get(position, -1); + } + + public void clear() { + mMap.clear(); + mOrder.clear(); + } + + } + + /** + * Get the shuffle edge for item at position when top of + * item is at y-coord top. Assumes that current item heights + * are consistent with current float view location and + * thus expanded positions and slide fraction. i.e. Should not be + * called between update of expanded positions/slide fraction + * and layoutChildren. + * + * @param position + * @param top + * @param height Height of item at position. If -1, this function + * calculates this height. + * + * @return Shuffle line between position-1 and position (for + * the given view of the list; that is, for when top of item at + * position has y-coord of given `top`). If + * floating View (treated as horizontal line) is dropped + * immediately above this line, it lands in position-1. If + * dropped immediately below this line, it lands in position. + */ + private int getShuffleEdge(int position, int top) { + final int numHeaders = getHeaderViewsCount(); + final int numFooters = getFooterViewsCount(); + + // shuffle edges are defined between items that can be + // dragged; there are N-1 of them if there are N draggable + // items. + + if (position <= numHeaders || (position >= getCount() - numFooters)) { + return top; + } + + int divHeight = getDividerHeight(); + + int edge; + + int maxBlankHeight = mFloatViewHeight - mItemHeightCollapsed; + int childHeight = getChildHeight(position); + int itemHeight = getItemHeight(position); + + // first calculate top of item given that floating View is + // centered over src position + int otop = top; + if (mSecondExpPos <= mSrcPos) { + // items are expanded on and/or above the source position + + if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) { + if (position == mSrcPos) { + otop = top + itemHeight - mFloatViewHeight; + } else { + int blankHeight = itemHeight - childHeight; + otop = top + blankHeight - maxBlankHeight; + } + } else if (position > mSecondExpPos && position <= mSrcPos) { + otop = top - maxBlankHeight; + } + + } else { + // items are expanded on and/or below the source position + + if (position > mSrcPos && position <= mFirstExpPos) { + otop = top + maxBlankHeight; + } else if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) { + int blankHeight = itemHeight - childHeight; + otop = top + blankHeight; + } + } + + // otop is set + if (position <= mSrcPos) { + edge = otop + (mFloatViewHeight - divHeight - getChildHeight(position - 1)) / 2; + } else { + edge = otop + (childHeight - divHeight - mFloatViewHeight) / 2; + } + + return edge; + } + + private boolean updatePositions() { + final int first = getFirstVisiblePosition(); + int startPos = mFirstExpPos; + View startView = getChildAt(startPos - first); + + if (startView == null) { + startPos = first + getChildCount() / 2; + startView = getChildAt(startPos - first); + } + int startTop = startView.getTop(); + + int itemHeight = startView.getHeight(); + + int edge = getShuffleEdge(startPos, startTop); + int lastEdge = edge; + + int divHeight = getDividerHeight(); + + // Log.d("mobeta", "float mid="+mFloatViewMid); + + int itemPos = startPos; + int itemTop = startTop; + if (mFloatViewMid < edge) { + // scanning up for float position + while (itemPos >= 0) { + itemPos--; + itemHeight = getItemHeight(itemPos); + + if (itemPos == 0) { + edge = itemTop - divHeight - itemHeight; + break; + } + + itemTop -= itemHeight + divHeight; + edge = getShuffleEdge(itemPos, itemTop); + + if (mFloatViewMid >= edge) { + break; + } + + lastEdge = edge; + } + } else { + // scanning down for float position + final int count = getCount(); + while (itemPos < count) { + if (itemPos == count - 1) { + edge = itemTop + divHeight + itemHeight; + break; + } + + itemTop += divHeight + itemHeight; + itemHeight = getItemHeight(itemPos + 1); + edge = getShuffleEdge(itemPos + 1, itemTop); + + // test for hit + if (mFloatViewMid < edge) { + break; + } + + lastEdge = edge; + itemPos++; + } + } + + final int numHeaders = getHeaderViewsCount(); + final int numFooters = getFooterViewsCount(); + + boolean updated = false; + + int oldFirstExpPos = mFirstExpPos; + int oldSecondExpPos = mSecondExpPos; + float oldSlideFrac = mSlideFrac; + + if (mAnimate) { + int edgeToEdge = Math.abs(edge - lastEdge); + + int edgeTop, edgeBottom; + if (mFloatViewMid < edge) { + edgeTop = lastEdge; + edgeBottom = edge; + } else { + edgeTop = edge; + edgeBottom = lastEdge; + } + + int slideRgnHeight = (int) (0.5f * mSlideRegionFrac * edgeToEdge); + float slideRgnHeightF = (float) slideRgnHeight; + int slideEdgeTop = edgeTop + slideRgnHeight; + int slideEdgeBottom = edgeBottom - slideRgnHeight; + + // Three regions + if (mFloatViewMid < slideEdgeTop) { + mFirstExpPos = itemPos - 1; + mSecondExpPos = itemPos; + mSlideFrac = 0.5f * ((float) (slideEdgeTop - mFloatViewMid)) / slideRgnHeightF; + } else if (mFloatViewMid < slideEdgeBottom) { + mFirstExpPos = itemPos; + mSecondExpPos = itemPos; + } else { + mFirstExpPos = itemPos; + mSecondExpPos = itemPos + 1; + mSlideFrac = 0.5f * (1.0f + ((float) (edgeBottom - mFloatViewMid)) + / slideRgnHeightF); + } + + } else { + mFirstExpPos = itemPos; + mSecondExpPos = itemPos; + } + + // correct for headers and footers + if (mFirstExpPos < numHeaders) { + itemPos = numHeaders; + mFirstExpPos = itemPos; + mSecondExpPos = itemPos; + } else if (mSecondExpPos >= getCount() - numFooters) { + itemPos = getCount() - numFooters - 1; + mFirstExpPos = itemPos; + mSecondExpPos = itemPos; + } + + if (mFirstExpPos != oldFirstExpPos || mSecondExpPos != oldSecondExpPos + || mSlideFrac != oldSlideFrac) { + updated = true; + } + + if (itemPos != mFloatPos) { + if (mDragListener != null) { + mDragListener.drag(mFloatPos - numHeaders, itemPos - numHeaders); + } + + mFloatPos = itemPos; + updated = true; + } + + return updated; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mTrackDragSort) { + mDragSortTracker.appendState(); + } + } + + private class SmoothAnimator implements Runnable { + protected long mStartTime; + + private float mDurationF; + + private float mAlpha; + private float mA, mB, mC, mD; + + private boolean mCanceled; + + public SmoothAnimator(float smoothness, int duration) { + mAlpha = smoothness; + mDurationF = (float) duration; + mA = mD = 1f / (2f * mAlpha * (1f - mAlpha)); + mB = mAlpha / (2f * (mAlpha - 1f)); + mC = 1f / (1f - mAlpha); + } + + public float transform(float frac) { + if (frac < mAlpha) { + return mA * frac * frac; + } else if (frac < 1f - mAlpha) { + return mB + mC * frac; + } else { + return 1f - mD * (frac - 1f) * (frac - 1f); + } + } + + public void start() { + mStartTime = SystemClock.uptimeMillis(); + mCanceled = false; + onStart(); + post(this); + } + + public void cancel() { + mCanceled = true; + } + + public void onStart() { + // stub + } + + public void onUpdate(float frac, float smoothFrac) { + // stub + } + + public void onStop() { + // stub + } + + @Override + public void run() { + if (mCanceled) { + return; + } + + float fraction = ((float) (SystemClock.uptimeMillis() - mStartTime)) / mDurationF; + + if (fraction >= 1f) { + onUpdate(1f, 1f); + onStop(); + } else { + onUpdate(fraction, transform(fraction)); + post(this); + } + } + } + + /** + * Centers floating View under touch point. + */ + private class LiftAnimator extends SmoothAnimator { + + private float mInitDragDeltaY; + private float mFinalDragDeltaY; + + public LiftAnimator(float smoothness, int duration) { + super(smoothness, duration); + } + + @Override + public void onStart() { + mInitDragDeltaY = mDragDeltaY; + mFinalDragDeltaY = mFloatViewHeightHalf; + } + + @Override + public void onUpdate(float frac, float smoothFrac) { + if (mDragState != DRAGGING) { + cancel(); + } else { + mDragDeltaY = (int) (smoothFrac * mFinalDragDeltaY + (1f - smoothFrac) + * mInitDragDeltaY); + mFloatLoc.y = mY - mDragDeltaY; + doDragFloatView(true); + } + } + } + + /** + * Centers floating View over drop slot before destroying. + */ + private class DropAnimator extends SmoothAnimator { + + private int mDropPos; + private int srcPos; + private float mInitDeltaY; + private float mInitDeltaX; + + public DropAnimator(float smoothness, int duration) { + super(smoothness, duration); + } + + @Override + public void onStart() { + mDropPos = mFloatPos; + srcPos = mSrcPos; + mDragState = DROPPING; + mInitDeltaY = mFloatLoc.y - getTargetY(); + mInitDeltaX = mFloatLoc.x - getPaddingLeft(); + } + + private int getTargetY() { + final int first = getFirstVisiblePosition(); + final int otherAdjust = (mItemHeightCollapsed + getDividerHeight()) / 2; + View v = getChildAt(mDropPos - first); + int targetY = -1; + if (v != null) { + if (mDropPos == srcPos) { + targetY = v.getTop(); + } else if (mDropPos < srcPos) { + // expanded down + targetY = v.getTop() - otherAdjust; + } else { + // expanded up + targetY = v.getBottom() + otherAdjust - mFloatViewHeight; + } + } else { + // drop position is not on screen?? no animation + cancel(); + } + + return targetY; + } + + @Override + public void onUpdate(float frac, float smoothFrac) { + final int targetY = getTargetY(); + final int targetX = getPaddingLeft(); + final float deltaY = mFloatLoc.y - targetY; + final float deltaX = mFloatLoc.x - targetX; + final float f = 1f - smoothFrac; + if (f < Math.abs(deltaY / mInitDeltaY) || f < Math.abs(deltaX / mInitDeltaX)) { + mFloatLoc.y = targetY + (int) (mInitDeltaY * f); + mFloatLoc.x = getPaddingLeft() + (int) (mInitDeltaX * f); + doDragFloatView(true); + } + } + + @Override + public void onStop() { + dropFloatView(); + } + + } + + /** + * Collapses expanded items. + */ + private class RemoveAnimator extends SmoothAnimator { + + private float mFloatLocX; + private float mFirstStartBlank; + private float mSecondStartBlank; + + private int mFirstChildHeight = -1; + private int mSecondChildHeight = -1; + + private int mFirstPos; + private int mSecondPos; + + public RemoveAnimator(float smoothness, int duration) { + super(smoothness, duration); + } + + @Override + public void onStart() { + mFirstChildHeight = -1; + mSecondChildHeight = -1; + mFirstPos = mFirstExpPos; + mSecondPos = mSecondExpPos; + mDragState = REMOVING; + + mFloatLocX = mFloatLoc.x; + if (mUseRemoveVelocity) { + float minVelocity = 2f * getWidth(); + if (mRemoveVelocityX == 0) { + mRemoveVelocityX = (mFloatLocX < 0 ? -1 : 1) * minVelocity; + } else { + minVelocity *= 2; + if (mRemoveVelocityX < 0 && mRemoveVelocityX > -minVelocity) + mRemoveVelocityX = -minVelocity; + else if (mRemoveVelocityX > 0 && mRemoveVelocityX < minVelocity) + mRemoveVelocityX = minVelocity; + } + } else { + destroyFloatView(); + } + } + + @Override + public void onUpdate(float frac, float smoothFrac) { + float f = 1f - smoothFrac; + + final int firstVis = getFirstVisiblePosition(); + View item = getChildAt(mFirstPos - firstVis); + ViewGroup.LayoutParams lp; + int blank; + + if (mUseRemoveVelocity) { + float dt = (float) (SystemClock.uptimeMillis() - mStartTime) / 1000; + if (dt == 0) + return; + float dx = mRemoveVelocityX * dt; + int w = getWidth(); + mRemoveVelocityX += (mRemoveVelocityX > 0 ? 1 : -1) * dt * w; + mFloatLocX += dx; + mFloatLoc.x = (int) mFloatLocX; + if (mFloatLocX < w && mFloatLocX > -w) { + mStartTime = SystemClock.uptimeMillis(); + doDragFloatView(true); + return; + } + } + + if (item != null) { + if (mFirstChildHeight == -1) { + mFirstChildHeight = getChildHeight(mFirstPos, item, false); + mFirstStartBlank = (float) (item.getHeight() - mFirstChildHeight); + } + blank = Math.max((int) (f * mFirstStartBlank), 1); + lp = item.getLayoutParams(); + lp.height = mFirstChildHeight + blank; + item.setLayoutParams(lp); + } + if (mSecondPos != mFirstPos) { + item = getChildAt(mSecondPos - firstVis); + if (item != null) { + if (mSecondChildHeight == -1) { + mSecondChildHeight = getChildHeight(mSecondPos, item, false); + mSecondStartBlank = (float) (item.getHeight() - mSecondChildHeight); + } + blank = Math.max((int) (f * mSecondStartBlank), 1); + lp = item.getLayoutParams(); + lp.height = mSecondChildHeight + blank; + item.setLayoutParams(lp); + } + } + } + + @Override + public void onStop() { + doRemoveItem(); + } + } + + public void removeItem(int which) { + mUseRemoveVelocity = false; + removeItem(which, 0); + } + + /** + * Removes an item from the list and animates the removal. + * + * @param which Position to remove (NOTE: headers/footers ignored! + * this is a position in your input ListAdapter). + * @param velocityX + */ + public void removeItem(int which, float velocityX) { + if (mDragState == IDLE || mDragState == DRAGGING) { + if (mDragState == IDLE) { + // called from outside drag-sort + mSrcPos = getHeaderViewsCount() + which; + mFirstExpPos = mSrcPos; + mSecondExpPos = mSrcPos; + mFloatPos = mSrcPos; + View v = getChildAt(mSrcPos - getFirstVisiblePosition()); + if (v != null) { + v.setVisibility(View.INVISIBLE); + } + } + + mDragState = REMOVING; + mRemoveVelocityX = velocityX; + + if (mInTouchEvent) { + switch (mCancelMethod) { + case ON_TOUCH_EVENT: + super.onTouchEvent(mCancelEvent); + break; + case ON_INTERCEPT_TOUCH_EVENT: + super.onInterceptTouchEvent(mCancelEvent); + break; + } + } + + if (mRemoveAnimator != null) { + mRemoveAnimator.start(); + } else { + doRemoveItem(which); + } + } + } + + /** + * Move an item, bypassing the drag-sort process. Simply calls + * through to {@link DropListener#drop(int, int)}. + * + * @param from Position to move (NOTE: headers/footers ignored! + * this is a position in your input ListAdapter). + * @param to Target position (NOTE: headers/footers ignored! + * this is a position in your input ListAdapter). + */ + public void moveItem(int from, int to) { + if (mDropListener != null) { + final int count = getInputAdapter().getCount(); + if (from >= 0 && from < count && to >= 0 && to < count) { + mDropListener.drop(from, to); + } + } + } + + /** + * Cancel a drag. Calls {@link #stopDrag(boolean, boolean)} with + * true as the first argument. + */ + public void cancelDrag() { + if (mDragState == DRAGGING) { + mDragScroller.stopScrolling(true); + destroyFloatView(); + clearPositions(); + adjustAllItems(); + + if (mInTouchEvent) { + mDragState = STOPPED; + } else { + mDragState = IDLE; + } + } + } + + private void clearPositions() { + mSrcPos = -1; + mFirstExpPos = -1; + mSecondExpPos = -1; + mFloatPos = -1; + } + + private void dropFloatView() { + // must set to avoid cancelDrag being called from the DataSetObserver + mDragState = DROPPING; + + if (mDropListener != null && mFloatPos >= 0 && mFloatPos < getCount()) { + final int numHeaders = getHeaderViewsCount(); + mDropListener.drop(mSrcPos - numHeaders, mFloatPos - numHeaders); + } + + destroyFloatView(); + + adjustOnReorder(); + clearPositions(); + adjustAllItems(); + + // now the drag is done + if (mInTouchEvent) { + mDragState = STOPPED; + } else { + mDragState = IDLE; + } + } + + private void doRemoveItem() { + doRemoveItem(mSrcPos - getHeaderViewsCount()); + } + + /** + * Removes dragged item from the list. Calls RemoveListener. + */ + private void doRemoveItem(int which) { + // must set to avoid cancelDrag being called from the DataSetObserver + mDragState = REMOVING; + + // end it + if (mRemoveListener != null) { + mRemoveListener.remove(which); + } + + destroyFloatView(); + + adjustOnReorder(); + clearPositions(); + + // now the drag is done + if (mInTouchEvent) { + mDragState = STOPPED; + } else { + mDragState = IDLE; + } + } + + private void adjustOnReorder() { + final int firstPos = getFirstVisiblePosition(); + if (mSrcPos < firstPos) { + // collapsed src item is off screen; + // adjust the scroll after item heights have been fixed + View v = getChildAt(0); + int top = 0; + if (v != null) { + top = v.getTop(); + } + setSelectionFromTop(firstPos - 1, top - getPaddingTop()); + } + } + + /** + * Stop a drag in progress. Pass true if you would + * like to remove the dragged item from the list. + * + * @param remove Remove the dragged item from the list. Calls + * a registered RemoveListener, if one exists. Otherwise, calls + * the DropListener, if one exists. + * + * @return True if the stop was successful. False if there is + * no floating View. + */ + public boolean stopDrag(boolean remove) { + mUseRemoveVelocity = false; + return stopDrag(remove, 0); + } + + public boolean stopDragWithVelocity(boolean remove, float velocityX) { + mUseRemoveVelocity = true; + return stopDrag(remove, velocityX); + } + + public boolean stopDrag(boolean remove, float velocityX) { + if (mFloatView != null) { + mDragScroller.stopScrolling(true); + + if (remove) { + removeItem(mSrcPos - getHeaderViewsCount(), velocityX); + } else { + if (mDropAnimator != null) { + mDropAnimator.start(); + } else { + dropFloatView(); + } + } + + if (mTrackDragSort) { + mDragSortTracker.stopTracking(); + } + + return true; + } else { + // stop failed + return false; + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mIgnoreTouchEvent) { + mIgnoreTouchEvent = false; + return false; + } + + if (!mDragEnabled) { + return super.onTouchEvent(ev); + } + + boolean more = false; + + boolean lastCallWasIntercept = mLastCallWasIntercept; + mLastCallWasIntercept = false; + + if (!lastCallWasIntercept) { + saveTouchCoords(ev); + } + + if (mDragState == DRAGGING) { + onDragTouchEvent(ev); + more = true; // give us more! + } else { + // TODO: what if float view is null because we dropped in middle + // of drag touch event? + + if (mDragState == IDLE) { + if (super.onTouchEvent(ev)) { + more = true; + } + } + + int action = ev.getAction() & MotionEvent.ACTION_MASK; + + switch (action) { + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + doActionUpOrCancel(); + break; + default: + if (more) { + mCancelMethod = ON_TOUCH_EVENT; + } + } + } + + return more; + } + + private void doActionUpOrCancel() { + mCancelMethod = NO_CANCEL; + mInTouchEvent = false; + if (mDragState == STOPPED) { + mDragState = IDLE; + } + mCurrFloatAlpha = mFloatAlpha; + mListViewIntercepted = false; + mChildHeightCache.clear(); + } + + private void saveTouchCoords(MotionEvent ev) { + int action = ev.getAction() & MotionEvent.ACTION_MASK; + if (action != MotionEvent.ACTION_DOWN) { + //mLastX = mX; + mLastY = mY; + } + mX = (int) ev.getX(); + mY = (int) ev.getY(); + if (action == MotionEvent.ACTION_DOWN) { + //mLastX = mX; + mLastY = mY; + } + //mOffsetX = (int) ev.getRawX() - mX; + //mOffsetY = (int) ev.getRawY() - mY; + } + + public boolean listViewIntercepted() { + return mListViewIntercepted; + } + + private boolean mListViewIntercepted = false; + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (!mDragEnabled) { + return super.onInterceptTouchEvent(ev); + } + + saveTouchCoords(ev); + mLastCallWasIntercept = true; + + int action = ev.getAction() & MotionEvent.ACTION_MASK; + + if (action == MotionEvent.ACTION_DOWN) { + if (mDragState != IDLE) { + // intercept and ignore + mIgnoreTouchEvent = true; + return true; + } + mInTouchEvent = true; + } + + boolean intercept = false; + + // the following deals with calls to super.onInterceptTouchEvent + if (mFloatView != null) { + // super's touch event cancelled in startDrag + intercept = true; + } else { + if (super.onInterceptTouchEvent(ev)) { + mListViewIntercepted = true; + intercept = true; + } + + switch (action) { + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + doActionUpOrCancel(); + break; + default: + if (intercept) { + mCancelMethod = ON_TOUCH_EVENT; + } else { + mCancelMethod = ON_INTERCEPT_TOUCH_EVENT; + } + } + } + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + mInTouchEvent = false; + } + + return intercept; + } + + /** + * Set the width of each drag scroll region by specifying + * a fraction of the ListView height. + * + * @param heightFraction Fraction of ListView height. Capped at + * 0.5f. + * + */ + public void setDragScrollStart(float heightFraction) { + setDragScrollStarts(heightFraction, heightFraction); + } + + /** + * Set the width of each drag scroll region by specifying + * a fraction of the ListView height. + * + * @param upperFrac Fraction of ListView height for up-scroll bound. + * Capped at 0.5f. + * @param lowerFrac Fraction of ListView height for down-scroll bound. + * Capped at 0.5f. + * + */ + public void setDragScrollStarts(float upperFrac, float lowerFrac) { + if (lowerFrac > 0.5f) { + mDragDownScrollStartFrac = 0.5f; + } else { + mDragDownScrollStartFrac = lowerFrac; + } + + if (upperFrac > 0.5f) { + mDragUpScrollStartFrac = 0.5f; + } else { + mDragUpScrollStartFrac = upperFrac; + } + + if (getHeight() != 0) { + updateScrollStarts(); + } + } + + private void continueDrag(int x, int y) { + // proposed position + mFloatLoc.x = x - mDragDeltaX; + mFloatLoc.y = y - mDragDeltaY; + + doDragFloatView(true); + + int minY = Math.min(y, mFloatViewMid + mFloatViewHeightHalf); + int maxY = Math.max(y, mFloatViewMid - mFloatViewHeightHalf); + + // get the current scroll direction + int currentScrollDir = mDragScroller.getScrollDir(); + + if (minY > mLastY && minY > mDownScrollStartY && currentScrollDir != DragScroller.DOWN) { + // dragged down, it is below the down scroll start and it is not + // scrolling up + + if (currentScrollDir != DragScroller.STOP) { + // moved directly from up scroll to down scroll + mDragScroller.stopScrolling(true); + } + + // start scrolling down + mDragScroller.startScrolling(DragScroller.DOWN); + } else if (maxY < mLastY && maxY < mUpScrollStartY && currentScrollDir != DragScroller.UP) { + // dragged up, it is above the up scroll start and it is not + // scrolling up + + if (currentScrollDir != DragScroller.STOP) { + // moved directly from down scroll to up scroll + mDragScroller.stopScrolling(true); + } + + // start scrolling up + mDragScroller.startScrolling(DragScroller.UP); + } + else if (maxY >= mUpScrollStartY && minY <= mDownScrollStartY + && mDragScroller.isScrolling()) { + // not in the upper nor in the lower drag-scroll regions but it is + // still scrolling + + mDragScroller.stopScrolling(true); + } + } + + private void updateScrollStarts() { + final int padTop = getPaddingTop(); + final int listHeight = getHeight() - padTop - getPaddingBottom(); + float heightF = (float) listHeight; + + mUpScrollStartYF = padTop + mDragUpScrollStartFrac * heightF; + mDownScrollStartYF = padTop + (1.0f - mDragDownScrollStartFrac) * heightF; + + mUpScrollStartY = (int) mUpScrollStartYF; + mDownScrollStartY = (int) mDownScrollStartYF; + + mDragUpScrollHeight = mUpScrollStartYF - padTop; + mDragDownScrollHeight = padTop + listHeight - mDownScrollStartYF; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + updateScrollStarts(); + } + + private void adjustAllItems() { + final int first = getFirstVisiblePosition(); + final int last = getLastVisiblePosition(); + + int begin = Math.max(0, getHeaderViewsCount() - first); + int end = Math.min(last - first, getCount() - 1 - getFooterViewsCount() - first); + + for (int i = begin; i <= end; ++i) { + View v = getChildAt(i); + if (v != null) { + adjustItem(first + i, v, false); + } + } + } + + /** + * Sets layout param height, gravity, and visibility on + * wrapped item. + */ + private void adjustItem(int position, View v, boolean invalidChildHeight) { + // Adjust item height + ViewGroup.LayoutParams lp = v.getLayoutParams(); + int height; + if (position != mSrcPos && position != mFirstExpPos && position != mSecondExpPos) { + height = ViewGroup.LayoutParams.WRAP_CONTENT; + } else { + height = calcItemHeight(position, v, invalidChildHeight); + } + + if (height != lp.height) { + lp.height = height; + v.setLayoutParams(lp); + } + + // Adjust item gravity + if (position == mFirstExpPos || position == mSecondExpPos) { + if (position < mSrcPos) { + ((DragSortItemView) v).setGravity(Gravity.BOTTOM); + } else if (position > mSrcPos) { + ((DragSortItemView) v).setGravity(Gravity.TOP); + } + } + + // Finally adjust item visibility + int oldVis = v.getVisibility(); + int vis = View.VISIBLE; + + if (position == mSrcPos && mFloatView != null) { + vis = View.INVISIBLE; + } + + if (vis != oldVis) { + v.setVisibility(vis); + } + } + + private int getChildHeight(int position) { + if (position == mSrcPos) { + return 0; + } + + View v = getChildAt(position - getFirstVisiblePosition()); + + if (v != null) { + // item is onscreen, therefore child height is valid, + // hence the "true" + return getChildHeight(position, v, false); + } else { + // item is offscreen + // first check cache for child height at this position + int childHeight = mChildHeightCache.get(position); + if (childHeight != -1) { + return childHeight; + } + + final ListAdapter adapter = getAdapter(); + int type = adapter.getItemViewType(position); + + // There might be a better place for checking for the following + final int typeCount = adapter.getViewTypeCount(); + if (typeCount != mSampleViewTypes.length) { + mSampleViewTypes = new View[typeCount]; + } + + if (type >= 0) { + if (mSampleViewTypes[type] == null) { + v = adapter.getView(position, null, this); + mSampleViewTypes[type] = v; + } else { + v = adapter.getView(position, mSampleViewTypes[type], this); + } + } else { + // type is HEADER_OR_FOOTER or IGNORE + v = adapter.getView(position, null, this); + } + + // current child height is invalid, hence "true" below + childHeight = getChildHeight(position, v, true); + + // cache it because this could have been expensive + mChildHeightCache.add(position, childHeight); + + return childHeight; + } + } + + private int getChildHeight(int position, View item, boolean invalidChildHeight) { + if (position == mSrcPos) { + return 0; + } + + View child; + if (position < getHeaderViewsCount() || position >= getCount() - getFooterViewsCount()) { + child = item; + } else { + child = ((ViewGroup) item).getChildAt(0); + } + + ViewGroup.LayoutParams lp = child.getLayoutParams(); + if (lp != null) { + if (lp.height > 0) { + return lp.height; + } + } + + int childHeight = child.getHeight(); + if (childHeight == 0 || invalidChildHeight) { + measureItem(child); + childHeight = child.getMeasuredHeight(); + } + + return childHeight; + } + + private int calcItemHeight(int position, View item, boolean invalidChildHeight) { + return calcItemHeight(position, getChildHeight(position, item, invalidChildHeight)); + } + + private int calcItemHeight(int position, int childHeight) { + boolean isSliding = mAnimate && mFirstExpPos != mSecondExpPos; + int maxNonSrcBlankHeight = mFloatViewHeight - mItemHeightCollapsed; + int slideHeight = (int) (mSlideFrac * maxNonSrcBlankHeight); + + int height; + + if (position == mSrcPos) { + if (mSrcPos == mFirstExpPos) { + if (isSliding) { + height = slideHeight + mItemHeightCollapsed; + } else { + height = mFloatViewHeight; + } + } else if (mSrcPos == mSecondExpPos) { + // if gets here, we know an item is sliding + height = mFloatViewHeight - slideHeight; + } else { + height = mItemHeightCollapsed; + } + } else if (position == mFirstExpPos) { + if (isSliding) { + height = childHeight + slideHeight; + } else { + height = childHeight + maxNonSrcBlankHeight; + } + } else if (position == mSecondExpPos) { + // we know an item is sliding (b/c 2ndPos != 1stPos) + height = childHeight + maxNonSrcBlankHeight - slideHeight; + } else { + height = childHeight; + } + + return height; + } + + @Override + public void requestLayout() { + if (!mBlockLayoutRequests) { + super.requestLayout(); + } + } + + private int adjustScroll(int movePos, View moveItem, int oldFirstExpPos, int oldSecondExpPos) { + int adjust = 0; + + final int childHeight = getChildHeight(movePos); + + int moveHeightBefore = moveItem.getHeight(); + int moveHeightAfter = calcItemHeight(movePos, childHeight); + + int moveBlankBefore = moveHeightBefore; + int moveBlankAfter = moveHeightAfter; + if (movePos != mSrcPos) { + moveBlankBefore -= childHeight; + moveBlankAfter -= childHeight; + } + + int maxBlank = mFloatViewHeight; + if (mSrcPos != mFirstExpPos && mSrcPos != mSecondExpPos) { + maxBlank -= mItemHeightCollapsed; + } + + if (movePos <= oldFirstExpPos) { + if (movePos > mFirstExpPos) { + adjust += maxBlank - moveBlankAfter; + } + } else if (movePos == oldSecondExpPos) { + if (movePos <= mFirstExpPos) { + adjust += moveBlankBefore - maxBlank; + } else if (movePos == mSecondExpPos) { + adjust += moveHeightBefore - moveHeightAfter; + } else { + adjust += moveBlankBefore; + } + } else { + if (movePos <= mFirstExpPos) { + adjust -= maxBlank; + } else if (movePos == mSecondExpPos) { + adjust -= moveBlankAfter; + } + } + + return adjust; + } + + private void measureItem(View item) { + ViewGroup.LayoutParams lp = item.getLayoutParams(); + if (lp == null) { + lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + item.setLayoutParams(lp); + } + + int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft() + + getListPaddingRight(), lp.width); + int hspec; + if (lp.height > 0) { + hspec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); + } else { + hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + item.measure(wspec, hspec); + } + + private void measureFloatView() { + if (mFloatView != null) { + measureItem(mFloatView); + mFloatViewHeight = mFloatView.getMeasuredHeight(); + mFloatViewHeightHalf = mFloatViewHeight / 2; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (mFloatView != null) { + if (mFloatView.isLayoutRequested()) { + measureFloatView(); + } + mFloatViewOnMeasured = true; // set to false after layout + } + mWidthMeasureSpec = widthMeasureSpec; + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + + if (mFloatView != null) { + if (mFloatView.isLayoutRequested() && !mFloatViewOnMeasured) { + // Have to measure here when usual android measure + // pass is skipped. This happens during a drag-sort + // when layoutChildren is called directly. + measureFloatView(); + } + mFloatView.layout(0, 0, mFloatView.getMeasuredWidth(), mFloatView.getMeasuredHeight()); + mFloatViewOnMeasured = false; + } + } + + protected boolean onDragTouchEvent(MotionEvent ev) { + int action = ev.getAction() & MotionEvent.ACTION_MASK; + switch (action) { + case MotionEvent.ACTION_CANCEL: + if (mDragState == DRAGGING) { + cancelDrag(); + } + doActionUpOrCancel(); + break; + case MotionEvent.ACTION_UP: + if (mDragState == DRAGGING) { + stopDrag(false); + } + doActionUpOrCancel(); + break; + case MotionEvent.ACTION_MOVE: + continueDrag((int) ev.getX(), (int) ev.getY()); + break; + } + + return true; + } + + /** + * Start a drag of item at position using the + * registered FloatViewManager. Calls through + * to {@link #startDrag(int,View,int,int,int)} after obtaining + * the floating View from the FloatViewManager. + * + * @param position Item to drag. + * @param dragFlags Flags that restrict some movements of the + * floating View. For example, set dragFlags |= + * ~{@link #DRAG_NEG_X} to allow dragging the floating + * View in all directions except off the screen to the left. + * @param deltaX Offset in x of the touch coordinate from the + * left edge of the floating View (i.e. touch-x minus float View + * left). + * @param deltaY Offset in y of the touch coordinate from the + * top edge of the floating View (i.e. touch-y minus float View + * top). + * + * @return True if the drag was started, false otherwise. This + * startDrag will fail if we are not currently in + * a touch event, there is no registered FloatViewManager, + * or the FloatViewManager returns a null View. + */ + public boolean startDrag(int position, int dragFlags, int deltaX, int deltaY) { + if (!mInTouchEvent || mFloatViewManager == null) { + return false; + } + + View v = mFloatViewManager.onCreateFloatView(position); + + if (v == null) { + return false; + } else { + return startDrag(position, v, dragFlags, deltaX, deltaY); + } + + } + + /** + * Start a drag of item at position without using + * a FloatViewManager. + * + * @param position Item to drag. + * @param floatView Floating View. + * @param dragFlags Flags that restrict some movements of the + * floating View. For example, set dragFlags |= + * ~{@link #DRAG_NEG_X} to allow dragging the floating + * View in all directions except off the screen to the left. + * @param deltaX Offset in x of the touch coordinate from the + * left edge of the floating View (i.e. touch-x minus float View + * left). + * @param deltaY Offset in y of the touch coordinate from the + * top edge of the floating View (i.e. touch-y minus float View + * top). + * + * @return True if the drag was started, false otherwise. This + * startDrag will fail if we are not currently in + * a touch event, floatView is null, or there is + * a drag in progress. + */ + public boolean startDrag(int position, View floatView, int dragFlags, int deltaX, int deltaY) { + if (mDragState != IDLE || !mInTouchEvent || mFloatView != null || floatView == null + || !mDragEnabled) { + return false; + } + + if (getParent() != null) { + getParent().requestDisallowInterceptTouchEvent(true); + } + + int pos = position + getHeaderViewsCount(); + mFirstExpPos = pos; + mSecondExpPos = pos; + mSrcPos = pos; + mFloatPos = pos; + + mDragState = DRAGGING; + mDragFlags = 0; + mDragFlags |= dragFlags; + + mFloatView = floatView; + measureFloatView(); // sets mFloatViewHeight + + mDragDeltaX = deltaX; + mDragDeltaY = deltaY; + + mFloatLoc.x = mX - mDragDeltaX; + mFloatLoc.y = mY - mDragDeltaY; + + // set src item invisible + final View srcItem = getChildAt(mSrcPos - getFirstVisiblePosition()); + + if (srcItem != null) { + srcItem.setVisibility(View.INVISIBLE); + } + + if (mTrackDragSort) { + mDragSortTracker.startTracking(); + } + + // once float view is created, events are no longer passed + // to ListView + switch (mCancelMethod) { + case ON_TOUCH_EVENT: + super.onTouchEvent(mCancelEvent); + break; + case ON_INTERCEPT_TOUCH_EVENT: + super.onInterceptTouchEvent(mCancelEvent); + break; + } + + requestLayout(); + + if (mLiftAnimator != null) { + mLiftAnimator.start(); + } + + return true; + } + + private void doDragFloatView(boolean forceInvalidate) { + int movePos = getFirstVisiblePosition() + getChildCount() / 2; + View moveItem = getChildAt(getChildCount() / 2); + + if (moveItem == null) { + return; + } + + doDragFloatView(movePos, moveItem, forceInvalidate); + } + + private void doDragFloatView(int movePos, View moveItem, boolean forceInvalidate) { + mBlockLayoutRequests = true; + + updateFloatView(); + + int oldFirstExpPos = mFirstExpPos; + int oldSecondExpPos = mSecondExpPos; + + boolean updated = updatePositions(); + + if (updated) { + adjustAllItems(); + int scroll = adjustScroll(movePos, moveItem, oldFirstExpPos, oldSecondExpPos); + + setSelectionFromTop(movePos, moveItem.getTop() + scroll - getPaddingTop()); + layoutChildren(); + } + + if (updated || forceInvalidate) { + invalidate(); + } + + mBlockLayoutRequests = false; + } + + /** + * Sets float View location based on suggested values and + * constraints set in mDragFlags. + */ + private void updateFloatView() { + + if (mFloatViewManager != null) { + mTouchLoc.set(mX, mY); + mFloatViewManager.onDragFloatView(mFloatView, mFloatLoc, mTouchLoc); + } + + final int floatX = mFloatLoc.x; + final int floatY = mFloatLoc.y; + + // restrict x motion + int padLeft = getPaddingLeft(); + if ((mDragFlags & DRAG_POS_X) == 0 && floatX > padLeft) { + mFloatLoc.x = padLeft; + } else if ((mDragFlags & DRAG_NEG_X) == 0 && floatX < padLeft) { + mFloatLoc.x = padLeft; + } + + // keep floating view from going past bottom of last header view + final int numHeaders = getHeaderViewsCount(); + final int numFooters = getFooterViewsCount(); + final int firstPos = getFirstVisiblePosition(); + final int lastPos = getLastVisiblePosition(); + + int topLimit = getPaddingTop(); + if (firstPos < numHeaders) { + topLimit = getChildAt(numHeaders - firstPos - 1).getBottom(); + } + if ((mDragFlags & DRAG_NEG_Y) == 0) { + if (firstPos <= mSrcPos) { + topLimit = Math.max(getChildAt(mSrcPos - firstPos).getTop(), topLimit); + } + } + // bottom limit is top of first footer View or + // bottom of last item in list + int bottomLimit = getHeight() - getPaddingBottom(); + if (lastPos >= getCount() - numFooters - 1) { + bottomLimit = getChildAt(getCount() - numFooters - 1 - firstPos).getBottom(); + } + if ((mDragFlags & DRAG_POS_Y) == 0) { + if (lastPos >= mSrcPos) { + bottomLimit = Math.min(getChildAt(mSrcPos - firstPos).getBottom(), bottomLimit); + } + } + + if (floatY < topLimit) { + mFloatLoc.y = topLimit; + } else if (floatY + mFloatViewHeight > bottomLimit) { + mFloatLoc.y = bottomLimit - mFloatViewHeight; + } + + // get y-midpoint of floating view (constrained to ListView bounds) + mFloatViewMid = mFloatLoc.y + mFloatViewHeightHalf; + } + + private void destroyFloatView() { + if (mFloatView != null) { + mFloatView.setVisibility(GONE); + if (mFloatViewManager != null) { + mFloatViewManager.onDestroyFloatView(mFloatView); + } + mFloatView = null; + invalidate(); + } + } + + /** + * Interface for customization of the floating View appearance + * and dragging behavior. Implement + * your own and pass it to {@link #setFloatViewManager}. If + * your own is not passed, the default {@link SimpleFloatViewManager} + * implementation is used. + */ + public interface FloatViewManager { + /** + * Return the floating View for item at position. + * DragSortListView will measure and layout this View for you, + * so feel free to just inflate it. You can help DSLV by + * setting some {@link ViewGroup.LayoutParams} on this View; + * otherwise it will set some for you (with a width of FILL_PARENT + * and a height of WRAP_CONTENT). + * + * @param position Position of item to drag (NOTE: + * position excludes header Views; thus, if you + * want to call {@link ListView#getChildAt(int)}, you will need + * to add {@link ListView#getHeaderViewsCount()} to the index). + * + * @return The View you wish to display as the floating View. + */ + public View onCreateFloatView(int position); + + /** + * Called whenever the floating View is dragged. Float View + * properties can be changed here. Also, the upcoming location + * of the float View can be altered by setting + * location.x and location.y. + * + * @param floatView The floating View. + * @param location The location (top-left; relative to DSLV + * top-left) at which the float + * View would like to appear, given the current touch location + * and the offset provided in {@link DragSortListView#startDrag}. + * @param touch The current touch location (relative to DSLV + * top-left). + * @param pendingScroll + */ + public void onDragFloatView(View floatView, Point location, Point touch); + + /** + * Called when the float View is dropped; lets you perform + * any necessary cleanup. The internal DSLV floating View + * reference is set to null immediately after this is called. + * + * @param floatView The floating View passed to + * {@link #onCreateFloatView(int)}. + */ + public void onDestroyFloatView(View floatView); + } + + public void setFloatViewManager(FloatViewManager manager) { + mFloatViewManager = manager; + } + + public void setDragListener(DragListener l) { + mDragListener = l; + } + + /** + * Allows for easy toggling between a DragSortListView + * and a regular old ListView. If enabled, items are + * draggable, where the drag init mode determines how + * items are lifted (see {@link setDragInitMode(int)}). + * If disabled, items cannot be dragged. + * + * @param enabled Set true to enable list + * item dragging + */ + public void setDragEnabled(boolean enabled) { + mDragEnabled = enabled; + } + + public boolean isDragEnabled() { + return mDragEnabled; + } + + /** + * This better reorder your ListAdapter! DragSortListView does not do this + * for you; doesn't make sense to. Make sure + * {@link BaseAdapter#notifyDataSetChanged()} or something like it is called + * in your implementation. Furthermore, if you have a choiceMode other than + * none and the ListAdapter does not return true for + * {@link ListAdapter#hasStableIds()}, you will need to call + * {@link #moveCheckState(int, int)} to move the check boxes along with the + * list items. + * + * @param l + */ + public void setDropListener(DropListener l) { + mDropListener = l; + } + + /** + * Probably a no-brainer, but make sure that your remove listener + * calls {@link BaseAdapter#notifyDataSetChanged()} or something like it. + * When an item removal occurs, DragSortListView + * relies on a redraw of all the items to recover invisible views + * and such. Strictly speaking, if you remove something, your dataset + * has changed... + * + * @param l + */ + public void setRemoveListener(RemoveListener l) { + mRemoveListener = l; + } + + public interface DragListener { + public void drag(int from, int to); + } + + /** + * Your implementation of this has to reorder your ListAdapter! + * Make sure to call + * {@link BaseAdapter#notifyDataSetChanged()} or something like it + * in your implementation. + * + * @author heycosmo + * + */ + public interface DropListener { + public void drop(int from, int to); + } + + /** + * Make sure to call + * {@link BaseAdapter#notifyDataSetChanged()} or something like it + * in your implementation. + * + * @author heycosmo + * + */ + public interface RemoveListener { + public void remove(int which); + } + + public interface DragSortListener extends DropListener, DragListener, RemoveListener { + } + + public void setDragSortListener(DragSortListener listener) { + setDropListener(listener); + setDragListener(listener); + setRemoveListener(listener); + } + + /** + * Completely custom scroll speed profile. Default increases linearly + * with position and is constant in time. Create your own by implementing + * {@link DragSortListView.DragScrollProfile}. + * + * @param scrollProfile + */ + public void setDragScrollProfile(DragScrollProfile scrollProfile) { + if (scrollProfile != null) { + mScrollProfile = scrollProfile; + } + } + + /** + * Use this to move the check state of an item from one position to another + * in a drop operation. If you have a choiceMode which is not none, this + * method must be called when the order of items changes in an underlying + * adapter which does not have stable IDs (see + * {@link ListAdapter#hasStableIds()}). This is because without IDs, the + * ListView has no way of knowing which items have moved where, and cannot + * update the check state accordingly. + *

+ * A word of warning about a "feature" in Android that you may run into when + * dealing with movable list items: for an adapter that does have + * stable IDs, ListView will attempt to locate each item based on its ID and + * move the check state from the item's old position to the new position — + * which is all fine and good (and removes the need for calling this + * function), except for the half-baked approach. Apparently to save time in + * the naive algorithm used, ListView will only search for an ID in the + * close neighborhood of the old position. If the user moves an item too far + * (specifically, more than 20 rows away), ListView will give up and just + * force the item to be unchecked. So if there is a reasonable chance that + * the user will move items more than 20 rows away from the original + * position, you may wish to use an adapter with unstable IDs and call this + * method manually instead. + * + * @param from + * @param to + */ + public void moveCheckState(int from, int to) { + // This method runs in O(n log n) time (n being the number of list + // items). The bottleneck is the call to AbsListView.setItemChecked, + // which is O(log n) because of the binary search involved in calling + // SparseBooleanArray.put(). + // + // To improve on the average time, we minimize the number of calls to + // setItemChecked by only calling it for items that actually have a + // changed state. This is achieved by building a list containing the + // start and end of the "runs" of checked items, and then moving the + // runs. Note that moving an item from A to B is essentially a rotation + // of the range of items in [A, B]. Let's say we have + // . . U V X Y Z . . + // and move U after Z. This is equivalent to a rotation one step to the + // left within the range you are moving across: + // . . V X Y Z U . . + // + // So, to perform the move we enumerate all the runs within the move + // range, then rotate each run one step to the left or right (depending + // on move direction). For example, in the list: + // X X . X X X . X + // we have two runs. One begins at the last item of the list and wraps + // around to the beginning, ending at position 1. The second begins at + // position 3 and ends at position 5. To rotate a run, regardless of + // length, we only need to set a check mark at one end of the run, and + // clear a check mark at the other end: + // X . X X X . X X + SparseBooleanArray cip = getCheckedItemPositions(); + int rangeStart = from; + int rangeEnd = to; + if (to < from) { + rangeStart = to; + rangeEnd = from; + } + rangeEnd += 1; + + int[] runStart = new int[cip.size()]; + int[] runEnd = new int[cip.size()]; + int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd); + if (runCount == 1 && (runStart[0] == runEnd[0])) { + // Special case where all items are checked, we can never set any + // item to false like we do below. + return; + } + + if (from < to) { + for (int i = 0; i != runCount; i++) { + setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd), true); + setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd), false); + } + + } else { + for (int i = 0; i != runCount; i++) { + setItemChecked(runStart[i], false); + setItemChecked(runEnd[i], true); + } + } + } + + /** + * Use this when an item has been deleted, to move the check state of all + * following items up one step. If you have a choiceMode which is not none, + * this method must be called when the order of items changes in an + * underlying adapter which does not have stable IDs (see + * {@link ListAdapter#hasStableIds()}). This is because without IDs, the + * ListView has no way of knowing which items have moved where, and cannot + * update the check state accordingly. + * + * See also further comments on {@link #moveCheckState(int, int)}. + * + * @param position + */ + public void removeCheckState(int position) { + SparseBooleanArray cip = getCheckedItemPositions(); + + if (cip.size() == 0) + return; + int[] runStart = new int[cip.size()]; + int[] runEnd = new int[cip.size()]; + int rangeStart = position; + int rangeEnd = cip.keyAt(cip.size() - 1) + 1; + int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd); + for (int i = 0; i != runCount; i++) { + if (!(runStart[i] == position || (runEnd[i] < runStart[i] && runEnd[i] > position))) { + // Only set a new check mark in front of this run if it does + // not contain the deleted position. If it does, we only need + // to make it one check mark shorter at the end. + setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd), true); + } + setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd), false); + } + } + + private static int buildRunList(SparseBooleanArray cip, int rangeStart, int rangeEnd, + int[] runStart, int[] runEnd) { + + int runCount = 0; + + int i = findFirstSetIndex(cip, rangeStart, rangeEnd); + if (i == -1) + return 0; + + int position = cip.keyAt(i); + int currentRunStart = position; + int currentRunEnd = currentRunStart + 1; + for (i++; i < cip.size() && (position = cip.keyAt(i)) < rangeEnd; i++) { + if (!cip.valueAt(i)) // not checked => not interesting + continue; + if (position == currentRunEnd) { + currentRunEnd++; + } else { + runStart[runCount] = currentRunStart; + runEnd[runCount] = currentRunEnd; + runCount++; + currentRunStart = position; + currentRunEnd = position + 1; + } + } + + if (currentRunEnd == rangeEnd) { + // rangeStart and rangeEnd are equivalent positions so to be + // consistent we translate them to the same integer value. That way + // we can check whether a run covers the entire range by just + // checking if the start equals the end position. + currentRunEnd = rangeStart; + } + runStart[runCount] = currentRunStart; + runEnd[runCount] = currentRunEnd; + runCount++; + + if (runCount > 1) { + if (runStart[0] == rangeStart && runEnd[runCount - 1] == rangeStart) { + // The last run ends at the end of the range, and the first run + // starts at the beginning of the range. So they are actually + // part of the same run, except they wrap around the end of the + // range. To avoid adjacent runs, we need to merge them. + runStart[0] = runStart[runCount - 1]; + runCount--; + } + } + return runCount; + } + + private static int rotate(int value, int offset, int lowerBound, int upperBound) { + int windowSize = upperBound - lowerBound; + + value += offset; + if (value < lowerBound) { + value += windowSize; + } else if (value >= upperBound) { + value -= windowSize; + } + return value; + } + + private static int findFirstSetIndex(SparseBooleanArray sba, int rangeStart, int rangeEnd) { + int size = sba.size(); + int i = insertionIndexForKey(sba, rangeStart); + while (i < size && sba.keyAt(i) < rangeEnd && !sba.valueAt(i)) + i++; + if (i == size || sba.keyAt(i) >= rangeEnd) + return -1; + return i; + } + + private static int insertionIndexForKey(SparseBooleanArray sba, int key) { + int low = 0; + int high = sba.size(); + while (high - low > 0) { + int middle = (low + high) >> 1; + if (sba.keyAt(middle) < key) + low = middle + 1; + else + high = middle; + } + return low; + } + + /** + * Interface for controlling + * scroll speed as a function of touch position and time. Use + * {@link DragSortListView#setDragScrollProfile(DragScrollProfile)} to + * set custom profile. + * + * @author heycosmo + * + */ + public interface DragScrollProfile { + /** + * Return a scroll speed in pixels/millisecond. Always return a + * positive number. + * + * @param w Normalized position in scroll region (i.e. w \in [0,1]). + * Small w typically means slow scrolling. + * @param t Time (in milliseconds) since start of scroll (handy if you + * want scroll acceleration). + * @return Scroll speed at position w and time t in pixels/ms. + */ + float getSpeed(float w, long t); + } + + private class DragScroller implements Runnable { + + private boolean mAbort; + + private long mPrevTime; + private long mCurrTime; + + private int dy; + private float dt; + private long tStart; + private int scrollDir; + + public final static int STOP = -1; + public final static int UP = 0; + public final static int DOWN = 1; + + private float mScrollSpeed; // pixels per ms + + private boolean mScrolling = false; + + public boolean isScrolling() { + return mScrolling; + } + + public int getScrollDir() { + return mScrolling ? scrollDir : STOP; + } + + public DragScroller() { + } + + public void startScrolling(int dir) { + if (!mScrolling) { + mAbort = false; + mScrolling = true; + tStart = SystemClock.uptimeMillis(); + mPrevTime = tStart; + scrollDir = dir; + post(this); + } + } + + public void stopScrolling(boolean now) { + if (now) { + DragSortListView.this.removeCallbacks(this); + mScrolling = false; + } else { + mAbort = true; + } + } + + @Override + public void run() { + if (mAbort) { + mScrolling = false; + return; + } + + final int first = getFirstVisiblePosition(); + final int last = getLastVisiblePosition(); + final int count = getCount(); + final int padTop = getPaddingTop(); + final int listHeight = getHeight() - padTop - getPaddingBottom(); + + int minY = Math.min(mY, mFloatViewMid + mFloatViewHeightHalf); + int maxY = Math.max(mY, mFloatViewMid - mFloatViewHeightHalf); + + if (scrollDir == UP) { + View v = getChildAt(0); + if (v == null) { + mScrolling = false; + return; + } else { + if (first == 0 && v.getTop() == padTop) { + mScrolling = false; + return; + } + } + mScrollSpeed = mScrollProfile.getSpeed((mUpScrollStartYF - maxY) + / mDragUpScrollHeight, mPrevTime); + } else { + View v = getChildAt(last - first); + if (v == null) { + mScrolling = false; + return; + } else { + if (last == count - 1 && v.getBottom() <= listHeight + padTop) { + mScrolling = false; + return; + } + } + mScrollSpeed = -mScrollProfile.getSpeed((minY - mDownScrollStartYF) + / mDragDownScrollHeight, mPrevTime); + } + + mCurrTime = SystemClock.uptimeMillis(); + dt = (float) (mCurrTime - mPrevTime); + + // dy is change in View position of a list item; i.e. positive dy + // means user is scrolling up (list item moves down the screen, + // remember + // y=0 is at top of View). + dy = (int) Math.round(mScrollSpeed * dt); + + int movePos; + if (dy >= 0) { + dy = Math.min(listHeight, dy); + movePos = first; + } else { + dy = Math.max(-listHeight, dy); + movePos = last; + } + + final View moveItem = getChildAt(movePos - first); + int top = moveItem.getTop() + dy; + + if (movePos == 0 && top > padTop) { + top = padTop; + } + + // always do scroll + mBlockLayoutRequests = true; + + setSelectionFromTop(movePos, top - padTop); + DragSortListView.this.layoutChildren(); + invalidate(); + + mBlockLayoutRequests = false; + + // scroll means relative float View movement + doDragFloatView(movePos, moveItem, false); + + mPrevTime = mCurrTime; + + post(this); + } + } + + // TODO: Bluuurgh... switch to SharedPreferences + private class DragSortTracker { + StringBuilder mBuilder = new StringBuilder(); + + File mFile; + + private int mNumInBuffer = 0; + private int mNumFlushes = 0; + + private boolean mTracking = false; + + public DragSortTracker() { + File root = Environment.getExternalStorageDirectory(); + mFile = new File(root, "dslv_state.txt"); + + if (!mFile.exists()) { + try { + mFile.createNewFile(); + Log.d("mobeta", "file created"); + } catch (IOException e) { + Log.w("mobeta", "Could not create dslv_state.txt"); + Log.d("mobeta", e.getMessage()); + } + } + + } + + public void startTracking() { + mBuilder.append("\n"); + mNumFlushes = 0; + mTracking = true; + } + + public void appendState() { + if (!mTracking) { + return; + } + + mBuilder.append("\n"); + final int children = getChildCount(); + final int first = getFirstVisiblePosition(); + mBuilder.append(" "); + for (int i = 0; i < children; ++i) { + mBuilder.append(first + i).append(","); + } + mBuilder.append("\n"); + + mBuilder.append(" "); + for (int i = 0; i < children; ++i) { + mBuilder.append(getChildAt(i).getTop()).append(","); + } + mBuilder.append("\n"); + mBuilder.append(" "); + for (int i = 0; i < children; ++i) { + mBuilder.append(getChildAt(i).getBottom()).append(","); + } + mBuilder.append("\n"); + + mBuilder.append(" ").append(mFirstExpPos).append("\n"); + mBuilder.append(" ") + .append(getItemHeight(mFirstExpPos) - getChildHeight(mFirstExpPos)) + .append("\n"); + mBuilder.append(" ").append(mSecondExpPos).append("\n"); + mBuilder.append(" ") + .append(getItemHeight(mSecondExpPos) - getChildHeight(mSecondExpPos)) + .append("\n"); + mBuilder.append(" ").append(mSrcPos).append("\n"); + mBuilder.append(" ").append(mFloatViewHeight + getDividerHeight()) + .append("\n"); + mBuilder.append(" ").append(getHeight()).append("\n"); + mBuilder.append(" ").append(mLastY).append("\n"); + mBuilder.append(" ").append(mFloatViewMid).append("\n"); + mBuilder.append(" "); + for (int i = 0; i < children; ++i) { + mBuilder.append(getShuffleEdge(first + i, getChildAt(i).getTop())).append(","); + } + mBuilder.append("\n"); + + mBuilder.append("\n"); + mNumInBuffer++; + + if (mNumInBuffer > 1000) { + flush(); + mNumInBuffer = 0; + } + } + + public void flush() { + if (!mTracking) { + return; + } + + // save to file on sdcard + try { + boolean append = true; + if (mNumFlushes == 0) { + append = false; + } + FileWriter writer = new FileWriter(mFile, append); + + writer.write(mBuilder.toString()); + mBuilder.delete(0, mBuilder.length()); + + writer.flush(); + writer.close(); + + mNumFlushes++; + } catch (IOException e) { + // do nothing + } + } + + public void stopTracking() { + if (mTracking) { + mBuilder.append("\n"); + flush(); + mTracking = false; + } + } + + } + +} From 81674054531a37a0e32a205ee8734c773742a309 Mon Sep 17 00:00:00 2001 From: kd7uiy Date: Mon, 30 Dec 2013 13:36:37 -0500 Subject: [PATCH 09/18] Converted to Unix format --- demo/AndroidManifest.xml | 102 +-- demo/res/layout/list_item_click_remove.xml | 58 +- .../res/layout/list_item_simple_checkable.xml | 46 +- .../sort_list_array_dialog_preference.xml | 36 +- demo/res/menu/activity_menu.xml | 12 +- demo/res/values/preferences_strings.xml | 40 +- demo/res/values/strings.xml | 614 +++++++++--------- demo/res/xml/pref_headers.xml | 18 +- demo/res/xml/pref_name.xml | 18 +- .../com/mobeta/android/demodslv/Launcher.java | 214 +++--- .../demodslv/MainSettingsActivity.java | 454 ++++++------- .../demodslv/MultipleChoiceListView.java | 130 ++-- .../demodslv/SortableListPreference.java | 590 ++++++++--------- 13 files changed, 1166 insertions(+), 1166 deletions(-) diff --git a/demo/AndroidManifest.xml b/demo/AndroidManifest.xml index 65a53b4..d0c0197 100644 --- a/demo/AndroidManifest.xml +++ b/demo/AndroidManifest.xml @@ -1,51 +1,51 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/res/layout/list_item_click_remove.xml b/demo/res/layout/list_item_click_remove.xml index 6223538..31333a0 100644 --- a/demo/res/layout/list_item_click_remove.xml +++ b/demo/res/layout/list_item_click_remove.xml @@ -1,29 +1,29 @@ - - - - - - + + + + + + diff --git a/demo/res/layout/list_item_simple_checkable.xml b/demo/res/layout/list_item_simple_checkable.xml index 5220bc4..838967c 100644 --- a/demo/res/layout/list_item_simple_checkable.xml +++ b/demo/res/layout/list_item_simple_checkable.xml @@ -1,23 +1,23 @@ - - - - - + + + + + diff --git a/demo/res/layout/sort_list_array_dialog_preference.xml b/demo/res/layout/sort_list_array_dialog_preference.xml index 22a74cd..7bacc38 100644 --- a/demo/res/layout/sort_list_array_dialog_preference.xml +++ b/demo/res/layout/sort_list_array_dialog_preference.xml @@ -1,18 +1,18 @@ - - - - - + + + + + diff --git a/demo/res/menu/activity_menu.xml b/demo/res/menu/activity_menu.xml index c9d1644..52abf94 100644 --- a/demo/res/menu/activity_menu.xml +++ b/demo/res/menu/activity_menu.xml @@ -1,6 +1,6 @@ - -

- - - - + + + + + + diff --git a/demo/res/values/preferences_strings.xml b/demo/res/values/preferences_strings.xml index 3776d1b..ee33381 100644 --- a/demo/res/values/preferences_strings.xml +++ b/demo/res/values/preferences_strings.xml @@ -1,20 +1,20 @@ - - - - Bob - Fred - George - - - Bob Menwaldo - Fred Frankfurd - George Stephanopolis - - - George - Fred - Bob - - Name Order Preference - Test - + + + + Bob + Fred + George + + + Bob Menwaldo + Fred Frankfurd + George Stephanopolis + + + George + Fred + Bob + + Name Order Preference + Test + diff --git a/demo/res/values/strings.xml b/demo/res/values/strings.xml index 6cb6f30..818a579 100644 --- a/demo/res/values/strings.xml +++ b/demo/res/values/strings.xml @@ -1,307 +1,307 @@ - - - Drag-Sort Demos - - Basic usage playground - Heteroheight - Warp - Background handle - Sections - CursorAdapter - Multiple-choice mode - Single-choice mode - - - - Use the menu to adjust settings related to item - removal and drag initiation. These options are - provided by the DragSortController class. - - - Demonstrates (tests) drag-sorting when list items - have varying heights. - - - Demonstrates drag-scroll customization; long lists - need fast scrolling. - - - Simple DragSortController customization. In this example, - the drag handle is a background drawable; therefore, the - drag handle hit detection mechanism needs modification. - Also, the floating View is given some flavor. - - - Demonstrates floating View control from a custom - FloatViewManager. Restrict item drags to list sections. - - - Demonstrates usage of the DragSortCursorAdapter class, - which abstracts drag-sorts with a remapping of Cursor - positions to ListView positions. - - - Uses Checkable list items in multiple-choice mode. - - - Uses Checkable list items to allow for selectable radio - buttons in single-choice mode. - - - OK - Cancel - Remove modes - Start-drag modes - Enable/Disable - Add header - Add footer - - Click remove - Fling remove - - - On down - On drag - On long press - - - Enable drag - Enable sort - Enable remove - - - Brad Mehldau - Joshua Redman - Chick Corea - Kurt Rosenwinkel - Miles Davis - Wayne Shorter - Michael Brecker - Herbie Hancock - Joe Zawinul - Brian Blade - Jeff Ballard - Larry Grenadier - Keith Jarrett - McCoy Tyner - Stephon Harris - Mark Turner - - - Largo, Art of the Trio 1-4, Highway Rider, Songs - Elastic, Momentum, Mood Swing, Back East - Light as a Feather, Akoustic Band, My Spanish Heart - Deep Song, Heartcore, Our Secret World, Reflections, The Remedy: Live at the Village Vanguard, The Enemies of Energy, The Next Step - - - - Afghanistan - Albania - Algeria - American Samoa - Andorra - Angola - Anguilla - Antigua and Barbuda - Argentina - Armenia - Australia - Austria - Azerbajan - Bahamas - Bahrain - Bangladesh - Barbados - Belarus - Belgium - Belize - Benin - Bermuda - Bhutan - Bolivia - Bosnia and Herzegovina - Botswana - Brazil - Brunei Darussalam - Bulgaria - Burkina Faso - Burundi - Cambodia - Cameroon - Canada - Chile - China - Colombia - Costa Rica - Cuba - Cyprus - Czech Republic - Democratic Republic Congo - Denmark - Djibouti - Dominican Republic - East Timor - Ecuador - Egypt - El Salvador - England - Eritrea - Estonia - Ethiopia - Faroe Islands - Fiji - Finland - France - French Polynesia - Gambia - Georgia (Sakartvelo) - Germany - Gabon - Ghana - Greece - Greenland - Kalaallit Nunaat - Grenada - Gouadeloupe - Guam - Guatemala - Guernsey - Guyana - Guyane - Haiti - Honduras - Hong Kong - Hrvatska (Croatia) - Hungary - Iceland - India - Indonesia - Iran - Iraq - Ireland - Israel - Italy - Jamaica - Japan - Jordan - Kazakhstan - Kenya - Korea Republic - Kosovo - Kurdistan - Kuwait - Kyrgyzstan - Laos - Latvia - Lebanon - Lesotho - Liberia - Libyan Arab Jamahiriya - Liechtenstein - Lithuania - Luxembourg - Macau - Macedonia - Malawi - Malaysia - Mali - Malta - Marshall Islands - Mauritania - Martinique - Mauritius - Mexico - Micronesia - Moldova - Monaco - Mongolia - Morocco - Mozambique - Namibia - Nepal - Netherlands - Netherlands Antilles - New Caledonia - New Zealand (Aotearoa) - Nicaragua - Nigeria - Niue - Norfolk Island - Northern Ireland - Northern Mariana Islands - Norway - Oman - Pakistan - Palau - Palestina - Panama - Papua New Guinea - Paraguay - Peru - Philippines - Portugal - Puerto Rico - Qatar - Reunion - Romania - Russian Federation (AsianPart) - Russian Federation (European Part) - Rwanda - Saint Kitts and Nevis - Saint Vincent and the Grenadines - Samoa (American Samoa) - Samoa (Western Samoa) - San Marino - Saudi Arabia - Scotland - Senegal - Seychelles - Sierra Leone - Singapore - Slovakia - Slovenia - Solomon Islands - Somalia - Somaliland - South Africa - Spain - Sri Lanka - Sudan - Suriname - Svalbard and Jan Mayen - Swaziland - Sweden - Switzerland - Syrian Arab Republic - Taiwan - Tanzania - Thailand - Tibet - Togo - Tonga - Trinidad and Tobago - Tunisia - Turkey - Turkmenistan - Turks and Caicos Islands - Uganda - Ukraine - United Arab Emirates - United Kingdom - Uruguay - USA - Uzbekistan - Vatican City State - Holy See - Venezuela - Viet Nam - Virgin Islands (British) - Virgin Islands (U.S.) - Wales - Yemen - Yugoslavia - Zambia - Zimbabwe - - Settings - Drag Item - Remove Item - + + + Drag-Sort Demos + + Basic usage playground + Heteroheight + Warp + Background handle + Sections + CursorAdapter + Multiple-choice mode + Single-choice mode + + + + Use the menu to adjust settings related to item + removal and drag initiation. These options are + provided by the DragSortController class. + + + Demonstrates (tests) drag-sorting when list items + have varying heights. + + + Demonstrates drag-scroll customization; long lists + need fast scrolling. + + + Simple DragSortController customization. In this example, + the drag handle is a background drawable; therefore, the + drag handle hit detection mechanism needs modification. + Also, the floating View is given some flavor. + + + Demonstrates floating View control from a custom + FloatViewManager. Restrict item drags to list sections. + + + Demonstrates usage of the DragSortCursorAdapter class, + which abstracts drag-sorts with a remapping of Cursor + positions to ListView positions. + + + Uses Checkable list items in multiple-choice mode. + + + Uses Checkable list items to allow for selectable radio + buttons in single-choice mode. + + + OK + Cancel + Remove modes + Start-drag modes + Enable/Disable + Add header + Add footer + + Click remove + Fling remove + + + On down + On drag + On long press + + + Enable drag + Enable sort + Enable remove + + + Brad Mehldau + Joshua Redman + Chick Corea + Kurt Rosenwinkel + Miles Davis + Wayne Shorter + Michael Brecker + Herbie Hancock + Joe Zawinul + Brian Blade + Jeff Ballard + Larry Grenadier + Keith Jarrett + McCoy Tyner + Stephon Harris + Mark Turner + + + Largo, Art of the Trio 1-4, Highway Rider, Songs + Elastic, Momentum, Mood Swing, Back East + Light as a Feather, Akoustic Band, My Spanish Heart + Deep Song, Heartcore, Our Secret World, Reflections, The Remedy: Live at the Village Vanguard, The Enemies of Energy, The Next Step + + + + Afghanistan + Albania + Algeria + American Samoa + Andorra + Angola + Anguilla + Antigua and Barbuda + Argentina + Armenia + Australia + Austria + Azerbajan + Bahamas + Bahrain + Bangladesh + Barbados + Belarus + Belgium + Belize + Benin + Bermuda + Bhutan + Bolivia + Bosnia and Herzegovina + Botswana + Brazil + Brunei Darussalam + Bulgaria + Burkina Faso + Burundi + Cambodia + Cameroon + Canada + Chile + China + Colombia + Costa Rica + Cuba + Cyprus + Czech Republic + Democratic Republic Congo + Denmark + Djibouti + Dominican Republic + East Timor + Ecuador + Egypt + El Salvador + England + Eritrea + Estonia + Ethiopia + Faroe Islands + Fiji + Finland + France + French Polynesia + Gambia + Georgia (Sakartvelo) + Germany + Gabon + Ghana + Greece + Greenland - Kalaallit Nunaat + Grenada + Gouadeloupe + Guam + Guatemala + Guernsey + Guyana + Guyane + Haiti + Honduras + Hong Kong + Hrvatska (Croatia) + Hungary + Iceland + India + Indonesia + Iran + Iraq + Ireland + Israel + Italy + Jamaica + Japan + Jordan + Kazakhstan + Kenya + Korea Republic + Kosovo + Kurdistan + Kuwait + Kyrgyzstan + Laos + Latvia + Lebanon + Lesotho + Liberia + Libyan Arab Jamahiriya + Liechtenstein + Lithuania + Luxembourg + Macau + Macedonia + Malawi + Malaysia + Mali + Malta + Marshall Islands + Mauritania + Martinique + Mauritius + Mexico + Micronesia + Moldova + Monaco + Mongolia + Morocco + Mozambique + Namibia + Nepal + Netherlands + Netherlands Antilles + New Caledonia + New Zealand (Aotearoa) + Nicaragua + Nigeria + Niue + Norfolk Island + Northern Ireland + Northern Mariana Islands + Norway + Oman + Pakistan + Palau + Palestina + Panama + Papua New Guinea + Paraguay + Peru + Philippines + Portugal + Puerto Rico + Qatar + Reunion + Romania + Russian Federation (AsianPart) + Russian Federation (European Part) + Rwanda + Saint Kitts and Nevis + Saint Vincent and the Grenadines + Samoa (American Samoa) + Samoa (Western Samoa) + San Marino + Saudi Arabia + Scotland + Senegal + Seychelles + Sierra Leone + Singapore + Slovakia + Slovenia + Solomon Islands + Somalia + Somaliland + South Africa + Spain + Sri Lanka + Sudan + Suriname + Svalbard and Jan Mayen + Swaziland + Sweden + Switzerland + Syrian Arab Republic + Taiwan + Tanzania + Thailand + Tibet + Togo + Tonga + Trinidad and Tobago + Tunisia + Turkey + Turkmenistan + Turks and Caicos Islands + Uganda + Ukraine + United Arab Emirates + United Kingdom + Uruguay + USA + Uzbekistan + Vatican City State - Holy See + Venezuela + Viet Nam + Virgin Islands (British) + Virgin Islands (U.S.) + Wales + Yemen + Yugoslavia + Zambia + Zimbabwe + + Settings + Drag Item + Remove Item + diff --git a/demo/res/xml/pref_headers.xml b/demo/res/xml/pref_headers.xml index 7746d7f..fbe320d 100644 --- a/demo/res/xml/pref_headers.xml +++ b/demo/res/xml/pref_headers.xml @@ -1,9 +1,9 @@ - - - - -
- + + + + +
+ diff --git a/demo/res/xml/pref_name.xml b/demo/res/xml/pref_name.xml index e34f2c6..3b5fbd7 100644 --- a/demo/res/xml/pref_name.xml +++ b/demo/res/xml/pref_name.xml @@ -1,9 +1,9 @@ - - - + + + diff --git a/demo/src/com/mobeta/android/demodslv/Launcher.java b/demo/src/com/mobeta/android/demodslv/Launcher.java index 8516a6b..bcfa930 100644 --- a/demo/src/com/mobeta/android/demodslv/Launcher.java +++ b/demo/src/com/mobeta/android/demodslv/Launcher.java @@ -1,107 +1,107 @@ -package com.mobeta.android.demodslv; - -import android.app.ListActivity; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; - -import java.util.ArrayList; -import java.util.Arrays; - -public class Launcher extends ListActivity { - - private ArrayList mActivities = null; - - private String[] mActTitles; - private String[] mActDescs; - - /** Called when the activity is first created. */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.launcher); - - try { - PackageInfo pi = getPackageManager().getPackageInfo( - "com.mobeta.android.demodslv", PackageManager.GET_ACTIVITIES); - - mActivities = new ArrayList(Arrays.asList(pi.activities)); - String[] excludeList = new String[]{getClass().getName(), - MainSettingsActivity.class.getName()}; - for (int i = 0; i < mActivities.size(); ++i) { - for (String name: excludeList) - { - if (name.equals(mActivities.get(i).name)) { - mActivities.remove(i); - break; - } - } - } - } catch (PackageManager.NameNotFoundException e) { - // Do nothing. Adapter will be empty. - } - - mActTitles = getResources().getStringArray(R.array.activity_titles); - mActDescs = getResources().getStringArray(R.array.activity_descs); - - setListAdapter(new MyAdapter()); - } - - @Override - protected void onListItemClick(ListView l, View v, int position, long id) { - Intent intent = new Intent(); - intent.setClassName(this, mActivities.get(position).name); - startActivity(intent); - } - - private class MyAdapter extends ArrayAdapter { - MyAdapter() { - super(Launcher.this, R.layout.launcher_item, R.id.activity_title, mActivities); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View v = super.getView(position, convertView, parent); - - TextView title = (TextView) v.findViewById(R.id.activity_title); - TextView desc = (TextView) v.findViewById(R.id.activity_desc); - - title.setText(mActTitles[position]); - desc.setText(mActDescs[position]); - return v; - } - - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.activity_menu, menu); - - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection - Intent intent; - switch (item.getItemId()) { - case R.id.menu_settings: - intent =new Intent(this,MainSettingsActivity.class); - startActivity(intent); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - -} +package com.mobeta.android.demodslv; + +import android.app.ListActivity; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Arrays; + +public class Launcher extends ListActivity { + + private ArrayList mActivities = null; + + private String[] mActTitles; + private String[] mActDescs; + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.launcher); + + try { + PackageInfo pi = getPackageManager().getPackageInfo( + "com.mobeta.android.demodslv", PackageManager.GET_ACTIVITIES); + + mActivities = new ArrayList(Arrays.asList(pi.activities)); + String[] excludeList = new String[]{getClass().getName(), + MainSettingsActivity.class.getName()}; + for (int i = 0; i < mActivities.size(); ++i) { + for (String name: excludeList) + { + if (name.equals(mActivities.get(i).name)) { + mActivities.remove(i); + break; + } + } + } + } catch (PackageManager.NameNotFoundException e) { + // Do nothing. Adapter will be empty. + } + + mActTitles = getResources().getStringArray(R.array.activity_titles); + mActDescs = getResources().getStringArray(R.array.activity_descs); + + setListAdapter(new MyAdapter()); + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + Intent intent = new Intent(); + intent.setClassName(this, mActivities.get(position).name); + startActivity(intent); + } + + private class MyAdapter extends ArrayAdapter { + MyAdapter() { + super(Launcher.this, R.layout.launcher_item, R.id.activity_title, mActivities); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View v = super.getView(position, convertView, parent); + + TextView title = (TextView) v.findViewById(R.id.activity_title); + TextView desc = (TextView) v.findViewById(R.id.activity_desc); + + title.setText(mActTitles[position]); + desc.setText(mActDescs[position]); + return v; + } + + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.activity_menu, menu); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + Intent intent; + switch (item.getItemId()) { + case R.id.menu_settings: + intent =new Intent(this,MainSettingsActivity.class); + startActivity(intent); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + +} diff --git a/demo/src/com/mobeta/android/demodslv/MainSettingsActivity.java b/demo/src/com/mobeta/android/demodslv/MainSettingsActivity.java index 15b5b5f..0497b34 100644 --- a/demo/src/com/mobeta/android/demodslv/MainSettingsActivity.java +++ b/demo/src/com/mobeta/android/demodslv/MainSettingsActivity.java @@ -1,227 +1,227 @@ -package com.mobeta.android.demodslv; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.res.Configuration; -import android.os.Build; -import android.os.Bundle; -import android.preference.CheckBoxPreference; -import android.preference.EditTextPreference; -import android.preference.ListPreference; -import android.preference.Preference; -import android.preference.PreferenceActivity; -import android.preference.PreferenceFragment; -import android.preference.PreferenceManager; -import android.support.v4.app.NavUtils; -import android.view.MenuItem; - -import java.util.List; - - -/** - * A {@link PreferenceActivity} that presents a set of application settings. On - * handset devices, settings are presented as a single list. On tablets, - * settings are split by category, with category headers shown to the left of - * the list of settings. - *

- * See - * Android Design: Settings for design guidelines and the Settings - * API Guide for more information on developing a Settings UI. - */ -public class MainSettingsActivity extends PreferenceActivity{ - private static final String TAG="Settings"; - /** - * Determines whether to always show the simplified settings UI, where - * settings are presented in a single list. When false, settings are shown - * as a master/detail two-pane view on tablets. When true, a single pane is - * shown on tablets. - */ - private static final boolean ALWAYS_SIMPLE_PREFS = false; - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - setupSimplePreferencesScreen(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) - getActionBar().setDisplayHomeAsUpEnabled(true); - } - - /** - * Shows the simplified settings UI if the device configuration if the - * device configuration dictates that a simplified, single-pane UI should be - * shown. - */ - @SuppressWarnings("deprecation") - private void setupSimplePreferencesScreen() { - if (!isSimplePreferences(this)) { - return; - } - - // In the simplified UI, fragments are not used at all and we instead - // use the older PreferenceActivity APIs. - - // Add 'general' preferences. - addPreferencesFromResource(R.xml.pref_name); - - - // Bind the summaries of EditText/List/Dialog/Ringtone preferences to - // their values. When their values change, their summaries are updated - // to reflect the new value, per the Android Design guidelines. - bindPreferenceSummaryToValue(findPreference("name_order")); - - } - - /** {@inheritDoc} */ - @Override - public boolean onIsMultiPane() { - return isLargeTablet(this) && !isSimplePreferences(this); - } - - /** - * Helper method to determine if the device has an extra-large screen. For - * example, 10" tablets are extra-large. - */ - private static boolean isLargeTablet(Context context) { - return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE; - } - - /** - * Determines whether the simplified settings UI should be shown. This is - * true if this is forced via {@link #ALWAYS_SIMPLE_PREFS}, or the device - * doesn't have newer APIs like {@link PreferenceFragment}, or the device - * doesn't have an extra-large screen. In these cases, a single-pane - * "simplified" settings UI should be shown. - */ - private static boolean isSimplePreferences(Context context) { - return ALWAYS_SIMPLE_PREFS - || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB; - } - - /** {@inheritDoc} */ - @Override - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public void onBuildHeaders(List

target) { - if (!isSimplePreferences(this)) { - loadHeadersFromResource(R.xml.pref_headers, target); - } - } - - /** - * A preference value change listener that updates the preference's summary - * to reflect its new value. - */ - private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object value) { - String stringValue = value.toString(); - - if (preference instanceof ListPreference) { - // For list preferences, look up the correct display value in - // the preference's 'entries' list. - ListPreference listPreference = (ListPreference) preference; - int index = listPreference.findIndexOfValue(stringValue); - - // Set the summary to reflect the new value. - preference - .setSummary(index >= 0 ? listPreference.getEntries()[index] - : null); - } else { - // For all other preferences, set the summary to the value's - // simple string representation. - preference.setSummary(stringValue); - } - return true; - } - }; - - /** - * Binds a preference's summary to its value. More specifically, when the - * preference's value is changed, its summary (line of text below the - * preference title) is updated to reflect the value. The summary is also - * immediately updated upon calling this method. The exact display format is - * dependent on the type of preference. - * - * @see #sBindPreferenceSummaryToValueListener - */ - private static void bindPreferenceSummaryToValue(Preference preference) { - // Set the listener to watch for value changes. - preference - .setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); - - if (preference instanceof CheckBoxPreference) - { - // Trigger the listener immediately with the preference's - // current value. - sBindPreferenceSummaryToValueListener.onPreferenceChange( - preference, - PreferenceManager.getDefaultSharedPreferences( - preference.getContext()).getBoolean(preference.getKey(),false)); - } - else if (preference instanceof EditTextPreference) - { - - // Trigger the listener immediately with the preference's - // current value. - sBindPreferenceSummaryToValueListener.onPreferenceChange( - preference, - PreferenceManager.getDefaultSharedPreferences( - preference.getContext()).getString(preference.getKey(),"")); - } - else if (preference instanceof SortableListPreference) - { - // Trigger the listener immediately with the preference's - // current value. - sBindPreferenceSummaryToValueListener.onPreferenceChange( - preference, - PreferenceManager.getDefaultSharedPreferences( - preference.getContext()).getString(preference.getKey(),"")); - } - else - { - throw new IllegalArgumentException("Attempting to bind to unknown type of preference!"); - } - } - - /** - * This fragment shows location update preferences only. It is used when the - * activity is showing a two-pane settings UI. - */ - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static class NameSelectionPreferenceFragment extends - PreferenceFragment { - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.pref_name); - - // Bind the summaries of EditText/List/Dialog/Ringtone preferences - // to their values. When their values change, their summaries are - // updated to reflect the new value, per the Android Design - // guidelines. - - bindPreferenceSummaryToValue(findPreference("name_order")); - } - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - protected boolean isValidFragment (String fragmentName) - { - if(MainSettingsActivity.class.getName().equals(fragmentName) || - NameSelectionPreferenceFragment.class.getName().equals(fragmentName)) - return true; - return false; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - NavUtils.navigateUpFromSameTask(this); - return true; - default: - return super.onOptionsItemSelected(item); - } - } -} +package com.mobeta.android.demodslv; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; +import android.support.v4.app.NavUtils; +import android.view.MenuItem; + +import java.util.List; + + +/** + * A {@link PreferenceActivity} that presents a set of application settings. On + * handset devices, settings are presented as a single list. On tablets, + * settings are split by category, with category headers shown to the left of + * the list of settings. + *

+ * See + * Android Design: Settings for design guidelines and the Settings + * API Guide for more information on developing a Settings UI. + */ +public class MainSettingsActivity extends PreferenceActivity{ + private static final String TAG="Settings"; + /** + * Determines whether to always show the simplified settings UI, where + * settings are presented in a single list. When false, settings are shown + * as a master/detail two-pane view on tablets. When true, a single pane is + * shown on tablets. + */ + private static final boolean ALWAYS_SIMPLE_PREFS = false; + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + setupSimplePreferencesScreen(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) + getActionBar().setDisplayHomeAsUpEnabled(true); + } + + /** + * Shows the simplified settings UI if the device configuration if the + * device configuration dictates that a simplified, single-pane UI should be + * shown. + */ + @SuppressWarnings("deprecation") + private void setupSimplePreferencesScreen() { + if (!isSimplePreferences(this)) { + return; + } + + // In the simplified UI, fragments are not used at all and we instead + // use the older PreferenceActivity APIs. + + // Add 'general' preferences. + addPreferencesFromResource(R.xml.pref_name); + + + // Bind the summaries of EditText/List/Dialog/Ringtone preferences to + // their values. When their values change, their summaries are updated + // to reflect the new value, per the Android Design guidelines. + bindPreferenceSummaryToValue(findPreference("name_order")); + + } + + /** {@inheritDoc} */ + @Override + public boolean onIsMultiPane() { + return isLargeTablet(this) && !isSimplePreferences(this); + } + + /** + * Helper method to determine if the device has an extra-large screen. For + * example, 10" tablets are extra-large. + */ + private static boolean isLargeTablet(Context context) { + return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE; + } + + /** + * Determines whether the simplified settings UI should be shown. This is + * true if this is forced via {@link #ALWAYS_SIMPLE_PREFS}, or the device + * doesn't have newer APIs like {@link PreferenceFragment}, or the device + * doesn't have an extra-large screen. In these cases, a single-pane + * "simplified" settings UI should be shown. + */ + private static boolean isSimplePreferences(Context context) { + return ALWAYS_SIMPLE_PREFS + || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB; + } + + /** {@inheritDoc} */ + @Override + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void onBuildHeaders(List

target) { + if (!isSimplePreferences(this)) { + loadHeadersFromResource(R.xml.pref_headers, target); + } + } + + /** + * A preference value change listener that updates the preference's summary + * to reflect its new value. + */ + private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object value) { + String stringValue = value.toString(); + + if (preference instanceof ListPreference) { + // For list preferences, look up the correct display value in + // the preference's 'entries' list. + ListPreference listPreference = (ListPreference) preference; + int index = listPreference.findIndexOfValue(stringValue); + + // Set the summary to reflect the new value. + preference + .setSummary(index >= 0 ? listPreference.getEntries()[index] + : null); + } else { + // For all other preferences, set the summary to the value's + // simple string representation. + preference.setSummary(stringValue); + } + return true; + } + }; + + /** + * Binds a preference's summary to its value. More specifically, when the + * preference's value is changed, its summary (line of text below the + * preference title) is updated to reflect the value. The summary is also + * immediately updated upon calling this method. The exact display format is + * dependent on the type of preference. + * + * @see #sBindPreferenceSummaryToValueListener + */ + private static void bindPreferenceSummaryToValue(Preference preference) { + // Set the listener to watch for value changes. + preference + .setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); + + if (preference instanceof CheckBoxPreference) + { + // Trigger the listener immediately with the preference's + // current value. + sBindPreferenceSummaryToValueListener.onPreferenceChange( + preference, + PreferenceManager.getDefaultSharedPreferences( + preference.getContext()).getBoolean(preference.getKey(),false)); + } + else if (preference instanceof EditTextPreference) + { + + // Trigger the listener immediately with the preference's + // current value. + sBindPreferenceSummaryToValueListener.onPreferenceChange( + preference, + PreferenceManager.getDefaultSharedPreferences( + preference.getContext()).getString(preference.getKey(),"")); + } + else if (preference instanceof SortableListPreference) + { + // Trigger the listener immediately with the preference's + // current value. + sBindPreferenceSummaryToValueListener.onPreferenceChange( + preference, + PreferenceManager.getDefaultSharedPreferences( + preference.getContext()).getString(preference.getKey(),"")); + } + else + { + throw new IllegalArgumentException("Attempting to bind to unknown type of preference!"); + } + } + + /** + * This fragment shows location update preferences only. It is used when the + * activity is showing a two-pane settings UI. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static class NameSelectionPreferenceFragment extends + PreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.pref_name); + + // Bind the summaries of EditText/List/Dialog/Ringtone preferences + // to their values. When their values change, their summaries are + // updated to reflect the new value, per the Android Design + // guidelines. + + bindPreferenceSummaryToValue(findPreference("name_order")); + } + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + protected boolean isValidFragment (String fragmentName) + { + if(MainSettingsActivity.class.getName().equals(fragmentName) || + NameSelectionPreferenceFragment.class.getName().equals(fragmentName)) + return true; + return false; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + default: + return super.onOptionsItemSelected(item); + } + } +} diff --git a/demo/src/com/mobeta/android/demodslv/MultipleChoiceListView.java b/demo/src/com/mobeta/android/demodslv/MultipleChoiceListView.java index 75e506c..afdad49 100644 --- a/demo/src/com/mobeta/android/demodslv/MultipleChoiceListView.java +++ b/demo/src/com/mobeta/android/demodslv/MultipleChoiceListView.java @@ -1,65 +1,65 @@ -package com.mobeta.android.demodslv; - -import android.app.ListActivity; -import android.os.Bundle; -import android.widget.ArrayAdapter; - -import com.mobeta.android.dslv.DragSortListView; -import com.mobeta.android.dslv.DragSortListView.RemoveListener; - -import java.util.ArrayList; -import java.util.Arrays; - - -public class MultipleChoiceListView extends ListActivity { - - private ArrayAdapter adapter; - - private final DragSortListView.DropListener mDropListener = - new DragSortListView.DropListener() { - @Override - public void drop(int from, int to) { - if (from != to) { - DragSortListView list = getListView(); - String item = adapter.getItem(from); - adapter.remove(item); - adapter.insert(item, to); - list.moveCheckState(from, to); - } - } - }; - - private final RemoveListener mRemoveListener = - new DragSortListView.RemoveListener() { - @Override - public void remove(int which) { - DragSortListView list = getListView(); - String item = adapter.getItem(which); - adapter.remove(item); - list.removeCheckState(which); - } - }; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.checkable_main); - - String[] array = getResources().getStringArray(R.array.jazz_artist_names); - ArrayList arrayList = new ArrayList(Arrays.asList(array)); - - adapter = new ArrayAdapter(this, R.layout.list_item_checkable, R.id.text, arrayList); - - setListAdapter(adapter); - - DragSortListView list = getListView(); - list.setDropListener(mDropListener); - list.setRemoveListener(mRemoveListener); - } - - @Override - public DragSortListView getListView() { - return (DragSortListView) super.getListView(); - } - -} +package com.mobeta.android.demodslv; + +import android.app.ListActivity; +import android.os.Bundle; +import android.widget.ArrayAdapter; + +import com.mobeta.android.dslv.DragSortListView; +import com.mobeta.android.dslv.DragSortListView.RemoveListener; + +import java.util.ArrayList; +import java.util.Arrays; + + +public class MultipleChoiceListView extends ListActivity { + + private ArrayAdapter adapter; + + private final DragSortListView.DropListener mDropListener = + new DragSortListView.DropListener() { + @Override + public void drop(int from, int to) { + if (from != to) { + DragSortListView list = getListView(); + String item = adapter.getItem(from); + adapter.remove(item); + adapter.insert(item, to); + list.moveCheckState(from, to); + } + } + }; + + private final RemoveListener mRemoveListener = + new DragSortListView.RemoveListener() { + @Override + public void remove(int which) { + DragSortListView list = getListView(); + String item = adapter.getItem(which); + adapter.remove(item); + list.removeCheckState(which); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.checkable_main); + + String[] array = getResources().getStringArray(R.array.jazz_artist_names); + ArrayList arrayList = new ArrayList(Arrays.asList(array)); + + adapter = new ArrayAdapter(this, R.layout.list_item_checkable, R.id.text, arrayList); + + setListAdapter(adapter); + + DragSortListView list = getListView(); + list.setDropListener(mDropListener); + list.setRemoveListener(mRemoveListener); + } + + @Override + public DragSortListView getListView() { + return (DragSortListView) super.getListView(); + } + +} diff --git a/demo/src/com/mobeta/android/demodslv/SortableListPreference.java b/demo/src/com/mobeta/android/demodslv/SortableListPreference.java index c2fdb5c..b7472b3 100644 --- a/demo/src/com/mobeta/android/demodslv/SortableListPreference.java +++ b/demo/src/com/mobeta/android/demodslv/SortableListPreference.java @@ -1,295 +1,295 @@ -/* - * Sortable Preference ListView. Allows for sorting items in a view, - * and selecting which ones to use. - * - * Example Usage (In a preference file) - * - * - * - * Original Source: https://github.com/kd7uiy/drag-sort-listview - * - * The MIT License (MIT) - * - * Copyright (c) 2013 The Making of a Ham, http://www.kd7uiy.com - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - * Code snippets copied from the following sources: - * https://gist.github.com/cardil/4754571 - * - * - */ - -package com.mobeta.android.demodslv; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; - -import com.mobeta.android.dslv.DragSortListView; -import com.mobeta.android.dslv.DragSortListView.DropListener; - -import android.app.AlertDialog.Builder; -import android.content.Context; -import android.content.res.TypedArray; -import android.preference.ListPreference; -import android.util.AttributeSet; -import android.view.View; -import android.widget.ArrayAdapter; - -public class SortableListPreference extends ListPreference { - private static final String TAG=SortableListPreference.class.getName(); - - DragSortListView mListView; - ArrayAdapter mAdapter; - - private static final String DEFAULT_SEPARATOR = "\u0001\u0007\u001D\u0007\u0001"; - String separator; - - private HashMap entryChecked; - - public SortableListPreference(Context context, AttributeSet attrs) { - super(context, attrs); - separator = DEFAULT_SEPARATOR; - setDialogLayoutResource(R.layout.sort_list_array_dialog_preference); - entryChecked = new HashMap(); - } - - public static CharSequence[] decodeValue(String input) - { - return decodeValue(input,DEFAULT_SEPARATOR); - } - - public static CharSequence[] decodeValue(String input,String separator) - { - if (input==null) - return null; - return input.split(separator); - } - - @Override - protected void onBindDialogView(View view) - { - super.onBindDialogView(view); - mListView= (DragSortListView) view.findViewById(android.R.id.list); - mAdapter =new ArrayAdapter(mListView.getContext(),R.layout.list_item_simple_checkable,R.id.text); - mListView.setAdapter(mAdapter); - //This will drop the item in the new location - mListView.setDropListener(new DropListener() - { - @Override - public void drop(int from, int to) { - CharSequence item = mAdapter.getItem(from); - mAdapter.remove(item); - mAdapter.insert(item, to); - mAdapter.notifyDataSetChanged(); - //Updates checked states - mListView.moveCheckState(from,to); - } - }); - // Setting the default values happens in onPrepareDialogBuilder - } - - @Override - protected void onPrepareDialogBuilder(Builder builder) { - CharSequence[] entries = getEntries(); - CharSequence[] entryValues = getEntryValues(); - if (entries == null || entryValues == null - || entries.length != entryValues.length) { - throw new IllegalStateException( - "SortableListPreference requires an entries array and an entryValues " - + "array which are both the same length"); - } - - CharSequence[] restoredValues=restoreEntries(); - for (CharSequence value:restoredValues) - { - mAdapter.add(entries[getValueIndex(value)]); - } - - } - - @Override - protected void onDialogClosed(boolean positiveResult) { - List values = new ArrayList(); - - CharSequence[] entryValues = getEntryValues(); - if (positiveResult && entryValues != null) { - for (int i = 0; i < entryValues.length; i++) { - String val = (String) mAdapter.getItem(i); - boolean isChecked=mListView.isItemChecked(i); - if (isChecked) - { - values.add(entryValues[getValueTitleIndex(val)]); - } - } - - String value = join(values, separator); - setSummary(prepareSummary(values)); - setValueAndEvent(value); - } - } - - private void setValueAndEvent(String value) { - if (callChangeListener(decodeValue(value,separator))) { - setValue(value); - } - } - - @Override - protected Object onGetDefaultValue(TypedArray typedArray, int index) { - return typedArray.getTextArray(index); - } - - @Override - protected void onSetInitialValue(boolean restoreValue, - Object rawDefaultValue) { - String value = null; - CharSequence[] defaultValue; - if (rawDefaultValue == null) { - defaultValue = new CharSequence[0]; - } else { - defaultValue = (CharSequence[]) rawDefaultValue; - } - List joined = Arrays.asList(defaultValue); - String joinedDefaultValue = join(joined, separator); - if (restoreValue) { - value = getPersistedString(joinedDefaultValue); - } else { - value = joinedDefaultValue; - } - - setSummary(prepareSummary(Arrays.asList(decodeValue(value,separator)))); - setValueAndEvent(value); - } - - private String prepareSummary(List joined) { - List titles = new ArrayList(); - CharSequence[] entryTitle = getEntries(); - for (CharSequence item : joined) { - int ix=getValueIndex(item); - titles.add((String) entryTitle[ix]); - } - return join(titles, ", "); - } - - public int getValueIndex(CharSequence item) - { - CharSequence[] entryValues = getEntryValues(); - int ix=0; - boolean found=false; - for (CharSequence value:entryValues) - { - if (value.equals(item)) - { - found=true; - break; - } - ix+=1; - } - if (!found) - throw new IllegalArgumentException(item+" not found in value list"); - return ix; - } - - public int getValueTitleIndex(CharSequence item) - { - CharSequence[] entries = getEntries(); - int ix=0; - boolean found=false; - for (CharSequence value:entries) - { - if (value.equals(item)) - { - found=true; - break; - } - ix+=1; - } - if (!found) - throw new IllegalArgumentException(item+" not found in value title list"); - return ix; - } - - private CharSequence[] restoreEntries() { - - ArrayList orderedList=new ArrayList(); - - int ix=0; - //Initially populated with all of the values in the determined list. - for (CharSequence value:decodeValue(getValue(),separator)) - { - orderedList.add(value); - mListView.setItemChecked(ix,true); - ix++; - } - - //This loop sets the default states, and adds to the name list if not on the list. - for (CharSequence value:getEntryValues()) - { - entryChecked.put(value,false); - if (!orderedList.contains(value)) - { - orderedList.add(value); - } - } - for (CharSequence value:orderedList) - { - if (entryChecked.containsKey(value)) - { - entryChecked.put(value,true); - } - else - { - throw new IllegalArgumentException("Invalid value "+value+" in key list"); - } - } - return orderedList.toArray(new CharSequence[0]); - } - - /** - * Joins array of object to single string by separator - * - * Credits to kurellajunior on this post - * http://snippets.dzone.com/posts/show/91 - * - * @param iterable - * any kind of iterable ex.: ["a", "b", "c"] - * @param separator - * separetes entries ex.: "," - * @return joined string ex.: "a,b,c" - */ - protected static String join(Iterable iterable, String separator) { - Iterator oIter; - if (iterable == null || (!(oIter = iterable.iterator()).hasNext())) - return ""; - StringBuilder oBuilder = new StringBuilder(String.valueOf(oIter.next())); - while (oIter.hasNext()) - oBuilder.append(separator).append(oIter.next()); - return oBuilder.toString(); - } -} +/* + * Sortable Preference ListView. Allows for sorting items in a view, + * and selecting which ones to use. + * + * Example Usage (In a preference file) + * + * + * + * Original Source: https://github.com/kd7uiy/drag-sort-listview + * + * The MIT License (MIT) + * + * Copyright (c) 2013 The Making of a Ham, http://www.kd7uiy.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Code snippets copied from the following sources: + * https://gist.github.com/cardil/4754571 + * + * + */ + +package com.mobeta.android.demodslv; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +import com.mobeta.android.dslv.DragSortListView; +import com.mobeta.android.dslv.DragSortListView.DropListener; + +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.content.res.TypedArray; +import android.preference.ListPreference; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ArrayAdapter; + +public class SortableListPreference extends ListPreference { + private static final String TAG=SortableListPreference.class.getName(); + + DragSortListView mListView; + ArrayAdapter mAdapter; + + private static final String DEFAULT_SEPARATOR = "\u0001\u0007\u001D\u0007\u0001"; + String separator; + + private HashMap entryChecked; + + public SortableListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + separator = DEFAULT_SEPARATOR; + setDialogLayoutResource(R.layout.sort_list_array_dialog_preference); + entryChecked = new HashMap(); + } + + public static CharSequence[] decodeValue(String input) + { + return decodeValue(input,DEFAULT_SEPARATOR); + } + + public static CharSequence[] decodeValue(String input,String separator) + { + if (input==null) + return null; + return input.split(separator); + } + + @Override + protected void onBindDialogView(View view) + { + super.onBindDialogView(view); + mListView= (DragSortListView) view.findViewById(android.R.id.list); + mAdapter =new ArrayAdapter(mListView.getContext(),R.layout.list_item_simple_checkable,R.id.text); + mListView.setAdapter(mAdapter); + //This will drop the item in the new location + mListView.setDropListener(new DropListener() + { + @Override + public void drop(int from, int to) { + CharSequence item = mAdapter.getItem(from); + mAdapter.remove(item); + mAdapter.insert(item, to); + mAdapter.notifyDataSetChanged(); + //Updates checked states + mListView.moveCheckState(from,to); + } + }); + // Setting the default values happens in onPrepareDialogBuilder + } + + @Override + protected void onPrepareDialogBuilder(Builder builder) { + CharSequence[] entries = getEntries(); + CharSequence[] entryValues = getEntryValues(); + if (entries == null || entryValues == null + || entries.length != entryValues.length) { + throw new IllegalStateException( + "SortableListPreference requires an entries array and an entryValues " + + "array which are both the same length"); + } + + CharSequence[] restoredValues=restoreEntries(); + for (CharSequence value:restoredValues) + { + mAdapter.add(entries[getValueIndex(value)]); + } + + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + List values = new ArrayList(); + + CharSequence[] entryValues = getEntryValues(); + if (positiveResult && entryValues != null) { + for (int i = 0; i < entryValues.length; i++) { + String val = (String) mAdapter.getItem(i); + boolean isChecked=mListView.isItemChecked(i); + if (isChecked) + { + values.add(entryValues[getValueTitleIndex(val)]); + } + } + + String value = join(values, separator); + setSummary(prepareSummary(values)); + setValueAndEvent(value); + } + } + + private void setValueAndEvent(String value) { + if (callChangeListener(decodeValue(value,separator))) { + setValue(value); + } + } + + @Override + protected Object onGetDefaultValue(TypedArray typedArray, int index) { + return typedArray.getTextArray(index); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, + Object rawDefaultValue) { + String value = null; + CharSequence[] defaultValue; + if (rawDefaultValue == null) { + defaultValue = new CharSequence[0]; + } else { + defaultValue = (CharSequence[]) rawDefaultValue; + } + List joined = Arrays.asList(defaultValue); + String joinedDefaultValue = join(joined, separator); + if (restoreValue) { + value = getPersistedString(joinedDefaultValue); + } else { + value = joinedDefaultValue; + } + + setSummary(prepareSummary(Arrays.asList(decodeValue(value,separator)))); + setValueAndEvent(value); + } + + private String prepareSummary(List joined) { + List titles = new ArrayList(); + CharSequence[] entryTitle = getEntries(); + for (CharSequence item : joined) { + int ix=getValueIndex(item); + titles.add((String) entryTitle[ix]); + } + return join(titles, ", "); + } + + public int getValueIndex(CharSequence item) + { + CharSequence[] entryValues = getEntryValues(); + int ix=0; + boolean found=false; + for (CharSequence value:entryValues) + { + if (value.equals(item)) + { + found=true; + break; + } + ix+=1; + } + if (!found) + throw new IllegalArgumentException(item+" not found in value list"); + return ix; + } + + public int getValueTitleIndex(CharSequence item) + { + CharSequence[] entries = getEntries(); + int ix=0; + boolean found=false; + for (CharSequence value:entries) + { + if (value.equals(item)) + { + found=true; + break; + } + ix+=1; + } + if (!found) + throw new IllegalArgumentException(item+" not found in value title list"); + return ix; + } + + private CharSequence[] restoreEntries() { + + ArrayList orderedList=new ArrayList(); + + int ix=0; + //Initially populated with all of the values in the determined list. + for (CharSequence value:decodeValue(getValue(),separator)) + { + orderedList.add(value); + mListView.setItemChecked(ix,true); + ix++; + } + + //This loop sets the default states, and adds to the name list if not on the list. + for (CharSequence value:getEntryValues()) + { + entryChecked.put(value,false); + if (!orderedList.contains(value)) + { + orderedList.add(value); + } + } + for (CharSequence value:orderedList) + { + if (entryChecked.containsKey(value)) + { + entryChecked.put(value,true); + } + else + { + throw new IllegalArgumentException("Invalid value "+value+" in key list"); + } + } + return orderedList.toArray(new CharSequence[0]); + } + + /** + * Joins array of object to single string by separator + * + * Credits to kurellajunior on this post + * http://snippets.dzone.com/posts/show/91 + * + * @param iterable + * any kind of iterable ex.: ["a", "b", "c"] + * @param separator + * separetes entries ex.: "," + * @return joined string ex.: "a,b,c" + */ + protected static String join(Iterable iterable, String separator) { + Iterator oIter; + if (iterable == null || (!(oIter = iterable.iterator()).hasNext())) + return ""; + StringBuilder oBuilder = new StringBuilder(String.valueOf(oIter.next())); + while (oIter.hasNext()) + oBuilder.append(separator).append(oIter.next()); + return oBuilder.toString(); + } +} From 49fa7756c34e544f797456bd67ac3f08e71dd67d Mon Sep 17 00:00:00 2001 From: kd7uiy Date: Mon, 30 Dec 2013 18:09:48 -0500 Subject: [PATCH 10/18] General code cleanup, Lint fixes, prevented a crash in Background Handle, and probably other classes that extend DSLVFragment. --- demo/AndroidManifest.xml | 1 + demo/res/layout/checkable_main.xml | 2 +- demo/res/layout/cursor_main.xml | 2 +- demo/res/layout/dslv_fragment_main.xml | 2 +- demo/res/layout/hetero_main.xml | 2 +- demo/res/layout/jazz_artist_list_item.xml | 1 + demo/res/layout/list_item_checkable.xml | 2 + demo/res/layout/list_item_handle_left.xml | 1 + demo/res/layout/list_item_handle_right.xml | 1 + demo/res/layout/list_item_radio.xml | 1 + demo/res/layout/section_div.xml | 1 + demo/res/layout/sections_main.xml | 2 +- .../sort_list_array_dialog_preference.xml | 23 +-- demo/res/layout/warp_main.xml | 2 +- demo/res/menu/activity_menu.xml | 7 +- demo/res/values/strings.xml | 2 +- .../mobeta/android/demodslv/DSLVFragment.java | 8 +- .../android/demodslv/DSLVFragmentClicks.java | 12 +- .../com/mobeta/android/demodslv/Launcher.java | 147 +++++++++--------- .../demodslv/MainSettingsActivity.java | 75 +++++---- .../demodslv/SortableListPreference.java | 126 ++++++--------- 21 files changed, 200 insertions(+), 220 deletions(-) diff --git a/demo/AndroidManifest.xml b/demo/AndroidManifest.xml index d0c0197..1e77dee 100644 --- a/demo/AndroidManifest.xml +++ b/demo/AndroidManifest.xml @@ -10,6 +10,7 @@ - + - + diff --git a/demo/res/layout/warp_main.xml b/demo/res/layout/warp_main.xml index 3631bd7..a2dfc12 100644 --- a/demo/res/layout/warp_main.xml +++ b/demo/res/layout/warp_main.xml @@ -1,7 +1,7 @@ - - - - + + + diff --git a/demo/res/values/strings.xml b/demo/res/values/strings.xml index 818a579..a1a7134 100644 --- a/demo/res/values/strings.xml +++ b/demo/res/values/strings.xml @@ -88,7 +88,7 @@ Mark Turner - Largo, Art of the Trio 1-4, Highway Rider, Songs + Largo, Art of the Trio 1–4, Highway Rider, Songs Elastic, Momentum, Mood Swing, Back East Light as a Feather, Akoustic Band, My Spanish Heart Deep Song, Heartcore, Our Secret World, Reflections, The Remedy: Live at the Village Vanguard, The Enemies of Energy, The Next Step diff --git a/demo/src/com/mobeta/android/demodslv/DSLVFragment.java b/demo/src/com/mobeta/android/demodslv/DSLVFragment.java index c7004ff..7765d97 100644 --- a/demo/src/com/mobeta/android/demodslv/DSLVFragment.java +++ b/demo/src/com/mobeta/android/demodslv/DSLVFragment.java @@ -3,6 +3,7 @@ import android.app.Activity; import android.os.Bundle; import android.support.v4.app.ListFragment; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -94,6 +95,7 @@ protected void setListAdapter() { String[] array = getResources().getStringArray(R.array.jazz_artist_names); List list = new ArrayList(Arrays.asList(array)); + mAdapter = new ArrayAdapter(getActivity(), getItemLayout(), R.id.text, list); setListAdapter(mAdapter); } @@ -129,7 +131,8 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, return mDslv; } - @Override + @SuppressWarnings("unchecked") + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); @@ -145,6 +148,8 @@ public void onActivityCreated(Bundle savedInstanceState) { headers = args.getInt("headers", 0); footers = args.getInt("footers", 0); } + + Log.v("DSLV","Adding Footers"); for (int i = 0; i < headers; i++) { addHeader(getActivity(), mDslv); @@ -154,6 +159,7 @@ public void onActivityCreated(Bundle savedInstanceState) { } setListAdapter(); + mAdapter=(ArrayAdapter) getListAdapter(); } diff --git a/demo/src/com/mobeta/android/demodslv/DSLVFragmentClicks.java b/demo/src/com/mobeta/android/demodslv/DSLVFragmentClicks.java index 8b80ceb..0373d81 100644 --- a/demo/src/com/mobeta/android/demodslv/DSLVFragmentClicks.java +++ b/demo/src/com/mobeta/android/demodslv/DSLVFragmentClicks.java @@ -1,5 +1,7 @@ package com.mobeta.android.demodslv; +import java.util.Locale; +import android.annotation.SuppressLint; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; @@ -19,25 +21,25 @@ public static DSLVFragmentClicks newInstance(int headers, int footers) { return f; } - @Override + @Override public void onActivityCreated(Bundle savedState) { super.onActivityCreated(savedState); ListView lv = getListView(); lv.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override + @Override public void onItemClick(AdapterView arg0, View arg1, int arg2, long arg3) { - String message = String.format("Clicked item %d", arg2); + String message = String.format(Locale.getDefault(),"Clicked item %d", arg2); Toast.makeText(getActivity(), message, Toast.LENGTH_SHORT).show(); } }); lv.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { - @Override + @Override public boolean onItemLongClick(AdapterView arg0, View arg1, int arg2, long arg3) { - String message = String.format("Long-clicked item %d", arg2); + String message = String.format(Locale.getDefault(),"Long-clicked item %d", arg2); Toast.makeText(getActivity(), message, Toast.LENGTH_SHORT).show(); return true; } diff --git a/demo/src/com/mobeta/android/demodslv/Launcher.java b/demo/src/com/mobeta/android/demodslv/Launcher.java index bcfa930..d3a7bfb 100644 --- a/demo/src/com/mobeta/android/demodslv/Launcher.java +++ b/demo/src/com/mobeta/android/demodslv/Launcher.java @@ -19,89 +19,88 @@ public class Launcher extends ListActivity { - private ArrayList mActivities = null; - - private String[] mActTitles; - private String[] mActDescs; - - /** Called when the activity is first created. */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.launcher); - - try { - PackageInfo pi = getPackageManager().getPackageInfo( - "com.mobeta.android.demodslv", PackageManager.GET_ACTIVITIES); - - mActivities = new ArrayList(Arrays.asList(pi.activities)); - String[] excludeList = new String[]{getClass().getName(), - MainSettingsActivity.class.getName()}; - for (int i = 0; i < mActivities.size(); ++i) { - for (String name: excludeList) - { - if (name.equals(mActivities.get(i).name)) { - mActivities.remove(i); - break; - } - } - } - } catch (PackageManager.NameNotFoundException e) { - // Do nothing. Adapter will be empty. - } - - mActTitles = getResources().getStringArray(R.array.activity_titles); - mActDescs = getResources().getStringArray(R.array.activity_descs); - - setListAdapter(new MyAdapter()); - } - - @Override - protected void onListItemClick(ListView l, View v, int position, long id) { - Intent intent = new Intent(); - intent.setClassName(this, mActivities.get(position).name); - startActivity(intent); - } - - private class MyAdapter extends ArrayAdapter { - MyAdapter() { - super(Launcher.this, R.layout.launcher_item, R.id.activity_title, mActivities); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View v = super.getView(position, convertView, parent); - - TextView title = (TextView) v.findViewById(R.id.activity_title); - TextView desc = (TextView) v.findViewById(R.id.activity_desc); - - title.setText(mActTitles[position]); - desc.setText(mActDescs[position]); - return v; - } - - } - + private ArrayList mActivities = null; + + private String[] mActTitles; + private String[] mActDescs; + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.launcher); + + try { + PackageInfo pi = getPackageManager().getPackageInfo( + "com.mobeta.android.demodslv", PackageManager.GET_ACTIVITIES); + + mActivities = new ArrayList(Arrays.asList(pi.activities)); + String[] excludeList = new String[]{getClass().getName(), + MainSettingsActivity.class.getName()}; + for (int i = 0; i < mActivities.size(); ++i) { + for (String name: excludeList) { + if (name.equals(mActivities.get(i).name)) { + mActivities.remove(i); + break; + } + } + } + } catch (PackageManager.NameNotFoundException e) { + // Do nothing. Adapter will be empty. + } + + mActTitles = getResources().getStringArray(R.array.activity_titles); + mActDescs = getResources().getStringArray(R.array.activity_descs); + + setListAdapter(new MyAdapter()); + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + Intent intent = new Intent(); + intent.setClassName(this, mActivities.get(position).name); + startActivity(intent); + } + + private class MyAdapter extends ArrayAdapter { + MyAdapter() { + super(Launcher.this, R.layout.launcher_item, R.id.activity_title, mActivities); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View v = super.getView(position, convertView, parent); + + TextView title = (TextView) v.findViewById(R.id.activity_title); + TextView desc = (TextView) v.findViewById(R.id.activity_desc); + + title.setText(mActTitles[position]); + desc.setText(mActDescs[position]); + return v; + } + + } + @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.activity_menu, menu); - + return true; } - + @Override public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection + // Handle item selection Intent intent; - switch (item.getItemId()) { - case R.id.menu_settings: - intent =new Intent(this,MainSettingsActivity.class); - startActivity(intent); - return true; - default: - return super.onOptionsItemSelected(item); - } + switch (item.getItemId()) { + case R.id.menu_settings: + intent =new Intent(this,MainSettingsActivity.class); + startActivity(intent); + return true; + default: + return super.onOptionsItemSelected(item); + } } } diff --git a/demo/src/com/mobeta/android/demodslv/MainSettingsActivity.java b/demo/src/com/mobeta/android/demodslv/MainSettingsActivity.java index 0497b34..60da1e9 100644 --- a/demo/src/com/mobeta/android/demodslv/MainSettingsActivity.java +++ b/demo/src/com/mobeta/android/demodslv/MainSettingsActivity.java @@ -30,7 +30,6 @@ * API Guide for more information on developing a Settings UI. */ public class MainSettingsActivity extends PreferenceActivity{ - private static final String TAG="Settings"; /** * Determines whether to always show the simplified settings UI, where * settings are presented in a single list. When false, settings are shown @@ -39,13 +38,16 @@ public class MainSettingsActivity extends PreferenceActivity{ */ private static final boolean ALWAYS_SIMPLE_PREFS = false; - @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); setupSimplePreferencesScreen(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) + //This doesn't always work right with HoneyComb, so I removed it. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + { getActionBar().setDisplayHomeAsUpEnabled(true); + } } /** @@ -64,16 +66,15 @@ private void setupSimplePreferencesScreen() { // Add 'general' preferences. addPreferencesFromResource(R.xml.pref_name); - + // Bind the summaries of EditText/List/Dialog/Ringtone preferences to // their values. When their values change, their summaries are updated // to reflect the new value, per the Android Design guidelines. bindPreferenceSummaryToValue(findPreference("name_order")); - + } - /** {@inheritDoc} */ @Override public boolean onIsMultiPane() { return isLargeTablet(this) && !isSimplePreferences(this); @@ -116,7 +117,7 @@ public void onBuildHeaders(List
target) { @Override public boolean onPreferenceChange(Preference preference, Object value) { String stringValue = value.toString(); - + if (preference instanceof ListPreference) { // For list preferences, look up the correct display value in // the preference's 'entries' list. @@ -125,8 +126,8 @@ public boolean onPreferenceChange(Preference preference, Object value) { // Set the summary to reflect the new value. preference - .setSummary(index >= 0 ? listPreference.getEntries()[index] - : null); + .setSummary(index >= 0 ? listPreference.getEntries()[index] + : null); } else { // For all other preferences, set the summary to the value's // simple string representation. @@ -148,10 +149,9 @@ public boolean onPreferenceChange(Preference preference, Object value) { private static void bindPreferenceSummaryToValue(Preference preference) { // Set the listener to watch for value changes. preference - .setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); + .setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); - if (preference instanceof CheckBoxPreference) - { + if (preference instanceof CheckBoxPreference) { // Trigger the listener immediately with the preference's // current value. sBindPreferenceSummaryToValueListener.onPreferenceChange( @@ -159,9 +159,8 @@ private static void bindPreferenceSummaryToValue(Preference preference) { PreferenceManager.getDefaultSharedPreferences( preference.getContext()).getBoolean(preference.getKey(),false)); } - else if (preference instanceof EditTextPreference) - { - + else if (preference instanceof EditTextPreference) { + // Trigger the listener immediately with the preference's // current value. sBindPreferenceSummaryToValueListener.onPreferenceChange( @@ -169,8 +168,7 @@ else if (preference instanceof EditTextPreference) PreferenceManager.getDefaultSharedPreferences( preference.getContext()).getString(preference.getKey(),"")); } - else if (preference instanceof SortableListPreference) - { + else if (preference instanceof SortableListPreference) { // Trigger the listener immediately with the preference's // current value. sBindPreferenceSummaryToValueListener.onPreferenceChange( @@ -178,50 +176,49 @@ else if (preference instanceof SortableListPreference) PreferenceManager.getDefaultSharedPreferences( preference.getContext()).getString(preference.getKey(),"")); } - else - { + else { throw new IllegalArgumentException("Attempting to bind to unknown type of preference!"); } } - + /** * This fragment shows location update preferences only. It is used when the * activity is showing a two-pane settings UI. */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) public static class NameSelectionPreferenceFragment extends - PreferenceFragment { + PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.pref_name); - + // Bind the summaries of EditText/List/Dialog/Ringtone preferences // to their values. When their values change, their summaries are // updated to reflect the new value, per the Android Design // guidelines. - + bindPreferenceSummaryToValue(findPreference("name_order")); } } - + @TargetApi(Build.VERSION_CODES.KITKAT) protected boolean isValidFragment (String fragmentName) { - if(MainSettingsActivity.class.getName().equals(fragmentName) || - NameSelectionPreferenceFragment.class.getName().equals(fragmentName)) - return true; - return false; + if(MainSettingsActivity.class.getName().equals(fragmentName) || + NameSelectionPreferenceFragment.class.getName().equals(fragmentName)) + return true; + return false; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + default: + return super.onOptionsItemSelected(item); + } } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - NavUtils.navigateUpFromSameTask(this); - return true; - default: - return super.onOptionsItemSelected(item); - } - } } diff --git a/demo/src/com/mobeta/android/demodslv/SortableListPreference.java b/demo/src/com/mobeta/android/demodslv/SortableListPreference.java index b7472b3..03d0c18 100644 --- a/demo/src/com/mobeta/android/demodslv/SortableListPreference.java +++ b/demo/src/com/mobeta/android/demodslv/SortableListPreference.java @@ -64,33 +64,35 @@ public class SortableListPreference extends ListPreference { private static final String TAG=SortableListPreference.class.getName(); - DragSortListView mListView; - ArrayAdapter mAdapter; - - private static final String DEFAULT_SEPARATOR = "\u0001\u0007\u001D\u0007\u0001"; - String separator; - + protected DragSortListView mListView; + protected ArrayAdapter mAdapter; + + public static final String DEFAULT_SEPARATOR = "\u0001\u0007\u001D\u0007\u0001"; + private String mSeparator; + private HashMap entryChecked; public SortableListPreference(Context context, AttributeSet attrs) { super(context, attrs); - separator = DEFAULT_SEPARATOR; + mSeparator = DEFAULT_SEPARATOR; setDialogLayoutResource(R.layout.sort_list_array_dialog_preference); entryChecked = new HashMap(); } - + public static CharSequence[] decodeValue(String input) { return decodeValue(input,DEFAULT_SEPARATOR); } - + public static CharSequence[] decodeValue(String input,String separator) { if (input==null) + { return null; + } return input.split(separator); } - + @Override protected void onBindDialogView(View view) { @@ -99,21 +101,19 @@ protected void onBindDialogView(View view) mAdapter =new ArrayAdapter(mListView.getContext(),R.layout.list_item_simple_checkable,R.id.text); mListView.setAdapter(mAdapter); //This will drop the item in the new location - mListView.setDropListener(new DropListener() - { + mListView.setDropListener(new DropListener() { @Override public void drop(int from, int to) { CharSequence item = mAdapter.getItem(from); - mAdapter.remove(item); - mAdapter.insert(item, to); - mAdapter.notifyDataSetChanged(); - //Updates checked states - mListView.moveCheckState(from,to); + mAdapter.remove(item); + mAdapter.insert(item, to); + //Updates checked states + mListView.moveCheckState(from,to); } }); // Setting the default values happens in onPrepareDialogBuilder } - + @Override protected void onPrepareDialogBuilder(Builder builder) { CharSequence[] entries = getEntries(); @@ -126,13 +126,12 @@ protected void onPrepareDialogBuilder(Builder builder) { } CharSequence[] restoredValues=restoreEntries(); - for (CharSequence value:restoredValues) - { + for (CharSequence value:restoredValues) { mAdapter.add(entries[getValueIndex(value)]); } } - + @Override protected void onDialogClosed(boolean positiveResult) { List values = new ArrayList(); @@ -142,24 +141,23 @@ protected void onDialogClosed(boolean positiveResult) { for (int i = 0; i < entryValues.length; i++) { String val = (String) mAdapter.getItem(i); boolean isChecked=mListView.isItemChecked(i); - if (isChecked) - { + if (isChecked) { values.add(entryValues[getValueTitleIndex(val)]); } } - String value = join(values, separator); + String value = join(values, mSeparator); setSummary(prepareSummary(values)); setValueAndEvent(value); } } - + private void setValueAndEvent(String value) { - if (callChangeListener(decodeValue(value,separator))) { + if (callChangeListener(decodeValue(value,mSeparator))) { setValue(value); } } - + @Override protected Object onGetDefaultValue(TypedArray typedArray, int index) { return typedArray.getTextArray(index); @@ -176,17 +174,17 @@ protected void onSetInitialValue(boolean restoreValue, defaultValue = (CharSequence[]) rawDefaultValue; } List joined = Arrays.asList(defaultValue); - String joinedDefaultValue = join(joined, separator); + String joinedDefaultValue = join(joined, mSeparator); if (restoreValue) { value = getPersistedString(joinedDefaultValue); } else { value = joinedDefaultValue; } - setSummary(prepareSummary(Arrays.asList(decodeValue(value,separator)))); + setSummary(prepareSummary(Arrays.asList(decodeValue(value,mSeparator)))); setValueAndEvent(value); } - + private String prepareSummary(List joined) { List titles = new ArrayList(); CharSequence[] entryTitle = getEntries(); @@ -196,81 +194,59 @@ private String prepareSummary(List joined) { } return join(titles, ", "); } - + public int getValueIndex(CharSequence item) { CharSequence[] entryValues = getEntryValues(); - int ix=0; - boolean found=false; - for (CharSequence value:entryValues) - { - if (value.equals(item)) - { - found=true; - break; + for (int i = 0; i < entryValues.length; i++) { + if (entryValues[i].equals(item)) { + return i; } - ix+=1; } - if (!found) - throw new IllegalArgumentException(item+" not found in value list"); - return ix; + throw new IllegalStateException(item+" not found in value list"); } - + public int getValueTitleIndex(CharSequence item) { CharSequence[] entries = getEntries(); - int ix=0; - boolean found=false; - for (CharSequence value:entries) - { - if (value.equals(item)) - { - found=true; - break; + for (int i = 0; i < entries.length; i++) { + if (entries[i].equals(item)) { + return i; } - ix+=1; } - if (!found) - throw new IllegalArgumentException(item+" not found in value title list"); - return ix; + throw new IllegalStateException(item+" not found in value title list"); } - + private CharSequence[] restoreEntries() { - + ArrayList orderedList=new ArrayList(); - int ix=0; //Initially populated with all of the values in the determined list. - for (CharSequence value:decodeValue(getValue(),separator)) - { + CharSequence[] values = decodeValue(getValue(), mSeparator); + for (int ix=0;ix["a", "b", "c"] * @param separator - * separetes entries ex.: "," + * Separates entries ex.: "," * @return joined string ex.: "a,b,c" */ protected static String join(Iterable iterable, String separator) { From 21ea943719fc6e7481377eac3b5c4b80b846e0f2 Mon Sep 17 00:00:00 2001 From: kd7uiy Date: Mon, 30 Dec 2013 18:21:21 -0500 Subject: [PATCH 11/18] Removed debug code --- demo/src/com/mobeta/android/demodslv/DSLVFragment.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/demo/src/com/mobeta/android/demodslv/DSLVFragment.java b/demo/src/com/mobeta/android/demodslv/DSLVFragment.java index 7765d97..8263865 100644 --- a/demo/src/com/mobeta/android/demodslv/DSLVFragment.java +++ b/demo/src/com/mobeta/android/demodslv/DSLVFragment.java @@ -3,7 +3,6 @@ import android.app.Activity; import android.os.Bundle; import android.support.v4.app.ListFragment; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -149,7 +148,6 @@ public void onActivityCreated(Bundle savedInstanceState) { footers = args.getInt("footers", 0); } - Log.v("DSLV","Adding Footers"); for (int i = 0; i < headers; i++) { addHeader(getActivity(), mDslv); From 8ebe125826f5b1f13b110da180eb6fb61df05451 Mon Sep 17 00:00:00 2001 From: kd7uiy Date: Thu, 6 Mar 2014 17:32:18 -0500 Subject: [PATCH 12/18] DragSortListPreference now available Finished the process to convert DragSortListPreference to a part of the main library. It now doesn't depend on external resources, and can be controlled via styleables. --- demo/.settings/org.eclipse.jdt.core.prefs | 4 + demo/AndroidManifest.xml | 4 +- demo/project.properties | 2 +- .../sort_list_array_dialog_preference.xml | 3 +- demo/res/xml/pref_headers.xml | 2 +- demo/res/xml/pref_name.xml | 11 +- .../com/mobeta/android/demodslv/Launcher.java | 5 +- ...ngsActivity.java => SettingsActivity.java} | 16 +-- library/.settings/org.eclipse.jdt.core.prefs | 4 + library/build.xml | 92 +++++++++++++ .../sort_list_array_dialog_preference.xml | 17 +++ library/res/values/dslv_attrs.xml | 82 ++++++----- .../android/dslv/DragSortListPreference.java | 130 +++++++++++------- 13 files changed, 260 insertions(+), 112 deletions(-) create mode 100644 demo/.settings/org.eclipse.jdt.core.prefs rename demo/src/com/mobeta/android/demodslv/{MainSettingsActivity.java => SettingsActivity.java} (92%) create mode 100644 library/.settings/org.eclipse.jdt.core.prefs create mode 100644 library/build.xml create mode 100644 library/res/layout/sort_list_array_dialog_preference.xml rename demo/src/com/mobeta/android/demodslv/SortableListPreference.java => library/src/com/mobeta/android/dslv/DragSortListPreference.java (66%) diff --git a/demo/.settings/org.eclipse.jdt.core.prefs b/demo/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..b080d2d --- /dev/null +++ b/demo/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/demo/AndroidManifest.xml b/demo/AndroidManifest.xml index 1e77dee..6d2c171 100644 --- a/demo/AndroidManifest.xml +++ b/demo/AndroidManifest.xml @@ -8,6 +8,8 @@ android:targetSdkVersion="19" /> + + - + diff --git a/demo/project.properties b/demo/project.properties index 3770f74..a6cf15d 100644 --- a/demo/project.properties +++ b/demo/project.properties @@ -12,4 +12,4 @@ # Project target. target=android-19 -android.library.reference.1=../library/ +android.library.reference.1=../library diff --git a/demo/res/layout/sort_list_array_dialog_preference.xml b/demo/res/layout/sort_list_array_dialog_preference.xml index 2e50193..07e0060 100644 --- a/demo/res/layout/sort_list_array_dialog_preference.xml +++ b/demo/res/layout/sort_list_array_dialog_preference.xml @@ -5,7 +5,8 @@ android:focusableInTouchMode="false" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="3dp" android:choiceMode="multipleChoice" dslv:sort_enabled="true" - dslv:remove_enabled="false" dslv:drag_handle_id="@id/drag_handle" /> + dslv:remove_enabled="false" + dslv:drag_handle_id="@id/drag_handle" /> diff --git a/demo/res/xml/pref_headers.xml b/demo/res/xml/pref_headers.xml index fbe320d..a32bebd 100644 --- a/demo/res/xml/pref_headers.xml +++ b/demo/res/xml/pref_headers.xml @@ -4,6 +4,6 @@
diff --git a/demo/res/xml/pref_name.xml b/demo/res/xml/pref_name.xml index 3b5fbd7..de008f5 100644 --- a/demo/res/xml/pref_name.xml +++ b/demo/res/xml/pref_name.xml @@ -1,9 +1,14 @@ - - + + android:title="@string/pref_name_selection" + dslv:array_adapter_view="@layout/list_item_simple_checkable" + dslv:text_field="@id/text" + dslv:pref_layout="@layout/sort_list_array_dialog_preference" + /> diff --git a/demo/src/com/mobeta/android/demodslv/Launcher.java b/demo/src/com/mobeta/android/demodslv/Launcher.java index d3a7bfb..5624177 100644 --- a/demo/src/com/mobeta/android/demodslv/Launcher.java +++ b/demo/src/com/mobeta/android/demodslv/Launcher.java @@ -36,7 +36,7 @@ public void onCreate(Bundle savedInstanceState) { mActivities = new ArrayList(Arrays.asList(pi.activities)); String[] excludeList = new String[]{getClass().getName(), - MainSettingsActivity.class.getName()}; + SettingsActivity.class.getName()}; for (int i = 0; i < mActivities.size(); ++i) { for (String name: excludeList) { if (name.equals(mActivities.get(i).name)) { @@ -78,7 +78,6 @@ public View getView(int position, View convertView, ViewGroup parent) { desc.setText(mActDescs[position]); return v; } - } @Override @@ -95,7 +94,7 @@ public boolean onOptionsItemSelected(MenuItem item) { Intent intent; switch (item.getItemId()) { case R.id.menu_settings: - intent =new Intent(this,MainSettingsActivity.class); + intent =new Intent(this,SettingsActivity.class); startActivity(intent); return true; default: diff --git a/demo/src/com/mobeta/android/demodslv/MainSettingsActivity.java b/demo/src/com/mobeta/android/demodslv/SettingsActivity.java similarity index 92% rename from demo/src/com/mobeta/android/demodslv/MainSettingsActivity.java rename to demo/src/com/mobeta/android/demodslv/SettingsActivity.java index 60da1e9..fc06f47 100644 --- a/demo/src/com/mobeta/android/demodslv/MainSettingsActivity.java +++ b/demo/src/com/mobeta/android/demodslv/SettingsActivity.java @@ -17,7 +17,6 @@ import java.util.List; - /** * A {@link PreferenceActivity} that presents a set of application settings. On * handset devices, settings are presented as a single list. On tablets, @@ -29,7 +28,7 @@ * href="http://developer.android.com/guide/topics/ui/settings.html">Settings * API Guide for more information on developing a Settings UI. */ -public class MainSettingsActivity extends PreferenceActivity{ +public class SettingsActivity extends PreferenceActivity { /** * Determines whether to always show the simplified settings UI, where * settings are presented in a single list. When false, settings are shown @@ -71,7 +70,6 @@ private void setupSimplePreferencesScreen() { // Bind the summaries of EditText/List/Dialog/Ringtone preferences to // their values. When their values change, their summaries are updated // to reflect the new value, per the Android Design guidelines. - bindPreferenceSummaryToValue(findPreference("name_order")); } @@ -168,14 +166,6 @@ else if (preference instanceof EditTextPreference) { PreferenceManager.getDefaultSharedPreferences( preference.getContext()).getString(preference.getKey(),"")); } - else if (preference instanceof SortableListPreference) { - // Trigger the listener immediately with the preference's - // current value. - sBindPreferenceSummaryToValueListener.onPreferenceChange( - preference, - PreferenceManager.getDefaultSharedPreferences( - preference.getContext()).getString(preference.getKey(),"")); - } else { throw new IllegalArgumentException("Attempting to bind to unknown type of preference!"); } @@ -197,15 +187,13 @@ public void onCreate(Bundle savedInstanceState) { // to their values. When their values change, their summaries are // updated to reflect the new value, per the Android Design // guidelines. - - bindPreferenceSummaryToValue(findPreference("name_order")); } } @TargetApi(Build.VERSION_CODES.KITKAT) protected boolean isValidFragment (String fragmentName) { - if(MainSettingsActivity.class.getName().equals(fragmentName) || + if(SettingsActivity.class.getName().equals(fragmentName) || NameSelectionPreferenceFragment.class.getName().equals(fragmentName)) return true; return false; diff --git a/library/.settings/org.eclipse.jdt.core.prefs b/library/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..b080d2d --- /dev/null +++ b/library/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/library/build.xml b/library/build.xml new file mode 100644 index 0000000..2f6f323 --- /dev/null +++ b/library/build.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library/res/layout/sort_list_array_dialog_preference.xml b/library/res/layout/sort_list_array_dialog_preference.xml new file mode 100644 index 0000000..9fe762e --- /dev/null +++ b/library/res/layout/sort_list_array_dialog_preference.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/library/res/values/dslv_attrs.xml b/library/res/values/dslv_attrs.xml index c7db955..49895ab 100644 --- a/library/res/values/dslv_attrs.xml +++ b/library/res/values/dslv_attrs.xml @@ -1,39 +1,45 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/demo/src/com/mobeta/android/demodslv/SortableListPreference.java b/library/src/com/mobeta/android/dslv/DragSortListPreference.java similarity index 66% rename from demo/src/com/mobeta/android/demodslv/SortableListPreference.java rename to library/src/com/mobeta/android/dslv/DragSortListPreference.java index 03d0c18..9783221 100644 --- a/demo/src/com/mobeta/android/demodslv/SortableListPreference.java +++ b/library/src/com/mobeta/android/dslv/DragSortListPreference.java @@ -42,7 +42,7 @@ * */ -package com.mobeta.android.demodslv; +package com.mobeta.android.dslv; import java.util.ArrayList; import java.util.Arrays; @@ -50,7 +50,6 @@ import java.util.Iterator; import java.util.List; -import com.mobeta.android.dslv.DragSortListView; import com.mobeta.android.dslv.DragSortListView.DropListener; import android.app.AlertDialog.Builder; @@ -58,11 +57,12 @@ import android.content.res.TypedArray; import android.preference.ListPreference; import android.util.AttributeSet; +import android.util.Log; import android.view.View; import android.widget.ArrayAdapter; -public class SortableListPreference extends ListPreference { - private static final String TAG=SortableListPreference.class.getName(); +public class DragSortListPreference extends ListPreference { + private static final String TAG = DragSortListPreference.class.getName(); protected DragSortListView mListView; protected ArrayAdapter mAdapter; @@ -72,43 +72,72 @@ public class SortableListPreference extends ListPreference { private HashMap entryChecked; - public SortableListPreference(Context context, AttributeSet attrs) { + private int mListPreference; + + private int mAdapterView; + + private int mTextField; + + public DragSortListPreference(Context context, AttributeSet attrs) { super(context, attrs); mSeparator = DEFAULT_SEPARATOR; - setDialogLayoutResource(R.layout.sort_list_array_dialog_preference); - entryChecked = new HashMap(); + if (attrs != null) { + TypedArray a = getContext().obtainStyledAttributes(attrs, + R.styleable.DragSortListPreference, 0, 0); + + for (int i = 0; i < attrs.getAttributeCount(); i++) { + Log.v(TAG, + attrs.getAttributeName(i) + "=" + + attrs.getAttributeNameResource(i)); + } + + mListPreference = a.getResourceId( + R.styleable.DragSortListPreference_pref_layout, + R.layout.sort_list_array_dialog_preference); + mAdapterView = a.getResourceId( + R.styleable.DragSortListPreference_array_adapter_view, + android.R.layout.simple_list_item_1); + mTextField = a.getResourceId( + R.styleable.DragSortListPreference_text_field, + android.R.id.text1); + + Log.v(TAG, "mListPref=" + mListPreference); + Log.v(TAG, "mAdapterView=" + mAdapterView); + Log.v(TAG, "mTextField=" + mTextField); + + a.recycle(); + } + setDialogLayoutResource(mListPreference); + entryChecked = new HashMap(); } - public static CharSequence[] decodeValue(String input) - { - return decodeValue(input,DEFAULT_SEPARATOR); + public static CharSequence[] decodeValue(String input) { + return decodeValue(input, DEFAULT_SEPARATOR); } - public static CharSequence[] decodeValue(String input,String separator) - { - if (input==null) - { + public static CharSequence[] decodeValue(String input, String separator) { + if (input == null) { return null; } return input.split(separator); } @Override - protected void onBindDialogView(View view) - { + protected void onBindDialogView(View view) { super.onBindDialogView(view); - mListView= (DragSortListView) view.findViewById(android.R.id.list); - mAdapter =new ArrayAdapter(mListView.getContext(),R.layout.list_item_simple_checkable,R.id.text); + mListView = (DragSortListView) view.findViewById(android.R.id.list); + mAdapter = new ArrayAdapter(mListView.getContext(), + mAdapterView, mTextField); mListView.setAdapter(mAdapter); - //This will drop the item in the new location + // This will drop the item in the new location mListView.setDropListener(new DropListener() { @Override public void drop(int from, int to) { CharSequence item = mAdapter.getItem(from); mAdapter.remove(item); mAdapter.insert(item, to); - //Updates checked states - mListView.moveCheckState(from,to); + // Updates checked states + mListView.moveCheckState(from, to); } }); // Setting the default values happens in onPrepareDialogBuilder @@ -116,6 +145,8 @@ public void drop(int from, int to) { @Override protected void onPrepareDialogBuilder(Builder builder) { + Log.v(TAG, "onPrepareDialogBuilder"); + CharSequence[] entries = getEntries(); CharSequence[] entryValues = getEntryValues(); if (entries == null || entryValues == null @@ -125,8 +156,8 @@ protected void onPrepareDialogBuilder(Builder builder) { + "array which are both the same length"); } - CharSequence[] restoredValues=restoreEntries(); - for (CharSequence value:restoredValues) { + CharSequence[] restoredValues = restoreEntries(); + for (CharSequence value : restoredValues) { mAdapter.add(entries[getValueIndex(value)]); } @@ -140,7 +171,7 @@ protected void onDialogClosed(boolean positiveResult) { if (positiveResult && entryValues != null) { for (int i = 0; i < entryValues.length; i++) { String val = (String) mAdapter.getItem(i); - boolean isChecked=mListView.isItemChecked(i); + boolean isChecked = mListView.isItemChecked(i); if (isChecked) { values.add(entryValues[getValueTitleIndex(val)]); } @@ -153,7 +184,7 @@ protected void onDialogClosed(boolean positiveResult) { } private void setValueAndEvent(String value) { - if (callChangeListener(decodeValue(value,mSeparator))) { + if (callChangeListener(decodeValue(value, mSeparator))) { setValue(value); } } @@ -181,7 +212,7 @@ protected void onSetInitialValue(boolean restoreValue, value = joinedDefaultValue; } - setSummary(prepareSummary(Arrays.asList(decodeValue(value,mSeparator)))); + setSummary(prepareSummary(Arrays.asList(decodeValue(value, mSeparator)))); setValueAndEvent(value); } @@ -189,59 +220,58 @@ private String prepareSummary(List joined) { List titles = new ArrayList(); CharSequence[] entryTitle = getEntries(); for (CharSequence item : joined) { - int ix=getValueIndex(item); + int ix = getValueIndex(item); titles.add((String) entryTitle[ix]); } return join(titles, ", "); } - public int getValueIndex(CharSequence item) - { + public int getValueIndex(CharSequence item) { CharSequence[] entryValues = getEntryValues(); for (int i = 0; i < entryValues.length; i++) { if (entryValues[i].equals(item)) { return i; } } - throw new IllegalStateException(item+" not found in value list"); + throw new IllegalStateException(item + " not found in value list"); } - public int getValueTitleIndex(CharSequence item) - { + public int getValueTitleIndex(CharSequence item) { CharSequence[] entries = getEntries(); for (int i = 0; i < entries.length; i++) { if (entries[i].equals(item)) { return i; } } - throw new IllegalStateException(item+" not found in value title list"); + throw new IllegalStateException(item + " not found in value title list"); } private CharSequence[] restoreEntries() { - ArrayList orderedList=new ArrayList(); + ArrayList orderedList = new ArrayList(); - //Initially populated with all of the values in the determined list. + // Initially populated with all of the values in the determined list. CharSequence[] values = decodeValue(getValue(), mSeparator); - for (int ix=0;ix["a", "b", "c"] + * any kind of iterable ex.: ["a", "b", "c"] * @param separator - * Separates entries ex.: "," + * Separates entries ex.: "," * @return joined string ex.: "a,b,c" */ protected static String join(Iterable iterable, String separator) { From 445b5389846513dec0d0c33169c6ae1efbc674cf Mon Sep 17 00:00:00 2001 From: Edwin Woudt Date: Sat, 9 Aug 2014 22:16:04 +0200 Subject: [PATCH 13/18] Fix bug where an empty preference set would lead to an IllegalStateException --- .../src/com/mobeta/android/dslv/DragSortListPreference.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/src/com/mobeta/android/dslv/DragSortListPreference.java b/library/src/com/mobeta/android/dslv/DragSortListPreference.java index 9783221..9b0dcac 100644 --- a/library/src/com/mobeta/android/dslv/DragSortListPreference.java +++ b/library/src/com/mobeta/android/dslv/DragSortListPreference.java @@ -119,6 +119,9 @@ public static CharSequence[] decodeValue(String input, String separator) { if (input == null) { return null; } + if (input.equals("")) { + return new CharSequence[0]; + } return input.split(separator); } From 3e24147b3413ba0684053d654b9482d2e04fa915 Mon Sep 17 00:00:00 2001 From: Greg Loesch Date: Mon, 18 Aug 2014 13:46:20 -0400 Subject: [PATCH 14/18] Update to gradle 1.12 and update build tools [build] --- build.gradle | 2 +- demo/build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- library/build.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 2d3bec7..01f561d 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,6 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:0.7.+' + classpath 'com.android.tools.build:gradle:0.12.2' } } diff --git a/demo/build.gradle b/demo/build.gradle index 06154ae..22940c0 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -7,7 +7,7 @@ dependencies { android { compileSdkVersion 19 - buildToolsVersion "19.0.0" + buildToolsVersion "19.1.0" sourceSets { main { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 60dc7c2..c27ba22 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.9-all.zip +distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip diff --git a/library/build.gradle b/library/build.gradle index fe7deed..193014e 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -6,7 +6,7 @@ dependencies { android { compileSdkVersion 19 - buildToolsVersion "19.0.0" + buildToolsVersion "19.1.0" sourceSets { main { From 609b5dee5adae5eaabb23650c53525fe07715072 Mon Sep 17 00:00:00 2001 From: Greg Loesch Date: Mon, 18 Aug 2014 13:50:57 -0400 Subject: [PATCH 15/18] Null protect onScroll logic - Add check to onScroll in DragSortController in response to instances seen when these events were null. --- .../android/dslv/DragSortController.java | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/library/src/com/mobeta/android/dslv/DragSortController.java b/library/src/com/mobeta/android/dslv/DragSortController.java index a6fdd3c..d20d692 100644 --- a/library/src/com/mobeta/android/dslv/DragSortController.java +++ b/library/src/com/mobeta/android/dslv/DragSortController.java @@ -382,31 +382,31 @@ public boolean onDown(MotionEvent ev) { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { - final int x1 = (int) e1.getX(); - final int y1 = (int) e1.getY(); - final int x2 = (int) e2.getX(); - final int y2 = (int) e2.getY(); - final int deltaX = x2 - mItemX; - final int deltaY = y2 - mItemY; - - if (mCanDrag && !mDragging && (mHitPos != MISS || mFlingHitPos != MISS)) { - if (mHitPos != MISS) { - if (mDragInitMode == ON_DRAG && Math.abs(y2 - y1) > mTouchSlop && mSortEnabled) { - startDrag(mHitPos, deltaX, deltaY); - } - else if (mDragInitMode != ON_DOWN && Math.abs(x2 - x1) > mTouchSlop - && mRemoveEnabled) - { - mIsRemoving = true; - startDrag(mFlingHitPos, deltaX, deltaY); - } - } else if (mFlingHitPos != MISS) { - if (Math.abs(x2 - x1) > mTouchSlop && mRemoveEnabled) { - mIsRemoving = true; - startDrag(mFlingHitPos, deltaX, deltaY); - } else if (Math.abs(y2 - y1) > mTouchSlop) { - mCanDrag = false; // if started to scroll the list then - // don't allow sorting nor fling-removing + if (e1 != null && e2 != null) { + final int x1 = (int) e1.getX(); + final int y1 = (int) e1.getY(); + final int x2 = (int) e2.getX(); + final int y2 = (int) e2.getY(); + final int deltaX = x2 - mItemX; + final int deltaY = y2 - mItemY; + + if (mCanDrag && !mDragging && (mHitPos != MISS || mFlingHitPos != MISS)) { + if (mHitPos != MISS) { + if (mDragInitMode == ON_DRAG && Math.abs(y2 - y1) > mTouchSlop && mSortEnabled) { + startDrag(mHitPos, deltaX, deltaY); + } else if (mDragInitMode != ON_DOWN && Math.abs(x2 - x1) > mTouchSlop + && mRemoveEnabled) { + mIsRemoving = true; + startDrag(mFlingHitPos, deltaX, deltaY); + } + } else if (mFlingHitPos != MISS) { + if (Math.abs(x2 - x1) > mTouchSlop && mRemoveEnabled) { + mIsRemoving = true; + startDrag(mFlingHitPos, deltaX, deltaY); + } else if (Math.abs(y2 - y1) > mTouchSlop) { + mCanDrag = false; // if started to scroll the list then + // don't allow sorting nor fling-removing + } } } } From 36a85b9ad3e46cfbf6f78c22a70ee0a9f903f19d Mon Sep 17 00:00:00 2001 From: Greg Loesch Date: Mon, 18 Aug 2014 13:52:45 -0400 Subject: [PATCH 16/18] Remove unnecessary conditional check. --- library/src/com/mobeta/android/dslv/DragSortController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/com/mobeta/android/dslv/DragSortController.java b/library/src/com/mobeta/android/dslv/DragSortController.java index d20d692..c09865e 100644 --- a/library/src/com/mobeta/android/dslv/DragSortController.java +++ b/library/src/com/mobeta/android/dslv/DragSortController.java @@ -399,7 +399,7 @@ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float d mIsRemoving = true; startDrag(mFlingHitPos, deltaX, deltaY); } - } else if (mFlingHitPos != MISS) { + } else { if (Math.abs(x2 - x1) > mTouchSlop && mRemoveEnabled) { mIsRemoving = true; startDrag(mFlingHitPos, deltaX, deltaY); From 8cf4619417acc94b20910a34fd004eba8d22f25c Mon Sep 17 00:00:00 2001 From: Greg Loesch Date: Mon, 18 Aug 2014 13:55:05 -0400 Subject: [PATCH 17/18] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6e9bc3f..2897f03 100644 --- a/README.md +++ b/README.md @@ -359,7 +359,7 @@ directory of your project you can do something like this: ```sh mkdir libraries cd libraries -git submodule add https://github.com/JayH5/drag-sort-listview.git +git submodule add https://github.com/loeschg/drag-sort-listview.git echo "include ':libraries:drag-sort-listview:library'" >> ../settings.gradle ``` Then add the following dependency to your build.gradle project: From b20f957388cb29ad31f3e887d226e87b62947b25 Mon Sep 17 00:00:00 2001 From: Greg Loesch Date: Mon, 18 Aug 2014 14:48:00 -0400 Subject: [PATCH 18/18] Add name/version and missing dependency [build] --- library/build.gradle | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/library/build.gradle b/library/build.gradle index 193014e..2b1459a 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -1,12 +1,24 @@ -apply plugin: 'android-library' +apply plugin: 'com.android.library' dependencies { compile 'com.android.support:support-v4:19.0.+' } +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:0.12.+' + } +} + android { compileSdkVersion 19 buildToolsVersion "19.1.0" + project.archivesBaseName = "drag-sort-listview" + project.version = android.defaultConfig.versionName sourceSets { main {