Android自定义控件——仿饿了么联动ListView
来源:互联网 发布:java简单五子棋源代码 编辑:程序博客网 时间:2024/05/22 04:55
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(); }
先来解释下firstVisibleItem,visibleItemCount,totalItemCount这三个变量是什么意思哈,挺重要的。
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
- android中仿<饿了么>listview与stickylistheaderslistview联动
- 仿饿了么美团点餐界面,listView的二级联动
- 仿饿了么,百度外卖这些App的双ListView列表联动效果
- Android自定义控件—仿仪表盘进度控件ArcProgressBar
- Android高级控件(六)——自定义ListView高仿一个QQ可拖拽列表的实现
- Android自定义控件——仿ios开关按钮
- Android自定义控件——仿ios开关按钮
- Android自定义控件——仿ios开关按钮
- Android自定义控件——仿ios开关按钮
- Android自定义控件——点赞效果(仿Twitter)
- 自定义控件—仿IOS7适用于Android的滑动开关
- Android 自定义ListView控件
- Android自定义控件(三)——有弹性的ListView
- Android自定义控件——有弹性的ListView,ScrollView
- Android自定义控件——有弹性的ListView,ScrollView
- Android笔记—Listview控件的自定义使用
- 仿IOS特效(一)——Android 自定义View实现3D滚轮效果的城市联动选择器
- hdoj2841 容斥原理+思维
- Microsot SQL-Server2016安装
- 【前端框架系列】Pure : 来自雅虎的纯 CSS 框架
- 二叉树的递归建立
- Robot Framework 自动化测试框架 学习方法 开源代码
- Android自定义控件——仿饿了么联动ListView
- 快速排序及优化(三路划分等)
- spring mvc +ibatis 2
- UVA.725 Division (暴力)
- 元认知能力
- CCFCSP201604-1折点计数
- qualcomm platform camera porting
- 《数字图像处理》第三版 更正印刷错误
- CI框架简介