/* * @copyright 2011 Philip Warner * @license GNU General Public License * * This file is part of Book Catalogue. * * Book Catalogue is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Book Catalogue is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Book Catalogue. If not, see <http://www.gnu.org/licenses/>. */ package com.eleybourn.bookcatalogue; import java.io.Serializable; import java.util.ArrayList; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import com.eleybourn.bookcatalogue.compat.BookCatalogueListActivity; import com.eleybourn.bookcatalogue.utils.Logger; import com.eleybourn.bookcatalogue.utils.Utils; import com.eleybourn.bookcatalogue.utils.ViewTagger; import com.eleybourn.bookcatalogue.widgets.TouchListView; /** * Base class for editing a list of objects. The inheritor must specify a view id * and a row view id to the constructor of this class. Each view can have the * following sub-view IDs present which will be automatically handled. Optional * IDs are noted: * * Main View: * - cancel * - confirm * - add (OPTIONAL) * * Row View (must have layout ID set to android:id="@+id/row"): * - position (OPTIONAL) * - up (OPTIONAL) * - down (OPTIONAL) * - delete (OPTIONAL) * * The row view is tagged using TAG_POSITION, defined in strings.xml, to save the rows position for * use when moving the row up/down or deleting it. * * Abstract methods are defined for specific tasks (Add, Save, Load etc). While would * be tempting to add local implementations the java generic model seems to prevent this. * * This Activity uses TouchListView from CommonsWare which is in turn based on Android code * for TouchIntercptor which was (reputedly) removed in Android 2.2. * * For this code to work, the main view must contain: * - a TouchListView with id = @+id/android:list * - the TouchListView must have the following attributes: * tlv:grabber="@+id/<SOME ID FOR AN IMAGE>" (eg. "@+id/grabber") * tlv:remove_mode="none" * tlv:normal_height="64dip" ---- or some simlar value * * Each row view must have: * - an ID of @+id/row * - an ImageView with an ID of "@+id/<SOME ID FOR AN IMAGE>" (eg. "@+id/grabber") * - (OPTIONAL) a subview with an ID of "@+id/row_details"; when clicked, this will result * in the onRowClick event. * * @author Philip Warner * * @param <T> */ abstract public class EditObjectList<T extends Serializable> extends BookCatalogueListActivity { // List protected ArrayList<T> mList = null; // Adapter used to manage list protected ArrayAdapter<T> mAdapter; // DB connection protected CatalogueDBAdapter mDbHelper; protected String mBookTitle; protected String mBookTitleLabel; // The key to use in the Bundle to get the array private String mKey; // The resource ID for the base view private int mBaseViewId; // The resource ID for the row view private int mRowViewId; // Row ID... mainly used (if list is from a book) to know if book is new. protected Long mRowId = null; /** * Called when user clicks the 'Add' button (if present). * * @param v The view that was clicked ('add' button). * * @return True if activity should exit, false to abort exit. */ abstract protected void onAdd(View v); /** * Call to set up the row view. * * @param target The target row view object * @param object The object (or type T) from which to draw values. */ abstract protected void onSetupView(View target, T object); /** * Called when an otherwise inactive part of the row is clicked. * * @param target The view clicked * @param object The object associated with this row */ abstract protected void onRowClick(View target, int position, T object); /** * Called when user clicks the 'Save' button (if present). Primary task is * to return a boolean indicating it is OK to continue. * * Can be overridden to perform other checks. * * @param i A newly created Intent to store output if necessary. * * @return True if activity should exit, false to abort exit. */ protected boolean onSave(Intent intent) { return true; }; /** * Called when user presses 'Cancel' button if present. Primary task is * return a boolean indicating it is OK to continue. * * Can be overridden to perform other checks. * * @return True if activity should exit, false to abort exit. */ protected boolean onCancel() { return true;}; /** * Called when the list had been modified in some way. */ protected void onListChanged() { }; /** * Called to get the list if it was not in the intent. */ protected ArrayList<T> getList() { return null; }; /** * Constructor * * @param baseViewId Resource id of base view * @param rowViewId Resource id of row view */ protected EditObjectList(String key, int baseViewId, int rowViewId) { mKey = key; mBaseViewId = baseViewId; mRowViewId = rowViewId; } /** * Update the current list */ protected void setList(ArrayList<T> newList) { final int savedRow = getListView().getFirstVisiblePosition(); View v = getListView().getChildAt(0); final int savedTop = v == null ? 0 : v.getTop(); mList = newList; // Set up list handling this.mAdapter = new ListAdapter(this, mRowViewId, mList); setListAdapter(this.mAdapter); getListView().post(new Runnable() { @Override public void run() { getListView().setSelectionFromTop(savedRow, savedTop); }}); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); try { // Setup the DB mDbHelper = new CatalogueDBAdapter(this); mDbHelper.open(); // Set the view setContentView(mBaseViewId); // Add handlers for 'Save', 'Cancel' and 'Add' setupListener(R.id.confirm, mSaveListener); setupListener(R.id.cancel, mCancelListener); setupListener(R.id.add, mAddListener); // Ask the subclass to setup the list; we need this before // building the adapter. if (savedInstanceState != null && mKey != null && savedInstanceState.containsKey(mKey)) { mList = Utils.getListFromBundle(savedInstanceState, mKey);//.getParcelableArrayList(mKey); } if (mList == null) { /* Get any information from the extras bundle */ Bundle extras = getIntent().getExtras(); if (extras != null && mKey != null) { mList = Utils.getListFromBundle(extras, mKey); // .getParcelableArrayList(mKey); } if (mList == null) mList = getList(); if (mList == null) { throw new RuntimeException("Unable to find list key '" + mKey + "' in passed data"); } } // Set up list handling this.mAdapter = new ListAdapter(this, mRowViewId, mList); setListAdapter(this.mAdapter); // Look for title and title_label Bundle extras = getIntent().getExtras(); if (extras != null) { mRowId = extras.getLong(CatalogueDBAdapter.KEY_ROWID); mBookTitleLabel = extras.getString("title_label"); mBookTitle = extras.getString("title"); setTextOrHideView(R.id.title_label, mBookTitleLabel); setTextOrHideView(R.id.title, mBookTitle); } TouchListView tlv=(TouchListView)getListView(); tlv.setDropListener(mDropListener); //tlv.setRemoveListener(onRemove); } catch (Exception e) { Logger.logError(e); } } /** * Handle drop events; also preserves current position. */ private TouchListView.DropListener mDropListener=new TouchListView.DropListener() { @Override public void drop(int from, final int to) { final ListView lv = getListView(); // Check if nothing to do; also avoids the nasty case where list size == 1 if (from == to) return; final int firstPos = lv.getFirstVisiblePosition(); T item=mAdapter.getItem(from); mAdapter.remove(item); mAdapter.insert(item, to); onListChanged(); int first2 = lv.getFirstVisiblePosition(); System.out.println(from + " -> " + to + ", first " + firstPos + "(" + first2 + ")"); final int newFirst = (to > from && from < firstPos) ? (firstPos - 1) : firstPos; View firstView = lv.getChildAt(0); final int offset = firstView.getTop(); lv.post(new Runnable() { @Override public void run() { System.out.println("Positioning to " + newFirst + "+{" + offset + "}"); lv.requestFocusFromTouch(); lv.setSelectionFromTop(newFirst, offset); lv.post(new Runnable() { @Override public void run() { for(int i = 0; ; i++) { View c = lv.getChildAt(i); if (c == null) break; if (lv.getPositionForView(c) == to) { lv.setSelectionFromTop(to, c.getTop()); //c.requestFocusFromTouch(); break; } } }}); }}); } }; /** * Utility routine to setup a listener for the specified view id * * @param id Resource ID * @param l Listener * * @return true if resource present, false if not */ private boolean setupListener(int id, OnClickListener l) { View v = this.findViewById(id); if (v == null) return false; v.setOnClickListener(l); return true; } /** * Utility routine to set a TextView to a string, or hide it on failure. * * @param id View ID * @param s String to set */ protected void setTextOrHideView(View v, int id, String s) { if (v != null && v.getId() != id) v = v.findViewById(id); setTextOrHideView(v,s); } protected void setTextOrHideView(View v, String s) { // If view is not present, just exit if (v == null) return; try { if (s != null && s.length() > 0) { ((TextView)v).setText(s); return; } } catch (Exception e) { Logger.logError(e); }; // If we get here, something went wrong. if (v != null) v.setVisibility(View.GONE); } protected void setTextOrHideView(int id, String s) { setTextOrHideView(this.findViewById(id), id, s); } /** * Handle 'Save' */ private OnClickListener mSaveListener = new OnClickListener() { @Override public void onClick(View v) { Intent i = new Intent(); i.putExtra(mKey, mList); if (onSave(i)) { setResult(RESULT_OK, i); finish(); } } }; /** * Handle 'Cancel' */ private OnClickListener mCancelListener = new OnClickListener() { @Override public void onClick(View v) { if (onCancel()) finish(); } }; /** * Handle 'Add' */ private OnClickListener mAddListener = new OnClickListener() { @Override public void onClick(View v) { onAdd(v); onListChanged(); } }; /** * Find the first ancestor that has the ID R.id.row. This * will be the complete row View. Use the TAG on that to get * the physical row number. * * @param v View to search from * * @return The row view. */ private Integer getViewRow(View v) { View pv = v; while(pv.getId() != R.id.row) { ViewParent p = pv.getParent(); if (!(p instanceof View)) throw new RuntimeException("Could not find row view in view ancestors"); pv = (View) p; } Object o = ViewTagger.getTag(pv, R.id.TAG_POSITION); if (o == null) throw new RuntimeException("A view with the tag R.id.row was found, but it is not the view for the row"); return (Integer) o; } /** * Handle deletion of a row */ private OnClickListener mRowDeleteListener = new OnClickListener() { @Override public void onClick(View v) { if (v == null) return; int pos = getViewRow(v); mList.remove(pos); mAdapter.notifyDataSetChanged(); onListChanged(); } }; /** * Handle moving a row UP */ private OnClickListener mRowUpListener = new OnClickListener() { @Override public void onClick(View v) { int pos = getViewRow(v); if (pos == 0) return; T old = mList.get(pos-1); mList.set(pos-1, mList.get(pos)); mList.set(pos, old); mAdapter.notifyDataSetChanged(); onListChanged(); } }; /** * Handle moving a row DOWN */ private OnClickListener mRowDownListener = new OnClickListener() { @Override public void onClick(View v) { int pos = getViewRow(v); if (pos == (mList.size()-1) ) return; T old = mList.get(pos); mList.set(pos, mList.get(pos+1)); mList.set(pos+1, old); mAdapter.notifyDataSetChanged(); onListChanged(); } }; /** * Handle moving a row DOWN */ private OnClickListener mRowClickListener = new OnClickListener() { @Override public void onClick(View v) { int pos = getViewRow(v); onRowClick(v, pos, mList.get(pos)); } }; /** * Adapter to manage the rows. * * @author Philip Warner */ final class ListAdapter extends ArrayAdapter<T> { // Flag fields to (slightly) optimize lookups and prevent looking for // fields that are not there. private boolean mCheckedFields = false; private boolean mHasPosition = false; private boolean mHasUp = false; private boolean mHasDown = false; private boolean mHasDelete = false; public ListAdapter(Context context, int textViewResourceId, ArrayList<T> items) { super(context, textViewResourceId, items); } @Override public View getView(int position, View convertView, ViewGroup parent) { // Get the view; if not defined, load it. View v = convertView; if (v == null) { LayoutInflater vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE); v = vi.inflate(mRowViewId, null); } // Save this views position ViewTagger.setTag(v, R.id.TAG_POSITION, Integer.valueOf(position)); { // Giving the whole row ad onClickListener seems to interfere // with drag/drop. View details = v.findViewById(R.id.row_details); if (details != null) { details.setOnClickListener(mRowClickListener); details.setFocusable(false); } } // Get the object, if not null, do some processing T o = mList.get(position); if (o != null) { // Try to set position value if (mHasPosition || !mCheckedFields) { TextView pt = (TextView) v.findViewById(R.id.row_position); if(pt != null){ mHasPosition = true; pt.setText(Long.toString(position+1)); } } // Try to set the UP handler if (mHasUp || !mCheckedFields) { ImageView up = (ImageView) v.findViewById(R.id.row_up); if (up != null) { up.setOnClickListener(mRowUpListener); mHasUp = true; } } // Try to set the DOWN handler if (mHasDown || !mCheckedFields) { ImageView dn = (ImageView) v.findViewById(R.id.row_down); if (dn != null) { dn.setOnClickListener(mRowDownListener); mHasDown = true; } } // Try to set the DELETE handler if (mHasDelete || !mCheckedFields) { ImageView del = (ImageView) v.findViewById(R.id.row_delete); if (del != null) { del.setImageResource(android.R.drawable.ic_delete); del.setOnClickListener(mRowDeleteListener); mHasDelete = true; } } // Ask the subclass to set other fields. try { onSetupView(v, o); } catch (Exception e) { Logger.logError(e); } mCheckedFields = true; } return v; } } /** * Ensure that the list is saved. */ @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); // save list outState.putSerializable(mKey, mList); } /** * This is totally bizarre. Without this piece of code, under Android 1.6, the * native onRestoreInstanceState() fails to restore custom classes, throwing * a ClassNotFoundException, when the activity is resumed. * * To test this, remove this line, edit a custom style, and save it. App will * crash in AVD under Android 1.6. * * It is not entirely clear how this happens but since the Bundle has a classLoader * it is fair to surmise that the code that creates the bundle determines the class * loader to use based (somehow) on the class being called, and if we don't implement * this method, then in Android 1.6, the class is a basic android class NOT and app * class. */ @Override public void onRestoreInstanceState(Bundle state) { super.onRestoreInstanceState(state); } @Override protected void onDestroy() { super.onDestroy(); if (mDbHelper != null) mDbHelper.close(); } }