Android自定义控件——仿饿了么联动ListView

本文介绍如何在Android中实现仿饿了么的联动ListView,详细讲解自定义ListView及其Adapter的过程,包括监听滑动事件、自定义Item布局、适配器的创建以及在MainActivity中的应用。通过实例代码展示了一个二级联动分组ListView的实现方法。
摘要由CSDN通过智能技术生成

Android自定义控件——仿饿了么联动ListView


前几天,群里一哥们儿私聊我,问我会不会二级联动,当时的我是一脸懵逼啊,曾经听人提起过,但是自己也没用过,也没尝试着去做,正好趁这个机会就学学呗,Demo还是这哥们儿给我的呢,诺,github链接:DoubleListViewLinkage,简书链接:羊皮书APP(Android版)开发系列(二十一)双联动分组ListView,类似于外卖点餐,但是很头疼的,一个Android小白,要看没有一行注释的代码,Oh My God!不多说了,开车吧~


我们先来看下效果哈,然后来分析是怎么实现的,如下图:

看到后,或许会感到一头雾水,首先,标题是怎么变的,然后左边的item又是怎么变的,然后我们的自定义到底在哪儿?

ListView的自定义是哪一块儿?

一开始我也不知道ListView的自定义,到底是自定义的哪一块儿,毕竟这个概念是比较重要的,因为既然我们都要自定义ListView了,但是不知道自定义哪里,岂不是很尴尬?我们先来一张静态的图哈,来看看到底是哪里需要自定义如下图:

再来看下自定义后的ListView,如下图:

右边的是哈,然后我们可以发现他们的Item是不同的,所以说,自定义ListView其实就是自定义Item,然后我们来分析下哈,自定义Item,说到底,要想实现这个效果的自定义Item就是加了一个头部,也就是标题啦,然后我们看效果图的时候,可以发现当第一个标题内的内容向上移动,消失的时候,那个标题也就消失了,所以我们还要实现这个随着标题内的最后一个内容消失的时候,该标题也要消失。


总结一下呢,我们自定义Item要完成的就是,“标题+内容”,从开始出现到消失,且显示第二个“标题+内容”的过程。


那就具体的来实现吧!


在上一环节我们分析了,到底要自定义哪里,且是怎样的一个过程,那么这一环节我们就来再深入一点儿哈,我们要自定义ListView那么肯定是要继承ListView的啦,况且我们要监听一下内容是什么时候消失的,那么我们就必须要实现AbsListView.OnScrollListener这个接口喽~然后alt+回车,把抽象方法都实现,还要实现那必须的三个构造方法哈,最终如下:

public class HaveHeaderListView extends ListView implements AbsListView.OnScrollListener {
   

    public HaveHeaderListView(Context context) {
        super(context);
    }

    public HaveHeaderListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public HaveHeaderListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {

    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {

    }
}

然后我们在构造方法中super下那个滑动监听哈。就是这句super.setOnScrollListener(this);
这样完事儿后,我们来写一下我们的这个Adapter,因为这个属于我们自定义的了,若是想以前那样写Adapter是肯定不可以的,所以来写下我们自己的Adapter吧。Adapter代码如下:

    public interface HaveHeaderAdapter {
        boolean isSectionHeader(int position);

        int getSectionForPosition(int position);

        View getSectionHeaderView(int section, View convertView, ViewGroup parent);

        int getSectionHeaderViewType(int section);

        int getCount();
    }

这个Adapter其实就是和我们的那个标题相对应的,看名字大家应该都知道,也就是仿着我们的那个BaseAdapter写的。然后我们需要几个变量,如下:

    private HaveHeaderAdapter mAdapter;
    //标题
    private View mCurrentHeader;
    //默认显示第几个标题
    private int mCurrentHeaderViewType = 0;
    //标题距顶部的距离
    private float mHeaderOffset;
    //是否显示
    private boolean mShouldPin = true;
    //当前部分
    private int mCurrentSection = 0;
    //宽度
    private int mWidthMode;
    //高度
    private int mHeightMode;

注释已经说明了哈,这里也就不啰嗦了,嘿嘿。
OK,所用的变量都有了,那么我们就来实现吧,既然是通过向上滑动和向下滑动来让mCurrentHeader(也就是标题既然这里我们都有相应的变量了,那么我们就用它在代码中真实的名字吧!)显示和隐藏的,那么主要逻辑和代码实现肯定是在onScroll里面了,先贴代码:

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        if (mOnScrollListener != null) {
            mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
        }
        if (mAdapter == null || mAdapter.getCount() == 0 || !mShouldPin) {
            //当适配器为空或适配器中无数据或mShouldPin为false或者可见视同中第一个索引小于0则return
            return;
        }
        //根据可见视图的第一个索引去获取section
        int section = mAdapter.getSectionForPosition(firstVisibleItem);
        //根据获取到的section去获取viewType
        int viewType = mAdapter.getSectionHeaderViewType(section);
        //获取标题
        mCurrentHeader = getSectionHeaderView(section, mCurrentHeader);
        //更换标题
        ensureHaveHeaderLayout(mCurrentHeader);
        //改成当前标题所对应的值
        mCurrentHeaderViewType = viewType;
        //设置标题距顶部距离
        mHeaderOffset = 0.0f;
        for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) {
            if (mAdapter.isSectionHeader(i)) {
                //得到真实的子Item的值
                View ChildView = getChildAt(i - firstVisibleItem);
                //得到子Item距顶部的距离
                float ChildViewTop = ChildView.getTop();
                //得到子Item的高度
                float ChildViewHeight = ChildView.getMeasuredHeight();
                //将子Item设置为显示
                ChildView.setVisibility(VISIBLE);
                if (ChildViewHeight >= ChildViewTop && ChildViewTop > 0) {
                    //当子Item的高度>子Item距顶部的距离时,则标题应该逐步消失
                    mHeaderOffset = ChildViewTop - ChildViewHeight;
                } else if (ChildViewTop <= 0) {
                    //子Item距离小于0则将头部设置为不显示
                    ChildView.setVisibility(INVISIBLE);
                }
            }
        }
        //刷新
        invalidate();
    }

先来解释下firstVisibleItemvisibleItemCounttotalItemCount这三个变量是什么意思哈,挺重要的。
firstVisibleItem,官方文档是这样写的:int: the index of the first visible cell (ignore if visibleItemCount == 0)
由于本人英语渣渣,经过不靠谱的有道翻译,再加上自己打log试,大致懂了,它其实就是可见View中的第一个索引,也就是在可见View中的第一个视图的索引值,再用下图来解释下,如下:

在该图中的firstVisibleItem就是“面食类”的索引值,它的索引就是0了,所以firstVisibleItem就是0了。
visibleItemCount,这个值想半天想不懂,然后经过刘某人的指点懂了,哈哈,就这个界面log值出来的和我数的值总是差1(我数的少),很纳闷儿,因为我们都知道计算机计数都是从0开始的,但是我若是从0开始数(面食类算第0个元素)就和log值出来的少1了,问刘某人后,老刘说最上面的那个也算,也就是说,visibleItemCount计数是从最上面的那个ListViewLinkage开始计的,恍然大悟啊~
totalItemCount,就简单了totalItemCount = firstVisibleItem + visibleItemCount;
然后剩下的……就是代码注释的那样了…
getSectionHeaderView()代码如下:

    private View getSectionHeaderView(int section, View oldView) {
        //是否显示,即,section不等于当前显示的section,且View不为空
        boolean shouldLayout = section != mCurrentSection || oldView == null;
        //获取View
        View view = mAdapter.getSectionHeaderView(section, oldView, this);
        if (shouldLayout) {
            //显示标头
            ensureHaveHeaderLayout(view);
            //并将section赋值给mCurrentSection
            mCurrentSection = section;
        }
        //返回加载好的View
        return view;
    }

ensureHaveHeaderLayout()代码如下:

    private void ensureHaveHeaderLayout(View header) {
        if (header.isLayoutRequested()) {
            //设置宽(返回值是测量值+mode值)
            int widthSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), mWidthMode);
            int heightSpec;
            //父布局参数
            ViewGroup.LayoutParams layoutParams = header.getLayoutParams();
            if (layoutParams != null && layoutParams.height > 0) {
                //若有父布局则header高为父布局的
                heightSpec = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
            } else {
                //否则,header高为自适应大小
                heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
            }
            //设置header宽高
            header.measure(widthSpec, heightSpec);
            //设置header相对于父布局的位置,左,上,右,下
            header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight());
        }
    }

只有这样还是不行的,虽然这里的逻辑有了,但是最重要的绘制还没有呢,重写dispatchDraw()方法,代码如下:

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (mAdapter == null || !mShouldPin || mCurrentHeader == null) {
            //adapter为空,mShouldPin为false,mCurrentHeader为空,则不绘制
            return;
        }
        //保存Canvas状态
        int saveCount = canvas.save();
        //平移
        canvas.translate(0, mHeaderOffset);
        //设置显示范围,左,上,右,下
        canvas.clipRect(0, 0, getWidth(), mCurrentHeader.getMeasuredHeight());
        mCurrentHeader.draw(canvas);
        //恢复Canvas状态
        canvas.restoreToCount(saveCount);
    }

同样,注释都写上了……

自定义控件怎么能少的了测量呢,重写onMeasure()方法,代码如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //宽
        mWidthMode = MeasureSpec.getMode(widthMeasureSpec);
        //高
        mHeightMode = MeasureSpec.getMode(heightMeasureSpec);
    }

当然,setAdapter()方法也要重写,代码如下:

    public void setAdapter(ListAdapter adapter) {
        mCurrentHeader = null;
        mAdapter = (HaveHeaderAdapter) adapter;
        super.setAdapter(adapter);
    }

由于现在的点击事件不同了,所以点击事件的代码如下:

    public static abstract class OnItemClickListener implements AdapterView.OnItemClickListener {
   
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int rawPosition, long id) {
            CustomizeLVBaseAdapter adapter;
            if (parent.getAdapter().getClass().equals(HeaderViewListAdapter.class)) {
                HeaderViewListAdapter wrapperAdapter = (HeaderViewListAdapter) parent.getAdapter();
                adapter = (CustomizeLVBaseAdapter) wrapperAdapter.getWrappedAdapter();
            } else {
                adapter = (CustomizeLVBaseAdapter) parent.getAdapter();
            }
            int section = adapter.getSectionForPosition(rawPosition);
            int position = adapter.getPositionInSectionForPosition(rawPosition);

            if (position == -1) {
                onSectionClick(parent, view, section, id);
            } else {
                onItemClick(parent, view, section, position, id);
            }
        }

        public abstract void onItemClick(AdapterView<?> adapterView, View view, int section, int position, long id);

        public abstract void onSectionClick(AdapterView<?> adapterView, View view, int section, long id);
    }

最后该自定义ListView的完整代码如下:


                
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值