Android 使ListAdapter成为可循环使用的可调整大小的视图

Android 使ListAdapter成为可循环使用的可调整大小的视图,android,android-listview,layoutparams,Android,Android Listview,Layoutparams,我正在创建一个自定义视图,它将有一个扩展和压缩状态——在压缩状态下,它将只显示一个标签和一个图标,在扩展状态下,它将在下面显示一条消息。以下是迄今为止它的工作原理的屏幕截图: 视图本身在测量后保留压缩和扩展状态的大小值,因此很容易在两种状态之间设置动画,并且在正常实践中使用视图时(例如在线性布局中),一切都按预期工作。通过调用getLayoutParams(),可以更改视图大小。height=newHeight;requestLayout() 但是,在列表视图中使用该视图时,该视图将被回收并保

我正在创建一个自定义视图,它将有一个扩展和压缩状态——在压缩状态下,它将只显示一个标签和一个图标,在扩展状态下,它将在下面显示一条消息。以下是迄今为止它的工作原理的屏幕截图:

视图
本身在测量后保留压缩和扩展状态的大小值,因此很容易在两种状态之间设置动画,并且在正常实践中使用视图时(例如在
线性布局
中),一切都按预期工作。通过调用
getLayoutParams(),可以更改视图大小。height=newHeight;requestLayout()


但是,在
列表视图中使用该视图时,该视图将被回收并保持其以前的高度。因此,如果视图在隐藏时展开,则在为下一个列表项回收视图时,它将显示为展开。即使我在
ListAdapter
中请求布局,它似乎也不会收到另一个布局传递。我考虑过使用具有两种不同视图类型(扩展视图和压缩视图)的回收器,但是大小会根据消息的大小而有所不同。在
列表视图
中重新连接视图时,是否有我可以侦听的事件?或者你对如何处理这个问题还有其他建议吗

编辑:这是我确定视图的展开高度和压缩高度的方式:

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

您是否可以重写适配器的getView方法,并检查convertView变量(它是第二个参数,至少在我正在查看的ArrayAdapter中是这样)。您应该能够对其调用GetLayoutParameters以获取其高度,并根据位置变量相应地调整它。

编辑:
makeMeasureSpec的两个调用的参数顺序固定。奇怪的是,它的工作方式不正确,所以我几乎想知道我是否在做一些多余的事情。不管怎样,我只是想指出一点——下面要下载的项目没有这些更正

好吧,我真的很困扰,我没能弄明白这一点,所以我决定更加熟悉布局和测量系统,这是我想出的解决方案

  • 一个自定义的
    ViewGroup
    扩展
    FrameLayout
    ,它承载一个直接子对象(如
    ScrollView
  • 自定义
    ListAdapter
    ,用于跟踪每个列表项的展开/折叠状态
  • 一个自定义的
    OnItemClickListener
    ,用于处理在折叠和展开状态之间设置动画的请求
  • 我想分享这段代码,以防其他人发现它有用。它应该是相当灵活的,但我毫不怀疑有缺陷和可以改进的地方。例如,我在编程上滚动
    列表视图时遇到问题(似乎没有一种方法可以真正滚动内容,而不仅仅是视图),因此每次更改视图大小时,我都使用
    smoothScrollToPosition(int)
    。这有一个硬编码的400毫秒的持续时间,这是不必要的,因此在将来我可能会尝试编写自己的版本,持续时间为0(即
    scrollToPosition(int)

    一般用途如下:

  • 列表项XML应该将您的
    ResizeLayout
    作为层次结构的根,从那里您可以构建任何想要的布局结构。基本上,只需将常规列表项布局包装在
    ResizeLayout
    标记中即可

  • 在布局中,应该有一个id为
    collapse\u to
    的视图。这是布局将环绕到的视图(即确定折叠高度的视图)

  • 如果要通过列表适配器进行回收,请执行以下重要操作:

    • 检索回收视图时,始终调用
      reuse()
      (例如
      convertView
    • 在返回回收视图之前,始终调用
      setIsExpanded(boolean)
      ;否则,它将保持在回收前的状态
  • 我可能最终会将其投入git回购,但目前代码如下:

    ResizeLayout.java 这是大部分代码。我还将包括我用于进一步测试的
    活动
    适配器
    。它们非常通用,但它们有效地说明了用途

    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 (e.g. 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 (i.e. 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);
        }
    }
    
    list_item.xml 基本列表项布局示例。只在顶部有一个图标和一个标题(图标被设置为
    collapse\u to
    视图)和一个下面对齐的消息视图

    <?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>
    
    
    
    现在我还没有在API 17之前的任何东西上测试过它,但是运行lint检查来发现新的API问题,这应该可以追溯到2.2(API 8)


    如果您想下载示例项目并自己玩,您可以下载。

    您可能应该始终在
    Adapter.getView(…)
    方法中的行中设置高度(顺便问一下,
    newHeight
    从何而来?),并跟踪其他位置的状态。也就是说,如果要将POJO馈送到
    适配器
    ,则向其添加一个简单的布尔值,指示折叠/扩展状态就足够了。或者,您可以使用类似于
    散列集的内容,并添加处于“扩展”状态的索引。@MH。嗯,视图的高度取决于其内容,因此新高度是在视图的重写
    onLayout
    方法中测量的。我计划稍后在ListAdapter中跟踪扩展/压缩状态,并相应地设置视图的状态。我已经为你的问题添加了这个,如果它有意义的话。我现在正在尝试使用
    onAttachedToWindow()
    OnAttachedFromWindow()
    执行某些操作。“在ListView中重新附加视图时,是否有我可以侦听的事件?”您是否忽略了明显的问题?当
    convertView!=null
    该视图来自废料堆。正如您所知,RecycleBin只有一个侦听器
    <?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>