Tuesday, May 17, 2011

ListPreference Using SQL Cursor

I wanted to have a ListPreference for my application that is filled with data contained in a SQLiteDatabase. Therefore, I wanted to have a cursor filling the mEntries and mEntryValues of the ListPreference class.

In order to do this, I extended the ListPreference class and simply added the following method to it. I named this class CursorListPreference.

public void setData(String inTableName, String inColumnName) {
    mySQLDataBase.open(); //Usually handled by a DB helper 
    
    CharSequence[] lEntries;
    CharSequence[] lEntryValues;
    
    Cursor lCursor = mySQLDataBase.query(
        inTableName, //Table to select from
        new String[]{"_id", inColumnName}, //Column to retreive
        null, null, null, null, 
        inColumnName); //Sorting
    if ((lCursor.getCount() == 0) || !lCursor.moveToFirst()) {
        lEntries = new CharSequence[]{};
        lEntryValues = new CharSequence[]{};
        Logger.i("No entry found for " + inColumnName + " in table " + inTableName);
    } else {
        lCursor.moveToFirst();
        lEntries = new CharSequence[lCursor.getCount()];
        lEntryValues = new CharSequence[lCursor.getCount()];
        int i = 0;
        do {
            lEntries[i] = lCursor.getString(lCursor.getColumnIndex(inColumnName));
            lEntryValues[i] = lCursor.getString(lCursor.getColumnIndex("_id"));
            ++i;
        } while (lCursor.moveToNext());
    }
    
    this.setEntries(lEntries);
    this.setEntryValues(lEntryValues);
    
    mySQLDataBase.close(); //Usually handled by a DB helper
}

Then, in my extension of PreferenceActivity, I could define the data that will be displayed in the list by providing the table name where to find the data and the column name to be displayed.
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
        
    //Load the preferences from an XML resource
    addPreferencesFromResource(R.xml.preferences);
        
    PreferenceScreen preferenceScreen = getPreferenceScreen();
    CursorListPreference lCameraPref = (CursorListPreference) preferenceScreen.findPreference("camera");
    lCameraPref.setData("camera", "model");
}

public static long getDefaultCamera(Context context) {
    return Long.valueOf(
       PreferenceManager.getDefaultSharedPreferences(context).getString("camera_pref_id", "1"));
}

In this example, I have a table containing a list of cameras, and I want the user to set the default camera model to be used.

With this approach, I can easily retrieve the default object since the database entry id is stored as the preference value. Moreover, as this class extends ListPreference, this preference will behave and look like any other ListPreference. Even the XML attributes are defined as usual.

Sunday, May 15, 2011

Not public ressources

When creating the graphics of my application, I like to reuse as much as I can the resources already provided by the Android framework. This allows my application to match as much as possible the already established graphic design of the platform. However, a lot of these resources are tagged as private.

In order to be able to use them without any compatibility issue, one need to make a local copy of the relevant file in its own project. However, in the early stage of the graphic design, I like to try different approach and I find it time consuming to copy all the 9.png and xml in my project. To avoid doing that, you can use the *android to access private resources.



Here, I'm using the private android transparent drawable as the background of my button. I can then check the result and see if I like it or not before copying the relevant files.

Note that in this case, it would be better to use a custom style that inherit from Widget.Button.Transparent.


  




This way, the resource files doesn't need to be duplicated.

Saturday, May 14, 2011

Adding a simple empty view in a ListActivity

The Android documentation shows how to set an empty view in the ListView of a ListActivity using XML layout. Since I wanted only a simple TextView to be displayed, I wanted to avoid modifying my layout. Therefore, I decided to create a TextView programatically.

TextView lEmpty = new TextView(MyListActivity.this);
lEmpty.setText("No entry");
getListView().setEmptyView(lEmpty);
lEmpty.setVisibility(View.GONE);
((ViewGroup)getListView().getParent()).addView(lEmpty);

At first, I didn't included the last two lines. As a result, the empty view was not showed. First, because the empty view wasn't added to the ViewGroup of the ListView. Also, if the visibility isn't set to GONE, then the view will always be rendered visible. These two operations are handled automatically when using declarative XML views.

Sunday, May 8, 2011

Hiding an entry of a list

When using a cursor adapter, like for instance the SimpleCursorAdapter, we can sometime have special row of data that we don't want to display. To remove those undesirable entry, one only need to set the visibility to gone when the specific row is bonded.

To do that, the class SimpleCursorAdapter need to be extended with a custom bindView method like the following:
@Override
public void bindView(View inView, Context inContext, Cursor inCursor) {
    super.bindView(inView, inContext, inCursor);
                   
    TextView lNameView =  (TextView) inView.findViewById(R.id.name);
    if(lNameView.getText().toString().equals("Name entry to be hidden")) {
        //Set the layout to gone
        inView.setVisibility(View.GONE); 
        //Go through all row layout child and set visibility to gone
        for(int i = 0; i < ((ViewGroup) inView).getChildCount(); ++i) {
            ((ViewGroup) inView).getChildAt(i).setVisibility(View.GONE);
        }
    }
}
In this custom binding method, the row is detected by a match on the name column. Then, the row layout and its childs have their visibility set to View.Gone. If only the row layout is set to gone, then the row will still have a defined height. The row completely disappear only if all the child are also set to gone.

While this code is enough to hide a line, it's not enough to ensure that all the line we want to keep are not hidden. In fact, when the view is reused later, the visibility property could still be set to gone for a line that should in fact have a visible state. Therefore, the following code is needed to ensure the correct visible state of all lines.
@Override
public void bindView(View inView, Context inContext, Cursor inCursor) {
    super.bindView(inView, inContext, inCursor);
                   
    TextView lNameView =  (TextView) inView.findViewById(R.id.name);
    if(lNameView.getText().toString().equals("Name entry to be hidden")) {
        //Set the layout to gone
        inView.setVisibility(View.GONE); 
        //Go through all row layout child and set visibility to gone
        for(int i = 0; i < ((ViewGroup) inView).getChildCount(); ++i) {
            ((ViewGroup) inView).getChildAt(i).setVisibility(View.GONE);
        }
    } else {
        //Set the layout to visible
        inView.setVisibility(View.VISIBLE);
        for(int i = 0; i < ((ViewGroup) inView).getChildCount(); ++i) {
            //Go through all row layout child and set visibility to visible
            ((ViewGroup) inView).getChildAt(i).setVisibility(View.VISIBLE);
        }
    }
}

Wednesday, May 4, 2011

Extending widgets

While extending the Spinner and EditText, I instinctively create the following pattern of object construction.

public MyEditText(Context context) {
    this(context, null);
}

public MyEditText(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public MyEditText(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs);

    //My custom initialization code
}

Here, the constructor are calling each other by adding the missing argument by a default one. However, this didn't work as I was first expecting it. When running the program, the widget was simply not displayed.

Looking closer at the widget constructor, EditText for instance, I realized that the widget is actually defining the default style.
public EditText(Context context, AttributeSet attrs) {
    this(context, attrs, com.android.internal.R.attr.editTextStyle);
}

In order to respect the default styling, I modified my construction code for customized widget to the following:
public MyEditText(Context context) {
    super(context);
    initialize();
}

public MyEditText(Context context, AttributeSet attrs) {
    super(context, attrs);
    initialize();
}

public MyEditText(Context mContext, AttributeSet mAttrs, int mDefStyle) {
    super(mContext, mAttrs, mDefStyle);
    initialize();
}

private void initialize() {
    //My custom initialization code
}

This way, I minimize the amount duplicated code and fully respect the framework styling.

Monday, May 2, 2011

Custom Spinner Dialog

While writing a data base related application, I found myself using a lot of spinner to give access to the data contained in some table using a SimpleCursorAdapter. This work very well, but I wanted to provide an easy way to fill in new data that would readily be available.

I therefore initially used buttons to popup a dialog to fill-in some information before updating the cursor. While being functional, I was still seeking for a solution that would not clutter the user interface.

I then came with a custom spinner that looks very like the normal ones, except that this spinner provides an additional button when the list is displayed to add a new element.




Clicking the add button close the current spinner dialog and start an activity with a dialog theme. A dialog could be used instead of an activity, but using an activity make it possible to Toast some message to the user when data are invalid before closing the dialog.

Once the data is validated and inserted into the data base, the dialog is closed. Then, the cursor associated to the spinner is updated and the newly added entry is selected.

That covers the intended behavior, so let's look at the code behind. The method used extends the Spinner class and override the performClick method that create an AlertDialog with a custom one displaying our custom dialog. The custom dialog could either be another AlertDialog with buttons added, but I decided to extends Dialog instead to have more flexibilities with the interface for more features.

So here goes the relevant code for implementing the spinner.


public class SpinnerCustomDialog extends Spinner implements DialogInterface.OnClickListener {
    protected OnAddListener mListener = null;
    
    public interface OnAddListener {
        void onAdd(Spinner inSpinner);
    }

    @Override
    public boolean performClick() {
        //boolean handled = super.performClick(); //TODO how to avoid this skip ?
        
        boolean handled = false;
        if (!handled) {
            handled = true;
            Context context = getContext();
            
            final DropDownAdapter adapter = new DropDownAdapter(getAdapter());
            SpinnerDialog lDialog = new SpinnerDialog(context, getSelectedItemPosition(), adapter, this);
            if (mPrompt != null) {
                lDialog.setTitle(mPrompt);
            }
            lDialog.show();
        }

        return handled;
    }
    
    @Override
    public void onClick(DialogInterface dialog, int which) {
        if(which == DialogInterface.BUTTON_POSITIVE) {
            if(mListener!=null)
                mListener.onAdd(this);
        } else {
            setSelection(which);
        }
        dialog.dismiss();
    }
    
    public void onAddReturn(int resultCode, Intent data) {
        if(resultCode == Activity.RESULT_OK) {
            //Update selection to the newly added item
            ((CursorAdapter)getAdapter()).getCursor().requery();
            int lPosition = AdapterUtil.getItemPositionById(((CursorAdapter)getAdapter()), data.getLongExtra("id", -1));
            if(lPosition >= 0)
                this.setSelection(lPosition);
        }
    }
}

To make it work, I had to paste some more methods from Spinner as even with the extend, some protected members were not visible. I still wonder why ....

The performClick simply create a Dialog and provide an adapter. The SimpleCursorAdapter is wrapped by the DropDownAdapter as done by the normal spinner to trigger call to getDropDownView instead of getView.

The dialog also need to receive a DialogInterface.OnClickListener to handle the item selection and button triggering.

This is done by the onClick method that is also defined in Spinner, but that we need to override to handle the button. The type of view clicked can be identified by the integer receive. An item will receive a positive value associated to his position, while a button will be negative.

When the add button is clicked, the event is forwarded to a method defined by the user through the
OnAddListener. Here, the intended action is to start a new activity. This is done with a short piece of code within the parent activity:

mSpinner.setOnAddListener(new SpinnerCustomDialog.OnAddListener(){
    public void onAdd(Spinner mInSpinner) {
        Intent lIntent = new Intent(WidgetTest.this, AddNewStringDataDialog.class);
        lIntent.putExtra("category", "Spinner1");
        startActivityForResult(lIntent,REQUEST_ADD_NEW);
    }
});

Here, the activity AddNewStringDataDialog is started to fill in the required date for a new table row associated to this spinner. When this dialog closes, the position of the newly inserted item need to be retrieved. This is done by calling the onAddReturn of the custom spinner in the onActivityResult of the calling activity as follow:
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch(requestCode) {
    case REQUEST_ADD_NEW:
        mSpinner.onAddReturn(resultCode, data);
        break;
}

The definition of the spinner dialog is quite straight forward and is as follow:
public class SpinnerDialog extends Dialog implements OnItemClickListener, View.OnClickListener {
    
    DialogInterface.OnClickListener mListener;
    
    SpinnerDialog(Context inContex, int inPosition, 
            ListAdapter inSpinnerAdapter, 
            DialogInterface.OnClickListener inListener) {
            
        super(inContex);
        
        this.setContentView(R.layout.dbspinner_dialog);
        
        mListener = inListener;
        
        ListView lList = (ListView) this.findViewById(R.id.list);
        lList.setAdapter(inSpinnerAdapter);
        lList.setOnItemClickListener(this);
        lList.setChoiceMode( ListView.CHOICE_MODE_SINGLE );
        lList.setSelection(inPosition);
        
        Button lButton = (Button) this.findViewById(R.id.add_btn);
        lButton.setOnClickListener(this);
    }

    public void onItemClick(AdapterView inParent, View inView, int inPosition, long inId) {
        if(mListener != null) 
            mListener.onClick(this, inPosition);
    }

    public void onClick(View inView) {
        if(mListener != null) 
            mListener.onClick(this, DialogInterface.BUTTON_POSITIVE);
    }   
}

The tricky part was to make its width expand fully. This was done by placing the button in an extra layout.

     
        
    
        

Closable Spinner

I was looking at a way to close a spinner within the code. In the API, I couldn't find any direct method to do so. As a result, I ended up extending the spinner class to have access to the protected method onDetachedFromWindow().

public class SpinnerClosable extends Spinner {

    public SpinnerClosable(Context context) {
        super(context);
    }

    public SpinnerClosable(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    
    public SpinnerClosable(Context mContext, AttributeSet mAttrs, int mDefStyle) {
        super(mContext, mAttrs, mDefStyle);
    }

    public void close() {
        onDetachedFromWindow();
    }

}