Как реализовать расширяемые панели в Android?
Есть ли простой способ создания расширяемых/сбрасываемых блоков, например, в официальном приложении на рынке?
Снимок экрана приложения Market, когда вы нажимаете кнопку "Дополнительно", раздел описания расширяется с анимацией:
![enter image description here]()
Я знаю SlidingDrawer, но, похоже, он не подходит для подобных вещей - он должен быть помещен в оверлей, и не поддерживает полуоткрытые состояния.
Update:
Вот мое полузадачное решение. Это пользовательский виджет, расширяющий LinearLayout
. Это своего рода работа, но не очень хорошо обрабатывает граничные случаи, например, высоту содержимого меньше, чем параметр collapsedHeight
. Я уверен, что с достаточным вниманием смотрю, копаю код и экспериментируя, что причуды могут быть исправлены. Был надеяться избежать этого и сэкономить некоторое время, используя готовое официальное или стороннее решение. Во всяком случае, вот он, код:
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>
Ответы
Ответ 1
Большое спасибо OP! Для всех, кого это интересует, я принял решение OP и немного уточнил его.
- Ручка выводит только при переполнении
- Добавлена возможность указывать продолжительность анимации через атрибут 'animationDuration'
- Добавлена возможность подключать прослушиватели событий, которые запускаются onExpand и onCollapse (это полезно, например, для изменения текста кнопки "Дополнительно" на "Меньше"
- По умолчанию по умолчанию
- Содержимое может быть изменено программно (то же самое с атрибутами)
Здесь обновленный код:
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>
См. пример использования OP для схемы XML выше. Вот пример для слушателей:
// 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");
}
});
Ответ 2
Я знаю, что это старый вопрос, но для тех, кто заинтересован, я сделал дополнения к тому, что делали ахал и Петерис Каун.
Дополнения
- Включен макет, который содержит горизонтальный вид и большую кнопку (см. изображение с вопроса Петериса Кауна)
- Макет, а не только кнопка удаляется, когда нет переполнения
- Скрытый текст отображается или скрывается в зависимости от состояния кнопки
Обновленный код
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>
Макет: 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 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");
}
});
}
}
Ответ 3
Вы пытались иметь ScrollView
с заданным размером, который вы делаете не кликабельным и не настраиваемым? Затем, когда вы его расширяете, вы можете просто оживить его.
Ответ 4
Великое расширение ахала.
Я немного изменил ваш код, чтобы исправить найденную ошибку.
Я добавил это вокруг строки 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, анимация не будет воспроизводиться, поэтому ей нужно было установить ее до 1 до анимации.