制作ListAdapter-recycleable可调整大小的视图

时间:2013-01-02 19:34:53

标签: android android-listview layoutparams

我正在创建一个具有扩展和压缩状态的自定义视图 - 在压缩状态下,它只显示标签和图标,并且在展开状态下,它将在下面显示一条消息。以下是目前为止如何运作的截图:

screenshot

View本身一旦测量就会保留浓缩和扩展状态的大小值,因此在两种状态之间动画很简单,并且在正常练习中使用视图时(例如在LinearLayout中)一切都按预期工作。通过调用getLayoutParams().height = newHeight; requestLayout();

来完成对视图大小的更改

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

编辑:这就是我如何确定视图的扩展和压缩高度:

@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 个答案:

答案 0 :(得分:7)

编辑:修复了makeMeasureSpec两次调用的参数顺序。奇怪的是,它的工作方式不正确,所以我几乎想知道我是否做了多余的事情。无论哪种方式,只是想指出它 - 下面的下载项目没有这些更正。

好的,所以我无法理解这一点,所以我决定更加熟悉布局和测量系统,这就是我提出的解决方案。

  1. 自定义ViewGroupFrameLayout,其中包含一个直接子项(例如ScrollView。)
  2. 自定义ListAdapter,用于处理跟踪每个列表项的展开/折叠状态。
  3. 自定义OnItemClickListener,用于处理折叠状态和展开状态之间的动画请求。
  4. ResizeLayout Screenshot

    我想分享此代码以防其他人发现它有用。它应该相当灵活,但我毫不怀疑存在可以改进的错误和事情。首先,我遇到了以编程方式滚动ListView的问题(似乎没有办法实际滚动内容而不仅仅是视图)所以我使用smoothScrollToPosition(int)对视图大小进行了每次更改。这是一个硬编码的400ms持续时间,这是不必要的,所以将来我可能会尝试编写我自己的版本,持续时间为0(即scrollToPosition(int))。

    一般用法如下:

    1. 您的列表项XML应该以{{1​​}}作为层次结构的根,然后您可以构建所需的任何布局结构。基本上只需将您的普通列表项布局包装在ResizeLayout标记中。

    2. 在您的布局中,您应该有一个ID为ResizeLayout的视图。这是布局将换行的视图(即,哪个视图决定了折叠高度)。

    3. 如果您通过列表适配器进行回收,则需要执行以下重要操作:

      • 检索回收的视图时,请务必致电collapse_to(例如reuse()
      • 在返回循环视图之前,务必调用convertView;否则它会保留它被回收之前的状态
    4. 我最终可能会把它扔进一个git repo,但现在这里是代码:

      ResizeLayout.java

      这是大部分代码。我还会包含我用于进一步测试的setIsExpanded(boolean)Activity。它们非常通用,但它们有效地说明了它的用途。

      Adapter

      MyActivity.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); } } ListActivity只是通用的main.xml LinearLayout子XML,用于ListView

      ListActivity

      list_item.xml

      基本列表项布局示例。只需在顶部有一个图标和标题(图标设置为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; } } } 视图),并在下方对齐消息视图。

      collapse_to

      现在我还没有在API 17之前对它进行任何测试,但是对NewApi问题运行lint检查说这应该可以追溯到2.2(API 8)。

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

答案 1 :(得分:0)

你可以覆盖你的适配器的getView方法,并检查convertView变量(它是第二个参数,至少在我正在看的ArrayAdapter中)。您应该可以在其上调用getLayoutParame来获取其高度,并根据位置变量进行相应调整。