Creación de una vista redimensionable ListAdapter-recycleable

Estoy trabajando en la creación de una vista personalizada que tendrá un estado expandido y condensado – en el estado condensado, que sólo mostrará una etiqueta y un icono, y en el estado expandido, se mostrará un mensaje por debajo de eso. Aquí hay una captura de pantalla de cómo funciona hasta ahora:

captura de pantalla

La View sí conserva los valores de tamaño para los estados condensados ​​y expandidos una vez medidos, por lo que es sencillo animar entre los dos estados, y al usar la vista en la práctica normal (por ejemplo, en LinearLayout ) todo funciona como se pretende. El cambio al tamaño de la vista se realiza llamando a getLayoutParams().height = newHeight; requestLayout(); getLayoutParams().height = newHeight; requestLayout();

Sin embargo, al usarlo en un ListView , la vista se recicla y mantiene su altura anterior. Por lo tanto, si la vista se expandió cuando estaba oculta, se mostrará como expandida cuando se recicla para el siguiente elemento de lista. No parece recibir otro pase de disposición, incluso si solicito un diseño en el ListAdapter . Pensé en usar un reciclador con dos tipos de vista diferentes (expandido y condensado), pero los tamaños variarán dependiendo del tamaño del mensaje. ¿Hay algún evento que pueda escuchar cuando la vista se vuelva a conectar en el ListView ? ¿O tienes otra sugerencia de cómo manejar esto?

EDIT: Así es como estoy determinando las alturas expandidas y condensadas para la vista:

 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if(r - l > 0 && b - t > 0 && dimensionsDirty) { int widthSpec = MeasureSpec.makeMeasureSpec(r - l, MeasureSpec.EXACTLY); messageView.setVisibility(GONE); measure(widthSpec, MeasureSpec.UNSPECIFIED); condensedHeight = getMeasuredHeight(); messageView.setVisibility(VISIBLE); measure(widthSpec, MeasureSpec.UNSPECIFIED); expandedHeight = getMeasuredHeight(); dimensionsDirty = false; } } 

2 Solutions collect form web for “Creación de una vista redimensionable ListAdapter-recycleable”

EDIT : Orden fijo de parámetros para ambas llamadas a makeMeasureSpec . Curiosamente, funcionó de manera incorrecta que yo tenía, así que casi me pregunto si estoy haciendo algo redundante. De cualquier manera, sólo quería señalarlo – el proyecto para descargar a continuación no tiene estas correcciones.

De acuerdo, así que realmente me molestaba que no pudiera entender esto, así que decidí familiarizarme con el sistema de diseño y medición, y aquí está la solución que he inventado.

  1. Un grupo de ViewGroup personalizado que amplía FrameLayout que aloja a un solo hijo directo (como ScrollView ).
  2. Un ListAdapter personalizado que controla el seguimiento del estado expandido / contraído de cada elemento de lista.
  3. Un OnItemClickListener personalizado para manejar las solicitudes de animación entre estados colapsados ​​y expandidos.

ResizeLayout Captura de pantalla

Me gustaría compartir este código en caso de que alguien lo encuentre útil. Debe ser bastante flexible, pero no tengo duda de que hay errores y cosas que podrían mejorarse. Por un lado, tenía problemas de desplazamiento mediante programación de ListView (no parece ser una forma de desplazar realmente el contenido en lugar de sólo la vista) por lo que utiliza smoothScrollToPosition(int) para cada cambio en el tamaño de la vista. Esto tiene una duración codificada de 400ms que es innecesaria, por lo que en el futuro podría tratar de escribir mi propia versión con una duración de 0 (es decir, scrollToPosition(int) ).

El uso general es el siguiente:

  1. Su elemento de lista XML debe tener su ResizeLayout como la raíz de la jerarquía, y desde allí puede construir cualquier estructura de diseño que desee. Básicamente, sólo envuelva su disposición de elementos de lista normal en una etiqueta ResizeLayout .

  2. En su diseño, debe tener una vista con el identificador collapse_to . Esta es la vista que el diseño se ajustará a (es decir, qué vista determina la altura colapsada).

  3. Cosas importantes que hacer si está reciclando a través de un adaptador de lista:

    • SIEMPRE llame a reuse() cuando recupere una vista reciclada (por ejemplo, convertView )
    • SIEMPRE llame a setIsExpanded(boolean) antes de devolver la vista reciclado; De lo contrario mantendrá el estado en el que se encontraba antes de ser reciclado

Puedo eventualmente lanzar esto en un reporte de git, pero por ahora aquí está el código:

ResizeLayout.java

Esta es la mayor parte del código. También incluiré mi Activity y el Adapter que utilicé para probar más abajo. Son bastante genéricos, pero ilustran el uso con eficacia.

 import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.animation.*; import android.widget.FrameLayout; /* * ResizeLayout * * Custom ViewGroup that allows you to specify a view in the child hierarchy to wrap to, and * allows for the view to be expanded to the full size of the content. * * Author: Kevin Coppock * Date: 2013/03/02 */ public class ResizeLayout extends FrameLayout { private static final int PX_PER_SEC = 900; //Pixels per Second to animate layout changes private final LayoutAnimation animation = new LayoutAnimation(); private final int wrapSpec = MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED); private int collapsedHeight = 0; private int expandedHeight = 0; private boolean contentsChanged = true; private State state = State.COLLAPSED; private OnLayoutChangedListener listener; public ResizeLayout(Context context) { super(context); } public ResizeLayout(Context context, AttributeSet attrs) { super(context, attrs); } public ResizeLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if(getChildCount() > 0) { View child = getChildAt(0); child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); } //If the layout parameters have changed and the view is animating, notify listeners if(changed && animation.isAnimating()) { switch(state) { case COLLAPSED: fireOnLayoutCollapsing(left, top, right, bottom); break; case EXPANDED: fireOnLayoutExpanding(left, top, right, bottom); break; } } } /** * Reset the internal state of the view to defaults. This should be called any time you change the contents * of this ResizeLayout (eg recycling through a ListAdapter) */ public void reuse() { collapsedHeight = expandedHeight = 0; contentsChanged = true; state = State.COLLAPSED; requestLayout(); } /** * Set the state of the view. This should ONLY be called after a call to reuse() as it does not animate * the view; it simply sets the internal state. An example of usage is in a ListAdapter -- if the view is * recycled, it may be in the incorrect state, so it should be set here to the correct state before layout. * @param isExpanded whether or not the view should be in the expanded state */ public void setIsExpanded(boolean isExpanded) { state = isExpanded ? State.EXPANDED : State.COLLAPSED; } /** * Animates the ResizeLayout between COLLAPSED and EXPANDED states, only if it is not currently animating. */ public void animateToNextState() { if(!animation.isAnimating()) { animation.reuse(state.getStartHeight(this), state.getEndHeight(this)); state = state.next(); startAnimation(animation); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); if(getChildCount() < 1) { //ResizeLayout has no child; default to spec, or padding if unspecified setMeasuredDimension( widthMode == MeasureSpec.UNSPECIFIED ? getPaddingLeft() + getPaddingRight() : width, heightMode == MeasureSpec.UNSPECIFIED ? getPaddingTop() + getPaddingBottom() : height ); return; } View child = getChildAt(0); //Get the only child of the ResizeLayout if(contentsChanged) { //If the contents of the view have changed (first run, or after reset from reuse()) contentsChanged = false; updateMeasurementsForChild(child, widthMeasureSpec, heightMeasureSpec); return; } //This state occurs on the second run. The child might be wrap_content, so the MeasureSpec will be unspecified. //Skip measuring the child and just accept the measurements from the first run. if(heightMode == MeasureSpec.UNSPECIFIED) { setMeasuredDimension(getWidth(), getHeight()); } else { //Likely in mid-animation; we have a fixed-height from the MeasureSpec so use it child.measure(widthMeasureSpec, heightMeasureSpec); setMeasuredDimension(child.getMeasuredWidth(), child.getMeasuredHeight()); } } /** * Sets the measured dimension for this ResizeLayout, getting the initial measurements * for the condensed and expanded heights from the child view. * @param child the child view of this ResizeLayout * @param widthSpec the width MeasureSpec from onMeasure() * @param heightSpec the height MeasureSpec from onMeasure() */ private void updateMeasurementsForChild(View child, int widthSpec, int heightSpec) { child.measure(widthSpec, wrapSpec); //Measure the child using WRAP_CONTENT for the height //Get the View that has been selected as the "collapse to" view (ID = R.id.collapse_to) View viewToCollapseTo = child.findViewById(R.id.collapse_to); if(viewToCollapseTo != null) { //The collapsed height should be the height of the collapseTo view + any top or bottom padding collapsedHeight = viewToCollapseTo.getMeasuredHeight() + child.getPaddingTop() + child.getPaddingBottom(); //The expanded height is simply the full height of the child (measured with WRAP_CONTENT) expandedHeight = child.getMeasuredHeight(); //Re-Measure the child to reflect the state of the view (COLLAPSED or EXPANDED) int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(state.getStartHeight(this), MeasureSpec.EXACTLY); child.measure(widthSpec, newHeightMeasureSpec); } setMeasuredDimension(child.getMeasuredWidth(), child.getMeasuredHeight()); } @Override public void addView(View child) { if(getChildCount() > 0) { throw new IllegalArgumentException("ResizeLayout can host only one direct child."); } else { super.addView(child); } } @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { if(getChildCount() > 0) { throw new IllegalArgumentException("ResizeLayout can host only one direct child."); } else { super.addView(child, index, params); } } @Override public void addView(View child, ViewGroup.LayoutParams params) { if(getChildCount() > 0) { throw new IllegalArgumentException("ResizeLayout can host only one direct child."); } else { super.addView(child, params); } } @Override public void addView(View child, int width, int height) { if(getChildCount() > 0) { throw new IllegalArgumentException("ResizeLayout can host only one direct child."); } else { super.addView(child, width, height); } } /** * Handles animating the view between its expanded and collapsed states by adjusting the * layout parameters of the containing object and requesting a layout pass. */ private class LayoutAnimation extends Animation implements Animation.AnimationListener { private int startHeight = 0, deltaHeight = 0; private boolean isAnimating = false; /** * Just a default interpolator and friction I think feels nice; can be changed. */ public LayoutAnimation() { setInterpolator(new DecelerateInterpolator(2.2f)); setAnimationListener(this); } /** * Sets the duration of the animation to a duration matching the specified value in * Pixels per Second (PPS). For example, if the view animation is 60 pixels, then a PPS of 60 * would set a duration of 1000ms (ie duration = (delta / pps) * 1000). PPS is used rather * than a fixed time so that the animation speed is consistent regardless of the contents * of the view. * @param pps the number of pixels per second to resize the layout by */ private void setDurationPixelsPerSecond(int pps) { setDuration((int) (((float) Math.abs(deltaHeight) / pps) * 1000)); } /** * Allows reuse of a single LayoutAnimation object. Call this before starting the animation * to restart the animation and set the new parameters * @param startHeight the height from which the animation should begin * @param endHeight the height at which the animation should end */ public void reuse(int startHeight, int endHeight) { reset(); setStartTime(0); this.startHeight = startHeight; this.deltaHeight = endHeight - startHeight; setDurationPixelsPerSecond(PX_PER_SEC); } /** * Applies the height transformation to this containing ResizeLayout * @param interpolatedTime the time (0.0 - 1.0) interpolated based on the set interpolator * @param t the transformation associated with the animation -- not used here */ @Override protected void applyTransformation(float interpolatedTime, Transformation t) { getLayoutParams().height = startHeight + (int)(deltaHeight * interpolatedTime); requestLayout(); } public boolean isAnimating() { return isAnimating; } @Override public void onAnimationStart(Animation animation) { isAnimating = true; } @Override public void onAnimationEnd(Animation animation) { isAnimating = false; } @Override public void onAnimationRepeat(Animation animation) { /*Not implemented*/ } } /** * Interface to listen for layout changes during an animation */ public interface OnLayoutChangedListener { public void onLayoutExpanding(int l, int t, int r, int b); public void onLayoutCollapsing(int l, int t, int r, int b); } /** * Sets a listener for changes to this view's layout * @param listener the listener for layout changes */ public void setOnBoundsChangedListener(OnLayoutChangedListener listener) { this.listener = listener; } private void fireOnLayoutExpanding(int l, int t, int r, int b) { if(listener != null) listener.onLayoutExpanding(l, t, r, b); } private void fireOnLayoutCollapsing(int l, int t, int r, int b) { if(listener != null) listener.onLayoutCollapsing(l, t, r, b); } protected enum State { COLLAPSED{ @Override public State next() { return EXPANDED; } @Override public int getEndHeight(ResizeLayout view) { return view.expandedHeight; } @Override public int getStartHeight(ResizeLayout view) { return view.collapsedHeight; } }, EXPANDED{ @Override public State next() { return COLLAPSED; } @Override public int getEndHeight(ResizeLayout view) { return view.collapsedHeight; } @Override public int getStartHeight(ResizeLayout view) { return view.expandedHeight; } }; public abstract State next(); public abstract int getStartHeight(ResizeLayout view); public abstract int getEndHeight(ResizeLayout view); } } 

MyActivity.java

Simplemente una ListActivity simple que utilicé para los propósitos de este ejemplo. main.xml es sólo el LinearLayout genérico con el lenguaje XML de ListView para una ListActivity .

 import android.app.ListActivity; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.TextView; import java.util.HashSet; import java.util.Set; public class MyActivity extends ListActivity implements ResizeLayout.OnLayoutChangedListener, AdapterView.OnItemClickListener { private MyAdapter myAdapter; private int clickedItemPosition; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); myAdapter = new MyAdapter(this); setListAdapter(myAdapter); getListView().setOnItemClickListener(this); getListView().setSelector(new ColorDrawable(Color.TRANSPARENT)); } @Override public void onLayoutExpanding(int l, int t, int r, int b) { //Keep the clicked view fully visible if it's expanding getListView().smoothScrollToPosition(clickedItemPosition); } @Override public void onLayoutCollapsing(int l, int t, int r, int b) { //Not handled currently } @Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { clickedItemPosition = i; myAdapter.toggleExpandedState(i); ((ResizeLayout) view).animateToNextState(); } private class MyAdapter extends BaseAdapter { private LayoutInflater inflater; private Set<Integer> expanded = new HashSet<Integer>(); public MyAdapter(Context ctx) { inflater = LayoutInflater.from(ctx); } @Override public int getCount() { return 100; } @Override public Object getItem(int i) { return i + 1; } @Override public long getItemId(int i) { return i; } public void toggleExpandedState(int position) { if (expanded.contains(position)) { expanded.remove(position); } else { expanded.add(position); } } @Override public View getView(int i, View convertView, ViewGroup viewGroup) { ResizeLayout layout = (ResizeLayout) convertView; TextView title; //New instance; no view to recycle. if (layout == null) { layout = (ResizeLayout) inflater.inflate(R.layout.list_item, viewGroup, false); layout.setOnBoundsChangedListener(MyActivity.this); layout.setTag(layout.findViewById(R.id.title)); } //Recycling a ResizeLayout; make sure to reset parameters with reuse() else layout.reuse(); //Set the state of the View -- otherwise it will be in whatever state it was before recycling layout.setIsExpanded(expanded.contains(i)); title = (TextView) layout.getTag(); title.setText("List Item #" + i); return layout; } } } 

List_item.xml

Ejemplo básico de disposición de elementos de lista. Sólo tiene un icono y un título en la parte superior (el icono se establece como el collapse_to ver) y una vista de mensaje alineada a continuación.

 <?xml version="1.0" encoding="utf-8"?> <com.example.resize.ResizeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" > <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:padding="10dp"> <ImageView android:id="@+id/collapse_to" android:src="@drawable/holoku" android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="centerInside" android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:contentDescription="@string/icon_desc" tools:ignore="UseCompoundDrawables" /> <TextView android:id="@+id/title" android:layout_width="match_parent" android:layout_height="0dp" android:layout_alignTop="@id/collapse_to" android:layout_alignBottom="@id/collapse_to" android:layout_toRightOf="@id/collapse_to" android:gravity="center_vertical" android:paddingLeft="20dp" android:textSize="20dp" android:textColor="#198EBC" /> <TextView android:id="@+id/text" android:layout_marginTop="10dp" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="12dp" android:textColor="#444444" android:layout_below="@id/collapse_to" android:text="@string/message" /> </RelativeLayout> </com.example.resize.ResizeLayout> 

Ahora no lo he probado en nada antes de la API 17, pero la ejecución de verificación de pelusa para los problemas de NewApi dice que esto debería funcionar tan atrás como 2,2 (API 8).

Si desea descargar el proyecto de ejemplo y jugar con él mismo, puede descargarlo aquí .

¿Puede anular el método getView de su adaptador y examinar la variable convertView (es el segundo parámetro, al menos en el ArrayAdapter que estoy viendo). Debería poder llamar a getLayoutParames para obtener su altura y ajustarla en función de la variable de posición.

  • Los métodos getTop (), getLeft (), getX (), getY (), getWidth (), getHeight () de la vista de Android
  • Android WindowManager TYPE_SYSTEM_ALERT Diseño en el tacto
  • Establecer margen en RecyclerView mediante programación
  • Añadir diseño con parámetros programáticamente Android
  • TYPE_SYSTEM_OVERLAY en ICS
  • Android Salir del modo de pantalla completa
  • android setLayoutParams para webview genera ERROR
  • Tostada personalizada en la parte superior de la pantalla
  • Cómo deshabilitar la captura de pantalla en Android fragmento?
  • Cómo encontrar el área de texto (altura / ancho) de TextView de forma programática en android
  • Restablecer LayoutParams a lo que se define en XML
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.