SlidingPaneLayout implementing swipe to remove items in list and undo action.

In my previous post I described how to work with the new SlidingPaneLayout.
Some readers asked me how to remove items from list with swipe gesture like in Google Hangouts or Gmail.

It is not very simple.... but I realized something like this.
As always it is just an example,do not take this code too seriously.

First of all, we have to integrate list with a SwipeListener.
There is a beatiful example in dashclock by Roman Nurik...so why not, we can use it.
You can find it here:SwipeDismissListViewTouchListener.
Pay attention: In source you can find:
// THIS IS A BETA! I DON'T RECOMMEND USING IT IN PRODUCTION CODE JUST YET....
We'll run the risk ... Dashclock is in Google Play, and I think that it will be maintained for a long time.

Now we can integrate our MyListFragment with this Listener.
It is very simple.
@Override
public void onActivityCreated(Bundle savedInstanceState) {
      super.onActivityCreated(savedInstanceState);
  
      // We use a arrayList to prevent UnsupportedOperationException
      // The ArrayAdapter, on being initialized by an array, converts the
      // array into a AbstractList (List) which cannot be modified.
      ArrayList itemslist = new ArrayList();
      itemslist.addAll(Arrays.asList(items));
      mAdapter = new ArrayAdapter(getActivity(),
                             android.R.layout.simple_list_item_1, itemslist);

      setListAdapter(mAdapter);

      // Create a ListView-specific touch listener.
      mListView = getListView();
      if (mListView != null) {
            mOnTouchListener = new SwipeDismissListViewTouchListener(mListView,mCallback);
            mListView.setOnTouchListener(mOnTouchListener);

            // Setting this scroll listener is required to ensure that during
            // ListView scrolling,
            // we don't look for swipes.
            mListView.setOnScrollListener(mOnTouchListener.makeScrollListener());
   
      }  
}
SwipeDismissListViewTouchListener does everything we need, manages swipe gestures, animations and we have to implement only listener's callback to remove items from our adapter.
public class MyListFragment extends ListFragment {

   /**
    * SwipeDismiss callback
    * 
    * Remove items, and show undobar
    */
    SwipeDismissListViewTouchListener.DismissCallbacks mCallback = 
                new SwipeDismissListViewTouchListener.DismissCallbacks() {
       
         @Override
         public void onDismiss(ListView listView, int[] reverseSortedPositions) {
   
             for (int position : reverseSortedPositions) {
                 String itemString=mAdapter.getItem(position);
                 mAdapter.remove(itemString);
             }
             mAdapter.notifyDataSetChanged();
         }

         @Override
         public boolean canDismiss(int position) {
             return position <= mAdapter.getCount() - 1;
         }
    };

}
The method onDismiss removes items form Adapter and notifies changes.
The method canDismiss is very useful with sectioned listviews, if the item cannot be dismissed.

It is enough to have a list with a simple swipe gesture implemented.

Now the listener animates items out when we have a space greater than a half or when we have a fast motion.
We can change this behavior, by changing these lines in SwipeDismissListViewTouchListener:

   if (Math.abs(deltaX) > mViewWidth / 2) {
       dismiss = true;
       dismissRight = deltaX > 0;
    } else if (mMinFlingVelocity <= absVelocityX && absVelocityX <= mMaxFlingVelocity
                      && absVelocityY < absVelocityX) {
       // dismiss only if flinging in the same direction as dragging
       dismiss = (velocityX < 0) == (deltaX < 0);
       dismissRight = mVelocityTracker.getXVelocity() > 0;
    }

Now we'll try to insert undo action.
Also in this case, we can find a awesome example by Roman Nurik...(thanks again for your code!).
You can find source here, and you can read about it in a Google+ post.

We'll copy UndoBarController's code, and we'll integrate our list.
First of all, we modify our fragment layout to insert a hidden undobar. Here you can find new list_fragment.xml.

<!--xml version="1.0" encoding="utf-8"?-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <ListView
            android:id="@android:id/list"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
        
    </RelativeLayout>

    <LinearLayout
        android:id="@+id/undobar"
        style="@style/UndoBar"
        android:orientation="horizontal" >

        <TextView
            android:id="@+id/undobar_message"
            style="@style/UndoBarMessage" />

        <Button
            android:id="@+id/undobar_button"
            style="@style/UndoBarButton" />
    </LinearLayout>

</FrameLayout>
Then we modify our MyListFragment to use this layout:

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {

          setHasOptionsMenu(true);
          return inflater.inflate(R.layout.list_fragment, container, false);
         //return super.onCreateView(inflater, container, savedInstanceState);
    }
Now we instantiate an undoBarController:
      @Override
      public void onActivityCreated(Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);

            .....
            if (mListView != null) {
                mOnTouchListener = new SwipeDismissListViewTouchListener(mListView,mCallback);
                mListView.setOnTouchListener(mOnTouchListener);

                .....
   
                //UndoController
                if (mUndoBarController==null)
                      mUndoBarController = new UndoBarController(
                                   getActivity().findViewById(R.id.undobar), this);
            }
  
     }
Now we can modify DismissCallback to show undobar.
public class MyListFragment extends ListFragment{

   /**
    * SwipeDismiss callback
    * 
    * Remove items, and show undobar
    */
    SwipeDismissListViewTouchListener.DismissCallbacks mCallback = 
                new SwipeDismissListViewTouchListener.DismissCallbacks() {
       
         @Override
         public void onDismiss(ListView listView, int[] reverseSortedPositions) {

            String[] itemStrings=new String[reverseSortedPositions.length];
            int[] itemPositions=new int[reverseSortedPositions.length];
            int i=0;
   
             for (int position : reverseSortedPositions) {
                 String itemString=mAdapter.getItem(position);
                 mAdapter.remove(itemString);
                
                 itemStrings[i]=itemString;
                 itemPositions[i]=position;
                 i++;
             }
             mAdapter.notifyDataSetChanged();


             //Show UndoBar
             UndoItem itemUndo=new UndoItem(itemStrings,itemPositions);
   
             //Undobar message
             Resources res = getResources();
             String messageUndoBar = res.getQuantityString(R.plurals.items,
                      reverseSortedPositions.length,reverseSortedPositions.length);
   
             mUndoBarController.showUndoBar(
                        false,
                        messageUndoBar,
                        itemUndo);
         }

         @Override
         public boolean canDismiss(int position) {
             return position <= mAdapter.getCount() - 1;
         }
    };
}
UndoItem is a simple Parcelable object which stores data by items removed.
public class UndoItem implements Parcelable {

   public String[] itemString;
   public int[] itemPosition;

   public UndoItem(String[] itemString, int[] itemPosition) {
       this.itemString = itemString;
       this.itemPosition = itemPosition;
   }

   protected UndoItem(Parcel in) {}

   public int describeContents() {
       return 0;
   }

   public void writeToParcel(Parcel dest, int flags) {}

   public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
       public UndoItem createFromParcel(Parcel in) {
           return new UndoItem(in);
       }

       public UndoItem[] newArray(int size) {
           return new UndoItem[size];
       }
   };
}
As a final step, we have to implement undobar callback.
public class MyListFragment extends ListFragment 
               implements UndoBarController.UndoListener {

    /**
     * Undo remove Action
     */
    @Override
    public void onUndo(Parcelable token) {
  
      //Restore items in lists (use reverseSortedOrder)
      if (token!=null){
 
         //Retrieve data from token  
         UndoItem item=(UndoItem)token;
         String[] itemStrings=item.itemString;
         int[] itemPositions=item.itemPosition;
   
         if (itemStrings!=null && itemPositions!=null){
             int end=itemStrings.length;
    
             for(int i=end-1;i>=0;i--){     
                String itemString=itemStrings[i];
                int itemPosition=itemPositions[i];
     
                mAdapter.insert(itemString,itemPosition);
                mAdapter.notifyDataSetChanged();
             }
         }
     }  
   } 

}
That's all... it works...(and certainly can be improved).



You can get code from GitHub:

Comments

Popular posts from this blog

AntiPattern: freezing a UI with Broadcast Receiver

NotificationListenerService and kitkat

How to centralize the support libraries dependencies in gradle