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

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的完整代码如下:

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.HeaderViewListAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;

import com.example.lilinxiong.listviewlinkage.Adapter.CustomizeLVBaseAdapter;

/** * 项目名: ListViewLinkage * 包名: com.example.lilinxiong.listviewlinkage.View * 文件名: HaveHeaderListView * 创建者: LLX * 创建时间: 2017/4/17 16:56 * 描述: 带有标题的ListView */
public class HaveHeaderListView extends ListView implements AbsListView.OnScrollListener {
    //滑动监听
    private OnScrollListener mOnScrollListener;

    //相对应的适配器
    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();
    }

    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;

    public HaveHeaderListView(Context context) {
        super(context);
        super.setOnScrollListener(this);
    }

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

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

    //重写绑定适配器
    @Override
    public void setAdapter(ListAdapter adapter) {
        mCurrentHeader = null;
        mAdapter = (HaveHeaderAdapter) adapter;
        super.setAdapter(adapter);
    }

    //滚动
    @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();
    }

    //滑动状态改变
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        if (mOnScrollListener != null) {
            mOnScrollListener.onScrollStateChanged(view, scrollState);
        }
    }

    //事件分发子组件绘制
    @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);
    }

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

    //设置滑动监听
    @Override
    public void setOnScrollListener(OnScrollListener l) {
        mOnScrollListener = l;
    }

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

    //显示标题
    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());
        }
    }

    //设置点击监听
    public void setOnItemClickListener(OnItemClickListener listener) {
        super.setOnItemClickListener(listener);
    }

    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完了,那么该相对应的自定义Adapter了吧~

普通的ListView的Adapter直接继承BaseAdapter就好了,但是我们这个自定义ListView的Adapter再继承BaseAdapter就不行了,因为有那个mCurrentHeader贼烦,好气啊,刚出一坑就又入一坑了,但是这个自定义Adapter的坑并不大,比起上面的那个ListView简单多了,首先我们要了解我们要写一个什么样的Adapter的,肯定是希望把我们那个有mCurrentHeader的相关数据加进去呗,并且我们在刚才的这个自定义ListView中已经都写了相应的Adapter了,现在只要实现就好了,即,自定义的Adapter应该extends BaseAdapter且!implements HaveHeaderListView.HaveHeaderAdapter,不多说,上代码啦~:

import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

import com.example.lilinxiong.listviewlinkage.View.HaveHeaderListView;

/** * 项目名: ListViewLinkage * 包名: com.example.lilinxiong.listviewlinkage.Adapter * 文件名: CustomizeLVBaseAdapter * 创建者: LLX * 创建时间: 2017/4/17 18:42 * 描述: 带有标题ListView的Adapter */
public abstract class CustomizeLVBaseAdapter extends BaseAdapter implements HaveHeaderListView.HaveHeaderAdapter {
    private static int HEADER_VIEW_TYPE = 0;
    private static int ITEM_VIEW_TYPE = 0;

    private SparseArray<Integer> mSectionPositionCache;
    private SparseArray<Integer> mSectionCache;
    private SparseArray<Integer> mSectionCountCache;
    private int mCount;
    private int mSectionCount;


    public CustomizeLVBaseAdapter() {
        super();
        mSectionPositionCache = new SparseArray<Integer>();
        mSectionCache = new SparseArray<Integer>();
        mSectionCountCache = new SparseArray<Integer>();
        mCount = -1;
        mSectionCount = -1;
    }

    @Override
    public void notifyDataSetChanged() {
        mSectionCache.clear();
        mSectionPositionCache.clear();
        mSectionCountCache.clear();
        mCount = -1;
        mSectionCount = -1;
        super.notifyDataSetChanged();
    }

    @Override
    public void notifyDataSetInvalidated() {
        mSectionCache.clear();
        mSectionPositionCache.clear();
        mSectionCountCache.clear();
        mCount = -1;
        mSectionCount = -1;
        super.notifyDataSetInvalidated();
    }

    @Override
    public final int getCount() {
        if (mCount >= 0) {
            return mCount;
        }
        int count = 0;
        for (int i = 0; i < internalGetSectionCount(); i++) {
            count += internalGetCountForSection(i);
            count++;
        }
        mCount = count;
        return count;
    }

    @Override
    public final Object getItem(int position) {
        return getItem(getSectionForPosition(position), getPositionInSectionForPosition(position));
    }

    @Override
    public final long getItemId(int position) {
        return getItemId(getSectionForPosition(position), getPositionInSectionForPosition(position));
    }

    @Override
    public final View getView(int position, View convertView, ViewGroup parent) {
        if (isSectionHeader(position)) {
            return getSectionHeaderView(getSectionForPosition(position), convertView, parent);
        }
        return getItemView(getSectionForPosition(position), getPositionInSectionForPosition(position), convertView, parent);
    }

    @Override
    public final int getItemViewType(int position) {
        if (isSectionHeader(position)) {
            return getItemViewTypeCount() + getSectionHeaderViewType(getSectionForPosition(position));
        }
        return getItemViewType(getSectionForPosition(position), getPositionInSectionForPosition(position));
    }

    @Override
    public final int getViewTypeCount() {
        return getItemViewTypeCount() + getSectionHeaderViewTypeCount();
    }

    public final int getSectionForPosition(int position) {
        Integer cachedSection = mSectionCache.get(position);
        if (cachedSection != null) {
            return cachedSection;
        }
        int sectionStart = 0;
        for (int i = 0; i < internalGetSectionCount(); i++) {
            int sectionCount = internalGetCountForSection(i);
            int sectionEnd = sectionStart + sectionCount + 1;
            if (position >= sectionStart && position < sectionEnd) {
                mSectionCache.put(position, i);
                return i;
            }
            sectionStart = sectionEnd;
        }
        return 0;
    }

    public int getPositionInSectionForPosition(int position) {
        Integer cachedPosition = mSectionPositionCache.get(position);
        if (cachedPosition != null) {
            return cachedPosition;
        }
        int sectionStart = 0;
        for (int i = 0; i < internalGetSectionCount(); i++) {
            int sectionCount = internalGetCountForSection(i);
            int sectionEnd = sectionStart + sectionCount + 1;
            if (position >= sectionStart && position < sectionEnd) {
                int positionInSection = position - sectionStart - 1;
                mSectionPositionCache.put(position, positionInSection);
                return positionInSection;
            }
            sectionStart = sectionEnd;
        }
        return 0;
    }

    public final boolean isSectionHeader(int position) {
        int sectionStart = 0;
        for (int i = 0; i < internalGetSectionCount(); i++) {
            if (position == sectionStart) {
                return true;
            } else if (position < sectionStart) {
                return false;
            }
            sectionStart += internalGetCountForSection(i) + 1;
        }
        return false;
    }

    public int getItemViewType(int section, int position) {
        return ITEM_VIEW_TYPE;
    }

    public int getItemViewTypeCount() {
        return 1;
    }

    public int getSectionHeaderViewType(int section) {
        return HEADER_VIEW_TYPE;
    }

    public int getSectionHeaderViewTypeCount() {
        return 1;
    }

    public abstract Object getItem(int section, int position);

    public abstract long getItemId(int section, int position);

    public abstract int getSectionCount();

    public abstract int getCountForSection(int section);

    public abstract View getItemView(int section, int position, View convertView, ViewGroup parent);

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

    private int internalGetCountForSection(int section) {
        Integer cachedSectionCount = mSectionCountCache.get(section);
        if (cachedSectionCount != null) {
            return cachedSectionCount;
        }
        int sectionCount = getCountForSection(section);
        mSectionCountCache.put(section, sectionCount);
        return sectionCount;
    }

    private int internalGetSectionCount() {
        if (mSectionCount >= 0) {
            return mSectionCount;
        }
        mSectionCount = getSectionCount();
        return mSectionCount;
    }
}

就不多解释了哈,因为这个……实在是没什么可解释的了。

布局!

布局……直接上代码吧,没啥说的。
activity_main.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal">

    <ListView  android:id="@+id/lv_left" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="2" android:divider="@null" android:scrollbars="none" />

    <com.example.lilinxiong.listviewlinkage.View.HaveHeaderListView  android:id="@+id/lv_right" android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginLeft="10dp" android:layout_weight="5" android:background="@android:color/white" />

</LinearLayout>

左侧ListView的Item,lv_item_left.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#EBEDF0" android:orientation="vertical">

    <TextView  android:id="@+id/lv_left_item_text" android:layout_width="match_parent" android:layout_height="60dp" android:layout_gravity="center" android:gravity="center" android:padding="10dp" android:text="面食类" android:textColor="#444444" />
</LinearLayout>

右侧ListView的标题Item,lv_customize_item_header.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#EBEDF0" android:orientation="horizontal">

    <TextView  android:id="@+id/lv_customize_item_header_text" android:layout_width="match_parent" android:layout_height="30dp" android:layout_gravity="center_vertical" android:layout_marginLeft="10dp" android:gravity="center_vertical" android:paddingLeft="6dp" android:text="面食类" android:textColor="#444444" />

</LinearLayout>

右侧ListView的内容Item,lv_customize_item_right.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#FFFFFF" android:orientation="horizontal">

    <ImageView  android:id="@+id/lv_customize_item_image" android:layout_width="50dp" android:layout_height="50dp" android:layout_gravity="center" android:scaleType="fitXY" android:src="@mipmap/ic_launcher" />

    <TextView  android:id="@+id/lv_customize_item_text" android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:gravity="center_vertical" android:paddingLeft="6dp" android:text="热干面" android:textColor="#2F333A" />
</LinearLayout>

两个ListView的Adapter!

左侧ListView的Adapter,LeftAdapter.java如下:

import android.content.Context;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

import com.example.lilinxiong.listviewlinkage.R;

import java.util.List;

/** * 项目名: ListViewLinkage * 包名: com.example.lilinxiong.listviewlinkage.Adapter * 文件名: LeftAdapter * 创建者: LLX * 创建时间: 2017/4/17 19:04 * 描述: 左侧Adapter */
public class LeftAdapter extends BaseAdapter {
    //标题
    private List<String> leftStr;
    //标志
    private List<Boolean> flagArray;
    private LayoutInflater inflater;

    public LeftAdapter(Context mContext, List<String> leftStr, List<Boolean> flagArray) {
        this.leftStr = leftStr;
        this.flagArray = flagArray;
        inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    @Override
    public int getCount() {
        return leftStr.size();
    }

    @Override
    public Object getItem(int position) {
        return position;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder = null;
        if (convertView == null) {
            holder = new ViewHolder();
            //加载
            convertView = inflater.inflate(R.layout.lv_item_left, parent, false);
            //绑定
            holder.lv_left_item_text = (TextView) convertView.findViewById(R.id.lv_left_item_text);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        //设置数据
        holder.lv_left_item_text.setText(leftStr.get(position));
        //根据标志位,设置背景颜色
        if (flagArray.get(position)) {
            holder.lv_left_item_text.setBackgroundColor(Color.rgb(255, 255, 255));
        } else {
            holder.lv_left_item_text.setBackgroundColor(Color.TRANSPARENT);
        }
        return convertView;
    }

    class ViewHolder {
        private TextView lv_left_item_text;
    }
}

右侧ListView的Adapter,RightAdapter.java如下:

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import com.example.lilinxiong.listviewlinkage.R;

import java.util.List;

/** * 项目名: ListViewLinkage * 包名: com.example.lilinxiong.listviewlinkage.Adapter * 文件名: RightAdapter * 创建者: LLX * 创建时间: 2017/4/17 19:03 * 描述: 右侧ListViewAdapter */
public class RightAdapter extends CustomizeLVBaseAdapter {
    //上下文
    private Context mContext;
    //标题
    private List<String> leftStr;
    //内容
    private List<List<String>> rightStr;
    private LayoutInflater inflater;

    public RightAdapter(Context mContext, List<String> leftStr, List<List<String>> rightStr) {
        this.mContext = mContext;
        this.leftStr = leftStr;
        this.rightStr = rightStr;
        //系统服务
        inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    @Override
    public Object getItem(int section, int position) {
        return rightStr.get(section).get(position);
    }

    @Override
    public long getItemId(int section, int position) {
        return position;
    }

    @Override
    public int getSectionCount() {
        return leftStr.size();
    }

    @Override
    public int getCountForSection(int section) {
        return rightStr.get(section).size();
    }

    @Override
    public View getItemView(final int section, final int position, View convertView, ViewGroup parent) {
        ChildViewHolder holder = null;
        if (convertView == null) {
            holder = new ChildViewHolder();
            //加载
            convertView = inflater.inflate(R.layout.lv_customize_item_right, parent, false);
            //绑定
            holder.lv_customize_item_image = (ImageView) convertView.findViewById(R.id.lv_customize_item_image);
            holder.lv_customize_item_text = (TextView) convertView.findViewById(R.id.lv_customize_item_text);
            convertView.setTag(holder);
        } else {
            holder = (ChildViewHolder) convertView.getTag();
        }
        //设置内容
        holder.lv_customize_item_text.setText(rightStr.get(section).get(position));
        //点击事件
        convertView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(mContext, rightStr.get(section).get(position), Toast.LENGTH_SHORT).show();
            }
        });
        return convertView;
    }

    @Override
    public View getSectionHeaderView(int section, View convertView, ViewGroup parent) {
        HeaderViewHolder holder = null;
        if (convertView == null) {
            holder = new HeaderViewHolder();
            //加载
            convertView = inflater.inflate(R.layout.lv_customize_item_header, parent, false);
            //绑定
            holder.lv_customize_item_header_text = (TextView) convertView.findViewById(R.id.lv_customize_item_header_text);
            convertView.setTag(holder);
        } else {
            holder = (HeaderViewHolder) convertView.getTag();
        }
        //不可点击
        convertView.setClickable(false);
        //设置标题
        holder.lv_customize_item_header_text.setText(leftStr.get(section));
        return convertView;
    }

    class ChildViewHolder {
        //Item图片
        private ImageView lv_customize_item_image;
        //Item内容
        private TextView lv_customize_item_text;
    }

    class HeaderViewHolder {
        //标题
        private TextView lv_customize_item_header_text;
    }
}

最后一步,MainActivity

所有的都准备好了,布局,Adapter,最后让我们在MainActivity中实现吧~

声明所需的变量:

    //左边的ListView
    private ListView lv_left;
    //左边ListView的Adapter
    private LeftAdapter leftAdapter;
    //左边的数据存储
    private List<String> leftStr;
    //左边数据的标志
    private List<Boolean> flagArray;
    //右边的ListView
    private HaveHeaderListView lv_right;
    //右边的ListView的Adapter
    private RightAdapter rightAdapter;
    //右边的数据存储
    private List<List<String>> rightStr;
    //是否滑动标志位
    private Boolean isScroll = false;

初始化控件**initView();**initView代码如下:

    private void initView() {
        lv_left = (ListView) findViewById(R.id.lv_left);
        leftStr = new ArrayList<>();
        flagArray = new ArrayList<>();
        leftAdapter = new LeftAdapter(MainActivity.this, leftStr, flagArray);
        lv_left.setAdapter(leftAdapter);
        lv_right = (HaveHeaderListView) findViewById(R.id.lv_right);
        rightStr = new ArrayList<List<String>>();
        rightAdapter = new RightAdapter(MainActivity.this, leftStr, rightStr);
        lv_right.setAdapter(rightAdapter);
    }

初始化数据**initData();**initData代码如下:

    private void initData() {
        //左边相关数据
        leftStr.add("面食类");
        leftStr.add("盖饭");
        leftStr.add("寿司");
        leftStr.add("烧烤");
        leftStr.add("酒水");
        leftStr.add("凉菜");
        leftStr.add("小吃");
        leftStr.add("粥");
        flagArray.add(true);
        flagArray.add(false);
        flagArray.add(false);
        flagArray.add(false);
        flagArray.add(false);
        flagArray.add(false);
        flagArray.add(false);
        flagArray.add(false);
        leftAdapter.notifyDataSetChanged();
        //右边相关数据
        //面食类
        List<String> food1 = new ArrayList<>();
        food1.add("热干面");
        food1.add("臊子面");
        food1.add("烩面");
        //盖饭
        List<String> food2 = new ArrayList<>();
        food2.add("番茄鸡蛋");
        food2.add("红烧排骨");
        food2.add("农家小炒肉");
        //寿司
        List<String> food3 = new ArrayList<>();
        food3.add("芝士");
        food3.add("丑小丫");
        food3.add("金枪鱼");
        //烧烤
        List<String> food4 = new ArrayList<>();
        food4.add("羊肉串");
        food4.add("烤鸡翅");
        food4.add("烤羊排");
        //酒水
        List<String> food5 = new ArrayList<>();
        food5.add("长城干红");
        food5.add("燕京鲜啤");
        food5.add("青岛鲜啤");
        //凉菜
        List<String> food6 = new ArrayList<>();
        food6.add("拌粉丝");
        food6.add("大拌菜");
        food6.add("菠菜花生");
        //小吃
        List<String> food7 = new ArrayList<>();
        food7.add("小食组");
        food7.add("紫薯");
        //粥
        List<String> food8 = new ArrayList<>();
        food8.add("小米粥");
        food8.add("大米粥");
        food8.add("南瓜粥");
        food8.add("玉米粥");
        food8.add("紫米粥");
        rightStr.add(food1);
        rightStr.add(food2);
        rightStr.add(food3);
        rightStr.add(food4);
        rightStr.add(food5);
        rightStr.add(food6);
        rightStr.add(food7);
        rightStr.add(food8);
        rightAdapter.notifyDataSetChanged();
    }

凑合看哈,实际开发中绝对不能这么干的,但是现在为了省事儿,为了数据不同,请各位大佬允许我这么干哈,嘿嘿!

现在控件绑定了,数据也有了,那就来处理下左边ListView的点击事件吧,逻辑就是,点击后,相应的标志位置为true,其他的为false,然后右边的ListView显示相应的位置,大致逻辑就是这个了,代码如下:

        lv_left.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                isScroll = false;
                for (int i = 0; i < leftStr.size(); i++) {
                    if (i == position) {
                        flagArray.set(i, true);
                    } else {
                        flagArray.set(i, false);
                    }
                }
                //更新
                leftAdapter.notifyDataSetChanged();
                int rightSection = 0;
                for (int i = 0; i < position; i++) {
                    //查找
                    rightSection += rightAdapter.getCountForSection(i) + 1;
                }
                //显示到rightSection所代表的标题
                lv_right.setSelection(rightSection);
            }
        });

右边的ListView的就比较简单了,通过上下滑动来判断该显示那个标题,且!相对应的标志位置为true,左边ListView根据标志位flagArray更新,具体代码如下:

        lv_right.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                switch (scrollState) {
                    // 当不滚动时
                    case AbsListView.OnScrollListener.SCROLL_STATE_IDLE:
                        // 判断滚动到底部
                        if (lv_right.getLastVisiblePosition() == (lv_right.getCount() - 1)) {
                            lv_left.setSelection(ListView.FOCUS_DOWN);
                        }
                        // 判断滚动到顶部
                        if (lv_right.getFirstVisiblePosition() == 0) {
                            lv_left.setSelection(0);
                        }
                        break;
                }

            }

            int y = 0;
            int x = 0;

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                if (isScroll) {
                    for (int i = 0; i < rightStr.size(); i++) {
                        if (i == rightAdapter.getSectionForPosition(lv_right.getFirstVisiblePosition())) {
                            flagArray.set(i, true);
                            //获取当前标题的标志位
                            x = i;
                        } else {
                            flagArray.set(i, false);
                        }
                    }
                    if (x != y) {
                        leftAdapter.notifyDataSetChanged();
                        //将之前的标志位赋值给y,下次判断
                        y = x;
                    }
                } else {
                    isScroll = true;
                }
            }
        });

OK,这样就好了,最后,MainActivity.java的完整代码如下:

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.ListView;

import com.example.lilinxiong.listviewlinkage.Adapter.LeftAdapter;
import com.example.lilinxiong.listviewlinkage.Adapter.RightAdapter;
import com.example.lilinxiong.listviewlinkage.View.HaveHeaderListView;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    //左边的ListView
    private ListView lv_left;
    //左边ListView的Adapter
    private LeftAdapter leftAdapter;
    //左边的数据存储
    private List<String> leftStr;
    //左边数据的标志
    private List<Boolean> flagArray;
    //右边的ListView
    private HaveHeaderListView lv_right;
    //右边的ListView的Adapter
    private RightAdapter rightAdapter;
    //右边的数据存储
    private List<List<String>> rightStr;
    //是否滑动标志位
    private Boolean isScroll = false;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化控件
        initView();
        //初始化数据
        initData();
        lv_left.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                isScroll = false;
                for (int i = 0; i < leftStr.size(); i++) {
                    if (i == position) {
                        flagArray.set(i, true);
                    } else {
                        flagArray.set(i, false);
                    }
                }
                //更新
                leftAdapter.notifyDataSetChanged();
                int rightSection = 0;
                for (int i = 0; i < position; i++) {
                    //查找
                    rightSection += rightAdapter.getCountForSection(i) + 1;
                }
                //显示到rightSection所代表的标题
                lv_right.setSelection(rightSection);
            }
        });
        lv_right.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                switch (scrollState) {
                    // 当不滚动时
                    case AbsListView.OnScrollListener.SCROLL_STATE_IDLE:
                        // 判断滚动到底部
                        if (lv_right.getLastVisiblePosition() == (lv_right.getCount() - 1)) {
                            lv_left.setSelection(ListView.FOCUS_DOWN);
                        }
                        // 判断滚动到顶部
                        if (lv_right.getFirstVisiblePosition() == 0) {
                            lv_left.setSelection(0);
                        }
                        break;
                }

            }

            int y = 0;
            int x = 0;

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                if (isScroll) {
                    for (int i = 0; i < rightStr.size(); i++) {
                        if (i == rightAdapter.getSectionForPosition(lv_right.getFirstVisiblePosition())) {
                            flagArray.set(i, true);
                            //获取当前标题的标志位
                            x = i;
                        } else {
                            flagArray.set(i, false);
                        }
                    }
                    if (x != y) {
                        leftAdapter.notifyDataSetChanged();
                        //将之前的标志位赋值给y,下次判断
                        y = x;
                    }
                } else {
                    isScroll = true;
                }
            }
        });
    }

    private void initData() {
        //左边相关数据
        leftStr.add("面食类");
        leftStr.add("盖饭");
        leftStr.add("寿司");
        leftStr.add("烧烤");
        leftStr.add("酒水");
        leftStr.add("凉菜");
        leftStr.add("小吃");
        leftStr.add("粥");
        flagArray.add(true);
        flagArray.add(false);
        flagArray.add(false);
        flagArray.add(false);
        flagArray.add(false);
        flagArray.add(false);
        flagArray.add(false);
        flagArray.add(false);
        leftAdapter.notifyDataSetChanged();
        //右边相关数据
        //面食类
        List<String> food1 = new ArrayList<>();
        food1.add("热干面");
        food1.add("臊子面");
        food1.add("烩面");
        //盖饭
        List<String> food2 = new ArrayList<>();
        food2.add("番茄鸡蛋");
        food2.add("红烧排骨");
        food2.add("农家小炒肉");
        //寿司
        List<String> food3 = new ArrayList<>();
        food3.add("芝士");
        food3.add("丑小丫");
        food3.add("金枪鱼");
        //烧烤
        List<String> food4 = new ArrayList<>();
        food4.add("羊肉串");
        food4.add("烤鸡翅");
        food4.add("烤羊排");
        //酒水
        List<String> food5 = new ArrayList<>();
        food5.add("长城干红");
        food5.add("燕京鲜啤");
        food5.add("青岛鲜啤");
        //凉菜
        List<String> food6 = new ArrayList<>();
        food6.add("拌粉丝");
        food6.add("大拌菜");
        food6.add("菠菜花生");
        //小吃
        List<String> food7 = new ArrayList<>();
        food7.add("小食组");
        food7.add("紫薯");
        //粥
        List<String> food8 = new ArrayList<>();
        food8.add("小米粥");
        food8.add("大米粥");
        food8.add("南瓜粥");
        food8.add("玉米粥");
        food8.add("紫米粥");
        rightStr.add(food1);
        rightStr.add(food2);
        rightStr.add(food3);
        rightStr.add(food4);
        rightStr.add(food5);
        rightStr.add(food6);
        rightStr.add(food7);
        rightStr.add(food8);
        rightAdapter.notifyDataSetChanged();
    }

    private void initView() {
        lv_left = (ListView) findViewById(R.id.lv_left);
        leftStr = new ArrayList<>();
        flagArray = new ArrayList<>();
        leftAdapter = new LeftAdapter(MainActivity.this, leftStr, flagArray);
        lv_left.setAdapter(leftAdapter);
        lv_right = (HaveHeaderListView) findViewById(R.id.lv_right);
        rightStr = new ArrayList<List<String>>();
        rightAdapter = new RightAdapter(MainActivity.this, leftStr, rightStr);
        lv_right.setAdapter(rightAdapter);
    }
}

这是第一个自己理解(当然也有那个Demo的帮助哈)的自定义控件,一开始感觉好难,好难,但是最后写出来后,发现也挺有趣的,但是说真的,坑真不少啊!

然后在布局的ImageView中用了android:scaleType属性,不懂的小伙伴可以去这里[Android] ImageView.ScaleType设置图解
在Adapter中使用了SparseArray<>不懂的小伙伴可以去这里Android编程之SparseArray详解
其他的就是大家常用的了,最后我们再来看下我们的效果图吧:

大家若是有什么不懂的,可以在下面评论区中留言哈,我看到后会回的,另外对android有兴趣的同学可以加我们程序员刘某人的群:555974449(若满则加后面的)、484167109,群里面有很多大神的,而且很热情,很热心的,大家不懂的可以问的。

到这里车开完了~~~送你到终点站源码地址,欢迎各位吐槽……

你可能感兴趣的:(android,ListView,控件)