web-dev-qa-db-fra.com

Comment implémenter des panneaux extensibles dans Android?

Existe-t-il un moyen facile de créer des blocs extensibles/pliables comme on le voit dans l'application officielle du marché?

Capture d'écran de l'application Market, lorsque vous cliquez sur le bouton "Plus", la section de description se développe avec une animation:

enter image description here

Je connais SlidingDrawer mais il ne semble pas convenir à des trucs comme celui-ci - il est censé être superposé et ne prend pas en charge les états semi-ouverts.

Mise à jour:

Voici ma solution à moitié fonctionnelle. Il s'agit d'un widget personnalisé qui étend LinearLayout. Cela fonctionne en quelque sorte, mais ne gère pas bien les cas Edge, comme la hauteur de contenu inférieure au paramètre collapsedHeight. Je suis sûr qu'avec suffisamment de regards, creuser dans le code et expérimenter les bizarreries pourraient être corrigées. J'espérais éviter de faire cela et gagner du temps en utilisant une solution officielle ou tierce prête à l'emploi. Bref, le voici, 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;
        }
    }
}

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

Et voici comment je l'utilise dans la mise en page:

<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
Pēteris Caune

Merci beaucoup OP! Pour toute personne intéressée, j'ai pris la solution d'OP et l'ai raffinée un peu.

  • La poignée ne s'affiche qu'en cas de débordement
  • Ajout de la possibilité de spécifier la durée de l'animation via l'attribut 'animationDuration'
  • Ajout de la possibilité d'attacher des écouteurs d'événements qui se déclenchent surExpand et onCollapse (ceci est utile pour par exemple changer le texte du bouton "Plus" en "Moins"
  • Réduit par défaut
  • Le contenu peut être modifié par programme (idem pour les attributs)

Voici le code mis à jour:

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

Et n'oubliez pas le 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>

Voir l'exemple d'utilisation d'OP pour la disposition XML ci-dessus. Voici un exemple pour les auditeurs:

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

Je sais que c'est une vieille question mais pour ceux qui sont intéressés, j'ai fait des ajouts à ce qu'a fait ahal et Pēteris Caune.

Ajouts

  1. Inclus une disposition pour contenir la vue horizontale et le bouton plus (se référer à l'image de la question de Pēteris Caune)
  2. La mise en page, au lieu de simplement le bouton est supprimé lorsqu'il n'y a pas de débordement
  3. Le texte masqué est affiché ou masqué selon l'état du bouton

Code mis à jour

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

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

Implémentation: 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
euniceadu

Avez-vous essayé d'avoir un ScrollView à une taille définie que vous rendez non cliquable et non focalisable? Ensuite, lorsque vous l'agrandissez, vous pouvez simplement l'animer en l'agrandissant.

9
CaseyB

Grande extension ahal. J'ai légèrement modifié votre code pour corriger un bug que j'ai trouvé.

J'ai ajouté ceci autour de la ligne 128, après a.setDuration(mAnimationDuration); dans 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();
}

J'ai trouvé que si la hauteur du contenu était de 0, alors l'animation ne serait pas lue, il fallait donc la mettre à 1 avant l'animation.

5
Dylan