Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement expandable panels in Android?

Tags:

android

widget

Is there an easy way to create expandable/collapsible blocks like seen in official market app?

Screenshot of Market app, when you click on "More" button, the description section expands with animation:

enter image description here

I know of SlidingDrawer but it doesn't seem to be suited for stuff like this--it's supposed to be put in overlay, and doesn't support half-open states.

Update:

Here's my half-working solution. It's a custom widget that extends LinearLayout. It kind-of works, but doesn't handle edge cases well, like content height smaller than collapsedHeight parameter. I'm sure with enough staring, digging in code and experimenting the quirks could be fixed. Was hoping to avoid doing that, and save some time by using a ready-made official or 3rd party solution. Anyway, here it is, code:

package com.example.androidapp.widgets;  import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.View; import android.view.animation.Animation; import android.view.animation.Transformation; import android.widget.LinearLayout;  import com.example.androidapp.R;  public class ExpandablePanel extends LinearLayout {      private final int mHandleId;     private final int mContentId;      private View mHandle;     private View mContent;      private boolean mExpanded = true;     private int mCollapsedHeight = 0;     private int mContentHeight = 0;      public ExpandablePanel(Context context) {         this(context, null);     }      public ExpandablePanel(Context context, AttributeSet attrs) {         super(context, attrs);          TypedArray a = context.obtainStyledAttributes(attrs,             R.styleable.ExpandablePanel, 0, 0);          // How high the content should be in "collapsed" state         mCollapsedHeight = (int) a.getDimension(             R.styleable.ExpandablePanel_collapsedHeight, 0.0f);          int handleId = a.getResourceId(R.styleable.ExpandablePanel_handle, 0);         if (handleId == 0) {             throw new IllegalArgumentException(                 "The handle attribute is required and must refer "                     + "to a valid child.");         }          int contentId = a.getResourceId(R.styleable.ExpandablePanel_content, 0);         if (contentId == 0) {             throw new IllegalArgumentException(                 "The content attribute is required and must refer "                     + "to a valid child.");         }          mHandleId = handleId;         mContentId = contentId;          a.recycle();     }      @Override     protected void onFinishInflate() {         super.onFinishInflate();          mHandle = findViewById(mHandleId);         if (mHandle == null) {             throw new IllegalArgumentException(                 "The handle attribute is must refer to an"                     + " existing child.");         }          mContent = findViewById(mContentId);         if (mContent == null) {             throw new IllegalArgumentException(                 "The content attribute is must refer to an"                     + " existing child.");         }          mHandle.setOnClickListener(new PanelToggler());     }      @Override     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {         if (mContentHeight == 0) {             // First, measure how high content wants to be             mContent.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED);             mContentHeight = mContent.getMeasuredHeight();         }          // Then let the usual thing happen         super.onMeasure(widthMeasureSpec, heightMeasureSpec);     }      private class PanelToggler implements OnClickListener {         public void onClick(View v) {             Animation a;             if (mExpanded) {                 a = new ExpandAnimation(mContentHeight, mCollapsedHeight);             } else {                 a = new ExpandAnimation(mCollapsedHeight, mContentHeight);             }             a.setDuration(500);             mContent.startAnimation(a);             mExpanded = !mExpanded;         }     }      private class ExpandAnimation extends Animation {         private final int mStartHeight;         private final int mDeltaHeight;          public ExpandAnimation(int startHeight, int endHeight) {             mStartHeight = startHeight;             mDeltaHeight = endHeight - startHeight;         }          @Override         protected void applyTransformation(float interpolatedTime,             Transformation t) {             android.view.ViewGroup.LayoutParams lp = mContent.getLayoutParams();             lp.height = (int) (mStartHeight + mDeltaHeight * interpolatedTime);             mContent.setLayoutParams(lp);         }          @Override         public boolean willChangeBounds() {             // TODO Auto-generated method stub             return true;         }     } } 

Here's res/values/attrs.xml:

<?xml version="1.0" encoding="utf-8"?> <resources>   <declare-styleable name="ExpandablePanel">     <attr name="handle" format="reference" />     <attr name="content" format="reference" />     <attr name="collapsedHeight" format="dimension" />   </declare-styleable> </resources> 

And here's how I use it in layout:

<com.example.androidapp.widgets.ExpandablePanel     android:orientation="vertical"     android:layout_height="wrap_content"     android:layout_width="fill_parent"     example:handle="@+id/expand"     example:content="@+id/value"     example:collapsedHeight="50dip">     <TextView         android:id="@id/value"         android:layout_width="fill_parent"         android:layout_height="wrap_content"         android:maxHeight="50dip"         />     <Button         android:id="@id/expand"         android:layout_width="wrap_content"         android:layout_height="wrap_content"         android:text="More" /> </com.example.androidapp.widgets.ExpandablePanel> 
like image 482
Pēteris Caune Avatar asked Mar 02 '11 09:03

Pēteris Caune


People also ask

What is expandable list view?

A view that shows items in a vertically scrolling two-level list. This differs from the ListView by allowing two levels: groups which can individually be expanded to show its children. The items come from the ExpandableListAdapter associated with this view.


1 Answers

Thanks very much OP! For anyone interested I took OP's solution and refined it a bit.

  • Handle only displays if there is overflow
  • Added ability to specify animation duration via 'animationDuration' attribute
  • Added ability to attach event listeners that get fired onExpand and onCollapse (this is useful for e.g changing the text of the "More" button to "Less"
  • Collapsed by default
  • Content can be modified programmatically (same with attributes)

Here's the updated code:

import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.View; import android.view.animation.Animation; import android.view.animation.Transformation; import android.widget.LinearLayout;  public class ExpandablePanel extends LinearLayout {      private final int mHandleId;     private final int mContentId;      private View mHandle;     private View mContent;      private boolean mExpanded = false;     private int mCollapsedHeight = 0;     private int mContentHeight = 0;     private int mAnimationDuration = 0;      private OnExpandListener mListener;      public ExpandablePanel(Context context) {         this(context, null);     }      public ExpandablePanel(Context context, AttributeSet attrs) {         super(context, attrs);         mListener = new DefaultOnExpandListener();          TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ExpandablePanel, 0, 0);          // How high the content should be in "collapsed" state         mCollapsedHeight = (int) a.getDimension(R.styleable.ExpandablePanel_collapsedHeight, 0.0f);          // How long the animation should take         mAnimationDuration = a.getInteger(R.styleable.ExpandablePanel_animationDuration, 500);          int handleId = a.getResourceId(R.styleable.ExpandablePanel_handle, 0);         if (handleId == 0) {             throw new IllegalArgumentException(                 "The handle attribute is required and must refer "                     + "to a valid child.");         }          int contentId = a.getResourceId(R.styleable.ExpandablePanel_content, 0);         if (contentId == 0) {             throw new IllegalArgumentException("The content attribute is required and must refer to a valid child.");         }          mHandleId = handleId;         mContentId = contentId;          a.recycle();     }      public void setOnExpandListener(OnExpandListener listener) {         mListener = listener;      }      public void setCollapsedHeight(int collapsedHeight) {         mCollapsedHeight = collapsedHeight;     }      public void setAnimationDuration(int animationDuration) {         mAnimationDuration = animationDuration;     }      @Override     protected void onFinishInflate() {         super.onFinishInflate();          mHandle = findViewById(mHandleId);         if (mHandle == null) {             throw new IllegalArgumentException(                 "The handle attribute is must refer to an"                     + " existing child.");         }          mContent = findViewById(mContentId);         if (mContent == null) {             throw new IllegalArgumentException(                 "The content attribute must refer to an"                     + " existing child.");         }          android.view.ViewGroup.LayoutParams lp = mContent.getLayoutParams();         lp.height = mCollapsedHeight;         mContent.setLayoutParams(lp);          mHandle.setOnClickListener(new PanelToggler());     }      @Override     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {         // First, measure how high content wants to be         mContent.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED);         mContentHeight = mContent.getMeasuredHeight();          if (mContentHeight < mCollapsedHeight) {             mHandle.setVisibility(View.GONE);         } else {             mHandle.setVisibility(View.VISIBLE);         }          // Then let the usual thing happen         super.onMeasure(widthMeasureSpec, heightMeasureSpec);     }      private class PanelToggler implements OnClickListener {         public void onClick(View v) {             Animation a;             if (mExpanded) {                 a = new ExpandAnimation(mContentHeight, mCollapsedHeight);                 mListener.onCollapse(mHandle, mContent);             } else {                 a = new ExpandAnimation(mCollapsedHeight, mContentHeight);                 mListener.onExpand(mHandle, mContent);             }             a.setDuration(mAnimationDuration);             mContent.startAnimation(a);             mExpanded = !mExpanded;         }     }      private class ExpandAnimation extends Animation {         private final int mStartHeight;         private final int mDeltaHeight;          public ExpandAnimation(int startHeight, int endHeight) {             mStartHeight = startHeight;             mDeltaHeight = endHeight - startHeight;         }          @Override         protected void applyTransformation(float interpolatedTime, Transformation t) {             android.view.ViewGroup.LayoutParams lp = mContent.getLayoutParams();             lp.height = (int) (mStartHeight + mDeltaHeight * interpolatedTime);             mContent.setLayoutParams(lp);         }          @Override         public boolean willChangeBounds() {             return true;         }     }      public interface OnExpandListener {         public void onExpand(View handle, View content);          public void onCollapse(View handle, View content);     }      private class DefaultOnExpandListener implements OnExpandListener {         public void onCollapse(View handle, View content) {}         public void onExpand(View handle, View content) {}     } } 

And don't forget the attrs.xml:

<?xml version="1.0" encoding="utf-8"?> <resources>     <declare-styleable name="ExpandablePanel">         <attr name="handle" format="reference" />         <attr name="content" format="reference" />         <attr name="collapsedHeight" format="dimension"/>         <attr name="animationDuration" format="integer"/>     </declare-styleable> </resources> 

See OP's example usage for the XML layout above. Here's an example for the listeners:

// Set expandable panel listener ExpandablePanel panel = (ExpandablePanel)view.findViewById(R.id.foo); panel.setOnExpandListener(new ExpandablePanel.OnExpandListener() {     public void onCollapse(View handle, View content) {         Button btn = (Button)handle;         btn.setText("More");     }     public void onExpand(View handle, View content) {         Button btn = (Button)handle;         btn.setText("Less");     } }); 
like image 188
ahal Avatar answered Sep 22 '22 07:09

ahal