0

I'm trying to make a simple to-do list where you would long-press an item to mark it as 'done', in which case it will be greyed out and strikethrough.

I'm working on the strikethrough first and found some sample code here creating a strikethrough text in Android? . However the problem is that the setPaintFlags() method only seems to work on TextView whereas the items on my list are String. I can't cast a String to a TextView, and I found a workaround here but apparently it's highly discouraged to do it: Cast String to TextView . Also I looked up SpannableString but it doesn't seem to work for strings of varying length.

So I'm back at square one - is it at all possible to implement what I'm trying to do? Or will I have to store my list items differently instead?

Relevant code:

public class MainActivity extends ActionBarActivity {
    private ArrayList<String> items;
    private ArrayAdapter<String> itemsAdapter;
    private ListView lvItems;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //Setting what the ListView will consist of
        lvItems = (ListView) findViewById(R.id.lvItems);

        readItems();
        itemsAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, items);
        lvItems.setAdapter(itemsAdapter);
        // Set up remove listener method call
        setupListViewListener();
    }

    //Attaches a long click listener to the listview
     private void setupListViewListener() {
        lvItems.setOnItemLongClickListener(
                new AdapterView.OnItemLongClickListener() {
                    @Override
                    public boolean onItemLongClick(AdapterView<?> adapter,
                                                   View item, int pos, long id) {


                        // Trying to make the onLongClick strikethrough the text 

                        String clickedItem = items.get(pos);
                        //What do I do here??

                        // Refresh the adapter
                        itemsAdapter.notifyDataSetChanged();
                        writeItems();
                        // Return true consumes the long click event (marks it handled)
                        return true;
                    }

                });
    }
Community
  • 1
  • 1
misaochan
  • 890
  • 2
  • 8
  • 25
  • could you post the code for 'itemsAdapter'? – Bö macht Blau Sep 15 '15 at 15:49
  • Done. (Sorry about the indentation, I can't find any way to format that properly and it's hard to see in the narrow editor window) – misaochan Sep 16 '15 at 03:09
  • I wanted to have a look at the code because I wanted to stay as close to your approach as possible with my answer – Bö macht Blau Sep 16 '15 at 09:06
  • @melange Let your IDE do the indenting for you. Once you get it properly indented, make sure that the least indented line has 4 spaces before it, then copy/paste in here. Done. I do it all the time and it works great. – JeffC Sep 25 '15 at 15:14

1 Answers1

3

Let's take a step back and consider your app. You want to show a list of jobs to the user. Each job has a description. And each job has two possible states: 'done' or 'not done'.

So I would like to introduce a class 'Job'

class Job
{
    private String    mDescription;
    private boolean   mDone;

    public Job(String description)
    {
         this.mDescription = description;
         this.mDone = false;
    }
    // ... generate the usual getters and setters here ;-)
    // especially:
    public boolean isDone()
    {
         return mIsDone;
    }
}

This way your ArrayList 'items' becomes be a ArrayList< Job >. Wether a job is done or not will be stored together with its description. This is important because you want to show the current state of the job to the user by changing the look of the UI element, but you need to keep track of the job's state on the data level as well.

The UI element - the TextView - will be configured to present information about the job to the user. One piece of information is the description. The TextView will store this as a String. The other piece of information is the state (done/ not done). The TextView will (in your app) store this by setting the strike-through flag and changing its color.

Because for performance reasons a ListView uses less elements than the data list ('items') contains, you have to write a custom adapter. For brevity's sake, I'm keeping the code very simple, but it's worth the time to read up on the View Holder pattern:

Let's use a layout file 'mytextviewlayout.xml' for the list rows:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          android:orientation="vertical"
          android:layout_width="match_parent"
          android:layout_height="match_parent">

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceMedium"
    android:text="Medium Text"
    android:id="@+id/textView"/>
</LinearLayout>

Now the code for the adapter looks like this:

EDIT changed from ArrayAdapter to BaseAdapter and added a view holder (see comments):

public class MyAdapter extends BaseAdapter
{
private ArrayList<Job>         mDatalist;
private int                    mLayoutID;
private Activity               mCtx;

private MyAdapter(){} // the adapter won't work with the standard constructor

public MyAdapter(Activity context, int resource, ArrayList<Job> objects)
{
    super();
    mLayoutID = resource;
    mDatalist = objects;
    mCtx = context;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View rowView = convertView;

    if (rowView == null) {
        LayoutInflater inflater = mCtx.getLayoutInflater();
        rowView = inflater.inflate(mLayoutID, null);

        ViewHolder viewHolder = new ViewHolder();
        viewHolder.tvDescription = (TextView) rowView.findViewById(R.id.textView);

        rowView.setTag(viewHolder); 
    }

    ViewHolder vholder = (ViewHolder) rowView.getTag();

    TextView tvJob = vholder.tvDescription;
    Job myJob = mDatalist.get(position);

    tvJob.setText(myJob.getJobDescription());
    if (myJob.isDone())
    {
        // apply changes to TextView
        tvJob.setPaintFlags(tvJob.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
        tvJob.setTextColor(Color.GRAY);
    }
    else
    {
        // show TextView as usual
        tvJob.setPaintFlags(tvJob.getPaintFlags() &    (~Paint.STRIKE_THRU_TEXT_FLAG));
        tvJob.setTextColor(Color.BLACK); // or whatever is needed...
    }

    return rowView;
}


@Override
public int getCount()
{
    return mDatalist.size();
}

@Override
public Object getItem(int position)
{
    return mDatalist.get(position);
}

@Override
public long getItemId(int position)
{
    return position;
}

static class ViewHolder
{
    public TextView tvDescription;
}

}

Due to the changed adapter,

in the MainActivity, you have to declare 'items' and 'itemsAdapter' as follows:

private ArrayList<Job> items;
private MyAdapter itemsAdapter;

...and in your 'onCreate()' method, you write:

itemsAdapter = new MyAdapter<String>(this, R.layout.mytextviewlayout, items);

Don't forget to change the 'readItems()' and 'writeItems()' methods because 'items' now is a ArrayList< Job >.

Then, finally, the 'onItemLongClick()' method:

EDIT use 'parent.getItemAtPosition()' instead of 'items.get()', see comments

@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id)
{
     // items.get(position).setDone(true);

     Object o = parent.getItemAtPosition(position);

     if (o instanceof Job)
     {
         ((Job) o).setDone(true);
     }
     // and now indeed the data set has changed :)

     itemsAdapter.notifyDataSetChanged(); 
     writeItems();
     return true;
 }
Bö macht Blau
  • 12,820
  • 5
  • 40
  • 61
  • 1
    there are 2 things that should be polished: 1. instead `items.get(position)` should be rather `parent.getItemAtPosition(position)` or better do not use `ArrayAdapter` as base class but `BaseAdapter` - (AA may use(after filtering) different collection as underlaying data - so source of items may be not `items`) (or do even both (use parent.... and use BaseAdapter)) 2. use view holder pattern ... **but yeah, that's the way** ... – Selvin Sep 16 '15 at 09:43
  • 1
    As I wrote, filtering may change underlaying data source ... so it would be no longer direct colletion passed through the constructor ... so if we do not use filtering there is no sens using ArrayAdaper ... in fact it has no any added values besides filtering and implementing few easy to implement abstract methods (getCount(), getItem(...), etc.) .... **thats why using `items.get(position)` with AA is not safe - consider situation when 3rd item was filtered out ... pos = 3 `items.get(pos )` will return 3rd item of original array `parent.getItemAtPosition(pos )` will return 4rd item** – Selvin Sep 16 '15 at 09:57