2013-06-20

Pull to Reveal Additional Options

Recently I have seen some screen designs for an Android app where the user can pull a listview to reveal additional options e.g. sort options or something like that. So I started to think about a possible approach for this.

Header views are not what we want so I thought about inheriting from ListView and add the functionality but aber a look at the source I thought there must be an easier solution to this.

So my solution was to create a subclass of LinearLayout which intercepts touch events. There are two children. A container for the additional options at the top and the ListView of cause.

And this turned out to be an easy and nice solution IMHO.

Here is the source of that touch interception LinearLayout:


 package de.mobilej.widget;  
 import android.content.Context;  
 import android.os.Bundle;  
 import android.os.Parcelable;  
 import android.util.AttributeSet;  
 import android.view.MotionEvent;  
 import android.view.MotionEvent.PointerCoords;  
 import android.view.View;  
 import android.view.animation.OvershootInterpolator;  
 import android.widget.LinearLayout;  
 import android.widget.ListView;  
 import de.mobilej.listboxtest.R;  
 public class TopDrawer extends LinearLayout {  
   private boolean headerVisible;  
   private ListView lv;  
   private int originalLvBottom;  
   PointerCoords pointerMoveStart = new PointerCoords();  
   boolean scrolling = false;  
   private View v;  
   public TopDrawer(Context context) {  
     super(context);  
   }  
   public TopDrawer(Context context, AttributeSet attrs) {  
     super(context, attrs);  
   }  
   public TopDrawer(Context context, AttributeSet attrs, int defStyle) {  
     super(context, attrs, defStyle);  
   }  
   @Override  
   protected void onLayout(boolean changed, int l, int t, int r, int b) {  
     super.onLayout(changed, l, t, r, b);  
     if (v == null) {  
       v = findViewById(R.id.header);  
       lv = (ListView) findViewById(R.id.mylist);  
     }  
     originalLvBottom = lv.getBottom();  
     if (!headerVisible) {  
       v.setTranslationY(-1 * v.getHeight());  
       lv.setTranslationY(-1 * v.getHeight());  
       lv.setBottom(originalLvBottom + v.getHeight());  
     }  
   }  
   public void colapse() {  
     headerVisible = false;  
     lv.setBottom(originalLvBottom + v.getHeight());  
     float curTrans = v.getTranslationY();  
     float destTrans = -1 * v.getHeight();  
     if (curTrans > destTrans) {  
       v.animate().translationY(destTrans).setDuration(300).setInterpolator(new OvershootInterpolator()).start();  
       lv.animate().translationY(destTrans).setDuration(300).setInterpolator(new OvershootInterpolator()).start();  
     }  
   }  
   @Override  
   public boolean onInterceptTouchEvent(MotionEvent ev) {  
     int action = ev.getActionMasked();  
     if (action == MotionEvent.ACTION_DOWN) {  
       scrolling = true;  
       ev.getPointerCoords(0, pointerMoveStart);  
     } else if (action == MotionEvent.ACTION_MOVE && scrolling) {  
       PointerCoords currentPos = new PointerCoords();  
       ev.getPointerCoords(0, currentPos);  
       if (pointerMoveStart.y - currentPos.y < 0) {  
         if (lv.getFirstVisiblePosition() == 0) {  
           View topChild = lv.getChildAt(0);  
           if (topChild.getY() == 0) {  
             return true;  
           }  
         }  
       }  
     }  
     return false;  
   }  
   @Override  
   public boolean onTouchEvent(MotionEvent ev) {  
     int action = ev.getActionMasked();  
     if (action == MotionEvent.ACTION_MOVE && scrolling && !headerVisible) {  
       PointerCoords currentPos = new PointerCoords();  
       ev.getPointerCoords(0, currentPos);  
       if (pointerMoveStart.y - currentPos.y < 0) {  
         if (lv.getFirstVisiblePosition() == 0) {  
           View topChild = lv.getChildAt(0);  
           if (topChild.getY() == 0) {  
             int hHeader = v.getHeight();  
             int moved = (int) (currentPos.y - pointerMoveStart.y);  
             int trans = -hHeader + moved;  
             if (trans > 0) {  
               trans = 0;  
               headerVisible = true;  
             }  
             v.setTranslationY(trans);  
             lv.setTranslationY(trans);  
             lv.setBottom(originalLvBottom - trans);  
             return true;  
           }  
         }  
       }  
     } else if (action == MotionEvent.ACTION_UP) {  
       scrolling = false;  
       if (!headerVisible) {  
         lv.setBottom(originalLvBottom + v.getHeight());  
         float curTrans = v.getTranslationY();  
         float destTrans = -1 * v.getHeight();  
         if (curTrans > destTrans) {  
           v.animate().translationY(destTrans).setDuration(300).setInterpolator(new OvershootInterpolator())  
               .start();  
           lv.animate().translationY(destTrans).setDuration(300).setInterpolator(new OvershootInterpolator())  
               .start();  
         }  
       }  
     }  
     return false;  
   }  
   @Override  
   protected void onRestoreInstanceState(Parcelable state) {  
     Bundle b = (Bundle) state;  
     if (b == null) {  
       return;  
     }  
     super.onRestoreInstanceState(b.getParcelable("SUPER"));  
     headerVisible = b.getBoolean("visible");  
   }  
   @Override  
   protected Parcelable onSaveInstanceState() {  
     Bundle b = new Bundle();  
     b.putParcelable("SUPER", super.onSaveInstanceState());  
     b.putBoolean("visible", headerVisible);  
     return b;  
   }  
 }  

It's not fully polished code but just a quick prove of concept. And it's also not possible currently to hide the additional views easily by e.g. overscrolling to the end of the list (in my example I have a button that calls the collape method). But all this can be added easily.

A possible layout file could look like this


 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   xmlns:tools="http://schemas.android.com/tools"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent"  
   android:orientation="vertical"  
   android:paddingBottom="@dimen/activity_vertical_margin"  
   android:paddingLeft="@dimen/activity_horizontal_margin"  
   android:paddingRight="@dimen/activity_horizontal_margin"  
   android:paddingTop="@dimen/activity_vertical_margin"  
   tools:context=".MainActivity" >  
   <TextView  
     android:id="@+id/textView1"  
     android:layout_width="wrap_content"  
     android:layout_height="wrap_content"  
     android:text="@string/hello_world" />  
   <de.mobilej.widget.TopDrawer  
     android:id="@+id/topDrawer"  
     android:layout_width="match_parent"  
     android:layout_height="wrap_content"  
     android:orientation="vertical" >  
     <LinearLayout  
       android:id="@+id/header"  
       android:layout_width="match_parent"  
       android:layout_height="wrap_content"  
       android:orientation="horizontal" >  
       <EditText  
         android:id="@+id/myEditText"  
         android:layout_width="0dp"  
         android:layout_height="wrap_content"  
         android:layout_weight="1" />  
       <ImageButton  
         android:id="@+id/button"  
         android:layout_width="wrap_content"  
         android:layout_height="wrap_content"  
         android:src="@android:drawable/btn_star" />  
     </LinearLayout>  
     <ListView  
       android:id="@+id/mylist"  
       android:layout_width="match_parent"  
       android:layout_height="wrap_content"  
       android:layout_alignParentLeft="true"  
       android:layout_below="@+id/textView1"  
       android:layout_marginTop="26dp" >  
     </ListView>  
   </de.mobilej.widget.TopDrawer>  
 </LinearLayout>  


The important things here are the IDs header and myList (which are not wisely choosen I have to admit).

Wth this idea also things like the Pull-To-Refresh as seen in the latest GMail for Android version are now very easy to implement.

1 comment:

  1. Thank you for sharing.
    I don't stop wondering how many trick you can do knowing how to code. That is why I consider data room providers to be a great solution for business.

    ReplyDelete