Swiping the ListView elements

I've had some time recently to play with my Android application. Before I was pretty busy with other projects and ideas and didn't had much time to play with the Android but fortunately  last few days I've had some time to play with Android development.

Anyway - I have a list view with items that I'm interacting with. I needed the nice method of enabling and disabling them. I could use standard approach and use checkbox or button but I didn't liked that approach much. So I've decided to use swiping to interact with the list. Swipe right to enable the item and swipe left to disable the item. Latest versions of android (I'm using 2.1) use that approach in contact list to make a call or  text message and I liked that a lot. So I've googled a bit but haven't found any solution that would fit exacly my requirements. I've found some similiar appraoches but not exacly what I wanted. My idea was that I could just move the item right and left dusring the swipe but I've ended up with big mess on screen during the swipe. I had to find some other solution. I tried some other approaches but In the end I leaned toward simplest possible solution - adjust the left padding of the item view. And it worked. I'm not going to put all the code here because most of it is just a simple and pretty standard code. Here is only my touch listener that I'm attaching to each view in the list.

 

OnTouchListener gestureListener = new View.OnTouchListener() {
            private int padding = 0;
            private int initialx = 0;
            private int currentx = 0;
            private  ViewHolder viewHolder;
            public boolean onTouch(View v, MotionEvent event) {
                if ( event.getAction() == MotionEvent.ACTION_DOWN)
                {
                    padding = 0;
                    initialx = (int) event.getX();
                    currentx = (int) event.getX();
                    viewHolder = ((ViewHolder) v.getTag());
                }
                if ( event.getAction() == MotionEvent.ACTION_MOVE)
                {
                    currentx = (int) event.getX();
                    padding = currentx - initialx;
                }
                
                if ( event.getAction() == MotionEvent.ACTION_UP || 
                     event.getAction() == MotionEvent.ACTION_CANCEL)
                {
                    padding = 0;
                    initialx = 0;
                    currentx = 0;
                }
                
                if(viewHolder != null)
                {
                    if(padding == 0)
                    {
                        v.setBackgroundColor(0xFF000000 );  
                        if(viewHolder.running)
                            v.setBackgroundColor(0xFF058805);
                    }
                    if(padding > 75)
                    {
                        viewHolder.running = true;
                        v.setBackgroundColor(0xFF00FF00 );  
                        viewHolder.icon.setImageResource(R.drawable.clock_running);
                    }
                    if(padding < -75)
                    {
                        viewHolder.running = false;
                        v.setBackgroundColor(0xFFFF0000 );  
                    }
                    v.setPadding(padding, 0,0, 0);
                }
                return true;
            }
        };

 

Code is pretty simple - just read the x and check the action. During the initial tap just read the x position and then calculate the distance between that initial position and current position and adjust the padding accordingly. It gives pretty nice item move that follows the finger.

If you don't know what the viewHolder is then I suggest you to check that article.

After receiving few questions and requests I realised that this article might not be so simple and obvious. So I've decided to put together some working example of  this code. Here it goes.

That's our activity:

 

public class SwipingActivity extends Activity {
    
    public OnTouchListener gestureListener;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        ListView lv = (ListView)findViewById(R.id.mylistview);
        
        gestureListener = new View.OnTouchListener() {
            private int padding = 0;
            private int initialx = 0;
            private int currentx = 0;
            private  ViewHolder viewHolder;
            public boolean onTouch(View v, MotionEvent event) {
                if ( event.getAction() == MotionEvent.ACTION_DOWN)
                {
                    padding = 0;
                    initialx = (int) event.getX();
                    currentx = (int) event.getX();
                    viewHolder = ((ViewHolder) v.getTag());
                }
                if ( event.getAction() == MotionEvent.ACTION_MOVE)
                {
                    currentx = (int) event.getX();
                    padding = currentx - initialx;
                }
                
                if ( event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL)
                {
                    padding = 0;
                    initialx = 0;
                    currentx = 0;
                }
                
                if(viewHolder != null)
                {
                    if(padding == 0)
                    {
                        v.setBackgroundColor(0xFF000000 );
                    }
                    if(padding > 75)
                    {
                        viewHolder.setRunning(true);
                    }
                    if(padding < -75)
                    {
                        viewHolder.setRunning(false);
                    }
                    v.setBackgroundColor(viewHolder.getColor());  
                    viewHolder.icon.setImageResource(viewHolder.getImageId());
                    v.setPadding(padding, 0,0, 0);
                }
                return true;
            }
        };
        
        ModelArrayAdapter adapter = new ModelArrayAdapter(this, getData(),gestureListener);
        lv.setAdapter(adapter);
    }
    
    public ArrayList<Model> getData()
    {
        ArrayList<Model> models = new ArrayList<Model>();
        for(int a=0;a<10;a++)
        {
            Model m = new Model(String.format("Item %d", a));
            models.add(m);
        }
        return models;
    }

    static class ViewHolder {
        protected TextView text;
        protected ImageView icon;
        protected CheckBox checkbox;
        protected int position;
        protected Model model;
        private int color;
        private int imageid;
        
        public ViewHolder()
        {
            position = 0;
            imageid = R.drawable.bullet_go;
            color = 0xFFFFFFFF;
        }
        public int getColor() {
            return color;
        }
        public int getImageId() {
            return imageid;
        }
        public void setRunning(boolean running) {
            model.setRuning(running);
            if(running)
            {
                color = 0xFFffffb6;
                imageid = R.drawable.bullet_green;
            }
            else
            {
                imageid = R.drawable.bullet_go;
                color = 0xFFFFFFFF;
            }
        }
    }
}

 

There comes the model - it's not important but I'll put if here to make the solution complete.

public class Model {
    private String name;
    private boolean selected;
    private Boolean running;
    public Model(String name) {
        this.name = name;
        selected = false;
        running = false;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public boolean isSelected() {
        return selected;
    }
    public void setSelected(boolean selected) {
        this.selected = selected;
    }
    public boolean isRunning() {
        return running;
    }
    public void setRuning(boolean running) {
        this.running = running;
    }
}

and finally our model adapter.

public class ModelArrayAdapter extends ArrayAdapter<Model>
{
    private ArrayList<Model> allModelItemsArray;
    private Activity context;
    private LayoutInflater inflator;
    private OnTouchListener listener;
    
    public ModelArrayAdapter(Activity context, ArrayList<Model> list,OnTouchListener _listener) {
        super(context, R.layout.singlerow, list);
        this.listener = _listener;
        this.context = context;
        this.allModelItemsArray = new ArrayList<Model>();

        this.allModelItemsArray.addAll(list);
        inflator = context.getLayoutInflater();
    }
    
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view = null;
        if(position > allModelItemsArray.size())
            return null;
        Model m = allModelItemsArray.get(position);
        final ViewHolder viewHolder = new ViewHolder();
        ViewHolder Holder = null;
        if (convertView == null) {
            
            view = inflator.inflate(R.layout.singlerow, null);
            
            view.setTag(viewHolder);
            
            viewHolder.text = (TextView) view.findViewById(R.id.label);
            viewHolder.checkbox = (CheckBox) view.findViewById(R.id.check);
            viewHolder.icon = (ImageView) view.findViewById(R.id.icon);
            viewHolder.checkbox.setTag(m);
            viewHolder.position = position;
            
            Holder = viewHolder;
        } else {
            view = convertView;
            Holder = ((ViewHolder) view.getTag());
        }
        
        if(this.listener != null)
            view.setOnTouchListener(this.listener);

        Holder.model = m;
        Holder.position = position;
        Holder.text.setText(m.getName());
        return view;
    }
}

And of course the layout xml files. That's the main.xml layout file.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
    <ListView android:id="@+id/mylistview" android:layout_width="fill_parent"
        android:layout_height="fill_parent" android:choiceMode="multipleChoice">
    </ListView>
</RelativeLayout>

And because our rows has custom layout here comes the layout for a row.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:background="#FFFFFF">
    <ImageView android:id="@+id/icon" android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/bullet_go" android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true">
    </ImageView>
     <TextView android:text="@+id/TextView01"
        android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge" android:id="@+id/label"
        android:layout_alignBaseline="@+id/check" android:layout_alignBottom="@+id/check"
        android:layout_toRightOf="@+id/icon" android:textColor="@color/gray">
    </TextView>
    <CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:id="@+id/check" android:layout_alignParentTop="true"
        android:layout_alignParentRight="true">
    </CheckBox>
</RelativeLayout>

And that's it. It works like a charm for me. Below you can find few screen shots of how it works on my device.

.

That's the default look.

 

Start swiping.

 

Swiping in progress - the distance moves is more than 75 which means that task is running and gets a different color.

 

And here we got few tasks in running state.