How to implement extensible panels in Android?

Is there an easy way to create expandable / dropable blocks, for example, in the official app on the market?

A screenshot of the Market application, when you click the "Advanced" button, the description section expands with animation:

enter image description here

I know SlidingDrawer , but it does not seem to be suitable for such things - it should be placed in the overlay, and does not support semi-open states.

Update:

Here is my semi-tasking solution. This is a custom widget that extends LinearLayout . This is a kind of work, but it does not handle boundary cases very well, for example, the content height is less than the collapsedHeight parameter. I am sure that with sufficient attention I look, dig code and experimenting that quirks can be fixed. I was hoping to avoid this and save some time using a ready-made official or third-party solution. Anyway, here it is, the 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 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 this is how I use it in the 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> 
+61
android widget
Mar 02 2018-11-11T00: 00Z
source share
4 answers

Thanks a lot OP! For everyone who is interested, I made an OP decision and clarified it a bit.

  • The handle only displays when overflowing.
  • Added the ability to specify the duration of the animation through the attribute 'animationDuration'
  • Added the ability to connect event listeners that are triggered by onExpand and onCollapse (this is useful, for example, to change the text of the "Advanced" button to "Less"
  • Default default
  • Content can be changed 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 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 an example of using OP for XML Schema above. Here is an example for 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"); } }); 
+54
Jun 12 2018-11-11T00:
source share

I know that this is an old question, but for those who are interested, I made additions to what Peteris Kaun did akhal.

Additions

  • A layout is included that contains a horizontal view and a large button (see image from a question by Peteris Kaun)
  • The layout, not just the button, is deleted when there is no overflow
  • Hidden text is displayed or hidden depending on the state of the button

Updated Code

ExpandablePanel Class

 package com.example.myandroidhustles; import com.example.myandroidhustles.R; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; 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 final int mViewGroupId; private final boolean isViewGroup; private View mHandle; private View mContent; private ViewGroup viewGroup; 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."); } int isViewGroupId = a.getResourceId(R.styleable.ExpandablePanel_isviewgroup, 0); int viewGroupId = a.getResourceId(R.styleable.ExpandablePanel_viewgroup, 0); // isViewGroup = findViewById(isViewGroupId); isViewGroup = a.getBoolean(R.styleable.ExpandablePanel_isviewgroup, false); if (isViewGroup) { mViewGroupId = viewGroupId; } else { mViewGroupId = 0; } 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."); } if(mViewGroupId != 0) { viewGroup = (ViewGroup) findViewById(mViewGroupId); } 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) { viewGroup.setVisibility(View.GONE); // mHandle.setVisibility(View.GONE); } else { viewGroup.setVisibility(View.VISIBLE); // 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); if(mContent.getLayoutParams().height == 0) //Need to do this or else the animation will not play if the height is 0 { android.view.ViewGroup.LayoutParams lp = mContent.getLayoutParams(); lp.height = 1; mContent.setLayoutParams(lp); mContent.requestLayout(); } 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) {} } } 

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="viewgroup" format="reference"/> <attr name="isviewgroup" format="boolean"/> <attr name="collapsedHeight" format="dimension"/> <attr name="animationDuration" format="integer"/> </declare-styleable> </resources> 

Layout: tryExpandablePanel.xml

 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:example="http://schemas.android.com/apk/res/com.example.myandroidhustles" android:layout_width="fill_parent" android:layout_height="match_parent" > <com.example.myandroidhustles.ExpandablePanel android:id="@+id/expandablePanel" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" example:collapsedHeight="50dip" example:content="@+id/value" example:handle="@+id/expand" example:isviewgroup="true" example:viewgroup="@+id/expandL" > <TextView android:id="@+id/value" android:layout_width="fill_parent" android:layout_height="wrap_content" android:maxHeight="100dip" /> <LinearLayout android:id="@+id/expandL" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:paddingLeft="10dp" android:weightSum="100" > <View android:id="@+id/view" android:layout_width="fill_parent" android:layout_height="1dp" android:layout_gravity="center_vertical|left" android:layout_weight="30" android:background="@android:color/darker_gray" /> <Button android:id="@+id/expand" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_gravity="right" android:layout_weight="70" android:text="More" /> </LinearLayout> </com.example.myandroidhustles.ExpandablePanel> </LinearLayout> 

Implementation: ExpandablePanelImplementation Class

 package com.example.myandroidhustles; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; public class ExpandablePanelImplementation extends Activity { ExpandablePanel panel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.tryexpandable); TextView text; text = (TextView)findViewById(R.id.value); text.setText("ksaflfsklafjsfj sdfjklds fj asklfjklasfjskladf fjslkafjf" + "asfkdaslfjsf;sjdaflkadsjflkdsajfkldsajflkdsanfvsjvfdskljflkdnjdsadf" + "askfvdsklfjvsdlkfjdsklvdkjkdsadsj;lkasjdfklvsddsjkdsljskldfj"); panel = (ExpandablePanel)findViewById(R.id.expandablePanel); panel.setOnExpandListener(new ExpandablePanel.OnExpandListener() { public void onCollapse(View handle, View content) { Button btn = (Button)handle; btn.setText("More"); panel.setCollapsedHeight(100); } public void onExpand(View handle, View content) { Button btn = (Button)handle; panel.setCollapsedHeight(50); btn.setText("Less"); } }); } } 
+10
Apr 03 '13 at 11:47 on
source share

Have you tried to have a ScrollView with a given size that you make not clickable and not customizable? Then, when you expand it, you can just revive it.

+9
May 23 '11 at 16:51
source share

Great expansion of akhala. I modified your code a bit to fix the error found.

I added this around line 128, after a.setDuration(mAnimationDuration); in PanelToggler

 if(mContent.getLayoutParams().height == 0) //Need to do this or else the animation will not play if the height is 0 { android.view.ViewGroup.LayoutParams lp = mContent.getLayoutParams(); lp.height = 1; mContent.setLayoutParams(lp); mContent.requestLayout(); } 

I found that if the content height is 0, the animation will not play, so she needed to set it to 1 before the animation.

+5
Jan 12 2018-12-12T00:
source share



All Articles