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

2023-09-12 00:41:07 作者:山有枢

有没有一种简单的方法来创建可扩展/折叠块就像在官方市场应用见过?

市场应用截图,当你点击更多按钮,则说明部分动画扩展:

我知道 SlidingDrawer 但它似乎并不能适合这样的东西--IT的应该放在叠加,并且不支持的半开状态。

更新:

下面是我的半个工作液。这是一个自定义窗口小部件,扩展的LinearLayout 。它慈祥的作品,但不处理边界情况良好,如内容的高度比 collapsedHeight 参数较小。我敢肯定,有足够的凝视,挖code和试验的怪癖可能是固定的。希望避免这样做,而且节省一些时间通过使用现成的官方或第三方解决方案。总之,这里是,code:

 包com.example.androidapp.widgets;

进口android.content.Context;
进口android.content.res.TypedArray;
进口android.util.AttributeSet;
进口android.view.View;
进口android.view.animation.Animation;
进口android.view.animation.Transformation;
进口android.widget.LinearLayout;

进口com.example.androidapp.R;

公共类ExpandablePanel扩展的LinearLayout {

    私人最终诠释mHandleId;
    私人最终诠释mContentId;

    私人查看mHandle;
    私人查看mContent;

    私人布尔mExpanded = TRUE;
    私人诠释mCollapsedHeight = 0;
    私人诠释mContentHeight = 0;

    公共ExpandablePanel(上下文的背景下){
        这(背景下,NULL);
    }

    公共ExpandablePanel(上下文的背景下,ATTRS的AttributeSet){
        超(背景下,ATTRS);

        TypedArray A = context.obtainStyledAttributes(ATTRS,
            R.styleable.ExpandablePanel,0,0);

        //有多高的含量应在崩溃状态
        mCollapsedHeight =(INT)a.getDimension(
            R.styleable.ExpandablePanel_collapsedHeight,0.0);

        INT handleId = a.getResourceId(R.styleable.ExpandablePanel_handle,0);
        如果(handleId == 0){
            抛出新抛出:IllegalArgumentException(
                句柄属性是必需的,必须参照
                    +以有效的孩子。);
        }

        INT内容ID = a.getResourceId(R.styleable.ExpandablePanel_content,0);
        如果(内容ID == 0){
            抛出新抛出:IllegalArgumentException(
                内容属性是必需的,必须参照
                    +以有效的孩子。);
        }

        mHandleId = handleId;
        mContentId =内容ID;

        a.recycle();
    }

    @覆盖
    保护无效onFinishInflate(){
        super.onFinishInflate();

        mHandle = findViewById(mHandleId);
        如果(mHandle == NULL){
            抛出新抛出:IllegalArgumentException(
                句柄属性是必须引用的
                    +已有的孩子。);
        }

        mContent = findViewById(mContentId);
        如果(mContent == NULL){
            抛出新抛出:IllegalArgumentException(
                内容属性是必须引用的
                    +已有的孩子。);
        }

        mHandle.setOnClickListener(新PanelToggler());
    }

    @覆盖
    保护无效onMeasure(INT widthMeasureSpec,诠释heightMeasureSpec){
        如果(mContentHeight == 0){
            //首先,衡量含量高,想成为
            mContent.measure(widthMeasureSpec,MeasureSpec.UNSPECIFIED);
            mContentHeight = mContent.getMeasuredHeight();
        }

        //然后让平常的事情发生
        super.onMeasure(widthMeasureSpec,heightMeasureSpec);
    }

    私有类PanelToggler实现OnClickListener {
        公共无效的onClick(视图v){
            动画;
            如果(mExpanded){
                A =新ExpandAnimation(mContentHeight,mCollapsedHeight);
            } 其他 {
                A =新ExpandAnimation(mCollapsedHeight,mContentHeight);
            }
            a.setDuration(500);
            mContent.startAnimation(一);
            !mExpanded = mExpanded;
        }
    }

    私有类ExpandAnimation扩展动画{
        私人最终诠释mStartHeight;
        私人最终诠释mDeltaHeight;

        公共ExpandAnimation(INT startHeight,诠释endHeight){
            mStartHeight = startHeight;
            mDeltaHeight = endHeight  -  startHeight;
        }

        @覆盖
        保护无效applyTransformation(浮动interpolatedTime,
            变换T){
            android.view.ViewGroup.LayoutParams LP = mContent.getLayoutParams();
            lp.height =(INT)(mStartHeight + mDeltaHeight * interpolatedTime);
            mContent.setLayoutParams(LP);
        }

        @覆盖
        公共布尔willChangeBounds(){
            // TODO自动生成方法存根
            返回true;
        }
    }
}
 

下面的 RES /价值/ attrs.xml

 < XML版本=1.0编码=UTF-8&GT?;
<资源>
  <申报,设置样式名称=ExpandablePanel>
    < attr指示NAME =手柄格式=参考/>
    < attr指示NAME =内容的格式=参考/>
    < attr指示NAME =collapsedHeight格式=维/>
  < /申报,设置样式>
< /资源>
 
谷歌I O大会前瞻 除了Android N还有啥

这就是我如何布局使用它:

 < com.example.androidapp.widgets.ExpandablePanel
    机器人:方向=垂直
    机器人:layout_height =WRAP_CONTENT
    机器人:layout_width =FILL_PARENT
    例如:处理=@ + ID /扩展
    例如:内容=@ + ID /值
    例如:collapsedHeight =50dip>
    <的TextView
        机器人:ID =@用户名/值
        机器人:layout_width =FILL_PARENT
        机器人:layout_height =WRAP_CONTENT
        机器人:=了maxHeight50dip
        />
    <按钮
        机器人:ID =@ ID /扩展
        机器人:layout_width =WRAP_CONTENT
        机器人:layout_height =WRAP_CONTENT
        机器人:文本=更多/>
< /com.example.androidapp.widgets.ExpandablePanel>
 

解决方案

非常感谢OP!对于任何有兴趣我把OP的解决方案和完善了一点。

只处理显示,如果有溢 添加到通过animationDuration'属性指定动画持续能力 添加附加的事件侦听器被解雇的能力onExpand和onCollapse(这是如改变更多按钮上的文本很有用的少 在默认情况下折叠 内容可以通过编程修改(同属性)

下面是更新后的code:

 进口android.content.Context;
进口android.content.res.TypedArray;
进口android.util.AttributeSet;
进口android.view.View;
进口android.view.animation.Animation;
进口android.view.animation.Transformation;
进口android.widget.LinearLayout;

公共类ExpandablePanel扩展的LinearLayout {

    私人最终诠释mHandleId;
    私人最终诠释mContentId;

    私人查看mHandle;
    私人查看mContent;

    私人布尔mExpanded = FALSE;
    私人诠释mCollapsedHeight = 0;
    私人诠释mContentHeight = 0;
    私人诠释mAnimationDuration = 0;

    私人OnExpandListener mListener;

    公共ExpandablePanel(上下文的背景下){
        这(背景下,NULL);
    }

    公共ExpandablePanel(上下文的背景下,ATTRS的AttributeSet){
        超(背景下,ATTRS);
        mListener =新DefaultOnExpandListener();

        TypedArray一个= context.obtainStyledAttributes(ATTRS,R.styleable.ExpandablePanel,0,0);

        //有多高的含量应在崩溃状态
        mCollapsedHeight =(INT)a.getDimension(R.styleable.ExpandablePanel_collapsedHeight,0.0);

        //多久动画应该采取
        mAnimationDuration = a.getInteger(R.styleable.ExpandablePanel_animationDuration,500);

        INT handleId = a.getResourceId(R.styleable.ExpandablePanel_handle,0);
        如果(handleId == 0){
            抛出新抛出:IllegalArgumentException(
                句柄属性是必需的,必须参照
                    +以有效的孩子。);
        }

        INT内容ID = a.getResourceId(R.styleable.ExpandablePanel_content,0);
        如果(内容ID == 0){
            抛出新抛出:IllegalArgumentException(内容属性是必需的,且必须指向一个有效的孩子。);
        }

        mHandleId = handleId;
        mContentId =内容ID;

        a.recycle();
    }

    公共无效setOnExpandListener(OnExpandListener监听器){
        mListener =侦听器;
    }

    公共无效setCollapsedHeight(INT collapsedHeight){
        mCollapsedHeight = collapsedHeight;
    }

    公共无效setAnimationDuration(INT animationDuration){
        mAnimationDuration = animationDuration;
    }

    @覆盖
    保护无效onFinishInflate(){
        super.onFinishInflate();

        mHandle = findViewById(mHandleId);
        如果(mHandle == NULL){
            抛出新抛出:IllegalArgumentException(
                句柄属性是必须引用的
                    +已有的孩子。);
        }

        mContent = findViewById(mContentId);
        如果(mContent == NULL){
            抛出新抛出:IllegalArgumentException(
                内容属性必须指向一个
                    +已有的孩子。);
        }

        android.view.ViewGroup.LayoutParams LP = mContent.getLayoutParams();
        lp.height = mCollapsedHeight;
        mContent.setLayoutParams(LP);

        mHandle.setOnClickListener(新PanelToggler());
    }

    @覆盖
    保护无效onMeasure(INT widthMeasureSpec,诠释heightMeasureSpec){
        //首先,衡量含量高,想成为
        mContent.measure(widthMeasureSpec,MeasureSpec.UNSPECIFIED);
        mContentHeight = mContent.getMeasuredHeight();

        如果(mContentHeight< mCollapsedHeight){
            mHandle.setVisibility(View.GONE);
        } 其他 {
            mHandle.setVisibility(View.VISIBLE);
        }

        //然后让平常的事情发生
        super.onMeasure(widthMeasureSpec,heightMeasureSpec);
    }

    私有类PanelToggler实现OnClickListener {
        公共无效的onClick(视图v){
            动画;
            如果(mExpanded){
                A =新ExpandAnimation(mContentHeight,mCollapsedHeight);
                mListener.onCollapse(mHandle,mContent);
            } 其他 {
                A =新ExpandAnimation(mCollapsedHeight,mContentHeight);
                mListener.onExpand(mHandle,mContent);
            }
            a.setDuration(mAnimationDuration);
            mContent.startAnimation(一);
            !mExpanded = mExpanded;
        }
    }

    私有类ExpandAnimation扩展动画{
        私人最终诠释mStartHeight;
        私人最终诠释mDeltaHeight;

        公共ExpandAnimation(INT startHeight,诠释endHeight){
            mStartHeight = startHeight;
            mDeltaHeight = endHeight  -  startHeight;
        }

        @覆盖
        保护无效applyTransformation(浮动interpolatedTime,变换T){
            android.view.ViewGroup.LayoutParams LP = mContent.getLayoutParams();
            lp.height =(INT)(mStartHeight + mDeltaHeight * interpolatedTime);
            mContent.setLayoutParams(LP);
        }

        @覆盖
        公共布尔willChangeBounds(){
            返回true;
        }
    }

    公共接口OnExpandListener {
        公共无效onExpand(查看拉手,查看内容);
        公共无效onCollapse(查看拉手,查看内容);
    }

    私有类DefaultOnExpandListener实现OnExpandListener {
        公共无效onCollapse(查看拉手,查看内容){}
        公共无效onExpand(查看拉手,查看内容){}
    }
}
 

和别忘了attrs.xml:

 < XML版本=1.0编码=UTF-8&GT?;
<资源>
    <申报,设置样式名称=ExpandablePanel>
        < attr指示NAME =手柄格式=参考/>
        < attr指示NAME =内容的格式=参考/>
        < attr指示NAME =collapsedHeight格式=维/>
        < attr指示NAME =animationDuration格式=整数/>
    < /申报,设置样式>
< /资源>
 

请参阅OP的使用例子对上面的XML布局。下面是听众的一个例子:

  //设置扩展面板监听器
ExpandablePanel面板=(ExpandablePanel)view.findViewById(R.id.foo);
panel.setOnExpandListener(新ExpandablePanel.OnExpandListener(){
    公共无效onCollapse(查看拉手,查看内容){
        按钮BTN =(按钮)处理;
        btn.setText(更多);
    }
    公共无效onExpand(查看拉手,查看内容){
        按钮BTN =(按钮)处理;
        btn.setText(减);
    }
});
 

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:

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>

解决方案

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