制作ListAdapter,反复使用可调整查看 [英] Making a ListAdapter-recycleable Resizable View

查看:284
本文介绍了制作ListAdapter,反复使用可调整查看的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在创建一个自定义视图,将有一个扩大和浓缩状态 - 在浓缩状态,它只会显示一个标签和一个图标,并在扩展状态下,它会显示在下面的留言。下面是它如何工作至今截图:

查看本身保留了一次测得的凝聚和扩大国家大小值,所以它是简单的两种状态之间设置动画,并在正常的做法使用视图(当例如,在一个的LinearLayout )一切都按计划进行。改变视图尺寸是通过调用 getLayoutParams()来实现高度= newHeight。 requestLayout();

然而,当在的ListView 使用它,认为被回收并保持其previous高度。因此,如果当它被隐藏的看法扩大,它会显示,当它被回收的下一个列​​表项的扩展。它似乎并没有收到其他的布局传递,即使我请求在 ListAdapter 的布局。我认为使用回收两个不同的视图类型(扩展和浓缩),但大小将取决于消息的大小而有所不同。有一个事件,我可以听视图时重新连接在的ListView 的呢?还是你对如何处理这又有何建议?

编辑:这就是我决定扩大和高度浓缩的观点:

  @覆盖
保护无效onLayout(布尔改变,诠释L,INT T,INT R,int b)在{
    super.onLayout(改变,L,T,R,B);
    如果(R  -  1大于0&安培;和b  - 吨大于0&安培;&安培; dimensionsDirty){
        INT widthSpec = MeasureSpec.makeMeasureSpec(R  -  1,MeasureSpec.EXACTLY);
        messageView.setVisibility(GONE);
        措施(widthSpec,MeasureSpec.UNSPECIFIED);
        condensedHeight = getMeasuredHeight();

        messageView.setVisibility(可见);
        措施(widthSpec,MeasureSpec.UNSPECIFIED);
        expandedHeight = getMeasuredHeight();

        dimensionsDirty = FALSE;
    }
}
 

解决方案

修改:对于这两个调用的参数固定为 makeMeasureSpec 。奇怪的是,它的工作不正确的方式,我有,所以我几乎不知道我在做什么多余的。无论哪种方式,只是想指出来 - 该项目下下载不具备这些修正

好了,所以这是真的困扰着我,我能不知道这一点,所以我决定让更熟悉的布局和测量系统,和这里的,我已经拿出了解决方案。

  1. 在自定义的ViewGroup 延长的FrameLayout 承载单一的直接子(如滚动型
  2. 在自定义 ListAdapter 处理跟踪每个项目的展开/折叠状态。
  3. 在自定义 OnItemClickListener 来处理请求折叠和展开状态之间进行动画处理。

我想万一有人分享这个code别人发现它是有用的。它应该是相当灵活的,但我毫不怀疑有错误,事情可能会有所改善。首先,我有问题编程方式滚动的ListView (似乎没有成为一个方式,实际上滚动的内容,而不是仅仅的看法),所以我用 smoothScrollToPosition(INT)每次更改视图尺寸。这有一个硬codeD 400ms的时间是没有必要的,所以在未来我可能会尝试使用的持续时间写我自己的版本0(即 scrollToPosition(INT) )。

一般用途如下:

  1. 您的列表项XML应该有你的 ResizeLayout 的层次结构的根,并从那里你可以建立任何你想要的布局结构。基本上只是包装你的普通列表项目布局在<$​​ C $ C> ResizeLayout 标记。

  2. 在你的布局,你应该有一个视图id为 collapse_to 。这是认为该布局将换到(即什么看法决定了倒塌的高度)。

  3. 重要的事情要做,如果你循环通过列表适配器:

    • 随时拨打再利用()在获取再生视图(如 convertView
    • 随时拨打 setIsExpanded(布尔)返回回收视图之前;否则将保留的状态是在它被回收之前

我最终可能会抛出此为一个混帐回购协议,但现在这里的code:

ResizeLayout.java

这是大部分的code。我会还包括我的活动适配器,我用于测试进一步下跌。他们是很普通的,但它们有效地说明了使用。

 进口android.content.Context;
进口android.util.AttributeSet;
进口android.util.Log;
进口android.view.View;
进口android.view.ViewGroup;
进口android.view.animation *。
进口android.widget.FrameLayout;

/ *
 * ResizeLayout
 *
 *自定义的ViewGroup,它允许您指定的子层次以便换到,和
 *允许视图扩大到内容的全尺寸。
 *
 *作者:凯文科波克
 *日期:2013年3月2日
 * /

公共类ResizeLayout扩展的FrameLayout {
    私有静态最终诠释PX_PER_SEC = 900;每秒的动画布局的变化//像素
    私人最终LayoutAnimation动画=新LayoutAnimation();
    私人最终诠释wrapSpec = MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT,MeasureSpec.UNSPECIFIED);

    私人诠释collapsedHeight = 0;
    私人诠释expandedHeight = 0;
    私人布尔contentsChanged = TRUE;
    私有状态状态= State.COLLAPSED;

    私人OnLayoutChangedListener监听;

    公共ResizeLayout(上下文的背景下){超(上下文); }
    公共ResizeLayout(上下文的背景下,ATTRS AttributeSet中){超(背景下,ATTRS); }
    公共ResizeLayout(上下文的背景下,ATTRS的AttributeSet,诠释defStyle){超(背景下,ATTRS,defStyle); }

    @覆盖
    保护无效onLayout(布尔改变,诠释离开,INT顶部,诠释权,诠释底部){
        如果(getChildCount()大于0){
            查看孩子= getChildAt(0);
            child.layout(0,0,child.getMeasuredWidth(),child.getMeasuredHeight());
        }

        //如果布局参数已经改变的看法是动画,通知监听器
        如果(改变&安培;&安培; animation.isAnimating()){
            开关(州){
                案例倒塌:fireOnLayoutCollapsing(左,上,右,下);打破;
                案例扩展为:fireOnLayoutExpanding(左,上,右,下);打破;
            }
        }
    }

    / **
     *把视图重置为默认的内部状态。这应该叫你修改的内容任何时候
     *本ResizeLayout(如循环通过ListAdapter)
     * /
    公共无效重用(){
        collapsedHeight = expandedHeight = 0;
        contentsChanged = TRUE;
        状态= State.COLLAPSED;
        requestLayout();
    }

    / **
     *设置视图的状态。这应仅在通话后调用重用(),因为它不动画
     * 风景;它只是设置的内部状态。用法的一个例子是在一个ListAdapter  - 如果视图是
     *循环使用,它可能会在不正确的状态,所以应该在这里设置为布局之前正确的状态。
     * @参数isExpanded是否视图应该在膨胀状态
     * /
    公共无效setIsExpanded(布尔isExpanded){
        状态= isExpanded? State.EXPANDED:State.COLLAPSED;
    }

    / **
     *动画展示折叠和展开状态之间的ResizeLayout,只有在当前没有动画。
     * /
    公共无效animateToNextState(){
        如果(!animation.isAnimating()){
            animation.reuse(state.getStartHeight(本),state.getEndHeight(本));
            状态= state.next();
            startAnimation(动画);
        }
    }

    @覆盖
    保护无效onMeasure(INT widthMeasureSpec,诠释heightMeasureSpec){
        INT宽度= MeasureSpec.getSize(widthMeasureSpec);
        INT高= MeasureSpec.getSize(heightMeasureSpec);
        INT widthMode = MeasureSpec.getMode(widthMeasureSpec);
        INT heightMode = MeasureSpec.getMode(heightMeasureSpec);

        如果(getChildCount()。1){// ResizeLayout没有子女;默认为SPEC,或填充,如果未指定
            setMeasuredDimension(
                widthMode == MeasureSpec.UNSPECIFIED? getPaddingLeft()+ getPaddingRight():宽度,
                heightMode == MeasureSpec.UNSPECIFIED? getPaddingTop()+ getPaddingBottom():高度
            );
            返回;
        }

        查看孩子= getChildAt(0); //获取ResizeLayout的唯一的孩子

        如果(contentsChanged){//如果视图中的内容已经改变(第一运行,或者从再利用复位后())
            contentsChanged = FALSE;
            updateMeasurementsForChild(孩子,widthMeasureSpec,heightMeasureSpec);
            返回;
        }

        //这种状态出现在第二次运行。孩子可能会WRAP_CONTENT,所以MeasureSpec将是不确定的。
        //跳过测量孩子,只是接受了第一次运行的测量。
        如果(heightMode == MeasureSpec.UNSPECIFIED){
            setMeasuredDimension(的getWidth(),的getHeight());
        } 其他 {
            //可能在中期的动画;我们有一个固定高度从MeasureSpec所以用它
            child.measure(widthMeasureSpec,heightMeasureSpec);
            setMeasuredDimension(child.getMeasuredWidth(),child.getMeasuredHeight());
        }
    }

    / **
     *设定测量尺度此ResizeLayout,获得初始测量
     *从子视图的凝聚和扩大的高度。
     * @参数孩子这ResizeLayout的子视图
     *参数widthSpec从onMeasure宽度MeasureSpec()
     *参数heightSpec高度MeasureSpec从onMeasure()
     * /
    私人无效updateMeasurementsForChild(查看孩子,诠释widthSpec,诠释heightSpec){
        child.measure(widthSpec,wrapSpec); //测量孩子使用WRAP_CONTENT的高度

        //获取已被选定为视野中的崩溃的观点(ID = R.id.collapse_to)
        查看viewToCollapseTo = child.findViewById(R.id.collapse_to);

        如果(viewToCollapseTo!= NULL){
            //倒塌的高度应collapseTo视图的高度+任何顶部或底部填充
            collapsedHeight = viewToCollapseTo.getMeasuredHeight()+ child.getPaddingTop()+ child.getPaddingBottom();

            //扩展的高度仅仅是孩子的全高(与WRAP_CONTENT测量)
            expandedHeight = child.getMeasuredHeight();

            //重新测量孩子反映视图状态(折叠或展开)
            INT newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(state.getStartHeight(本),MeasureSpec.EXACTLY);
            child.measure(widthSpec,newHeightMeasureSpec);
        }
        setMeasuredDimension(child.getMeasuredWidth(),child.getMeasuredHeight());
    }

    @覆盖
    公共无效addView(查看孩子){
        如果(getChildCount()大于0){
            抛出新抛出:IllegalArgumentException(ResizeLayout只能承载一个直接的孩子。);
        } 其他 {
            super.addView(子);
        }
    }

    @覆盖
    公共无效addView(查看孩子,INT指数,ViewGroup.LayoutParams PARAMS){
        如果(getChildCount()大于0){
            抛出新抛出:IllegalArgumentException(ResizeLayout只能承载一个直接的孩子。);
        } 其他 {
            super.addView(儿童,指数,则params);
        }
    }

    @覆盖
    公共无效addView(查看孩子,ViewGroup.LayoutParams PARAMS){
        如果(getChildCount()大于0){
            抛出新抛出:IllegalArgumentException(ResizeLayout只能承载一个直接的孩子。);
        } 其他 {
            super.addView(孩子,则params);
        }
    }

    @覆盖
    公共无效addView(查看孩子,诠释的宽度,高度INT){
        如果(getChildCount()大于0){
            抛出新抛出:IllegalArgumentException(ResizeLayout只能承载一个直接的孩子。);
        } 其他 {
            super.addView(子,宽度,高度);
        }
    }

    / **
     *处理通过调整动画的展开和折叠状态之间切换视图
     *包含对象和请求的布局传递的布局参数。
     * /
    私有类LayoutAnimation扩展动画实现Animation.AnimationListener {
        私人诠释startHeight = 0,deltaHeight = 0;
        私人布尔isAnimating = FALSE;

        / **
         *就像一个默认的内插器和摩擦我觉得感觉很好;可以改变的。
         * /
        公共LayoutAnimation(){
            setInterpolator(新DecelerateInterpolator(2.2F));
            setAnimationListener(本);
        }

        / **
         *设置动画的持续时间匹配在规定值的持续时间
         *像素每秒(PPS)。例如,如果视图动画是60个像素,60然后将PPS
         *将设置1000毫秒的持续时间(即持续时间=(△/ PPS)* 1000)。采用PPS而
         *不是一个固定的时间,使动画速度是一致的,无论内容
         *视图。
         *参数PPS每秒的像素数由以调整布局
         * /
        私人无效setDurationPixelsPerSecond(INT PPS){
            setDuration((INT)(((浮点)Math.abs(deltaHeight)/ PPS)* 1000));
        }

        / **
         *允许一个单一的LayoutAnimation对象的重用。在启动动画之前调用此
         *重新启动动画,并设置新的参数
         *参数startHeight从动画应该开始的高度
         *参数endHeight在该动画应该结束的高度
         * /
        公共无效重用(INT startHeight,诠释endHeight){
            复位();
            setStartTime(0);
            this.startHeight = startHeight;
            this.deltaHeight = endHeight  -  startHeight;
            setDurationPixelsPerSecond(PX_PER_SEC);
        }

        / **
         *适用身高改造该含ResizeLayout
         * @参数interpolatedTime的时间(0.0  -  1.0)内插基于设定的内插器
         *参数吨,动画相结合的转变 - 不是用在这里
         * /
        @覆盖
        保护无效applyTransformation(浮动interpolatedTime,变换T){
            。getLayoutParams()高度= startHeight +(INT)(deltaHeight * interpolatedTime);
            requestLayout();
        }

        公共布尔isAnimating(){
            返回isAnimating;
        }

        @覆盖
        公共无效onAnimationStart(动画动画){
            isAnimating = TRUE;
        }

        @覆盖
        公共无效onAnimationEnd(动画动画){
            isAnimating = FALSE;
        }

        @覆盖
        公共无效onAnimationRepeat(动画动画){
            /*未实现*/
        }
    }

    / **
     *接口为动画中监听布局更改
     * /
    公共接口OnLayoutChangedListener {
        公共无效onLayoutExpanding(INT L,INT T,INT R,INT B);
        公共无效onLayoutCollapsing(INT L,INT T,INT R,INT B);
    }

    / **
     *设置一个监听器改变这一观点的布局
     *参数监听器监听的布局更改
     * /
    公共无效setOnBoundsChangedListener(OnLayoutChangedListener监听器){
        this.listener =侦听器;
    }

    私人无效fireOnLayoutExpanding(INT L,INT T,INT R,int b)在{
        如果(听者!= NULL)listener.onLayoutExpanding(L,T,R,B);
    }

    私人无效fireOnLayoutCollapsing(INT L,INT T,INT R,int b)在{
        如果(听者!= NULL)listener.onLayoutCollapsing(L,T,R,B);
    }

    受保护的枚举国家{
        倒塌{
            @覆盖
            下一个公开状态(){
                返回扩大;
            }

            @覆盖
            公众诠释getEndHeight(ResizeLayout视图){
                返回view.expandedHeight;
            }

            @覆盖
            公众诠释getStartHeight(ResizeLayout视图){
                返回view.collapsedHeight;
            }
        },
        膨胀{
            @覆盖
            下一个公开状态(){
                返回倒塌;
            }

            @覆盖
            公众诠释getEndHeight(ResizeLayout视图){
                返回view.collapsedHeight;
            }

            @覆盖
            公众诠释getStartHeight(ResizeLayout视图){
                返回view.expandedHeight;
            }
        };

        公共抽象状态下一个();
        公共抽象INT getStartHeight(ResizeLayout视图);
        公共抽象INT getEndHeight(ResizeLayout视图);
    }
}
 

MyActivity.java

只是一个简单的 ListActivity ,我用这个例子的目的。 的main.xml 只是普通的的LinearLayout 的ListView 子XML为 ListActivity

 进口android.app.ListActivity;
进口android.content.Context;
进口android.graphics.Color;
进口android.graphics.drawable.ColorDrawable;
进口android.os.Bundle;
进口android.view.LayoutInflater;
进口android.view.View;
进口android.view.ViewGroup;
进口android.widget.AdapterView;
进口android.widget.BaseAdapter;
进口android.widget.TextView;

进口java.util.HashSet中;
进口java.util.Set中;

公共类MyActivity扩展ListActivity实现ResizeLayout.OnLayoutChangedListener,AdapterView.OnItemClickListener {
    私人MyAdapter myAdapter;
    私人诠释clickedItemPosition;

    @覆盖
    公共无效的onCreate(包savedInstanceState){
        super.onCreate(savedInstanceState);
        的setContentView(R.layout.main);
        myAdapter =新MyAdapter(本);
        setListAdapter(myAdapter);
        getListView()setOnItemClickListener(本)。
        getListView()setSelector(新ColorDrawable(Color.TRANSPARENT))。
    }

    @覆盖
    公共无效onLayoutExpanding(INT L,INT T,INT R,int b)在{
        //保持点击查看完全可见,如果它的扩张
        。getListView()smoothScrollToPosition(clickedItemPosition);
    }

    @覆盖
    公共无效onLayoutCollapsing(INT L,INT T,INT R,int b)在{
        //目前未办理
    }

    @覆盖
    公共无效onItemClick(适配器视图&LT;&GT;适配器视图,视图中查看,INT I,长L){
        clickedItemPosition =我;
        myAdapter.toggleExpandedState(ⅰ);
        ((ResizeLayout)视图).animateToNextState();
    }

    私有类MyAdapter扩展了BaseAdapter {
        私人LayoutInflater充气;
        私人设置&LT;整数GT;扩展=新的HashSet&LT;整数GT;();

        公共MyAdapter(上下文CTX){
            充气= LayoutInflater.from(CTX);
        }

        @覆盖
        公众诠释getCount将(){
            返回100;
        }

        @覆盖
        公共对象的getItem(int i)以{
            返回I + 1;
        }

        @覆盖
        众长getItemId(int i)以{
            返回我;
        }

        公共无效toggleExpandedState(INT位置){
            如果(expanded.contains(位置)){
                expanded.remove(位置);
            } 其他 {
                expanded.add(位置);
            }
        }

        @覆盖
        公共查看getView(INT I,查看convertView,ViewGroup中的ViewGroup){
            ResizeLayout布局=(ResizeLayout)convertView;
            TextView的称号;

            //新的实例;无以回收再利用。
            如果(布局== NULL){
                布局=(ResizeLayout)inflater.inflate(R.layout.list_item,ViewGroup中,假);
                layout.setOnBoundsChangedListener(MyActivity.this);
                layout.setTag(layout.findViewById(R.id.title));
            }

            //再造一个ResizeLayout;一定要重新重用参数()
            别的layout.reuse();

            //设置视图状态 - 否则它会在任何状态是回收之前
            layout.setIsExpanded(expanded.contains(ⅰ));

            标题=(TextView中)layout.getTag();
            title.setText(目​​录编号+ I);

            返回布局;
        }
    }
}
 

list_item.xml

基本列表项目布局实例。只是有一个图标,并在顶部标题(图标设置为 collapse_to 视图)和下方排列的消息视图。

 &LT; XML版本=1.0编码=UTF-8&GT?;
&LT; com.example.resize.ResizeLayout
    的xmlns:机器人=htt​​p://schemas.android.com/apk/res/android
    的xmlns:工具=htt​​p://schemas.android.com/tool​​s
    机器人:layout_width =match_parent
    机器人:layout_height =WRAP_CONTENT
    &GT;

    &LT; RelativeLayout的
        机器人:layout_width =match_parent
        机器人:layout_height =match_parent
        机器人:填充=10dp&GT;

        &LT; ImageView的
            机器人:ID =@ + ID / collapse_to
            机器人:SRC =@可绘制/ holoku
            机器人:layout_width =WRAP_CONTENT
            机器人:layout_height =WRAP_CONTENT
            机器人:scaleType =centerInside
            机器人:layout_alignParentTop =真
            机器人:layout_alignParentLeft =真
            机器人:contentDescription =@字符串/ icon_desc
            工具:忽略=UseCompoundDrawables
            /&GT;

        &LT;的TextView
            机器人:ID =@ + ID /标题
            机器人:layout_width =match_parent
            机器人:layout_height =0dp
            机器人:layout_alignTop =@ ID / collapse_to
            机器人:layout_alignBottom =@ ID / collapse_to
            机器人:layout_toRightOf =@ ID / collapse_to
            机器人:重力=center_vertical
            机器人:以下属性来=20dp
            机器人:TEXTSIZE =20dp
            机器人:文字颜色=#198EBC
            /&GT;

        &LT;的TextView
            机器人:ID =@ + ID /文
            机器人:layout_marginTop =10dp
            机器人:layout_width =match_parent
            机器人:layout_height =WRAP_CONTENT
            机器人:TEXTSIZE =12dp
            机器人:文字颜色=#444444
            机器人:layout_below =@ ID / collapse_to
            机器人:文本=@字符串/消息
            /&GT;
    &LT; / RelativeLayout的&GT;
&LT; /com.example.resize.ResizeLayout>
 

现在我没有测试它之前,API的17任何东西,但运行皮棉检查NewApi问题说,这应该可以追溯到2.2(API 8)。

如果你想下载示例项目,并与它自己玩,你可以下载它的这里

I'm working on creating a custom view that will have an expanded and condensed state -- in the condensed state, it will just show a label and an icon, and in the expanded state, it will show a message below that. Here is a screenshot of how it works so far:

The View itself retains size values for the condensed and expanded states once measured, so it's simple to animate between the two states, and when using the view in normal practice (e.g. in a LinearLayout) everything works as intended. The change to the view size is done by calling getLayoutParams().height = newHeight; requestLayout();

However, when using it in a ListView, the view is recycled and maintains its previous height. So if the view was expanded when it was hidden, it will show as expanded when it is recycled for the next list item. It does not seem to receive another layout pass, even if I request a layout in the ListAdapter. I considered using a recycler with two different view types (expanded and condensed), but the sizes will vary depending on the size of the message. Is there an event I can listen for when the view is reattached in the ListView? Or do you have another suggestion of how to handle this?

EDIT: This is how I'm determining the expanded and condensed heights for the view:

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

解决方案

EDIT: Fixed order of parameters for both calls to makeMeasureSpec. Oddly, it worked the incorrect way that I had it, so I almost wonder if I'm doing something redundant. Either way, just wanted to point it out -- the project to download below doesn't have these corrections.

Okay, so it was really bothering me that I couldn't figure this out, so I decided to get more familiar with the layout and measurement system, and here's the solution that I've come up with.

  1. A custom ViewGroup extending FrameLayout that hosts a single direct child (like ScrollView.)
  2. A custom ListAdapter that handles tracking the expanded/collapsed state of each list item.
  3. A custom OnItemClickListener to handle requests to animate between collapsed and expanded states.

I'd like to share this code in case anyone else finds it useful. It should be fairly flexible, but I have no doubt there are bugs and things that could be improved. For one, I had issues programmatically scrolling the ListView (there doesn't seem to be a way to actually scroll the contents rather than just the view) so I used smoothScrollToPosition(int) for each change to the view size. This has a hardcoded 400ms duration which is unnecessary, so in the future I might try to write my own version with a duration of 0 (i.e. scrollToPosition(int)).

The general use is as follows:

  1. Your list item XML should have your ResizeLayout as the root of the hierarchy, and from there you can build any layout structure you want. Basically just wrap your normal list item layout in a ResizeLayout tag.

  2. In your layout, you should have one view with the id collapse_to. This is the view that the layout will wrap to (i.e. what view determines the collapsed height).

  3. Important things to do if you're recycling through a list adapter:

    • ALWAYS call reuse() when you retrieve a recycled view (e.g. convertView)
    • ALWAYS call setIsExpanded(boolean) before returning the recycled view; otherwise it will retain the state it was in before it was recycled

I may eventually throw this into a git repo, but for now here's the code:

ResizeLayout.java

This is the bulk of the code. I'll also include my Activity and Adapter that I used for testing further down. They're quite generic, but they illustrate the use effectively.

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

MyActivity.java

Just a simple ListActivity that I used for the purposes of this example. main.xml is just the generic LinearLayout with ListView child XML for a 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

Basic list item layout example. Just has an icon and a title on the top (the icon is set as the collapse_to view) and a message view aligned below.

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

Now I haven't tested it on anything prior to API 17, but running lint checks for NewApi problems says this should work as far back as 2.2 (API 8).

If you'd like to download the sample project and play with it yourself you can download it here.

这篇关于制作ListAdapter,反复使用可调整查看的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆