package org.openintents.shopping.ui.widget; import android.app.Activity; import android.app.Dialog; import android.app.LoaderManager; import android.content.ActivityNotFoundException; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.CursorLoader; import android.content.Intent; import android.content.Loader; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.support.design.widget.Snackbar; import android.support.v4.widget.SearchViewCompat; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextPaint; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.text.style.ImageSpan; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.CheckBox; import android.widget.ImageView; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.SimpleCursorAdapter; import android.widget.SimpleCursorAdapter.ViewBinder; import android.widget.TextView; import org.openintents.distribution.DownloadAppDialog; import org.openintents.shopping.R; import org.openintents.shopping.ShoppingApplication; import org.openintents.shopping.SyncSupport; import org.openintents.shopping.library.provider.ShoppingContract; import org.openintents.shopping.library.provider.ShoppingContract.Contains; import org.openintents.shopping.library.provider.ShoppingContract.ContainsFull; import org.openintents.shopping.library.provider.ShoppingContract.Status; import org.openintents.shopping.library.util.ShoppingUtils; import org.openintents.shopping.provider.ShoppingProvider; import org.openintents.shopping.theme.ThemeAttributes; import org.openintents.shopping.theme.ThemeShoppingList; import org.openintents.shopping.theme.ThemeUtils; import org.openintents.shopping.ui.PreferenceActivity; import org.openintents.shopping.ui.ShoppingActivity; import org.openintents.shopping.ui.ShoppingTotalsHandler; import org.openintents.shopping.ui.SnackbarUndoMultipleItemStatusOperation; import org.openintents.shopping.ui.SnackbarUndoSingleItemStatusOperation; import org.openintents.shopping.ui.UndoListener; import org.openintents.shopping.ui.dialog.EditItemDialog; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.Locale; /** * View to show a shopping list with its items */ public class ShoppingItemsView extends ListView implements LoaderManager.LoaderCallbacks<Cursor> { private final static String TAG = "ShoppingListView"; private final static boolean debug = false; private Typeface mCurrentTypeface; public int mPriceVisibility; public int mTagsVisibility; public int mQuantityVisibility; public int mUnitsVisibility; public int mPriorityVisibility; public String mTextTypeface; public float mTextSize; public boolean mTextUpperCaseFont; public int mTextColor; public int mTextColorPrice; public int mTextColorChecked; public int mTextColorPriority; public boolean mShowCheckBox; public boolean mShowStrikethrough; public String mTextSuffixUnchecked; public String mTextSuffixChecked; public int mBackgroundPadding; public int mUpdateLastListPosition; public int mLastListPosition; public int mLastListTop; public long mNumChecked; public long mNumUnchecked; private ThemeAttributes mThemeAttributes; private PackageManager mPackageManager; private String mPackageName; private NumberFormat mPriceFormatter = DecimalFormat .getNumberInstance(Locale.ENGLISH); public int mMode = ShoppingActivity.MODE_IN_SHOP; private String mFilter; private boolean mInSearch; public int mModeBeforeSearch; public Cursor mCursorItems; private Activity mCursorActivity; private View mThemedBackground; private long mListId; private long mFocusItemId = -1; private Drawable mDefaultDivider; private int mDragPos; // which item is being dragged private int mFirstDragPos; // where was the dragged item originally private int mDragPoint; // at what offset inside the item did the user grab // it private int mCoordOffset; // the difference between screen coordinates and // coordinates in this view private WindowManager mWindowManager; private WindowManager.LayoutParams mWindowParams; private Rect mTempRect = new Rect(); // dragging elements private Bitmap mDragBitmap; private ImageView mDragView; private int mHeight; private int mUpperBound; private int mLowerBound; private int mTouchSlop; private int mItemHeightHalf; private int mItemHeightNormal; private int mItemHeightExpanded; private DragListener mDragListener; private DropListener mDropListener; private ActionBarListener mActionBarListener; private UndoListener mUndoListener; private Snackbar mSnackbar; private SyncSupport mSyncSupport; private ShoppingTotalsHandler mTotalsHandler; /** * Extend the SimpleCursorAdapter to strike through items. if STATUS == * Shopping.Status.BOUGHT */ public class mSimpleCursorAdapter extends SimpleCursorAdapter implements ViewBinder { private class mItemRowState { public View mParentView; public TextView mNameView; public TextView mQuantityView; public TextView mUnitsView; public TextView mPriceView; public TextView mPriorityView; public TextView mTagsView; public CheckBox mCheckView; public ImageView mNoCheckView; public Cursor mCursor; public int mCursorPos; private class mItemClickListener implements OnClickListener { private String mLogMessage; private EditItemDialog.FieldType mFieldType; public void onClick(View v) { if (debug) { Log.d(TAG, mLogMessage); } if (mListener != null) { mItemRowState state = (mItemRowState) v.getTag(); mListener.onCustomClick(state.mCursor, state.mCursorPos, mFieldType, v); } } public mItemClickListener(String logMessage, EditItemDialog.FieldType fieldType) { mLogMessage = logMessage; mFieldType = fieldType; } } private class mItemToggleListener implements OnClickListener { private String mLogMessage; public void onClick(View v) { if (debug) { Log.d(TAG, mLogMessage); } mItemRowState state = (mItemRowState) v.getTag(); toggleItemBought(state.mCursorPos); } public mItemToggleListener(String logMessage) { mLogMessage = logMessage; } } public mItemRowState(View view) { // This class is here to initialize state information related // to a single reusable item row, to reduce the amount of // setup that needs to be done each time the row is reused. // // Callbacks can be bound up-front here if they depend on cursor position. mParentView = view; mNameView = (TextView) view.findViewById(R.id.name); mPriceView = (TextView) view.findViewById(R.id.price); mTagsView = (TextView) view.findViewById(R.id.tags); mQuantityView = (TextView) view.findViewById(R.id.quantity); mUnitsView = (TextView) view.findViewById(R.id.units); mPriorityView = (TextView) view.findViewById(R.id.priority); mCheckView = (CheckBox) view.findViewById(R.id.check); mNoCheckView = (ImageView) view.findViewById(R.id.nocheck); mParentView.setTag(this); mNameView.setTag(this); mPriceView.setTag(this); mTagsView.setTag(this); mQuantityView.setTag(this); mUnitsView.setTag(this); mPriorityView.setTag(this); mCheckView.setTag(this); mNoCheckView.setTag(this); mQuantityView.setOnClickListener(new mItemClickListener("Quantity Click ", EditItemDialog.FieldType.QUANTITY)); mPriceView.setOnClickListener(new mItemClickListener("Click on price: ", EditItemDialog.FieldType.PRICE)); mUnitsView.setOnClickListener(new mItemClickListener("Click on units: ", EditItemDialog.FieldType.UNITS)); mPriorityView.setOnClickListener(new mItemClickListener("Click on priority: ", EditItemDialog.FieldType.PRIORITY)); mTagsView.setOnClickListener(new mItemClickListener("Click on tags: ", EditItemDialog.FieldType.TAGS)); mCheckView.setOnClickListener(new mItemToggleListener("Click: ")); // also check around check box RelativeLayout l = (RelativeLayout) view.findViewById(R.id.check_surround); l.setTag(this); l.setOnClickListener(new mItemToggleListener("Click around: ")); // Check for clicks on and around item text RelativeLayout r = (RelativeLayout) view.findViewById(R.id.description); r.setTag(this); r.setOnClickListener(new mItemClickListener("Click on description: ", EditItemDialog.FieldType.ITEMNAME)); mPriceView.setVisibility(mPriceVisibility); mTagsView.setVisibility(mTagsVisibility); mQuantityView.setVisibility(mQuantityVisibility); mUnitsView.setVisibility(mUnitsVisibility); mPriorityView.setVisibility(mPriorityVisibility); } } /** * Constructor simply calls super class. * * @param context Context. * @param layout Layout. * @param c Cursor. * @param from Projection from. * @param to Projection to. */ mSimpleCursorAdapter(final Context context, final int layout, final Cursor c, final String[] from, final int[] to) { super(context, layout, c, from, to); super.setViewBinder(this); mPriceFormatter.setMaximumFractionDigits(2); mPriceFormatter.setMinimumFractionDigits(2); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { View view = super.newView(context, cursor, parent); mItemRowState rowState = new mItemRowState(view); // sets view tags return view; } /** * Additionally to the standard bindView, we also check for STATUS, and * strike the item through if BOUGHT. */ @Override public void bindView(final View view, final Context context, final Cursor cursor) { super.bindView(view, context, cursor); long status = cursor.getLong(ShoppingActivity.mStringItemsSTATUS); mItemRowState state = (mItemRowState) view.getTag(); state.mCursorPos = cursor.getPosition(); state.mCursor = cursor; // set style for name view and friends TextView[] styled_as_name = {state.mNameView, state.mUnitsView, state.mQuantityView}; int i; for (i = 0; i < styled_as_name.length; i++) { TextView t = styled_as_name[i]; // Set font if (mCurrentTypeface != null) { t.setTypeface(mCurrentTypeface); } // Set size t.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); // Check for upper case: if (mTextUpperCaseFont) { // Only upper case should be displayed CharSequence cs = t.getText(); t.setText(cs.toString().toUpperCase()); } t.setTextColor(mTextColor); if (status == ShoppingContract.Status.BOUGHT) { t.setTextColor(mTextColorChecked); if (mShowStrikethrough) { // We have bought the item, // so we strike it through: // First convert text to 'spannable' t.setText(t.getText(), TextView.BufferType.SPANNABLE); Spannable str = (Spannable) t.getText(); // Strikethrough str.setSpan(new StrikethroughSpan(), 0, str.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); // apply color // TODO: How to get color from resource? // Drawable colorStrikethrough = context // .getResources().getDrawable(R.drawable.strikethrough); // str.setSpan(new ForegroundColorSpan(0xFF006600), 0, // str.setSpan(new ForegroundColorSpan // (getResources().getColor(R.color.darkgreen)), 0, // str.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); // color: 0x33336600 } if (i == 0 && mTextSuffixChecked != null) { // very simple t.append(mTextSuffixChecked); } } else { // item not bought: if (i == 0 && mTextSuffixUnchecked != null) { t.append(mTextSuffixUnchecked); } } } // we have a check box now.. more visual and gets the point across if (debug) { Log.i(TAG, "bindview: pos = " + cursor.getPosition()); } // set style for check box if (mShowCheckBox) { state.mCheckView.setVisibility(CheckBox.VISIBLE); state.mCheckView.setChecked(status == ShoppingContract.Status.BOUGHT); } else { state.mCheckView.setVisibility(CheckBox.GONE); } if (mMode == ShoppingActivity.MODE_IN_SHOP) { state.mNoCheckView.setVisibility(ImageView.GONE); } else { // mMode == ShoppingActivity.MODE_ADD_ITEMS if (status == ShoppingContract.Status.REMOVED_FROM_LIST) { state.mNoCheckView.setVisibility(ImageView.VISIBLE); if (mShowCheckBox) { // replace check box state.mCheckView.setVisibility(CheckBox.INVISIBLE); } } else { state.mNoCheckView.setVisibility(ImageView.INVISIBLE); } } } private void hideTextView(TextView view) { view.setVisibility(View.GONE); view.setText(""); } private class ClickableNoteSpan extends ClickableSpan { public void onClick(View view) { Intent i = new Intent(Intent.ACTION_VIEW); mItemRowState state = (mItemRowState) view.getTag(); int cursorpos = state.mCursorPos; if (debug) { Log.d(TAG, "Click on has_note: " + cursorpos); } mCursorItems.moveToPosition(cursorpos); long note_id = mCursorItems.getLong(ShoppingActivity.mStringItemsITEMID); Uri uri = ContentUris.withAppendedId(ShoppingContract.Notes.CONTENT_URI, note_id); i.setData(uri); Context context = getContext(); try { context.startActivity(i); } catch (ActivityNotFoundException e) { // we could add a simple edit note dialog, but for now... Dialog g = new DownloadAppDialog(context, R.string.notepad_not_available, R.string.notepad, R.string.notepad_package, R.string.notepad_website); g.show(); } } } private class ClickableItemSpan extends ClickableSpan { public void onClick(View view) { if (debug) { Log.d(TAG, "Click on description: "); } if (mListener != null) { mItemRowState state = (mItemRowState) view.getTag(); int cursorpos = state.mCursorPos; mListener.onCustomClick(mCursorItems, cursorpos, EditItemDialog.FieldType.ITEMNAME, view); } } public void updateDrawState(TextPaint ds) { // Override the parent's method to avoid having the text // in this span look like a link. } } private class SpannedStringBuilder extends SpannableStringBuilder { public SpannedStringBuilder appendSpannedString(Object o, CharSequence text) { int spanStart = length(); super.append(text); setSpan(o, spanStart, spanStart + text.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); return this; } public SpannedStringBuilder appendSpannedString(Object o, Object p, CharSequence text) { int spanStart = length(); super.append(text); setSpan(o, spanStart, spanStart + text.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); setSpan(p, spanStart, spanStart + text.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); return this; } } public boolean setViewValue(View view, Cursor cursor, int i) { int id = view.getId(); long price = 0; boolean hasPrice = false; String tags = null; String priceString = null; boolean hasTags = false; mItemRowState state = (mItemRowState) view.getTag(); if (mPriceVisibility == View.VISIBLE) { price = getQuantityPrice(cursor); hasPrice = (price != 0); } if (mTagsVisibility == View.VISIBLE) { tags = cursor.getString(ShoppingActivity.mStringItemsITEMTAGS); hasTags = !TextUtils.isEmpty(tags); } if (id == R.id.name) { boolean hasNote = cursor .getInt(ShoppingActivity.mStringItemsITEMHASNOTE) != 0; String name = cursor .getString(ShoppingActivity.mStringItemsITEMNAME); TextView tv = (TextView) view; SpannedStringBuilder name_etc = new SpannedStringBuilder(); name_etc.appendSpannedString(new ClickableItemSpan(), name); if (name.equalsIgnoreCase(mFilter)) { name_etc.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, name_etc.length(), 0); } if (hasNote) { Drawable d = getResources().getDrawable(R.drawable.ic_launcher_notepad_small); float ratio = d.getIntrinsicWidth() / d.getIntrinsicHeight(); d.setBounds(0, 0, (int) (ratio * mTextSize), (int) mTextSize); ImageSpan noteimgspan = new ImageSpan(d, ImageSpan.ALIGN_BASELINE); name_etc.appendSpannedString(noteimgspan, new ClickableNoteSpan(), "\u00A0"); } if (hasPrice) { // set price text while setting name, so that correct size is known below priceString = mPriceFormatter.format(price * 0.01d); state.mPriceView.setText(priceString); } if (hasPrice && !hasTags) { TextPaint paint = state.mPriceView.getPaint(); Rect bounds = new Rect(); ColorDrawable price_overlay = new ColorDrawable(); price_overlay.setAlpha(0); paint.getTextBounds(priceString, 0, priceString.length(), bounds); price_overlay.setBounds(0, 0, bounds.width(), bounds.height()); ImageSpan priceimgspan = new ImageSpan(price_overlay, ImageSpan.ALIGN_BASELINE); name_etc.appendSpannedString(priceimgspan, " "); } tv.setText(name_etc); tv.setMovementMethod(LinkMovementMethod.getInstance()); return true; } else if (id == R.id.price) { TextView tv = (TextView) view; if (hasPrice) { tv.setVisibility(View.VISIBLE); tv.setTextColor(mTextColorPrice); } else { hideTextView(tv); } return true; } else if (id == R.id.tags) { TextView tv = (TextView) view; if (hasTags) { tv.setVisibility(View.VISIBLE); tv.setTextColor(mTextColorPrice); tv.setText(tags); if (hasPrice) { // don't overlap the price RelativeLayout.LayoutParams rlp = (RelativeLayout.LayoutParams) tv.getLayoutParams(); rlp.addRule(RelativeLayout.LEFT_OF, R.id.price); } } else { hideTextView(tv); } return true; } else if (id == R.id.quantity) { String quantity = cursor.getString(ShoppingActivity.mStringItemsQUANTITY); TextView tv = (TextView) view; if (mQuantityVisibility == View.VISIBLE && !TextUtils.isEmpty(quantity)) { tv.setVisibility(View.VISIBLE); // tv.setTextColor(mPriceTextColor); tv.setText(quantity + " "); } else { hideTextView(tv); } return true; } else if (id == R.id.units) { String units = cursor.getString(ShoppingActivity.mStringItemsITEMUNITS); String quantity = cursor.getString(ShoppingActivity.mStringItemsQUANTITY); TextView tv = (TextView) view; // looks more natural if you only show units when showing qty. if (mUnitsVisibility == View.VISIBLE && mQuantityVisibility == View.VISIBLE && !TextUtils.isEmpty(units) && !TextUtils.isEmpty(quantity)) { tv.setVisibility(View.VISIBLE); // tv.setTextColor(mPriceTextColor); tv.setText(units + " "); } else { hideTextView(tv); } return true; } else if (id == R.id.priority) { String priority = cursor.getString(ShoppingActivity.mStringItemsPRIORITY); TextView tv = (TextView) view; if (mPriorityVisibility == View.VISIBLE && !TextUtils.isEmpty(priority)) { tv.setVisibility(View.VISIBLE); tv.setTextColor(mTextColorPriority); tv.setText("-" + priority + "- "); } else { hideTextView(tv); } return true; } else { return false; } } @Override public void setViewBinder(ViewBinder viewBinder) { throw new RuntimeException("this adapter implements setViewValue"); } } private class SearchQueryListener extends SearchViewCompat.OnQueryTextListenerCompat { public boolean onQueryTextChange(String query) { boolean isIconified = SearchViewCompat.isIconified(mSearchView); String prevFilter = mFilter; if (isIconified) { // Something tries to restore the query text after the drawer is dismissed, but // it doesn't re-expand the search view. Force the query string empty when it is // not shown, and switch back to non-search mode. if (query != null && query.length() > 0) { SearchViewCompat.setQuery(mSearchView, "", false); } query = null; if (mInSearch) { mMode = mModeBeforeSearch; mInSearch = false; } } if (mInSearch == false && !isIconified) { mInSearch = true; mModeBeforeSearch = mMode; mMode = ShoppingActivity.MODE_ADD_ITEMS; } if (query == null || query.length() == 0) { mFilter = null; } else { mFilter = query; } if ((prevFilter == null && mFilter == null) || (prevFilter != null && prevFilter.equals(mFilter))) { return true; } fillItems(mCursorActivity, mListId); return true; } public boolean onQueryTextSubmit(String query) { if (query.length() > 0) { insertNewItem(mCursorActivity, query, null, null, null, null); SearchViewCompat.setQuery(mSearchView, "", false); fillItems(mCursorActivity, mListId); } return true; } } private class SearchDismissedListener extends SearchViewCompat.OnCloseListenerCompat { public boolean onClose() { if (mInSearch) { mMode = mModeBeforeSearch; } mInSearch = false; mFilter = null; fillItems(mCursorActivity, mListId); // invalidate(); return false; } } private View mSearchView; public View getSearchView() { Context context = getContext(); if (PreferenceActivity.getUsingHoloSearchFromPrefs(context)) { mSearchView = SearchViewCompat.newSearchView(mCursorActivity); if (mSearchView != null) { SearchViewCompat.setSubmitButtonEnabled(mSearchView, true); SearchViewCompat.setInputType(mSearchView, PreferenceActivity.getSearchInputTypeFromPrefs(context)); SearchViewCompat.setOnQueryTextListener(mSearchView, new SearchQueryListener()); SearchViewCompat.setOnCloseListener(mSearchView, new SearchDismissedListener()); SearchViewCompat.setImeOptions(mSearchView, EditorInfo.IME_ACTION_UNSPECIFIED); } } return mSearchView; } private void disposeItemsCursor() { if (mCursorActivity != null) { mCursorActivity.stopManagingCursor(mCursorItems); mCursorActivity = null; } mCursorItems.deactivate(); if (!mCursorItems.isClosed()) { mCursorItems.close(); } mCursorItems = null; } public ShoppingItemsView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } public ShoppingItemsView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public ShoppingItemsView(Context context) { super(context); init(); } private void init() { mItemHeightNormal = 45; mItemHeightHalf = mItemHeightNormal / 2; mItemHeightExpanded = 90; // Remember standard divider mDefaultDivider = getDivider(); mSyncSupport = ((ShoppingApplication) getContext().getApplicationContext()).dependencies().getSyncSupport(getContext()); } public void initTotals() { // Can't be called during init because that happens while // still inflating the parent activity, so findViewById // doesn't work yet. mTotalsHandler = new ShoppingTotalsHandler(this); } public void setActionBarListener(ActionBarListener listener) { mActionBarListener = listener; } public void setUndoListener(UndoListener listener) { mUndoListener = listener; } public void onResume() { setFastScrollEnabled(PreferenceActivity.getFastScrollEnabledFromPrefs(getContext())); } public void onPause() { } public long getListId() { return mListId; } @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { CursorLoader loader = new CursorLoader(mCursorActivity); createItemsCursor(mListId, loader); return loader; } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor items) { // Get a cursor for all items that are contained // in currently selected shopping list. mCursorItems = items; // Activate the following for a striped list. // setupListStripes(mListItems, this); if (mCursorItems == null) { Log.e(TAG, "missing shopping provider"); setAdapter(new ArrayAdapter<>(this.getContext(), android.R.layout.simple_list_item_1, new String[]{"no shopping provider"})); return; } int layout_row = R.layout.list_item_shopping_item; int size = PreferenceActivity.getFontSizeFromPrefs(getContext()); if (size < 3) { layout_row = R.layout.list_item_shopping_item_small; } Context context = getContext(); // If background is light, we apply the light holo theme to widgets. // determine color from text color: int gray = (Color.red(mTextColor) + Color.green(mTextColor) + Color.blue(mTextColor)); if (gray < 3 * 128) { // dark text color <-> light background color => use light holo theme. context = new ContextThemeWrapper(context, android.R.style.Theme_Holo_Light); } mSimpleCursorAdapter adapter = (mSimpleCursorAdapter) getAdapter(); if (adapter != null) { adapter.swapCursor(mCursorItems); } else { adapter = new mSimpleCursorAdapter( context, // Use a template that displays a text view layout_row, // Give the cursor to the list adapter mCursorItems, // Map the IMAGE and NAME to... new String[]{ContainsFull.ITEM_NAME, /* * ContainsFull.ITEM_IMAGE * , */ ContainsFull.ITEM_TAGS, ContainsFull.ITEM_PRICE, ContainsFull.QUANTITY, ContainsFull.PRIORITY, ContainsFull.ITEM_UNITS }, // the view defined in the XML template new int[]{R.id.name, /* R.id.image_URI, */R.id.tags, R.id.price, R.id.quantity, R.id.priority, R.id.units} ); setAdapter(adapter); } if (mFocusItemId != -1) { // Set the item that we have just selected: // Get position of ID: mCursorItems.moveToPosition(-1); while (mCursorItems.moveToNext()) { if (mCursorItems.getLong(ShoppingActivity.mStringItemsITEMID) == mFocusItemId) { int pos = mCursorItems.getPosition(); // scroll item near top, but not all the way to top, to provide context. pos = Math.max(pos - 3, 0); postDelayedSetSelection(pos); break; } } if (!mInSearch) { mFocusItemId = -1; } } } @Override public void onLoaderReset(Loader<Cursor> loader) { mCursorItems = null; } /** * @param activity Activity to manage the cursor. * @param listId * @return */ public void fillItems(Activity activity, long listId) { mCursorItems = null; mCursorActivity = activity; mListId = listId; setSearchModePref(); activity.getLoaderManager().restartLoader(ShoppingActivity.LOADER_ITEMS, null, this); updateTotal(); } private void setSearchModePref() { // this is not a real user preference, just used to communicate // current app state to the content provider. SharedPreferences sp = mCursorActivity.getSharedPreferences( "org.openintents.shopping_preferences", Context.MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putBoolean("_searching", mInSearch); editor.apply(); } private Cursor createItemsCursor(long listId, CursorLoader loader) { String sortOrder = PreferenceActivity.getSortOrderFromPrefs(this .getContext(), mMode, listId); boolean hideBought = PreferenceActivity .getHideCheckedItemsFromPrefs(this.getContext()); String selection; String[] selection_args = new String[]{String.valueOf(listId)}; if (mFilter != null) { selection = "list_id = ? AND " + ContainsFull.ITEM_NAME + " like '%" + ShoppingProvider.escapeSQLChars(mFilter) + "%' ESCAPE '`'"; } else if (mMode == ShoppingActivity.MODE_IN_SHOP) { if (hideBought) { selection = "list_id = ? AND " + Contains.STATUS + " == " + Status.WANT_TO_BUY; } else { selection = "list_id = ? AND " + Contains.STATUS + " <> " + Status.REMOVED_FROM_LIST; } } else { selection = "list_id = ? "; } if (loader != null) { loader.setUri(ContainsFull.CONTENT_URI); loader.setProjection(ShoppingActivity.PROJECTION_ITEMS); loader.setSelection(selection); loader.setSelectionArgs(selection_args); loader.setSortOrder(sortOrder); return null; } return getContext().getContentResolver().query( ContainsFull.CONTENT_URI, ShoppingActivity.PROJECTION_ITEMS, selection, selection_args, sortOrder); } /** * Set theme according to Id. * * @param themeName */ public void setListTheme(String themeName) { int size = PreferenceActivity.getFontSizeFromPrefs(getContext()); // backward compatibility: if (themeName == null) { setLocalStyle(R.style.Theme_ShoppingList, size); } else if (themeName.equals("1")) { setLocalStyle(R.style.Theme_ShoppingList, size); } else if (themeName.equals("2")) { setLocalStyle(R.style.Theme_ShoppingList_Classic, size); } else if (themeName.equals("3")) { setLocalStyle(R.style.Theme_ShoppingList_Android, size); } else { // New styles: boolean themeFound = setRemoteStyle(themeName, size); if (!themeFound) { // Some error occured, let's use default style: setLocalStyle(R.style.Theme_ShoppingList, size); } } invalidate(); if (mCursorItems != null) { requery(); } } private void setLocalStyle(int styleResId, int size) { String styleName = getResources().getResourceName(styleResId); boolean themefound = setRemoteStyle(styleName, size); if (!themefound) { // Actually this should never happen. Log.e(TAG, "Local theme not found: " + styleName); } } private boolean setRemoteStyle(String styleName, int size) { if (TextUtils.isEmpty(styleName)) { if (debug) { Log.e(TAG, "Empty style name: " + styleName); } return false; } mPackageManager = getContext().getPackageManager(); mPackageName = ThemeUtils.getPackageNameFromStyle(styleName); if (mPackageName == null) { Log.e(TAG, "Invalid style name: " + styleName); return false; } Context c = null; try { c = getContext().createPackageContext(mPackageName, 0); } catch (NameNotFoundException e) { Log.e(TAG, "Package for style not found: " + mPackageName + ", " + styleName); return false; } Resources res = c.getResources(); int themeid = res.getIdentifier(styleName, null, null); if (themeid == 0) { Log.e(TAG, "Theme name not found: " + styleName); return false; } try { mThemeAttributes = new ThemeAttributes(c, mPackageName, themeid); mTextTypeface = mThemeAttributes.getString(ThemeShoppingList.textTypeface); mCurrentTypeface = createTypeface(mTextTypeface); mTextUpperCaseFont = mThemeAttributes.getBoolean( ThemeShoppingList.textUpperCaseFont, false); mTextColor = mThemeAttributes.getColor(ThemeShoppingList.textColor, android.R.color.white); mTextColorPrice = mThemeAttributes.getColor(ThemeShoppingList.textColorPrice, android.R.color.white); // Use color of price if color of priority has not been defined mTextColorPriority = mThemeAttributes.getColor(ThemeShoppingList.textColorPriority, mTextColorPrice); if (size == 0) { mTextSize = getTextSizeTiny(mThemeAttributes); } else if (size == 1) { mTextSize = getTextSizeSmall(mThemeAttributes); } else if (size == 2) { mTextSize = getTextSizeMedium(mThemeAttributes); } else { mTextSize = getTextSizeLarge(mThemeAttributes); } if (debug) { Log.d(TAG, "textSize: " + mTextSize); } mTextColorChecked = mThemeAttributes.getColor(ThemeShoppingList.textColorChecked, android.R.color.white); mShowCheckBox = mThemeAttributes.getBoolean(ThemeShoppingList.showCheckBox, true); mShowStrikethrough = mThemeAttributes.getBoolean( ThemeShoppingList.textStrikethroughChecked, false); mTextSuffixUnchecked = mThemeAttributes .getString(ThemeShoppingList.textSuffixUnchecked); mTextSuffixChecked = mThemeAttributes .getString(ThemeShoppingList.textSuffixChecked); // field was named divider, until a conflict with the appcompat library // forced us to rename it. To continue to support old themes, check for // shopping_divider first, but if it's not found, check for divider also. int divider = mThemeAttributes.getInteger(ThemeShoppingList.shopping_divider, 0); if (divider == 0) { divider = mThemeAttributes.getInteger(ThemeShoppingList.divider, 0); } Drawable div; if (divider > 0) { div = getResources().getDrawable(divider); } else if (divider < 0) { div = null; } else { div = mDefaultDivider; } setDivider(div); return true; } catch (UnsupportedOperationException e) { // This exception is thrown e.g. if one attempts // to read an integer attribute as dimension. Log.e(TAG, "UnsupportedOperationException", e); return false; } catch (NumberFormatException e) { // This exception is thrown e.g. if one attempts // to read a string as integer. Log.e(TAG, "NumberFormatException", e); return false; } } private Typeface createTypeface(String typeface) { Typeface newTypeface = null; try { // Look for special cases: if ("monospace".equals(typeface)) { newTypeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL); } else if ("sans".equals(typeface)) { newTypeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL); } else if ("serif".equals(typeface)) { newTypeface = Typeface.create(Typeface.SERIF, Typeface.NORMAL); } else if (!TextUtils.isEmpty(typeface)) { try { if (debug) { Log.d(TAG, "Reading typeface: package: " + mPackageName + ", typeface: " + typeface); } Resources remoteRes = mPackageManager .getResourcesForApplication(mPackageName); newTypeface = Typeface.createFromAsset(remoteRes .getAssets(), typeface); if (debug) { Log.d(TAG, "Result: " + newTypeface); } } catch (NameNotFoundException e) { Log.e(TAG, "Package not found for Typeface", e); } } } catch (RuntimeException e) { Log.e(TAG, "type face can't be made " + typeface); } return newTypeface; } /** * Must be called after setListTheme(); */ public void applyListTheme() { if (mThemedBackground != null) { mBackgroundPadding = mThemeAttributes.getDimensionPixelOffset( ThemeShoppingList.backgroundPadding, -1); int backgroundPaddingLeft = mThemeAttributes.getDimensionPixelOffset( ThemeShoppingList.backgroundPaddingLeft, mBackgroundPadding); int backgroundPaddingTop = mThemeAttributes.getDimensionPixelOffset( ThemeShoppingList.backgroundPaddingTop, mBackgroundPadding); int backgroundPaddingRight = mThemeAttributes.getDimensionPixelOffset( ThemeShoppingList.backgroundPaddingRight, mBackgroundPadding); int backgroundPaddingBottom = mThemeAttributes.getDimensionPixelOffset( ThemeShoppingList.backgroundPaddingBottom, mBackgroundPadding); try { Resources remoteRes = mPackageManager .getResourcesForApplication(mPackageName); int resid = mThemeAttributes.getResourceId(ThemeShoppingList.background, 0); if (resid != 0) { Drawable d = remoteRes.getDrawable(resid); mThemedBackground.setBackgroundDrawable(d); } else { // remove background mThemedBackground.setBackgroundResource(0); } } catch (NameNotFoundException e) { Log.e(TAG, "Package not found for Theme background.", e); } catch (Resources.NotFoundException e) { Log.e(TAG, "Resource not found for Theme background.", e); } // Apply padding if (mBackgroundPadding >= 0 || backgroundPaddingLeft >= 0 || backgroundPaddingTop >= 0 || backgroundPaddingRight >= 0 || backgroundPaddingBottom >= 0) { mThemedBackground.setPadding(backgroundPaddingLeft, backgroundPaddingTop, backgroundPaddingRight, backgroundPaddingBottom); } else { // 9-patches do the padding automatically // todo clear padding } } } private float getTextSizeTiny(ThemeAttributes ta) { float size = ta.getDimensionPixelOffset(ThemeShoppingList.textSizeTiny, -1); if (size == -1) { // Try to obtain from small: size = (12f / 18f) * getTextSizeSmall(ta); } return size; } private float getTextSizeSmall(ThemeAttributes ta) { float size = ta.getDimensionPixelOffset( ThemeShoppingList.textSizeSmall, -1); if (size == -1) { // Try to obtain from small: size = (18f / 23f) * getTextSizeMedium(ta); } return size; } private float getTextSizeMedium(ThemeAttributes ta) { final float scale = getResources().getDisplayMetrics().scaledDensity; return ta.getDimensionPixelOffset( ThemeShoppingList.textSizeMedium, (int) (23 * scale + 0.5f)); } private float getTextSizeLarge(ThemeAttributes ta) { float size = ta.getDimensionPixelOffset( ThemeShoppingList.textSizeLarge, -1); if (size == -1) { // Try to obtain from small: size = (28f / 23f) * getTextSizeMedium(ta); } return size; } public void setThemedBackground(View background) { mThemedBackground = background; } /** * set the status of all items according to the parameter * * @param on if true all want_to_buy items are set to bought, if false all bought items are set to want_to_buy */ public void toggleAllItems(boolean on) { int op_type = on ? SnackbarUndoMultipleItemStatusOperation.MARK_ALL : SnackbarUndoMultipleItemStatusOperation.UNMARK_ALL; SnackbarUndoMultipleItemStatusOperation op = null; if (mUndoListener != null) { op = new SnackbarUndoMultipleItemStatusOperation(this, mCursorActivity, op_type, mListId, false); } for (int i = 0; i < mCursorItems.getCount(); i++) { mCursorItems.moveToPosition(i); long oldstatus = mCursorItems .getLong(ShoppingActivity.mStringItemsSTATUS); // Toggle status ON: // bought -> bought // want_to_buy -> bought // removed_from_list -> removed_from_list // Toggle status OFF: // bought -> want_to_buy // want_to_buy -> want_to_buy // removed_from_list -> removed_from_list long newstatus; boolean doUpdate; if (on) { newstatus = ShoppingContract.Status.BOUGHT; doUpdate = (oldstatus == ShoppingContract.Status.WANT_TO_BUY); } else { newstatus = ShoppingContract.Status.WANT_TO_BUY; doUpdate = (oldstatus == ShoppingContract.Status.BOUGHT); } if (doUpdate) { final ContentValues values = new ContentValues(); values.put(ShoppingContract.Contains.STATUS, newstatus); if (debug) { Log.d(TAG, "update row " + mCursorItems.getString(0) + ", newstatus " + newstatus); } final Uri itemUri = Uri.withAppendedPath(Contains.CONTENT_URI, mCursorItems.getString(0)); getContext().getContentResolver().update( itemUri, values, null, null ); pushUpdatedItemToWear(values, itemUri); } } requery(); invalidate(); if (mUndoListener != null) { mUndoListener.onUndoAvailable(op); } } public void toggleItemBought(int position) { boolean shouldFocusItem = false; if (mCursorItems.getCount() <= position) { Log.e(TAG, "toggle inexistent item. Probably clicked too quickly?"); return; } mCursorItems.moveToPosition(position); long oldstatus = mCursorItems .getLong(ShoppingActivity.mStringItemsSTATUS); // Toggle status depending on mode: long newstatus = ShoppingContract.Status.WANT_TO_BUY; if (mMode == ShoppingActivity.MODE_IN_SHOP) { if (oldstatus == ShoppingContract.Status.WANT_TO_BUY) { newstatus = ShoppingContract.Status.BOUGHT; } // else old was BOUGHT, new should be WANT_TO_BUY, which is the default. } else { // MODE_ADD_ITEMS or MODE_PICK_ITEMS_DLG // when we are in integrated add items mode, all three states // might be displayed, but the user can only create two of them. // want_to_buy-> removed_from_list // bought -> want_to_buy // removed_from_list -> want_to_buy if (oldstatus == ShoppingContract.Status.WANT_TO_BUY) { newstatus = ShoppingContract.Status.REMOVED_FROM_LIST; shouldFocusItem = mInSearch && mFilter != null && mFilter.length() > 0; } else { // old is REMOVE_FROM_LIST or BOUGHT, new is WANT_TO_BUY, which is the default. if (mInSearch) { shouldFocusItem = true; } } } String contains_id = mCursorItems.getString(0); final ContentValues values = new ContentValues(); values.put(ShoppingContract.Contains.STATUS, newstatus); if (debug) { Log.d(TAG, "update row " + mCursorItems.getString(0) + ", newstatus " + newstatus); } if (mInSearch && newstatus == ShoppingContract.Status.WANT_TO_BUY) { long item_id = mCursorItems.getLong(ShoppingActivity.mStringItemsITEMID); ShoppingUtils.addDefaultsToAddedItem(getContext(), mListId, item_id); } if (shouldFocusItem) { mFocusItemId = mCursorItems.getLong(ShoppingActivity.mStringItemsITEMID); } final Uri itemUri = Uri.withAppendedPath(Contains.CONTENT_URI, contains_id); getContext().getContentResolver().update( itemUri, values, null, null ); pushUpdatedItemToWear(values, itemUri); boolean affectsSort = PreferenceActivity.prefsStatusAffectsSort(getContext(), mMode); boolean hidesItem = true /* TODO */; if (mUndoListener != null && (affectsSort || hidesItem)) { String item_name = mCursorItems.getString(ShoppingActivity.mStringItemsITEMNAME); SnackbarUndoSingleItemStatusOperation op = new SnackbarUndoSingleItemStatusOperation(this, getContext(), contains_id, item_name, oldstatus, newstatus, 0, false); mUndoListener.onUndoAvailable(op); } requery(); if (affectsSort) { invalidate(); } } public boolean cleanupList() { boolean nothingdeleted; SnackbarUndoMultipleItemStatusOperation op = null; if (mUndoListener != null) { op = new SnackbarUndoMultipleItemStatusOperation(this, mCursorActivity, SnackbarUndoMultipleItemStatusOperation.CLEAN_LIST, mListId, false); } // by changing state ContentValues values = new ContentValues(); values.put(Contains.STATUS, Status.REMOVED_FROM_LIST); if (PreferenceActivity.getResetQuantity(getContext())) { values.put(Contains.QUANTITY, ""); } nothingdeleted = getContext().getContentResolver().update( Contains.CONTENT_URI, values, ShoppingContract.Contains.LIST_ID + " = " + mListId + " AND " + ShoppingContract.Contains.STATUS + " = " + ShoppingContract.Status.BOUGHT, null ) == 0; requery(); if (mUndoListener != null) { mUndoListener.onUndoAvailable(op); } return !nothingdeleted; } /** * @param activity Activity to manage new Cursor. * @param newItem * @param quantity * @param price * @param barcode */ public void insertNewItem(Activity activity, String newItem, String quantity, String priority, String price, String barcode) { String list_id = null; if (PreferenceActivity.getCompleteFromCurrentListOnlyFromPrefs(getContext())) { list_id = String.valueOf(mListId); } newItem = newItem.trim(); long itemId = ShoppingUtils.updateOrCreateItem(getContext(), newItem, null, price, barcode, list_id); if (debug) { Log.i(TAG, "Insert new item. " + " itemId = " + itemId + ", listId = " + mListId); } boolean resetQuantity = PreferenceActivity.getResetQuantity(getContext()); ShoppingUtils.addItemToList(getContext(), itemId, mListId, Status.WANT_TO_BUY, priority, quantity, false, false, resetQuantity); ShoppingUtils.addDefaultsToAddedItem(getContext(), mListId, itemId); mFocusItemId = itemId; fillItems(activity, mListId); } public boolean isWearSupportAvailable() { return mSyncSupport != null && mSyncSupport.isAvailable(); } public void pushItemsToWear() { if (mSyncSupport.isAvailable()) { new Thread() { @Override public void run() { Cursor cursor = createItemsCursor(mListId, null); Log.d(TAG, "pushing " + cursor.getCount() + " items"); cursor.moveToFirst(); while (cursor.moveToNext()) { mSyncSupport.pushListItem(mListId, cursor); } } }.start(); } } private void pushUpdatedItemToWear(final ContentValues values, final Uri itemUri) { if (mSyncSupport.isAvailable() && mSyncSupport.isSyncEnabled()) new Thread() { @Override public void run() { mSyncSupport.updateListItem(mListId, itemUri, values); } }.start(); } /** * Post setSelection delayed, because onItemSelected() may be called more * than once, leading to fillItems() being called more than once as well. * Posting delayed ensures that items added through intents that return * results (like a barcode scanner) are put into visible position. * * @param pos */ void postDelayedSetSelection(final int pos) { // set immediately setSelection(pos); // if for any reason this does not work, a delayed version // will succeed: postDelayed(new Runnable() { @Override public void run() { setSelection(pos); } }, 1000); } public void requery() { if (debug) { Log.d(TAG, "requery()"); } // Test for null pointer exception (issue 313) if (mCursorItems != null) { mCursorItems.requery(); updateTotal(); if (mUpdateLastListPosition > 0) { if (debug) { Log.d(TAG, "Restore list position: pos: " + mLastListPosition + ", top: " + mLastListTop + ", tries: " + mUpdateLastListPosition); } setSelectionFromTop(mLastListPosition, mLastListTop); mUpdateLastListPosition--; } } } /** * Update the text fields for "Total:" and "Checked:" with corresponding * price information. */ public void updateTotal() { if (debug) { Log.d(TAG, "updateTotal()"); } mTotalsHandler.update(mCursorActivity.getLoaderManager(), mListId); } public void updateNumChecked(long numChecked, long numUnchecked) { mNumChecked = numChecked; mNumUnchecked = numUnchecked; // Update ActionBar in ShoppingActivity // for the "Clean up list" command if (mActionBarListener != null && !mInSearch) { mActionBarListener.updateActionBar(); } } private long getQuantityPrice(Cursor cursor) { long price = cursor.getLong(ShoppingActivity.mStringItemsITEMPRICE); if (price != 0) { String quantityString = cursor .getString(ShoppingActivity.mStringItemsQUANTITY); if (!TextUtils.isEmpty(quantityString)) { try { double quantity = Double.parseDouble(quantityString); price = (long) (price * quantity); } catch (NumberFormatException e) { // do nothing } } } return price; } private OnCustomClickListener mListener; private boolean mDragAndDropEnabled; public void setCustomClickListener(OnCustomClickListener listener) { mListener = listener; } public interface OnCustomClickListener { public void onCustomClick(Cursor c, int pos, EditItemDialog.FieldType field, View v); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (mDragAndDropEnabled) { if (mDragListener != null || mDropListener != null) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: int x = (int) ev.getX(); int y = (int) ev.getY(); int itemnum = pointToPosition(x, y); if (itemnum == AdapterView.INVALID_POSITION) { break; } ViewGroup item = (ViewGroup) getChildAt(itemnum - getFirstVisiblePosition()); mDragPoint = y - item.getTop(); mCoordOffset = ((int) ev.getRawY()) - y; item.setDrawingCacheEnabled(true); Bitmap bitmap = Bitmap.createBitmap(item.getDrawingCache()); startDragging(bitmap, y); mDragPos = itemnum; mFirstDragPos = mDragPos; mHeight = getHeight(); int touchSlop = mTouchSlop; mUpperBound = Math.min(y - touchSlop, mHeight / 3); mLowerBound = Math.max(y + touchSlop, mHeight * 2 / 3); return false; default: break; } } } return super.onInterceptTouchEvent(ev); } private int myPointToPosition(int x, int y) { if (y < 0) { int pos = myPointToPosition(x, y + mItemHeightNormal); if (pos > 0) { return pos - 1; } } Rect frame = mTempRect; final int count = getChildCount(); for (int i = count - 1; i >= 0; i--) { final View child = getChildAt(i); child.getHitRect(frame); if (frame.contains(x, y)) { return getFirstVisiblePosition() + i; } } return INVALID_POSITION; } private int getItemForPosition(int y) { int adjustedy = y - mDragPoint - mItemHeightHalf; int pos = myPointToPosition(0, adjustedy); if (pos >= 0) { if (pos <= mFirstDragPos) { pos += 1; } } else if (adjustedy < 0) { pos = 0; } return pos; } private void adjustScrollBounds(int y) { if (y >= mHeight / 3) { mUpperBound = mHeight / 3; } if (y <= mHeight * 2 / 3) { mLowerBound = mHeight * 2 / 3; } } private void unExpandViews(boolean deletion) { for (int i = 0; ; i++) { View v = getChildAt(i); if (v == null) { if (deletion) { int position = getFirstVisiblePosition(); int y = getChildAt(0).getTop(); setAdapter(getAdapter()); setSelectionFromTop(position, y); } layoutChildren(); v = getChildAt(i); if (v == null) { break; } } ViewGroup.LayoutParams params = v.getLayoutParams(); params.height = mItemHeightNormal; v.setLayoutParams(params); v.setVisibility(View.VISIBLE); } } private void doExpansion() { int childnum = mDragPos - getFirstVisiblePosition(); if (mDragPos > mFirstDragPos) { childnum++; } View first = getChildAt(mFirstDragPos - getFirstVisiblePosition()); for (int i = 0; ; i++) { View vv = getChildAt(i); if (vv == null) { break; } int height = mItemHeightNormal; int visibility = View.VISIBLE; if (vv.equals(first)) { if (mDragPos == mFirstDragPos) { visibility = View.INVISIBLE; } else { height = 1; } } else if (i == childnum) { if (mDragPos < getCount() - 1) { height = mItemHeightExpanded; } } ViewGroup.LayoutParams params = vv.getLayoutParams(); params.height = height; vv.setLayoutParams(params); vv.setVisibility(visibility); } } @Override public boolean onTouchEvent(MotionEvent ev) { if ((mDragListener != null || mDropListener != null) && mDragView != null) { int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: Rect r = mTempRect; mDragView.getDrawingRect(r); stopDragging(); if (mDropListener != null && mDragPos >= 0 && mDragPos < getCount()) { mDropListener.drop(mFirstDragPos, mDragPos); } unExpandViews(false); break; case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: int x = (int) ev.getX(); int y = (int) ev.getY(); dragView(x, y); int itemnum = getItemForPosition(y); if (itemnum >= 0) { if (action == MotionEvent.ACTION_DOWN || itemnum != mDragPos) { if (mDragListener != null) { mDragListener.drag(mDragPos, itemnum); } mDragPos = itemnum; doExpansion(); } int speed = 0; adjustScrollBounds(y); if (y > mLowerBound) { // scroll the list up a bit speed = y > (mHeight + mLowerBound) / 2 ? 16 : 4; } else if (y < mUpperBound) { // scroll the list down a bit speed = y < mUpperBound / 2 ? -16 : -4; } if (speed != 0) { int ref = pointToPosition(0, mHeight / 2); if (ref == AdapterView.INVALID_POSITION) { // we hit a divider or an invisible view, check // somewhere else ref = pointToPosition(0, mHeight / 2 + getDividerHeight() + 64); } View v = getChildAt(ref - getFirstVisiblePosition()); if (v != null) { int pos = v.getTop(); setSelectionFromTop(ref, pos - speed); } } } break; default: break; } return true; } return super.onTouchEvent(ev); } private void startDragging(Bitmap bm, int y) { stopDragging(); mWindowParams = new WindowManager.LayoutParams(); mWindowParams.gravity = Gravity.TOP; mWindowParams.x = 0; mWindowParams.y = y - mDragPoint + mCoordOffset; mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT; mWindowParams.width = WindowManager.LayoutParams.WRAP_CONTENT; mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; mWindowParams.format = PixelFormat.TRANSLUCENT; mWindowParams.windowAnimations = 0; Context context = getContext(); ImageView v = new ImageView(context); int backGroundColor = context.getResources() .getColor(R.color.darkgreen); v.setBackgroundColor(backGroundColor); v.setImageBitmap(bm); mDragBitmap = bm; mWindowManager = (WindowManager) context .getSystemService(Context.WINDOW_SERVICE); mWindowManager.addView(v, mWindowParams); mDragView = v; } private void dragView(int x, int y) { mWindowParams.y = y - mDragPoint + mCoordOffset; mWindowManager.updateViewLayout(mDragView, mWindowParams); } private void stopDragging() { if (mDragView != null) { WindowManager wm = (WindowManager) getContext().getSystemService( Context.WINDOW_SERVICE); wm.removeView(mDragView); mDragView.setImageDrawable(null); mDragView = null; } if (mDragBitmap != null) { mDragBitmap.recycle(); mDragBitmap = null; } } public void setDragListener(DragListener l) { mDragListener = l; } public void setDropListener(DropListener l) { mDropListener = l; } public interface DragListener { void drag(int from, int to); } public interface DropListener { void drop(int from, int to); } public interface RemoveListener { void remove(int which); } public interface ActionBarListener { void updateActionBar(); } }