Refactored the BoardEditor.

Proper moving of the items
captchafix
Florens Douwes 11 years ago
parent bb8bfe7984
commit b86d8be9b8
  1. 49
      Chan/assets/html/licences.html
  2. 6
      Chan/res/layout/board_edit.xml
  3. 26
      Chan/res/layout/board_edit_item.xml
  4. 28
      Chan/res/layout/board_view.xml
  5. 2
      Chan/res/values/strings.xml
  6. 469
      Chan/src/com/mobeta/android/dslv/DragSortController.java
  7. 239
      Chan/src/com/mobeta/android/dslv/DragSortCursorAdapter.java
  8. 92
      Chan/src/com/mobeta/android/dslv/DragSortItemView.java
  9. 46
      Chan/src/com/mobeta/android/dslv/DragSortItemViewCheckable.java
  10. 2974
      Chan/src/com/mobeta/android/dslv/DragSortListView.java
  11. 155
      Chan/src/com/mobeta/android/dslv/ResourceDragSortCursorAdapter.java
  12. 448
      Chan/src/com/mobeta/android/dslv/SimpleDragSortCursorAdapter.java
  13. 87
      Chan/src/com/mobeta/android/dslv/SimpleFloatViewManager.java
  14. 10
      Chan/src/org/floens/chan/core/manager/BoardManager.java
  15. 1
      Chan/src/org/floens/chan/database/DatabaseManager.java
  16. 16
      Chan/src/org/floens/chan/ui/SwipeDismissListViewTouchListener.java
  17. 2
      Chan/src/org/floens/chan/ui/activity/BaseActivity.java
  18. 168
      Chan/src/org/floens/chan/ui/activity/BoardEditor.java
  19. 84
      Chan/src/org/floens/chan/ui/adapter/BoardEditAdapter.java
  20. 117
      Chan/src/org/floens/chan/ui/view/DragGripView.java
  21. 596
      Chan/src/org/floens/chan/ui/view/DynamicListView.java

@ -58,7 +58,7 @@ limitations under the License.
</pre> </pre>
<br> <br>
<h3>jsoup License</h3> <h3>Jsoup</h3>
<pre> <pre>
<code> <code>
The jsoup code-base (include source and compiled packages) are distributed under the open source MIT license as described below. The jsoup code-base (include source and compiled packages) are distributed under the open source MIT license as described below.
@ -95,6 +95,51 @@ limitations under the License.
</code> </code>
</pre> </pre>
<br> <br>
<h3>This software includes several Android classes from DashClock</h3>
<a href="https://code.google.com/p/dashclock/">https://code.google.com/p/dashclock/</a>
<pre>
<code>
Copyright 2013 Google Inc.
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.
</code>
</pre>
<br>
<h3>DragSortListView</h3>
<a href="https://github.com/bauerca/drag-sort-listview">https://github.com/bauerca/drag-sort-listview</a>
<pre>
<code>
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.
</code>
</pre>
<br>
</body> </body>
</html> </html>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<org.floens.chan.ui.view.DynamicListView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/board_edit_list" >
</org.floens.chan.ui.view.DynamicListView>

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<org.floens.chan.ui.view.DragGripView
android:id="@+id/drag_handle"
android:layout_width="64dp"
android:layout_height="match_parent"
android:layout_gravity="fill_vertical|start"
android:paddingBottom="12dp"
android:paddingStart="16dp"
android:paddingTop="12dp"
android:color="#2333" />
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="36dp"
android:gravity="center_vertical"
android:minHeight="50sp"
android:textSize="24sp" />
</FrameLayout>

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<TextView
android:id="@+id/board_view_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:gravity="center_vertical"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:minHeight="60dp"
/>
<Button
android:id="@+id/board_view_delete"
android:layout_alignParentRight="true"
android:layout_marginTop="10dp"
android:layout_marginRight="10dp"
android:layout_height="40dp"
android:layout_width="40dp"
android:focusable="false"
android:focusableInTouchMode="false"
android:background="@drawable/ic_action_discard" />
</RelativeLayout>

@ -45,7 +45,7 @@
<string name="board_add_success">Added board</string> <string name="board_add_success">Added board</string>
<string name="board_add_duplicate">Board already added</string> <string name="board_add_duplicate">Board already added</string>
<string name="board_add_unknown_title">Unknown board code</string> <string name="board_add_unknown_title">Unknown board code</string>
<string name="board_add_unknown">The board with code CODE is not know. Press OK to add it anyway.</string> <string name="board_add_unknown">The board with code CODE is not known. Press OK to add it anyway.</string>
<string name="drawer_open">Open drawer</string> <string name="drawer_open">Open drawer</string>
<string name="drawer_close">Close drawer</string> <string name="drawer_close">Close drawer</string>

@ -0,0 +1,469 @@
package com.mobeta.android.dslv;
import android.graphics.Point;
import android.view.GestureDetector;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.AdapterView;
/**
* Class that starts and stops item drags on a {@link DragSortListView} based on
* touch gestures. This class also inherits from {@link SimpleFloatViewManager},
* which provides basic float View creation.
*
* An instance of this class is meant to be passed to the methods
* {@link DragSortListView#setTouchListener()} and
* {@link DragSortListView#setFloatViewManager()} of your
* {@link DragSortListView} instance.
*/
public class DragSortController extends SimpleFloatViewManager implements View.OnTouchListener,
GestureDetector.OnGestureListener {
/**
* Drag init mode enum.
*/
public static final int ON_DOWN = 0;
public static final int ON_DRAG = 1;
public static final int ON_LONG_PRESS = 2;
private int mDragInitMode = ON_DOWN;
private boolean mSortEnabled = true;
/**
* Remove mode enum.
*/
public static final int CLICK_REMOVE = 0;
public static final int FLING_REMOVE = 1;
/**
* The current remove mode.
*/
private int mRemoveMode;
private boolean mRemoveEnabled = false;
private boolean mIsRemoving = false;
private GestureDetector mDetector;
private GestureDetector mFlingRemoveDetector;
private int mTouchSlop;
public static final int MISS = -1;
private int mHitPos = MISS;
private int mFlingHitPos = MISS;
private int mClickRemoveHitPos = MISS;
private int[] mTempLoc = new int[2];
private int mItemX;
private int mItemY;
private int mCurrX;
private int mCurrY;
private boolean mDragging = false;
private float mFlingSpeed = 500f;
private int mDragHandleId;
private int mClickRemoveId;
private int mFlingHandleId;
private boolean mCanDrag;
private DragSortListView mDslv;
private int mPositionX;
/**
* Calls {@link #DragSortController(DragSortListView, int)} with a 0 drag
* handle id, FLING_RIGHT_REMOVE remove mode, and ON_DOWN drag init. By
* default, sorting is enabled, and removal is disabled.
*
* @param dslv
* The DSLV instance
*/
public DragSortController(DragSortListView dslv) {
this(dslv, 0, ON_DOWN, FLING_REMOVE);
}
public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode) {
this(dslv, dragHandleId, dragInitMode, removeMode, 0);
}
public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode,
int clickRemoveId) {
this(dslv, dragHandleId, dragInitMode, removeMode, clickRemoveId, 0);
}
/**
* By default, sorting is enabled, and removal is disabled.
*
* @param dslv
* The DSLV instance
* @param dragHandleId
* The resource id of the View that represents the drag handle in
* a list item.
*/
public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode,
int clickRemoveId, int flingHandleId) {
super(dslv);
mDslv = dslv;
mDetector = new GestureDetector(dslv.getContext(), this);
mFlingRemoveDetector = new GestureDetector(dslv.getContext(), mFlingRemoveListener);
mFlingRemoveDetector.setIsLongpressEnabled(false);
mTouchSlop = ViewConfiguration.get(dslv.getContext()).getScaledTouchSlop();
mDragHandleId = dragHandleId;
mClickRemoveId = clickRemoveId;
mFlingHandleId = flingHandleId;
setRemoveMode(removeMode);
setDragInitMode(dragInitMode);
}
public int getDragInitMode() {
return mDragInitMode;
}
/**
* Set how a drag is initiated. Needs to be one of {@link ON_DOWN},
* {@link ON_DRAG}, or {@link ON_LONG_PRESS}.
*
* @param mode
* The drag init mode.
*/
public void setDragInitMode(int mode) {
mDragInitMode = mode;
}
/**
* Enable/Disable list item sorting. Disabling is useful if only item
* removal is desired. Prevents drags in the vertical direction.
*
* @param enabled
* Set <code>true</code> to enable list item sorting.
*/
public void setSortEnabled(boolean enabled) {
mSortEnabled = enabled;
}
public boolean isSortEnabled() {
return mSortEnabled;
}
/**
* One of {@link CLICK_REMOVE}, {@link FLING_RIGHT_REMOVE},
* {@link FLING_LEFT_REMOVE}, {@link SLIDE_RIGHT_REMOVE}, or
* {@link SLIDE_LEFT_REMOVE}.
*/
public void setRemoveMode(int mode) {
mRemoveMode = mode;
}
public int getRemoveMode() {
return mRemoveMode;
}
/**
* Enable/Disable item removal without affecting remove mode.
*/
public void setRemoveEnabled(boolean enabled) {
mRemoveEnabled = enabled;
}
public boolean isRemoveEnabled() {
return mRemoveEnabled;
}
/**
* Set the resource id for the View that represents the drag handle in a
* list item.
*
* @param id
* An android resource id.
*/
public void setDragHandleId(int id) {
mDragHandleId = id;
}
/**
* Set the resource id for the View that represents the fling handle in a
* list item.
*
* @param id
* An android resource id.
*/
public void setFlingHandleId(int id) {
mFlingHandleId = id;
}
/**
* Set the resource id for the View that represents click removal button.
*
* @param id
* An android resource id.
*/
public void setClickRemoveId(int id) {
mClickRemoveId = id;
}
/**
* Sets flags to restrict certain motions of the floating View based on
* DragSortController settings (such as remove mode). Starts the drag on the
* DragSortListView.
*
* @param position
* The list item position (includes headers).
* @param deltaX
* Touch x-coord minus left edge of floating View.
* @param deltaY
* Touch y-coord minus top edge of floating View.
*
* @return True if drag started, false otherwise.
*/
public boolean startDrag(int position, int deltaX, int deltaY) {
int dragFlags = 0;
if (mSortEnabled && !mIsRemoving) {
dragFlags |= DragSortListView.DRAG_POS_Y | DragSortListView.DRAG_NEG_Y;
}
if (mRemoveEnabled && mIsRemoving) {
dragFlags |= DragSortListView.DRAG_POS_X;
dragFlags |= DragSortListView.DRAG_NEG_X;
}
mDragging = mDslv.startDrag(position - mDslv.getHeaderViewsCount(), dragFlags, deltaX, deltaY);
return mDragging;
}
@Override
public boolean onTouch(View v, MotionEvent ev) {
if (!mDslv.isDragEnabled() || mDslv.listViewIntercepted()) {
return false;
}
mDetector.onTouchEvent(ev);
if (mRemoveEnabled && mDragging && mRemoveMode == FLING_REMOVE) {
mFlingRemoveDetector.onTouchEvent(ev);
}
int action = ev.getAction() & MotionEvent.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_DOWN:
mCurrX = (int) ev.getX();
mCurrY = (int) ev.getY();
break;
case MotionEvent.ACTION_UP:
if (mRemoveEnabled && mIsRemoving) {
int x = mPositionX >= 0 ? mPositionX : -mPositionX;
int removePoint = mDslv.getWidth() / 2;
if (x > removePoint) {
mDslv.stopDragWithVelocity(true, 0);
}
}
case MotionEvent.ACTION_CANCEL:
mIsRemoving = false;
mDragging = false;
break;
}
return false;
}
/**
* Overrides to provide fading when slide removal is enabled.
*/
@Override
public void onDragFloatView(View floatView, Point position, Point touch) {
if (mRemoveEnabled && mIsRemoving) {
mPositionX = position.x;
}
}
/**
* Get the position to start dragging based on the ACTION_DOWN MotionEvent.
* This function simply calls {@link #dragHandleHitPosition(MotionEvent)}.
* Override to change drag handle behavior; this function is called
* internally when an ACTION_DOWN event is detected.
*
* @param ev
* The ACTION_DOWN MotionEvent.
*
* @return The list position to drag if a drag-init gesture is detected;
* MISS if unsuccessful.
*/
public int startDragPosition(MotionEvent ev) {
return dragHandleHitPosition(ev);
}
public int startFlingPosition(MotionEvent ev) {
return mRemoveMode == FLING_REMOVE ? flingHandleHitPosition(ev) : MISS;
}
/**
* Checks for the touch of an item's drag handle (specified by
* {@link #setDragHandleId(int)}), and returns that item's position if a
* drag handle touch was detected.
*
* @param ev
* The ACTION_DOWN MotionEvent.
*
* @return The list position of the item whose drag handle was touched; MISS
* if unsuccessful.
*/
public int dragHandleHitPosition(MotionEvent ev) {
return viewIdHitPosition(ev, mDragHandleId);
}
public int flingHandleHitPosition(MotionEvent ev) {
return viewIdHitPosition(ev, mFlingHandleId);
}
public int viewIdHitPosition(MotionEvent ev, int id) {
final int x = (int) ev.getX();
final int y = (int) ev.getY();
int touchPos = mDslv.pointToPosition(x, y); // includes headers/footers
final int numHeaders = mDslv.getHeaderViewsCount();
final int numFooters = mDslv.getFooterViewsCount();
final int count = mDslv.getCount();
// Log.d("mobeta", "touch down on position " + itemnum);
// We're only interested if the touch was on an
// item that's not a header or footer.
if (touchPos != AdapterView.INVALID_POSITION && touchPos >= numHeaders && touchPos < (count - numFooters)) {
final View item = mDslv.getChildAt(touchPos - mDslv.getFirstVisiblePosition());
final int rawX = (int) ev.getRawX();
final int rawY = (int) ev.getRawY();
View dragBox = id == 0 ? item : (View) item.findViewById(id);
if (dragBox != null) {
dragBox.getLocationOnScreen(mTempLoc);
if (rawX > mTempLoc[0] && rawY > mTempLoc[1] && rawX < mTempLoc[0] + dragBox.getWidth()
&& rawY < mTempLoc[1] + dragBox.getHeight()) {
mItemX = item.getLeft();
mItemY = item.getTop();
return touchPos;
}
}
}
return MISS;
}
@Override
public boolean onDown(MotionEvent ev) {
if (mRemoveEnabled && mRemoveMode == CLICK_REMOVE) {
mClickRemoveHitPos = viewIdHitPosition(ev, mClickRemoveId);
}
mHitPos = startDragPosition(ev);
if (mHitPos != MISS && mDragInitMode == ON_DOWN) {
startDrag(mHitPos, (int) ev.getX() - mItemX, (int) ev.getY() - mItemY);
}
mIsRemoving = false;
mCanDrag = true;
mPositionX = 0;
mFlingHitPos = startFlingPosition(ev);
return true;
}
@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
}
}
}
// return whatever
return false;
}
@Override
public void onLongPress(MotionEvent e) {
// Log.d("mobeta", "lift listener long pressed");
if (mHitPos != MISS && mDragInitMode == ON_LONG_PRESS) {
mDslv.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
startDrag(mHitPos, mCurrX - mItemX, mCurrY - mItemY);
}
}
// complete the OnGestureListener interface
@Override
public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
// complete the OnGestureListener interface
@Override
public boolean onSingleTapUp(MotionEvent ev) {
if (mRemoveEnabled && mRemoveMode == CLICK_REMOVE) {
if (mClickRemoveHitPos != MISS) {
mDslv.removeItem(mClickRemoveHitPos - mDslv.getHeaderViewsCount());
}
}
return true;
}
// complete the OnGestureListener interface
@Override
public void onShowPress(MotionEvent ev) {
// do nothing
}
private GestureDetector.OnGestureListener mFlingRemoveListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// Log.d("mobeta", "on fling remove called");
if (mRemoveEnabled && mIsRemoving) {
int w = mDslv.getWidth();
int minPos = w / 5;
if (velocityX > mFlingSpeed) {
if (mPositionX > -minPos) {
mDslv.stopDragWithVelocity(true, velocityX);
}
} else if (velocityX < -mFlingSpeed) {
if (mPositionX < minPos) {
mDslv.stopDragWithVelocity(true, velocityX);
}
}
mIsRemoving = false;
}
return false;
}
};
}

@ -0,0 +1,239 @@
package com.mobeta.android.dslv;
import java.util.ArrayList;
import android.content.Context;
import android.database.Cursor;
import android.support.v4.widget.CursorAdapter;
import android.util.SparseIntArray;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListAdapter;
/**
* A subclass of {@link android.widget.CursorAdapter} that provides reordering
* of the elements in the Cursor based on completed drag-sort operations. The
* reordering is a simple mapping of list positions into Cursor positions (the
* Cursor is unchanged). To persist changes made by drag-sorts, one can retrieve
* the mapping with the {@link #getCursorPositions()} method, which returns the
* reordered list of Cursor positions.
*
* An instance of this class is passed to
* {@link DragSortListView#setAdapter(ListAdapter)} and, since this class
* implements the {@link DragSortListView.DragSortListener} interface, it is
* automatically set as the DragSortListener for the DragSortListView instance.
*/
public abstract class DragSortCursorAdapter extends CursorAdapter implements DragSortListView.DragSortListener {
public static final int REMOVED = -1;
/**
* Key is ListView position, value is Cursor position
*/
private final SparseIntArray mListMapping = new SparseIntArray();
private final ArrayList<Integer> mRemovedCursorPositions = new ArrayList<Integer>();
@SuppressWarnings("deprecation")
public DragSortCursorAdapter(Context context, Cursor c) {
super(context, c);
}
public DragSortCursorAdapter(Context context, Cursor c, boolean autoRequery) {
super(context, c, autoRequery);
}
public DragSortCursorAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
}
/**
* Swaps Cursor and clears list-Cursor mapping.
*
* @see android.widget.CursorAdapter#swapCursor(android.database.Cursor)
*/
@Override
public Cursor swapCursor(Cursor newCursor) {
Cursor old = super.swapCursor(newCursor);
resetMappings();
return old;
}
/**
* Changes Cursor and clears list-Cursor mapping.
*
* @see android.widget.CursorAdapter#changeCursor(android.database.Cursor)
*/
@Override
public void changeCursor(Cursor cursor) {
super.changeCursor(cursor);
resetMappings();
}
/**
* Resets list-cursor mapping.
*/
public void reset() {
resetMappings();
notifyDataSetChanged();
}
private void resetMappings() {
mListMapping.clear();
mRemovedCursorPositions.clear();
}
@Override
public Object getItem(int position) {
return super.getItem(mListMapping.get(position, position));
}
@Override
public long getItemId(int position) {
return super.getItemId(mListMapping.get(position, position));
}
@Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
return super.getDropDownView(mListMapping.get(position, position), convertView, parent);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
return super.getView(mListMapping.get(position, position), convertView, parent);
}
/**
* On drop, this updates the mapping between Cursor positions and ListView
* positions. The Cursor is unchanged. Retrieve the current mapping with
* {@link getCursorPositions()}.
*
* @see DragSortListView.DropListener#drop(int, int)
*/
@Override
public void drop(int from, int to) {
if (from != to) {
int cursorFrom = mListMapping.get(from, from);
if (from > to) {
for (int i = from; i > to; --i) {
mListMapping.put(i, mListMapping.get(i - 1, i - 1));
}
} else {
for (int i = from; i < to; ++i) {
mListMapping.put(i, mListMapping.get(i + 1, i + 1));
}
}
mListMapping.put(to, cursorFrom);
cleanMapping();
notifyDataSetChanged();
}
}
/**
* On remove, this updates the mapping between Cursor positions and ListView
* positions. The Cursor is unchanged. Retrieve the current mapping with
* {@link getCursorPositions()}.
*
* @see DragSortListView.RemoveListener#remove(int)
*/
@Override
public void remove(int which) {
int cursorPos = mListMapping.get(which, which);
if (!mRemovedCursorPositions.contains(cursorPos)) {
mRemovedCursorPositions.add(cursorPos);
}
int newCount = getCount();
for (int i = which; i < newCount; ++i) {
mListMapping.put(i, mListMapping.get(i + 1, i + 1));
}
mListMapping.delete(newCount);
cleanMapping();
notifyDataSetChanged();
}
/**
* Does nothing. Just completes DragSortListener interface.
*/
@Override
public void drag(int from, int to) {
// do nothing
}
/**
* Remove unnecessary mappings from sparse array.
*/
private void cleanMapping() {
ArrayList<Integer> toRemove = new ArrayList<Integer>();
int size = mListMapping.size();
for (int i = 0; i < size; ++i) {
if (mListMapping.keyAt(i) == mListMapping.valueAt(i)) {
toRemove.add(mListMapping.keyAt(i));
}
}
size = toRemove.size();
for (int i = 0; i < size; ++i) {
mListMapping.delete(toRemove.get(i));
}
}
@Override
public int getCount() {
return super.getCount() - mRemovedCursorPositions.size();
}
/**
* Get the Cursor position mapped to by the provided list position (given
* all previously handled drag-sort operations).
*
* @param position
* List position
*
* @return The mapped-to Cursor position
*/
public int getCursorPosition(int position) {
return mListMapping.get(position, position);
}
/**
* Get the current order of Cursor positions presented by the list.
*/
public ArrayList<Integer> getCursorPositions() {
ArrayList<Integer> result = new ArrayList<Integer>();
for (int i = 0; i < getCount(); ++i) {
result.add(mListMapping.get(i, i));
}
return result;
}
/**
* Get the list position mapped to by the provided Cursor position. If the
* provided Cursor position has been removed by a drag-sort, this returns
* {@link #REMOVED}.
*
* @param cursorPosition
* A Cursor position
* @return The mapped-to list position or REMOVED
*/
public int getListPosition(int cursorPosition) {
if (mRemovedCursorPositions.contains(cursorPosition)) {
return REMOVED;
}
int index = mListMapping.indexOfValue(cursorPosition);
if (index < 0) {
return cursorPosition;
} else {
return mListMapping.keyAt(index);
}
}
}

@ -0,0 +1,92 @@
package com.mobeta.android.dslv;
import android.content.Context;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
/**
* Lightweight ViewGroup that wraps list items obtained from user's ListAdapter.
* ItemView expects a single child that has a definite height (i.e. the child's
* layout height is not MATCH_PARENT). The width of ItemView will always match
* the width of its child (that is, the width MeasureSpec given to ItemView is
* passed directly to the child, and the ItemView measured width is set to the
* child's measured width). The height of ItemView can be anything; the
*
*
* The purpose of this class is to optimize slide shuffle animations.
*/
public class DragSortItemView extends ViewGroup {
private int mGravity = Gravity.TOP;
public DragSortItemView(Context context) {
super(context);
// always init with standard ListView layout params
setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
//setClipChildren(true);
}
public void setGravity(int gravity) {
mGravity = gravity;
}
public int getGravity() {
return mGravity;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final View child = getChildAt(0);
if (child == null) {
return;
}
if (mGravity == Gravity.TOP) {
child.layout(0, 0, getMeasuredWidth(), child.getMeasuredHeight());
} else {
child.layout(0, getMeasuredHeight() - child.getMeasuredHeight(), getMeasuredWidth(), getMeasuredHeight());
}
}
/**
*
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int height = MeasureSpec.getSize(heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final View child = getChildAt(0);
if (child == null) {
setMeasuredDimension(0, width);
return;
}
if (child.isLayoutRequested()) {
// Always let child be as tall as it wants.
measureChild(child, widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
}
if (heightMode == MeasureSpec.UNSPECIFIED) {
ViewGroup.LayoutParams lp = getLayoutParams();
if (lp.height > 0) {
height = lp.height;
} else {
height = child.getMeasuredHeight();
}
}
setMeasuredDimension(width, height);
}
}

@ -0,0 +1,46 @@
package com.mobeta.android.dslv;
import android.content.Context;
import android.view.View;
import android.widget.Checkable;
/**
* Lightweight ViewGroup that wraps list items obtained from user's ListAdapter.
* ItemView expects a single child that has a definite height (i.e. the child's
* layout height is not MATCH_PARENT). The width of ItemView will always match
* the width of its child (that is, the width MeasureSpec given to ItemView is
* passed directly to the child, and the ItemView measured width is set to the
* child's measured width). The height of ItemView can be anything; the
*
*
* The purpose of this class is to optimize slide shuffle animations.
*/
public class DragSortItemViewCheckable extends DragSortItemView implements Checkable {
public DragSortItemViewCheckable(Context context) {
super(context);
}
@Override
public boolean isChecked() {
View child = getChildAt(0);
if (child instanceof Checkable)
return ((Checkable) child).isChecked();
else
return false;
}
@Override
public void setChecked(boolean checked) {
View child = getChildAt(0);
if (child instanceof Checkable)
((Checkable) child).setChecked(checked);
}
@Override
public void toggle() {
View child = getChildAt(0);
if (child instanceof Checkable)
((Checkable) child).toggle();
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,155 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mobeta.android.dslv;
import android.content.Context;
import android.database.Cursor;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
// taken from v4 rev. 10 ResourceCursorAdapter.java
/**
* Static library support version of the framework's
* {@link android.widget.ResourceCursorAdapter}. Used to write apps that run on
* platforms prior to Android 3.0. When running on Android 3.0 or above, this
* implementation is still used; it does not try to switch to the framework's
* implementation. See the framework SDK documentation for a class overview.
*/
public abstract class ResourceDragSortCursorAdapter extends DragSortCursorAdapter {
private int mLayout;
private int mDropDownLayout;
private LayoutInflater mInflater;
/**
* Constructor the enables auto-requery.
*
* @deprecated This option is discouraged, as it results in Cursor queries
* being performed on the application's UI thread and thus can
* cause poor responsiveness or even Application Not Responding
* errors. As an alternative, use
* {@link android.app.LoaderManager} with a
* {@link android.content.CursorLoader}.
*
* @param context
* The context where the ListView associated with this adapter is
* running
* @param layout
* resource identifier of a layout file that defines the views
* for this list item. Unless you override them later, this will
* define both the item views and the drop down views.
*/
@Deprecated
public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c) {
super(context, c);
mLayout = mDropDownLayout = layout;
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
/**
* Constructor with default behavior as per
* {@link CursorAdapter#CursorAdapter(Context, Cursor, boolean)}; it is
* recommended you not use this, but instead
* {@link #ResourceCursorAdapter(Context, int, Cursor, int)}. When using
* this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER} will always be
* set.
*
* @param context
* The context where the ListView associated with this adapter is
* running
* @param layout
* resource identifier of a layout file that defines the views
* for this list item. Unless you override them later, this will
* define both the item views and the drop down views.
* @param c
* The cursor from which to get the data.
* @param autoRequery
* If true the adapter will call requery() on the cursor whenever
* it changes so the most recent data is always displayed. Using
* true here is discouraged.
*/
public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c, boolean autoRequery) {
super(context, c, autoRequery);
mLayout = mDropDownLayout = layout;
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
/**
* Standard constructor.
*
* @param context
* The context where the ListView associated with this adapter is
* running
* @param layout
* Resource identifier of a layout file that defines the views
* for this list item. Unless you override them later, this will
* define both the item views and the drop down views.
* @param c
* The cursor from which to get the data.
* @param flags
* Flags used to determine the behavior of the adapter, as per
* {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}.
*/
public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c, int flags) {
super(context, c, flags);
mLayout = mDropDownLayout = layout;
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
/**
* Inflates view(s) from the specified XML file.
*
* @see android.widget.CursorAdapter#newView(android.content.Context,
* android.database.Cursor, ViewGroup)
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return mInflater.inflate(mLayout, parent, false);
}
@Override
public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) {
return mInflater.inflate(mDropDownLayout, parent, false);
}
/**
* <p>
* Sets the layout resource of the item views.
* </p>
*
* @param layout
* the layout resources used to create item views
*/
public void setViewResource(int layout) {
mLayout = layout;
}
/**
* <p>
* Sets the layout resource of the drop down views.
* </p>
*
* @param dropDownLayout
* the layout resources used to create drop down views
*/
public void setDropDownViewResource(int dropDownLayout) {
mDropDownLayout = dropDownLayout;
}
}

@ -0,0 +1,448 @@
/*
* Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mobeta.android.dslv;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
// taken from sdk/sources/android-16/android/widget/SimpleCursorAdapter.java
/**
* An easy adapter to map columns from a cursor to TextViews or ImageViews
* defined in an XML file. You can specify which columns you want, which views
* you want to display the columns, and the XML file that defines the appearance
* of these views.
*
* Binding occurs in two phases. First, if a
* {@link android.widget.SimpleCursorAdapter.ViewBinder} is available,
* {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
* is invoked. If the returned value is true, binding has occured. If the
* returned value is false and the view to bind is a TextView,
* {@link #setViewText(TextView, String)} is invoked. If the returned value is
* false and the view to bind is an ImageView,
* {@link #setViewImage(ImageView, String)} is invoked. If no appropriate
* binding can be found, an {@link IllegalStateException} is thrown.
*
* If this adapter is used with filtering, for instance in an
* {@link android.widget.AutoCompleteTextView}, you can use the
* {@link android.widget.SimpleCursorAdapter.CursorToStringConverter} and the
* {@link android.widget.FilterQueryProvider} interfaces to get control over the
* filtering process. You can refer to
* {@link #convertToString(android.database.Cursor)} and
* {@link #runQueryOnBackgroundThread(CharSequence)} for more information.
*/
public class SimpleDragSortCursorAdapter extends ResourceDragSortCursorAdapter {
/**
* A list of columns containing the data to bind to the UI. This field
* should be made private, so it is hidden from the SDK. {@hide}
*/
protected int[] mFrom;
/**
* A list of View ids representing the views to which the data must be
* bound. This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected int[] mTo;
private int mStringConversionColumn = -1;
private CursorToStringConverter mCursorToStringConverter;
private ViewBinder mViewBinder;
String[] mOriginalFrom;
/**
* Constructor the enables auto-requery.
*
* @deprecated This option is discouraged, as it results in Cursor queries
* being performed on the application's UI thread and thus can
* cause poor responsiveness or even Application Not Responding
* errors. As an alternative, use
* {@link android.app.LoaderManager} with a
* {@link android.content.CursorLoader}.
*/
@Deprecated
public SimpleDragSortCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to) {
super(context, layout, c);
mTo = to;
mOriginalFrom = from;
findColumns(c, from);
}
/**
* Standard constructor.
*
* @param context
* The context where the ListView associated with this
* SimpleListItemFactory is running
* @param layout
* resource identifier of a layout file that defines the views
* for this list item. The layout file should include at least
* those named views defined in "to"
* @param c
* The database cursor. Can be null if the cursor is not
* available yet.
* @param from
* A list of column names representing the data to bind to the
* UI. Can be null if the cursor is not available yet.
* @param to
* The views that should display column in the "from" parameter.
* These should all be TextViews. The first N views in this list
* are given the values of the first N columns in the from
* parameter. Can be null if the cursor is not available yet.
* @param flags
* Flags used to determine the behavior of the adapter, as per
* {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}.
*/
public SimpleDragSortCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to, int flags) {
super(context, layout, c, flags);
mTo = to;
mOriginalFrom = from;
findColumns(c, from);
}
/**
* Binds all of the field names passed into the "to" parameter of the
* constructor with their corresponding cursor columns as specified in the
* "from" parameter.
*
* Binding occurs in two phases. First, if a
* {@link android.widget.SimpleCursorAdapter.ViewBinder} is available,
* {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
* is invoked. If the returned value is true, binding has occured. If the
* returned value is false and the view to bind is a TextView,
* {@link #setViewText(TextView, String)} is invoked. If the returned value
* is false and the view to bind is an ImageView,
* {@link #setViewImage(ImageView, String)} is invoked. If no appropriate
* binding can be found, an {@link IllegalStateException} is thrown.
*
* @throws IllegalStateException
* if binding cannot occur
*
* @see android.widget.CursorAdapter#bindView(android.view.View,
* android.content.Context, android.database.Cursor)
* @see #getViewBinder()
* @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder)
* @see #setViewImage(ImageView, String)
* @see #setViewText(TextView, String)
*/
@Override
public void bindView(View view, Context context, Cursor cursor) {
final ViewBinder binder = mViewBinder;
final int count = mTo.length;
final int[] from = mFrom;
final int[] to = mTo;
for (int i = 0; i < count; i++) {
final View v = view.findViewById(to[i]);
if (v != null) {
boolean bound = false;
if (binder != null) {
bound = binder.setViewValue(v, cursor, from[i]);
}
if (!bound) {
String text = cursor.getString(from[i]);
if (text == null) {
text = "";
}
if (v instanceof TextView) {
setViewText((TextView) v, text);
} else if (v instanceof ImageView) {
setViewImage((ImageView) v, text);
} else {
throw new IllegalStateException(v.getClass().getName() + " is not a "
+ " view that can be bounds by this SimpleCursorAdapter");
}
}
}
}
}
/**
* Returns the {@link ViewBinder} used to bind data to views.
*
* @return a ViewBinder or null if the binder does not exist
*
* @see #bindView(android.view.View, android.content.Context,
* android.database.Cursor)
* @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder)
*/
public ViewBinder getViewBinder() {
return mViewBinder;
}
/**
* Sets the binder used to bind data to views.
*
* @param viewBinder
* the binder used to bind data to views, can be null to remove
* the existing binder
*
* @see #bindView(android.view.View, android.content.Context,
* android.database.Cursor)
* @see #getViewBinder()
*/
public void setViewBinder(ViewBinder viewBinder) {
mViewBinder = viewBinder;
}
/**
* Called by bindView() to set the image for an ImageView but only if there
* is no existing ViewBinder or if the existing ViewBinder cannot handle
* binding to an ImageView.
*
* By default, the value will be treated as an image resource. If the value
* cannot be used as an image resource, the value is used as an image Uri.
*
* Intended to be overridden by Adapters that need to filter strings
* retrieved from the database.
*
* @param v
* ImageView to receive an image
* @param value
* the value retrieved from the cursor
*/
public void setViewImage(ImageView v, String value) {
try {
v.setImageResource(Integer.parseInt(value));
} catch (NumberFormatException nfe) {
v.setImageURI(Uri.parse(value));
}
}
/**
* Called by bindView() to set the text for a TextView but only if there is
* no existing ViewBinder or if the existing ViewBinder cannot handle
* binding to a TextView.
*
* Intended to be overridden by Adapters that need to filter strings
* retrieved from the database.
*
* @param v
* TextView to receive text
* @param text
* the text to be set for the TextView
*/
public void setViewText(TextView v, String text) {
v.setText(text);
}
/**
* Return the index of the column used to get a String representation of the
* Cursor.
*
* @return a valid index in the current Cursor or -1
*
* @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
* @see #setStringConversionColumn(int)
* @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
* @see #getCursorToStringConverter()
*/
public int getStringConversionColumn() {
return mStringConversionColumn;
}
/**
* Defines the index of the column in the Cursor used to get a String
* representation of that Cursor. The column is used to convert the Cursor
* to a String only when the current CursorToStringConverter is null.
*
* @param stringConversionColumn
* a valid index in the current Cursor or -1 to use the default
* conversion mechanism
*
* @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
* @see #getStringConversionColumn()
* @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
* @see #getCursorToStringConverter()
*/
public void setStringConversionColumn(int stringConversionColumn) {
mStringConversionColumn = stringConversionColumn;
}
/**
* Returns the converter used to convert the filtering Cursor into a String.
*
* @return null if the converter does not exist or an instance of
* {@link android.widget.SimpleCursorAdapter.CursorToStringConverter}
*
* @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
* @see #getStringConversionColumn()
* @see #setStringConversionColumn(int)
* @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
*/
public CursorToStringConverter getCursorToStringConverter() {
return mCursorToStringConverter;
}
/**
* Sets the converter used to convert the filtering Cursor into a String.
*
* @param cursorToStringConverter
* the Cursor to String converter, or null to remove the
* converter
*
* @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
* @see #getStringConversionColumn()
* @see #setStringConversionColumn(int)
* @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
*/
public void setCursorToStringConverter(CursorToStringConverter cursorToStringConverter) {
mCursorToStringConverter = cursorToStringConverter;
}
/**
* Returns a CharSequence representation of the specified Cursor as defined
* by the current CursorToStringConverter. If no CursorToStringConverter has
* been set, the String conversion column is used instead. If the conversion
* column is -1, the returned String is empty if the cursor is null or
* Cursor.toString().
*
* @param cursor
* the Cursor to convert to a CharSequence
*
* @return a non-null CharSequence representing the cursor
*/
@Override
public CharSequence convertToString(Cursor cursor) {
if (mCursorToStringConverter != null) {
return mCursorToStringConverter.convertToString(cursor);
} else if (mStringConversionColumn > -1) {
return cursor.getString(mStringConversionColumn);
}
return super.convertToString(cursor);
}
/**
* Create a map from an array of strings to an array of column-id integers
* in cursor c. If c is null, the array will be discarded.
*
* @param c
* the cursor to find the columns from
* @param from
* the Strings naming the columns of interest
*/
private void findColumns(Cursor c, String[] from) {
if (c != null) {
int i;
int count = from.length;
if (mFrom == null || mFrom.length != count) {
mFrom = new int[count];
}
for (i = 0; i < count; i++) {
mFrom[i] = c.getColumnIndexOrThrow(from[i]);
}
} else {
mFrom = null;
}
}
@Override
public Cursor swapCursor(Cursor c) {
// super.swapCursor() will notify observers before we have
// a valid mapping, make sure we have a mapping before this
// happens
findColumns(c, mOriginalFrom);
return super.swapCursor(c);
}
/**
* Change the cursor and change the column-to-view mappings at the same
* time.
*
* @param c
* The database cursor. Can be null if the cursor is not
* available yet.
* @param from
* A list of column names representing the data to bind to the
* UI. Can be null if the cursor is not available yet.
* @param to
* The views that should display column in the "from" parameter.
* These should all be TextViews. The first N views in this list
* are given the values of the first N columns in the from
* parameter. Can be null if the cursor is not available yet.
*/
public void changeCursorAndColumns(Cursor c, String[] from, int[] to) {
mOriginalFrom = from;
mTo = to;
// super.changeCursor() will notify observers before we have
// a valid mapping, make sure we have a mapping before this
// happens
findColumns(c, mOriginalFrom);
super.changeCursor(c);
}
/**
* This class can be used by external clients of SimpleCursorAdapter to bind
* values fom the Cursor to views.
*
* You should use this class to bind values from the Cursor to views that
* are not directly supported by SimpleCursorAdapter or to change the way
* binding occurs for views supported by SimpleCursorAdapter.
*
* @see SimpleCursorAdapter#bindView(android.view.View,
* android.content.Context, android.database.Cursor)
* @see SimpleCursorAdapter#setViewImage(ImageView, String)
* @see SimpleCursorAdapter#setViewText(TextView, String)
*/
public static interface ViewBinder {
/**
* Binds the Cursor column defined by the specified index to the
* specified view.
*
* When binding is handled by this ViewBinder, this method must return
* true. If this method returns false, SimpleCursorAdapter will attempts
* to handle the binding on its own.
*
* @param view
* the view to bind the data to
* @param cursor
* the cursor to get the data from
* @param columnIndex
* the column at which the data can be found in the cursor
*
* @return true if the data was bound to the view, false otherwise
*/
boolean setViewValue(View view, Cursor cursor, int columnIndex);
}
/**
* This class can be used by external clients of SimpleCursorAdapter to
* define how the Cursor should be converted to a String.
*
* @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
*/
public static interface CursorToStringConverter {
/**
* Returns a CharSequence representing the specified Cursor.
*
* @param cursor
* the cursor for which a CharSequence representation is
* requested
*
* @return a non-null CharSequence representing the cursor
*/
CharSequence convertToString(Cursor cursor);
}
}

@ -0,0 +1,87 @@
package com.mobeta.android.dslv;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Point;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ListView;
/**
* Simple implementation of the FloatViewManager class. Uses list items as they
* appear in the ListView to create the floating View.
*/
public class SimpleFloatViewManager implements DragSortListView.FloatViewManager {
private Bitmap mFloatBitmap;
private ImageView mImageView;
private int mFloatBGColor = Color.BLACK;
private ListView mListView;
public SimpleFloatViewManager(ListView lv) {
mListView = lv;
}
public void setBackgroundColor(int color) {
mFloatBGColor = color;
}
/**
* This simple implementation creates a Bitmap copy of the list item
* currently shown at ListView <code>position</code>.
*/
@Override
public View onCreateFloatView(int position) {
// Guaranteed that this will not be null? I think so. Nope, got
// a NullPointerException once...
View v = mListView.getChildAt(position + mListView.getHeaderViewsCount() - mListView.getFirstVisiblePosition());
if (v == null) {
return null;
}
v.setPressed(false);
// Create a copy of the drawing cache so that it does not get
// recycled by the framework when the list tries to clean up memory
//v.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
v.setDrawingCacheEnabled(true);
mFloatBitmap = Bitmap.createBitmap(v.getDrawingCache());
v.setDrawingCacheEnabled(false);
if (mImageView == null) {
mImageView = new ImageView(mListView.getContext());
}
mImageView.setBackgroundColor(mFloatBGColor);
mImageView.setPadding(0, 0, 0, 0);
mImageView.setImageBitmap(mFloatBitmap);
mImageView.setLayoutParams(new ViewGroup.LayoutParams(v.getWidth(), v.getHeight()));
return mImageView;
}
/**
* This does nothing
*/
@Override
public void onDragFloatView(View floatView, Point position, Point touch) {
// do nothing
}
/**
* Removes the Bitmap from the ImageView created in onCreateFloatView() and
* tells the system to recycle it.
*/
@Override
public void onDestroyFloatView(View floatView) {
((ImageView) floatView).setImageDrawable(null);
mFloatBitmap.recycle();
mFloatBitmap = null;
}
}

@ -10,7 +10,6 @@ import org.floens.chan.chan.ChanUrls;
import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Board;
import org.floens.chan.core.net.BoardsRequest; import org.floens.chan.core.net.BoardsRequest;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import org.floens.chan.utils.Time;
import com.android.volley.Response; import com.android.volley.Response;
import com.android.volley.VolleyError; import com.android.volley.VolleyError;
@ -80,10 +79,8 @@ public class BoardManager {
} }
public void updateSavedBoards() { public void updateSavedBoards() {
long start = Time.get();
ChanApplication.getDatabaseManager().updateBoards(allBoards); ChanApplication.getDatabaseManager().updateBoards(allBoards);
reloadSavedKeysValues(); reloadSavedKeysValues();
Logger.d(TAG, "updateSavedBoards took " + Time.get(start));
} }
private void reloadSavedKeysValues() { private void reloadSavedKeysValues() {
@ -101,7 +98,6 @@ public class BoardManager {
} }
private void storeBoards() { private void storeBoards() {
long start = Time.get();
Logger.d(TAG, "Storing boards in database"); Logger.d(TAG, "Storing boards in database");
for (Board test : allBoards) { for (Board test : allBoards) {
@ -111,13 +107,9 @@ public class BoardManager {
} }
ChanApplication.getDatabaseManager().setBoards(allBoards); ChanApplication.getDatabaseManager().setBoards(allBoards);
Logger.d(TAG, "storeBoards took " + Time.get(start));
} }
private void loadBoards() { private void loadBoards() {
long start = Time.get();
allBoards = ChanApplication.getDatabaseManager().getBoards(); allBoards = ChanApplication.getDatabaseManager().getBoards();
if (allBoards.size() == 0) { if (allBoards.size() == 0) {
Logger.d(TAG, "Loading default boards"); Logger.d(TAG, "Loading default boards");
@ -126,8 +118,6 @@ public class BoardManager {
} }
reloadSavedKeysValues(); reloadSavedKeysValues();
Logger.d(TAG, "loadBoards took " + Time.get(start));
} }
private void setBoardsFromServer(List<Board> list) { private void setBoardsFromServer(List<Board> list) {

@ -172,6 +172,7 @@ public class DatabaseManager {
o += "Loadable rows: " + helper.loadableDao.countOf() + "\n"; o += "Loadable rows: " + helper.loadableDao.countOf() + "\n";
o += "Pin rows: " + helper.pinDao.countOf() + "\n"; o += "Pin rows: " + helper.pinDao.countOf() + "\n";
o += "SavedReply rows: " + helper.savedDao.countOf() + "\n"; o += "SavedReply rows: " + helper.savedDao.countOf() + "\n";
o += "Board rows: " + helper.boardsDao.countOf() + "\n";
} catch (SQLException e) { } catch (SQLException e) {
e.printStackTrace(); e.printStackTrace();
} }

@ -1,5 +1,3 @@
package org.floens.chan.ui;
/* /*
* Copyright 2013 Google Inc. * Copyright 2013 Google Inc.
* *
@ -16,6 +14,8 @@ package org.floens.chan.ui;
* limitations under the License. * limitations under the License.
*/ */
package org.floens.chan.ui;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -74,7 +74,7 @@ import android.widget.ListView;
*/ */
public class SwipeDismissListViewTouchListener implements View.OnTouchListener { public class SwipeDismissListViewTouchListener implements View.OnTouchListener {
// Cached ViewConfiguration and system-wide constant values // Cached ViewConfiguration and system-wide constant values
private final int mSlop; private int mSlop;
private final int mMinFlingVelocity; private final int mMinFlingVelocity;
private final int mMaxFlingVelocity; private final int mMaxFlingVelocity;
private final long mAnimationTime; private final long mAnimationTime;
@ -129,7 +129,7 @@ public class SwipeDismissListViewTouchListener implements View.OnTouchListener {
*/ */
public SwipeDismissListViewTouchListener(ListView listView, DismissCallbacks callbacks) { public SwipeDismissListViewTouchListener(ListView listView, DismissCallbacks callbacks) {
ViewConfiguration vc = ViewConfiguration.get(listView.getContext()); ViewConfiguration vc = ViewConfiguration.get(listView.getContext());
mSlop = Math.max(1, (int) (vc.getScaledTouchSlop() * 0.5f)); mSlop = vc.getScaledTouchSlop();
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity() * 16; mMinFlingVelocity = vc.getScaledMinimumFlingVelocity() * 16;
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
mAnimationTime = listView.getContext().getResources().getInteger(android.R.integer.config_shortAnimTime); mAnimationTime = listView.getContext().getResources().getInteger(android.R.integer.config_shortAnimTime);
@ -137,6 +137,14 @@ public class SwipeDismissListViewTouchListener implements View.OnTouchListener {
mCallbacks = callbacks; mCallbacks = callbacks;
} }
public int getSlop() {
return mSlop;
}
public void setSlop(int slop) {
mSlop = slop;
}
/** /**
* Enables or disables (pauses or resumes) watching for swipe-to-dismiss * Enables or disables (pauses or resumes) watching for swipe-to-dismiss
* gestures. * gestures.

@ -150,6 +150,8 @@ public abstract class BaseActivity extends Activity implements PanelSlideListene
} }
}); });
touchListener.setSlop((int) (touchListener.getSlop() * 0.4f));
pinDrawerView.setOnTouchListener(touchListener); pinDrawerView.setOnTouchListener(touchListener);
pinDrawerView.setOnScrollListener(touchListener.makeScrollListener()); pinDrawerView.setOnScrollListener(touchListener.makeScrollListener());
} }

@ -8,15 +8,16 @@ import org.floens.chan.ChanApplication;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Board;
import org.floens.chan.ui.adapter.BoardEditAdapter; import org.floens.chan.ui.SwipeDismissListViewTouchListener;
import org.floens.chan.ui.view.DynamicListView;
import org.floens.chan.utils.Logger;
import org.floens.chan.utils.Utils; import org.floens.chan.utils.Utils;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -30,28 +31,80 @@ import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView; import android.widget.AutoCompleteTextView;
import android.widget.Filter; import android.widget.Filter;
import android.widget.Filterable; import android.widget.Filterable;
import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.mobeta.android.dslv.DragSortController;
import com.mobeta.android.dslv.DragSortListView;
;
public class BoardEditor extends Activity { public class BoardEditor extends Activity {
private final BoardManager boardManager = ChanApplication.getBoardManager(); private final BoardManager boardManager = ChanApplication.getBoardManager();
private List<Board> list; private List<Board> list;
private DynamicListView<Board> listView; private DragSortListView listView;
private BoardEditAdapter adapter; private BoardEditAdapter adapter;
@SuppressWarnings("unchecked")
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.board_edit);
list = boardManager.getSavedBoards(); list = boardManager.getSavedBoards();
listView = (DynamicListView<Board>) findViewById(R.id.board_edit_list); adapter = new BoardEditAdapter(this, 0, list);
listView = new DragSortListView(this, null);
listView.setAdapter(adapter);
listView.setDivider(new ColorDrawable(Color.TRANSPARENT));
final DragSortController controller = new NiceDragSortController(listView, adapter);
listView.setFloatViewManager(controller);
listView.setOnTouchListener(controller);
listView.setDragEnabled(true);
listView.setDropListener(new DragSortListView.DropListener() {
@Override
public void drop(int from, int to) {
if (from != to) {
Board board = list.remove(from);
list.add(to, board);
adapter.notifyDataSetChanged();
}
}
});
final SwipeDismissListViewTouchListener touchListener = new SwipeDismissListViewTouchListener(listView,
new SwipeDismissListViewTouchListener.DismissCallbacks() {
@Override
public void onDismiss(ListView listView, int[] reverseSortedPositions) {
for (int position : reverseSortedPositions) {
Board b = adapter.getItem(position);
adapter.remove(b);
b.saved = false;
}
adapter.notifyDataSetChanged();
}
@Override
public boolean canDismiss(int position) {
return list.size() > 1;
}
});
listView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
return controller.onTouch(view, motionEvent)
|| (!listView.isDragging() && touchListener.onTouch(view, motionEvent));
}
});
listView.setOnScrollListener(touchListener.makeScrollListener());
reload(); setContentView(listView);
} }
@Override @Override
@ -68,10 +121,6 @@ public class BoardEditor extends Activity {
} }
} }
public void onDeleteClicked(Board board) {
removeBoard(board.value);
}
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.board_edit, menu); getMenuInflater().inflate(R.menu.board_edit, menu);
@ -90,6 +139,7 @@ public class BoardEditor extends Activity {
} }
private void addBoard(final String value) { private void addBoard(final String value) {
// Duplicate
for (Board board : list) { for (Board board : list) {
if (board.value.equals(value)) { if (board.value.equals(value)) {
Toast.makeText(this, R.string.board_add_duplicate, Toast.LENGTH_LONG).show(); Toast.makeText(this, R.string.board_add_duplicate, Toast.LENGTH_LONG).show();
@ -98,26 +148,32 @@ public class BoardEditor extends Activity {
} }
} }
// Normal add
List<Board> all = ChanApplication.getBoardManager().getAllBoards(); List<Board> all = ChanApplication.getBoardManager().getAllBoards();
for (Board board : all) { for (Board board : all) {
if (board.value.equals(value)) { if (board.value.equals(value)) {
board.saved = true; board.saved = true;
list.add(board); list.add(board);
adapter.notifyDataSetChanged();
Toast.makeText(this, getString(R.string.board_add_success) + " " + board.key, Toast.LENGTH_LONG).show(); Toast.makeText(this, getString(R.string.board_add_success) + " " + board.key, Toast.LENGTH_LONG).show();
reload();
return; return;
} }
} }
// Unknown
String message = getString(R.string.board_add_unknown).replace("CODE", value); String message = getString(R.string.board_add_unknown).replace("CODE", value);
new AlertDialog.Builder(this).setTitle(R.string.board_add_unknown_title).setMessage(message) new AlertDialog.Builder(this).setTitle(R.string.board_add_unknown_title).setMessage(message)
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
if (list != null) if (list != null) {
list.add(new Board(value, value, true, true)); Board b = new Board(value, value, true, true);
list.add(b);
adapter.notifyDataSetChanged();
boardManager.getAllBoards().add(b);
}
} }
}).setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { }).setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
@Override @Override
@ -126,28 +182,6 @@ public class BoardEditor extends Activity {
}).create().show(); }).create().show();
} }
private void removeBoard(String value) {
if (list.size() <= 1)
return;
for (int i = 0; i < list.size(); i++) {
Board b = list.get(i);
if (b.value == value) {
list.remove(i);
b.saved = false;
break;
}
}
reload();
}
private void reload() {
adapter = new BoardEditAdapter(this, R.layout.board_view, list, this);
listView.setArrayList(list);
listView.setAdapter(adapter);
}
private void showAddBoardDialog() { private void showAddBoardDialog() {
final AutoCompleteTextView text = new AutoCompleteTextView(this); final AutoCompleteTextView text = new AutoCompleteTextView(this);
text.setSingleLine(); text.setSingleLine();
@ -159,7 +193,8 @@ public class BoardEditor extends Activity {
text.setThreshold(1); text.setThreshold(1);
text.setDropDownHeight(ViewGroup.LayoutParams.WRAP_CONTENT); text.setDropDownHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
AlertDialog dialog = new AlertDialog.Builder(this).setPositiveButton(R.string.add, new DialogInterface.OnClickListener() { AlertDialog dialog = new AlertDialog.Builder(this)
.setPositiveButton(R.string.add, new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface d, int which) { public void onClick(DialogInterface d, int which) {
String value = text.getText().toString(); String value = text.getText().toString();
@ -294,14 +329,11 @@ public class BoardEditor extends Activity {
boolean showUnsafe = false; boolean showUnsafe = false;
for (Board has : currentlyEditing) { for (Board has : currentlyEditing) {
if (!has.workSafe) { if (!has.workSafe) {
Logger.test("Unsafe: " + has.key + ", " + has.workSafe);
showUnsafe = true; showUnsafe = true;
break; break;
} }
} }
Logger.test("Showing unsafe: " + showUnsafe + ", " + currentlyEditing.size());
List<Board> s = new ArrayList<Board>(); List<Board> s = new ArrayList<Board>();
for (Board b : ChanApplication.getBoardManager().getAllBoards()) { for (Board b : ChanApplication.getBoardManager().getAllBoards()) {
if (showUnsafe || b.workSafe) if (showUnsafe || b.workSafe)
@ -310,4 +342,56 @@ public class BoardEditor extends Activity {
return s; return s;
} }
} }
private class NiceDragSortController extends DragSortController {
private final ListView listView;
private final ArrayAdapter<Board> adapter;
public NiceDragSortController(DragSortListView listView, ArrayAdapter<Board> adapter) {
super(listView, R.id.drag_handle, DragSortController.ON_DOWN, 0);
this.listView = listView;
this.adapter = adapter;
setSortEnabled(true);
setRemoveEnabled(false);
}
@Override
public View onCreateFloatView(int position) {
return adapter.getView(position, null, listView);
}
@Override
public void onDragFloatView(View floatView, Point floatPoint, Point touchPoint) {
}
@Override
public void onDestroyFloatView(View floatView) {
}
}
private class BoardEditAdapter extends ArrayAdapter<Board> {
public BoardEditAdapter(Context context, int textViewResourceId, List<Board> objects) {
super(context, textViewResourceId, objects);
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public long getItemId(int position) {
return getItem(position).hashCode();
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
View inflated = LayoutInflater.from(getContext()).inflate(R.layout.board_edit_item, null);
TextView text = (TextView) inflated.findViewById(R.id.text);
Board b = getItem(position);
text.setText(b.value + " - " + b.key);
return inflated;
}
}
} }

@ -1,84 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.floens.chan.ui.adapter;
import java.util.HashMap;
import java.util.List;
import org.floens.chan.R;
import org.floens.chan.core.model.Board;
import org.floens.chan.ui.activity.BoardEditor;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.TextView;
public class BoardEditAdapter extends ArrayAdapter<Board> {
HashMap<Board, Integer> mIdMap = new HashMap<Board, Integer>();
private final BoardEditor editor;
public BoardEditAdapter(Context context, int textViewResourceId, List<Board> objects, BoardEditor editor) {
super(context, textViewResourceId, objects);
this.editor = editor;
for (int i = 0; i < objects.size(); ++i) {
mIdMap.put(objects.get(i), i);
}
}
@Override
public long getItemId(int position) {
if (position < 0 || position >= mIdMap.size()) {
return -1;
}
Board item = getItem(position);
return mIdMap.get(item);
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
String text = getItem(position).key;
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.board_view, null);
TextView textView = (TextView) view.findViewById(R.id.board_view_text);
textView.setText(text);
Button button = (Button) view.findViewById(R.id.board_view_delete);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
editor.onDeleteClicked(getItem(position));
}
});
return view;
}
@Override
public boolean hasStableIds() {
return true;
}
}

@ -0,0 +1,117 @@
/*
* Copyright 2013 Google Inc.
*
* 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 org.floens.chan.ui.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
public class DragGripView extends View {
private static final int[] ATTRS = new int[]{
android.R.attr.gravity,
android.R.attr.color,
};
private static final int HORIZ_RIDGES = 2;
private int mGravity = Gravity.START;
private int mColor = 0xff666666;
private final Paint mRidgePaint;
private final RectF mTempRectF = new RectF();
private final float mRidgeSize;
private final float mRidgeGap;
// private int mWidth;
private int mHeight;
public DragGripView(Context context) {
this(context, null, 0);
}
public DragGripView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DragGripView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS);
mGravity = a.getInteger(0, mGravity);
mColor = a.getColor(1, mColor);
a.recycle();
final DisplayMetrics res = getResources().getDisplayMetrics();
mRidgeSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, res);
mRidgeGap = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, res);
mRidgePaint = new Paint();
mRidgePaint.setColor(mColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
View.resolveSize(
(int) (HORIZ_RIDGES * (mRidgeSize + mRidgeGap) - mRidgeGap)
+ getPaddingLeft() + getPaddingRight(),
widthMeasureSpec),
View.resolveSize(
(int) mRidgeSize,
heightMeasureSpec));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// float drawWidth = HORIZ_RIDGES * (mRidgeSize + mRidgeGap) - mRidgeGap;
float drawLeft = getPaddingLeft();
int vertRidges = (int) ((mHeight - getPaddingTop() - getPaddingBottom() + mRidgeGap)
/ (mRidgeSize + mRidgeGap));
float drawHeight = vertRidges * (mRidgeSize + mRidgeGap) - mRidgeGap;
float drawTop = getPaddingTop()
+ ((mHeight - getPaddingTop() - getPaddingBottom()) - drawHeight) / 2;
for (int y = 0; y < vertRidges; y++) {
for (int x = 0; x < HORIZ_RIDGES; x++) {
mTempRectF.set(
drawLeft + x * (mRidgeSize + mRidgeGap),
drawTop + y * (mRidgeSize + mRidgeGap),
drawLeft + x * (mRidgeSize + mRidgeGap) + mRidgeSize,
drawTop + y * (mRidgeSize + mRidgeGap) + mRidgeSize);
canvas.drawOval(mTempRectF, mRidgePaint);
}
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mHeight = h;
// mWidth = w;
}
}

@ -1,596 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.floens.chan.ui.view;
import java.util.List;
import org.floens.chan.ui.adapter.BoardEditAdapter;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ListView;
/**
* The dynamic listview is an extension of listview that supports cell dragging
* and swapping.
*
* This layout is in charge of positioning the hover cell in the correct
* location on the screen in response to user touch events. It uses the position
* of the hover cell to determine when two cells should be swapped. If two cells
* should be swapped, all the corresponding data set and layout changes are
* handled here.
*
* If no cell is selected, all the touch events are passed down to the listview
* and behave normally. If one of the items in the listview experiences a long
* press event, the contents of its current visible state are captured as a
* bitmap and its visibility is set to INVISIBLE. A hover cell is then created
* and added to this layout as an overlaying BitmapDrawable above the listview.
* Once the hover cell is translated some distance to signify an item swap, a
* data set change accompanied by animation takes place. When the user releases
* the hover cell, it animates into its corresponding position in the listview.
*
* When the hover cell is either above or below the bounds of the listview, this
* listview also scrolls on its own so as to reveal additional content.
*/
public class DynamicListView<T> extends ListView {
private final int SMOOTH_SCROLL_AMOUNT_AT_EDGE = 15;
private final int MOVE_DURATION = 150;
private final int LINE_THICKNESS = 10;
public List<T> mCheeseList;
private int mLastEventY = -1;
private int mDownY = -1;
private int mDownX = -1;
private int mTotalOffset = 0;
private boolean mCellIsMobile = false;
private boolean mIsMobileScrolling = false;
private int mSmoothScrollAmountAtEdge = 0;
private final int INVALID_ID = -1;
private long mAboveItemId = INVALID_ID;
private long mMobileItemId = INVALID_ID;
private long mBelowItemId = INVALID_ID;
private BitmapDrawable mHoverCell;
private Rect mHoverCellCurrentBounds;
private Rect mHoverCellOriginalBounds;
private final int INVALID_POINTER_ID = -1;
private int mActivePointerId = INVALID_POINTER_ID;
private boolean mIsWaitingForScrollFinish = false;
private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
public DynamicListView(Context context) {
super(context);
init(context);
}
public DynamicListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
public DynamicListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public void init(Context context) {
setOnItemLongClickListener(mOnItemLongClickListener);
setOnScrollListener(mScrollListener);
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
mSmoothScrollAmountAtEdge = (int) (SMOOTH_SCROLL_AMOUNT_AT_EDGE / metrics.density);
}
/**
* Listens for long clicks on any items in the listview. When a cell has
* been selected, the hover cell is created and set up.
*/
private final AdapterView.OnItemLongClickListener mOnItemLongClickListener = new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> arg0, View arg1, int pos, long id) {
mTotalOffset = 0;
int position = pointToPosition(mDownX, mDownY);
int itemNum = position - getFirstVisiblePosition();
View selectedView = getChildAt(itemNum);
mMobileItemId = getAdapter().getItemId(position);
mHoverCell = getAndAddHoverView(selectedView);
selectedView.setVisibility(INVISIBLE);
mCellIsMobile = true;
updateNeighborViewsForID(mMobileItemId);
return true;
}
};
/**
* Creates the hover cell with the appropriate bitmap and of appropriate
* size. The hover cell's BitmapDrawable is drawn on top of the bitmap every
* single time an invalidate call is made.
*/
private BitmapDrawable getAndAddHoverView(View v) {
int w = v.getWidth();
int h = v.getHeight();
int top = v.getTop();
int left = v.getLeft();
Bitmap b = getBitmapWithBorder(v);
BitmapDrawable drawable = new BitmapDrawable(getResources(), b);
mHoverCellOriginalBounds = new Rect(left, top, left + w, top + h);
mHoverCellCurrentBounds = new Rect(mHoverCellOriginalBounds);
drawable.setBounds(mHoverCellCurrentBounds);
return drawable;
}
/** Draws a black border over the screenshot of the view passed in. */
private Bitmap getBitmapWithBorder(View v) {
Bitmap bitmap = getBitmapFromView(v);
Canvas can = new Canvas(bitmap);
Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
Paint paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(LINE_THICKNESS);
paint.setColor(Color.BLACK);
can.drawBitmap(bitmap, 0, 0, null);
can.drawRect(rect, paint);
return bitmap;
}
/** Returns a bitmap showing a screenshot of the view passed in. */
private Bitmap getBitmapFromView(View v) {
Bitmap bitmap = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
v.draw(canvas);
return bitmap;
}
/**
* Stores a reference to the views above and below the item currently
* corresponding to the hover cell. It is important to note that if this
* item is either at the top or bottom of the list, mAboveItemId or
* mBelowItemId may be invalid.
*/
private void updateNeighborViewsForID(long itemID) {
int position = getPositionForID(itemID);
BoardEditAdapter adapter = ((BoardEditAdapter) getAdapter());
mAboveItemId = adapter.getItemId(position - 1);
mBelowItemId = adapter.getItemId(position + 1);
}
/** Retrieves the view in the list corresponding to itemID */
public View getViewForID(long itemID) {
int firstVisiblePosition = getFirstVisiblePosition();
BoardEditAdapter adapter = ((BoardEditAdapter) getAdapter());
for (int i = 0; i < getChildCount(); i++) {
View v = getChildAt(i);
int position = firstVisiblePosition + i;
long id = adapter.getItemId(position);
if (id == itemID) {
return v;
}
}
return null;
}
/** Retrieves the position in the list corresponding to itemID */
public int getPositionForID(long itemID) {
View v = getViewForID(itemID);
if (v == null) {
return -1;
} else {
return getPositionForView(v);
}
}
/**
* dispatchDraw gets invoked when all the child views are about to be drawn.
* By overriding this method, the hover cell (BitmapDrawable) can be drawn
* over the listview's items whenever the listview is redrawn.
*/
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mHoverCell != null) {
mHoverCell.draw(canvas);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mDownX = (int) event.getX();
mDownY = (int) event.getY();
mActivePointerId = event.getPointerId(0);
break;
case MotionEvent.ACTION_MOVE:
if (mActivePointerId == INVALID_POINTER_ID) {
break;
}
int pointerIndex = event.findPointerIndex(mActivePointerId);
mLastEventY = (int) event.getY(pointerIndex);
int deltaY = mLastEventY - mDownY;
if (mCellIsMobile) {
mHoverCellCurrentBounds.offsetTo(mHoverCellOriginalBounds.left, mHoverCellOriginalBounds.top + deltaY
+ mTotalOffset);
mHoverCell.setBounds(mHoverCellCurrentBounds);
invalidate();
handleCellSwitch();
mIsMobileScrolling = false;
handleMobileCellScroll();
return false;
}
break;
case MotionEvent.ACTION_UP:
touchEventsEnded();
break;
case MotionEvent.ACTION_CANCEL:
touchEventsCancelled();
break;
case MotionEvent.ACTION_POINTER_UP:
/* If a multitouch event took place and the original touch dictating
* the movement of the hover cell has ended, then the dragging event
* ends and the hover cell is animated to its corresponding position
* in the listview. */
pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
touchEventsEnded();
}
break;
default:
break;
}
return super.onTouchEvent(event);
}
/**
* This method determines whether the hover cell has been shifted far enough
* to invoke a cell swap. If so, then the respective cell swap candidate is
* determined and the data set is changed. Upon posting a notification of
* the data set change, a layout is invoked to place the cells in the right
* place. Using a ViewTreeObserver and a corresponding OnPreDrawListener, we
* can offset the cell being swapped to where it previously was and then
* animate it to its new position.
*/
private void handleCellSwitch() {
final int deltaY = mLastEventY - mDownY;
int deltaYTotal = mHoverCellOriginalBounds.top + mTotalOffset + deltaY;
View belowView = getViewForID(mBelowItemId);
View mobileView = getViewForID(mMobileItemId);
View aboveView = getViewForID(mAboveItemId);
boolean isBelow = (belowView != null) && (deltaYTotal > belowView.getTop());
boolean isAbove = (aboveView != null) && (deltaYTotal < aboveView.getTop());
if (isBelow || isAbove) {
final long switchItemID = isBelow ? mBelowItemId : mAboveItemId;
View switchView = isBelow ? belowView : aboveView;
final int originalItem = getPositionForView(mobileView);
if (switchView == null) {
updateNeighborViewsForID(mMobileItemId);
return;
}
swapElements(mCheeseList, originalItem, getPositionForView(switchView));
((BaseAdapter) getAdapter()).notifyDataSetChanged();
mDownY = mLastEventY;
final int switchViewStartTop = switchView.getTop();
mobileView.setVisibility(View.VISIBLE);
switchView.setVisibility(View.INVISIBLE);
updateNeighborViewsForID(mMobileItemId);
final ViewTreeObserver observer = getViewTreeObserver();
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
observer.removeOnPreDrawListener(this);
View switchView = getViewForID(switchItemID);
mTotalOffset += deltaY;
int switchViewNewTop = switchView.getTop();
int delta = switchViewStartTop - switchViewNewTop;
switchView.setTranslationY(delta);
ObjectAnimator animator = ObjectAnimator.ofFloat(switchView, View.TRANSLATION_Y, 0);
animator.setDuration(MOVE_DURATION);
animator.start();
return true;
}
});
}
}
private void swapElements(List<T> arrayList, int indexOne, int indexTwo) {
T temp = arrayList.get(indexOne);
arrayList.set(indexOne, arrayList.get(indexTwo));
arrayList.set(indexTwo, temp);
}
/**
* Resets all the appropriate fields to a default state while also animating
* the hover cell back to its correct location.
*/
private void touchEventsEnded() {
final View mobileView = getViewForID(mMobileItemId);
if (mCellIsMobile || mIsWaitingForScrollFinish) {
mCellIsMobile = false;
mIsWaitingForScrollFinish = false;
mIsMobileScrolling = false;
mActivePointerId = INVALID_POINTER_ID;
// If the autoscroller has not completed scrolling, we need to wait for it to
// finish in order to determine the final location of where the hover cell
// should be animated to.
if (mScrollState != OnScrollListener.SCROLL_STATE_IDLE) {
mIsWaitingForScrollFinish = true;
return;
}
mHoverCellCurrentBounds.offsetTo(mHoverCellOriginalBounds.left, mobileView.getTop());
ObjectAnimator hoverViewAnimator = ObjectAnimator.ofObject(mHoverCell, "bounds", sBoundEvaluator,
mHoverCellCurrentBounds);
hoverViewAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
invalidate();
}
});
hoverViewAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
setEnabled(false);
}
@Override
public void onAnimationEnd(Animator animation) {
mAboveItemId = INVALID_ID;
mMobileItemId = INVALID_ID;
mBelowItemId = INVALID_ID;
mobileView.setVisibility(VISIBLE);
mHoverCell = null;
setEnabled(true);
invalidate();
}
});
hoverViewAnimator.start();
} else {
touchEventsCancelled();
}
}
/**
* Resets all the appropriate fields to a default state.
*/
private void touchEventsCancelled() {
View mobileView = getViewForID(mMobileItemId);
if (mCellIsMobile) {
mAboveItemId = INVALID_ID;
mMobileItemId = INVALID_ID;
mBelowItemId = INVALID_ID;
mobileView.setVisibility(VISIBLE);
mHoverCell = null;
invalidate();
}
mCellIsMobile = false;
mIsMobileScrolling = false;
mActivePointerId = INVALID_POINTER_ID;
}
/**
* This TypeEvaluator is used to animate the BitmapDrawable back to its
* final location when the user lifts his finger by modifying the
* BitmapDrawable's bounds.
*/
private final static TypeEvaluator<Rect> sBoundEvaluator = new TypeEvaluator<Rect>() {
@Override
public Rect evaluate(float fraction, Rect startValue, Rect endValue) {
return new Rect(interpolate(startValue.left, endValue.left, fraction), interpolate(startValue.top,
endValue.top, fraction), interpolate(startValue.right, endValue.right, fraction), interpolate(
startValue.bottom, endValue.bottom, fraction));
}
public int interpolate(int start, int end, float fraction) {
return (int) (start + fraction * (end - start));
}
};
/**
* Determines whether this listview is in a scrolling state invoked by the
* fact that the hover cell is out of the bounds of the listview;
*/
private void handleMobileCellScroll() {
mIsMobileScrolling = handleMobileCellScroll(mHoverCellCurrentBounds);
}
/**
* This method is in charge of determining if the hover cell is above or
* below the bounds of the listview. If so, the listview does an appropriate
* upward or downward smooth scroll so as to reveal new items.
*/
public boolean handleMobileCellScroll(Rect r) {
int offset = computeVerticalScrollOffset();
int height = getHeight();
int extent = computeVerticalScrollExtent();
int range = computeVerticalScrollRange();
int hoverViewTop = r.top;
int hoverHeight = r.height();
if (hoverViewTop <= 0 && offset > 0) {
smoothScrollBy(-mSmoothScrollAmountAtEdge, 0);
return true;
}
if (hoverViewTop + hoverHeight >= height && (offset + extent) < range) {
smoothScrollBy(mSmoothScrollAmountAtEdge, 0);
return true;
}
return false;
}
public void setArrayList(List<T> cheeseList) {
mCheeseList = cheeseList;
}
/**
* This scroll listener is added to the listview in order to handle cell
* swapping when the cell is either at the top or bottom edge of the
* listview. If the hover cell is at either edge of the listview, the
* listview will begin scrolling. As scrolling takes place, the listview
* continuously checks if new cells became visible and determines whether
* they are potential candidates for a cell swap.
*/
private final AbsListView.OnScrollListener mScrollListener = new AbsListView.OnScrollListener() {
private int mPreviousFirstVisibleItem = -1;
private int mPreviousVisibleItemCount = -1;
private int mCurrentFirstVisibleItem;
private int mCurrentVisibleItemCount;
private int mCurrentScrollState;
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
mCurrentFirstVisibleItem = firstVisibleItem;
mCurrentVisibleItemCount = visibleItemCount;
mPreviousFirstVisibleItem = (mPreviousFirstVisibleItem == -1) ? mCurrentFirstVisibleItem
: mPreviousFirstVisibleItem;
mPreviousVisibleItemCount = (mPreviousVisibleItemCount == -1) ? mCurrentVisibleItemCount
: mPreviousVisibleItemCount;
checkAndHandleFirstVisibleCellChange();
checkAndHandleLastVisibleCellChange();
mPreviousFirstVisibleItem = mCurrentFirstVisibleItem;
mPreviousVisibleItemCount = mCurrentVisibleItemCount;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
mCurrentScrollState = scrollState;
mScrollState = scrollState;
isScrollCompleted();
}
/**
* This method is in charge of invoking 1 of 2 actions. Firstly, if the
* listview is in a state of scrolling invoked by the hover cell being
* outside the bounds of the listview, then this scrolling event is
* continued. Secondly, if the hover cell has already been released,
* this invokes the animation for the hover cell to return to its
* correct position after the listview has entered an idle scroll state.
*/
private void isScrollCompleted() {
if (mCurrentVisibleItemCount > 0 && mCurrentScrollState == SCROLL_STATE_IDLE) {
if (mCellIsMobile && mIsMobileScrolling) {
handleMobileCellScroll();
} else if (mIsWaitingForScrollFinish) {
touchEventsEnded();
}
}
}
/**
* Determines if the listview scrolled up enough to reveal a new cell at
* the top of the list. If so, then the appropriate parameters are
* updated.
*/
public void checkAndHandleFirstVisibleCellChange() {
if (mCurrentFirstVisibleItem != mPreviousFirstVisibleItem) {
if (mCellIsMobile && mMobileItemId != INVALID_ID) {
updateNeighborViewsForID(mMobileItemId);
handleCellSwitch();
}
}
}
/**
* Determines if the listview scrolled down enough to reveal a new cell
* at the bottom of the list. If so, then the appropriate parameters are
* updated.
*/
public void checkAndHandleLastVisibleCellChange() {
int currentLastVisibleItem = mCurrentFirstVisibleItem + mCurrentVisibleItemCount;
int previousLastVisibleItem = mPreviousFirstVisibleItem + mPreviousVisibleItemCount;
if (currentLastVisibleItem != previousLastVisibleItem) {
if (mCellIsMobile && mMobileItemId != INVALID_ID) {
updateNeighborViewsForID(mMobileItemId);
handleCellSwitch();
}
}
}
};
}
Loading…
Cancel
Save