在listview中实现一个可以悬浮在listview顶部且可以被下一个titleBar推动并取代顶部titleBar

来源:互联网 发布:js截取url后面的 编辑:程序博客网 时间:2024/04/24 05:00

1.实现带首字母Title的ListView


重点在MyAdapter的注释


MainActivity.java

public class MainActivity extends Activity {private MyAdapter mAdapter;private ListView mListView;private List<String> mList;    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        init();    }    private void init() {        mList=new ArrayList<String>();        String a="";        for(int i=0;i<26;i++){            a=((char)('A'+i))+"";            mList.add(a);            mList.add(a);            mList.add(a);        }        mListView=(ListView)findViewById(R.id.lv);        mAdapter=new MyAdapter();        mAdapter.update(mList,MainActivity.this);        mListView.setAdapter(mAdapter);           }}



MyAdapter.java

public class MyAdapter extends BaseAdapter{    private List<String> mList;private Context mContext;public void update(List<String> list,Context context){    mContext=context;    Collections.sort(list);//如果是乱序输入的字母,需要排序一下,不然getView的时候会显示很多相同的Title    mList=list;   this.notifyDataSetChanged();} class ViewHolder{        TextView title;        TextView content;    }    @Override    public int getCount() {        return mList.size();    }    @Override    public Object getItem(int position) {        return mList.get(position);    }    @Override    public long getItemId(int position) {        return position;    }    @Override    public View getView(int position, View convertView, ViewGroup parent) {        ViewHolder holder=new ViewHolder();//听说用convertView是提升ListView性能的好习惯        if(convertView!=null){            holder=(ViewHolder)convertView.getTag();        }else{            convertView=LayoutInflater.from(mContext).inflate(R.layout.item, null);            holder.title=(TextView) convertView.findViewById(R.id.item_title);            holder.content=(TextView)convertView.findViewById(R.id.item_content);            convertView.setTag(holder);        }        holder.content.setText(mList.get(position));//显示title的逻辑/*看到下面的item.xml文件就可以知道,其实每个listview item都是包含了title和content两个TextView的,只不过title都被隐藏了,我们只要把在首字母相同的分块中的第一个item的title显示出来并setText为该分块的首字母即可*///判断一个item是否是一个分块的第一个item的办法:该item的首字母与它的前一个item首字母不相同        String cur=mList.get(position);        String pre=position-1>=0?mList.get(position-1):"";//mList的第一个元素做特殊处理,防止数组越界        if(!(pre.equals(cur))){            holder.title.setVisibility(View.VISIBLE);            holder.title.setText(cur);        }else{            holder.title.setVisibility(View.GONE);        }        return convertView;    }  }


item.xml

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="fill_parent"    android:layout_height="fill_parent"    android:orientation="vertical" >    <TextViewandroid:id="@+id/item_title"android:layout_width="fill_parent"android:layout_height="wrap_content"android:gravity="center_vertical"android:textColor="#ffffff"android:textSize="18sp"android:visibility="gone"android:text="A"android:paddingLeft="10dip"android:background="#40E0D0" />    <TextView        android:id="@+id/item_content"        android:layout_width="fill_parent"    android:layout_height="wrap_content"    android:gravity="center_vertical"android:textColor="#000000"android:textSize="18sp"android:text="content"android:paddingLeft="10dip"android:background="#ffffff" />        /></LinearLayout>


main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="fill_parent"    android:layout_height="fill_parent" >    <ListViewandroid:id="@+id/lv"        android:layout_width="fill_parent"        android:layout_height="fill_parent"android:divider="@null"        /></RelativeLayout>

2.实现可以推动的title


实际上,实现可以被推动的Title的原理是新增一个独立的TextView显示在顶端,配合listView的滚动


上图顶部的B-Title,它是独立于listview中所有item 的一个textView,浮在顶部,我们需要的效果是:当下一个title触碰到该TextView的时候,触发推动的效果。

title.xml

<?xml version="1.0" encoding="utf-8"?><TextView xmlns:android="http://schemas.android.com/apk/res/android"    android:id="@+id/header"    android:layout_width="fill_parent"    android:layout_height="wrap_content"    android:gravity="center_vertical"    android:textColor="#ffffff"    android:textSize="18sp"    android:visibility="gone"    android:text="A"    android:paddingLeft="10dip"    android:background="#40E0D0" />
这时候,我们需要判断,这个独立Title(TextView)的状态:什么时候隐藏(INVISIBLE_STATE=0),什么时候显示(SHOW_STATE=1)以及什么时候触发推动的效果(PUSHING_STATE=2)。

这里会用到SectionIndexer的接口,我们改写上面的MyAdapter,让它实现SectionIndexer:

public class MyAdapter extends BaseAdapter implements SectionIndexer,OnScrollListener{    private List<String> mList;private Context context;private String sections[];public void update(List<String> list,Context context){    this.context=context;    sections=new String[26];//实验传入的是三个一组的26个字母,所以写成了硬代码26,大家灵活判断一下长度    Collections.sort(list);   mList=list;   int pos=0;   for(int i=0;i<mList.size();i++){       String cur=mList.get(i);       String pre=(i-1)>=0?mList.get(i-1):"";       if(!(pre.equals(cur))){           sections[pos]=cur;           pos++;       }   }   this.notifyDataSetChanged();}    @Override    public int getCount() {        return mList.size();    }    @Override    public Object getItem(int position) {        return mList.get(position);    }    @Override    public long getItemId(int position) {        return position;    }    @Override    public View getView(int position, View convertView, ViewGroup parent) {        ViewHolder holder=new ViewHolder();        if(convertView!=null){            holder=(ViewHolder)convertView.getTag();        }else{            convertView=LayoutInflater.from(context).inflate(R.layout.list_item, null);            holder.title=(TextView) convertView.findViewById(R.id.item_header);            holder.content=(TextView)convertView.findViewById(R.id.item_content);            convertView.setTag(holder);        }        holder.content.setText(mList.get(position));        String cur=mList.get(position);        String pre=position-1>=0?mList.get(position-1):"";        if(!(pre.equals(cur))){            holder.title.setVisibility(View.VISIBLE);            holder.title.setText(cur);        }else{            holder.title.setVisibility(View.GONE);        }        return convertView;    }    class ViewHolder{        TextView title;        TextView content;    }    public int getTitleState(int position) {        if (position < 0 || getCount() == 0) {                       return 0;        }        int index = getSectionForPosition(position);        if(index==-1||index>sections.length){                        return 0;        }        int section = getSectionForPosition(position);        int nextSectionPosition = getPositionForSection(section + 1);        if (nextSectionPosition != -1 && position == nextSectionPosition - 1) {                                   return 2;        }                return 1;    }    @Override    public int getPositionForSection(int section) {                String sec=sections[section];        int pos=mList.indexOf(sec);        return pos;    }    @Override    public int getSectionForPosition(int position) {        String a=mList.get(position);        for(int i=0;i<sections.length;i++){            if(sections[i]==a){                return i;            }        }        return -1;    }    @Override    public Object[] getSections() {        return sections;    }    public void setTitleText(View mHeader, int firstVisiblePosition) {        String title=mList.get(firstVisiblePosition);        TextView sectionHeader = (TextView) mHeader;        sectionHeader.setText(title);    }    @Override    public void onScroll(AbsListView view, int firstVisibleItem,            int visibleItemCount, int totalItemCount) {        if(view instanceof PushTitleListView){            System.out.println("onScroll");            ((MyListView)view).titleLayout(firstVisibleItem);        }    }    @Override    public void onScrollStateChanged(AbsListView view, int scrollState) {            }}


我也是学习Android不久,很多东西不是很懂,所以大家不要喷我,简单的说说我对Section的理解,如图中的每个字母的区,前面三个A(title不算,一共三个A)构成的分块(区)是一个Section,定为Sections的第0个Section,然后后面三个B也是一个Section,是Sections的第1个Section,后面以此类推。

接下来根据上面约定的基础介绍一下实现SectionIndexer接口会实现下面几个方法:
 @Override
    public int getPositionForSection(int section) {
        
        String sec=sections[section];
        int pos=mList.indexOf(sec);
        return pos;
    }
getPositionForSection(int section):根据Section在Sections的位置(第几个Section)返回该Section第一个item在全部listview中的位置
例如:根据文章第一张图片,传入0(第0个Section-“A”)它的第一个item在listview的位置是0,传入1(第1个Section-“B”),它的第一个item在listview的位置是3。


    @Override
    public int getSectionForPosition(int position) {
        String a=mList.get(position);
        for(int i=0;i<sections.length;i++){
            if(sections[i]==a){
                return i;
            }
        }
        return -1;
    }
getSectionForPosition(int position):根据listview某个item的位置返回该item所在Section在Sections的位置


例如:根据文章第一张图片,传入0,1,2(listview的第0,1,2个item都是A)都会返回0,(A-section在Sections的位置是0),传入 4(listview的第四个item),会返回1(listview的第四个item是“B”,属于Sections中的第1个section),传入10(“D”)返回3。


    @Override
    public Object[] getSections() {
        return sections;
    }
上面这个方法不做介绍,实际代码中没有用到,有兴趣的同学可以研究一下官方文档。
有了Section的概念,我们现在可以方便而准确定位每一个Title的位置了(当然还有其他方法,这里是顺带介绍一下SectionIndexer)。接下来介绍一下如何判断独立Title的三个状态:什么时候隐藏(INVISIBLE_STATE=0),什么时候显示(SHOW_STATE=1)以及什么时候触发推动的效果(PUSHING_STATE=2)。
重点是什么时候触发推动效果(PUSHING_STATE=2):


如图,当第一个可视的Item(“B”)(其position由getFirstVisiblePosition()或者onScroll中的参数firstVisibleItem可以得到)的下一个item(“C”)刚好是下一个Section的第一个item,就可以返回PUSHING_STATE=2的状态,提示独立title不断地layout。下面就是获取title状态的方法:
//参数position 传入的是当前可视的第一个item在整个listview中的位置  
  public int getTitleState(int position) {
        if (position < 0 || getCount() == 0) {
           
            return 0;
        }
        int index = getSectionForPosition(position);
        if(index==-1||index>sections.length){
            
            return 0;
        }
//当前可视的第一个item所在的section
        int section = getSectionForPosition(position);
//下一个section的首位置
        int nextSectionPosition = getPositionForSection(section + 1);
//如果下一个section的首位置等于当前可视的第一个item的位置+1,可以返回推动状态(2)了
        if (nextSectionPosition != -1 && nextSectionPosition == position + 1) {
           
            return 2;
        }
        
        return 1;
    }
ps:还是建议大家用Magic Number来描述这三个状态,我是偷懒了,直接返回0,1,2。



有了判断三个状态的方法,我们就可以在listview中调用此方法,根据结果进行独立title的layout
MyListView.java


public class MyListView extends android.widget.ListView {    private View mTitle;        private boolean visible;        private int width;        private int height;        private MyAdapter mAdapter;    public MyListView(Context context) {        super(context);    }    public MyListView(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);    }    public MyListView(Context context, AttributeSet attrs) {        super(context, attrs);    }    @Override    protected void dispatchDraw(Canvas canvas) {        super.dispatchDraw(canvas);        if(visible){            drawChild(canvas, mTitle, getDrawingTime());        }    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        if(mTitle!=null){            measureChild(mTitle, widthMeasureSpec, heightMeasureSpec);            width=mTitle.getMeasuredWidth();            height=mTitle.getMeasuredHeight();        }    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        super.onLayout(changed, l, t, r, b);        if(mTitle!=null){            mTitle.layout(0, 0, width, height);            titleLayout(getFirstVisiblePosition());        }    }    public void setTitle(View view){        mTitle=view;        if(mTitle!=null){            setFadingEdgeLength(0);        }        requestLayout();    }    public void titleLayout(int firstVisiblePosition) {        if(mTitle==null){            return;        }        if(mAdapter==null||!(mAdapter instanceof MyAdapter)){            return;        }        int state=0;                state = mAdapter.getTitleState(firstVisiblePosition);        switch(state){        case 0:            visible=false;            break;        case 1:            if(mTitle.getTop()!=0){                mTitle.layout(0, 0, width, height);            }            mAdapter.setTitleText(mTitle,firstVisiblePosition);            visible=true;            break;        case 2:            View firstView=getChildAt(0);            if(firstView!=null){                int bottom=firstView.getBottom();                int headerHeight=mTitle.getHeight();                int top;                if(bottom<headerHeight){                    top=(bottom-headerHeight);                }else{                    top=0;                }                mAdapter.setTitleText(mTitle, firstVisiblePosition);                if(mTitle.getTop()!=top){                    mTitle.layout(0, top, width, height+top);                }                visible=true;            }            break;        }    }    @Override    public void setAdapter(ListAdapter adapter) {        if(adapter instanceof MyAdapter){            mAdapter=(MyAdapter) adapter;            super.setAdapter(adapter);        }    }            }




MainActivity.java

public class MainActivity extends Activity {private MyAdapter mAdapter;private MyListView mListView;private List<String> mList;    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        init();    }    private void init() {        mList=new ArrayList<String>();        String a="";        for(int i=0;i<26;i++){            a=((char)('A'+i))+"";            mList.add(a);            mList.add(a);            mList.add(a);        }        mListView=(MyListView)findViewById(R.id.lv);        mAdapter=new MyAdapter();        mAdapter.update(mList,MainActivity.this);        mListView.setTitle(LayoutInflater.from(MainActivity.this).inflate(R.layout.title, mListView, false));        mListView.setAdapter(mAdapter);        mListView.setOnScrollListener(mAdapter);            }}
main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="fill_parent"    android:layout_height="fill_parent" ><!-- 注意改一下包名,改成你的包名-->    <com.lk.test.MyListViewandroid:id="@+id/lv"        android:layout_width="fill_parent"        android:layout_height="fill_parent"android:divider="@null"        /></RelativeLayout>





MyListView中的titleLayout方法:
 public void titleLayout(int firstVisiblePosition) {
        if(mTitle==null){
            return;
        }
        if(mAdapter==null||!(mAdapter instanceof MyAdapter)){
            return;
        }
        int state=0;
        
        state = mAdapter.getTitleState(firstVisiblePosition);
        switch(state){
        case 0:
            visible=false;
            break;
        case 1:
            if(mTitle.getTop()!=0){
                mTitle.layout(0, 0, width, height);
            }
            mAdapter.setTitleText(mTitle,firstVisiblePosition);
            visible=true;
            break;
        case 2:
            View firstView=getChildAt(0);
            if(firstView!=null){
                int bottom=firstView.getBottom();
                int itemHeight=mTitle.getHeight();
                int top;
                if(bottom<itemHeight){
                    top=(bottom-itemHeight);
                }else{
                    top=0;
                }
                mAdapter.setTitleText(mTitle, firstVisiblePosition);
                if(mTitle.getTop()!=top){
                    mTitle.layout(0, top, width, height+top);
                }
                visible=true;
            }
            break;
        }
    }
case 2也就是PUSHING_STATE的时候,调用getChildAt(0)得到当前可视的第一个view,不断的获取它的bottom在Y坐标轴的值bottom(bottom=firstView.getBottom()),如果该值开始小于一个item的高(itemHeight=mTitle.getHeight(),此时注意:独立title的高必须与item中的title高一样,都是wrap_content,否则在推动的时候会有一点误差)的时候,说明第一个item的顶部开始超出y的正轴,准备移动到负轴了。定义一个变量int top=bottom-itemHeight,top的值是负数,也恰好是第一个可视item顶部边界(firstView.getTop())所在Y轴的值,这时候我们令独立Title的位置画的跟第一个可视item的位置一样,调用:mTitle.layout(0,top,width,height+top),其中width和height是在onMeasure的时候确定的,是title的宽度和高度;
case 1的时候只需在mTitle.getTop()!=0的时候调用mTitle.layout(0,0,width,height),将其显示在listview的顶部。


最后在Adapter中重写onScroll监听器,在onScroll的时候不断调用titleLayout方法。


写代码的时候发现两个现象:
(如果在onMeasure,onLayout,dispatchDraw中打log,可以看到执行顺序大致为onMeasure-onMeasure-onLayout-dispatchDraw,其中onMeasure会被调用两次,第一次得到的宽高什么的都是0)
在listview.setOnScrollListener的时候,调用的是:
AbsListview的方法
  public void setOnScrollListener(OnScrollListener l) {
        mOnScrollListener = l;
        invokeOnItemScrollListener();
    }




    /**
     * Notify our scroll listener (if there is one) of a change in scroll state
     */
    void invokeOnItemScrollListener() {
        if (mFastScroller != null) {
            mFastScroller.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
        }
        if (mOnScrollListener != null) {
            mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
        }
        onScrollChanged(0, 0, 0, 0); // dummy values, View's implementation does not use these.
    }
所以onScroll会在这里被触发一次。




其他类中的部分方法不做解释,比较容易看懂。
就先写到这里吧,该回家吃饭了,公司空调都关了。。。。

第一次写博客,代码区不咋会用,乱的不能看了,抱歉