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

这篇博客介绍了如何在ListView中实现带有首字母Title的列表,并实现标题栏可以被下一个标题推动的效果。通过实现SectionIndexer接口,精确定位每个Title的位置,并通过判断状态来控制独立Title的显示、隐藏和推动效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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" >
    <TextView
	android: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" >


    <ListView
	android: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.MyListView
	android: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会在这里被触发一次。




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

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






                
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值