如何在Android中实现可展开的面板?

有没有一种简单的方法来创build可扩展/可折叠的块像在官方的市场应用程序中看到?

市场应用程序的屏幕截图,当您点击“更多”button时,说明部分将扩展animation:

在这里输入图像说明

我知道SlidingDrawer,但它似乎不适合这样的东西 – 它应该被放在覆盖,不支持半开放状态。

更新:

这是我的半工作解决scheme。 这是一个扩展LinearLayout的自定义小部件。 这种types的工作,但不能很好地处理边缘情况,比如内容高度小于collapsedHeight参数。 我确信有足够的注意力,挖掘代码和试验怪癖可以修复。 希望避免这样做,并使用现成的官方或第三方解决scheme节省一些时间。 无论如何,这是代码:

 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; } } } 

这里是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> 

以下是我如何在布局中使用它:

 <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> 

非常感谢OP! 对于任何感兴趣的人来说,我把OP的解决scheme做了一些改进。

  • 只有在出现溢出时才处理显示
  • 增加了通过“animationDuration”属性指定animation持续时间的function
  • 增加了附加事件侦听器的能力,这些事件侦听器在onExpand和onCollapse上被触发(这对于例如将“更多”button的文本更改为“更less”
  • 默认情况下已折叠
  • 内容可以以编程方式修改(与属性相同)

这是更新的代码:

 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) {} } } 

不要忘了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> 

请参阅上面的XML布局的OP的示例用法。 以下是听众的例子:

 // 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"); } }); 

我知道这是一个古老的问题,但对于那些有兴趣的人,我增加了什么阿哈尔和PēterisCaune做。

附加

  1. 包含一个布局来包含水平视图和更多的button(参考PēterisCaune的问题的图像)
  2. 当没有溢出时,布局,而不是只是button被删除
  3. 隐藏的文本显示或隐藏取决于button的状态

更新的代码

ExpandablePanel类

 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> 

布局: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> 

实现:ExpandablePanelImplementation类

 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"); } }); } } 

你有没有试过有一个设置的大小,你不能点击和不可聚焦的ScrollView ? 然后,当你扩大它,你可以animation它变得更大。

伟大的延伸阿哈尔。 我修改了你的代码以修复我发现的错误。

我在a.setDuration(mAnimationDuration);之后的第128行添加了这个a.setDuration(mAnimationDuration); 在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(); } 

我发现如果内容高度为0,那么animation将不会播放,所以在animation之前必须将其设置为1。