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) {
return ;
}
int section = mAdapter.getSectionForPosition(firstVisibleItem);
int viewType = mAdapter.getSectionHeaderViewType(section);
mCurrentHeader = getSectionHeaderView(section, mCurrentHeader);
ensureHaveHeaderLayout(mCurrentHeader);
mCurrentHeaderViewType = viewType;
mHeaderOffset = 0.0 f;
for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) {
if (mAdapter.isSectionHeader(i)) {
View ChildView = getChildAt(i - firstVisibleItem);
float ChildViewTop = ChildView.getTop();
float ChildViewHeight = ChildView.getMeasuredHeight();
ChildView.setVisibility(VISIBLE);
if (ChildViewHeight >= ChildViewTop && ChildViewTop > 0 ) {
mHeaderOffset = ChildViewTop - ChildViewHeight;
} else if (ChildViewTop <= 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) {
boolean shouldLayout = section != mCurrentSection || oldView == null ;
View view = mAdapter.getSectionHeaderView(section, oldView, this );
if (shouldLayout) {
ensureHaveHeaderLayout(view);
mCurrentSection = section;
}
return view;
}
ensureHaveHeaderLayout() 代码如下:
private void ensureHaveHeaderLayout(View header ) {
if (header . isLayoutRequested()) {
int widthSpec = MeasureSpec. makeMeasureSpec(getMeasuredWidth(), mWidthMode);
int heightSpec;
ViewGroup. LayoutParams layoutParams = header . getLayoutParams();
if (layoutParams != null && layoutParams. height > 0 ) {
heightSpec = MeasureSpec. makeMeasureSpec(layoutParams. height, MeasureSpec. EXACTLY);
} else {
heightSpec = MeasureSpec. makeMeasureSpec(0 , MeasureSpec. UNSPECIFIED);
}
header . measure(widthSpec, heightSpec);
header . layout(0 , 0 , header . getMeasuredWidth(), header . getMeasuredHeight());
}
}
只有这样还是不行的,虽然这里的逻辑有了,但是最重要的绘制还没有呢,重写dispatchDraw() 方法,代码如下:
@Override
protected void dispatchDraw (Canvas canvas) {
super .dispatchDraw(canvas);
if (mAdapter == null || !mShouldPin || mCurrentHeader == null ) {
return ;
}
int saveCount = canvas.save();
canvas.translate(0 , mHeaderOffset);
canvas.clipRect(0 , 0 , getWidth(), mCurrentHeader.getMeasuredHeight());
mCurrentHeader.draw(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) {
return ;
}
int section = mAdapter.getSectionForPosition(firstVisibleItem);
int viewType = mAdapter.getSectionHeaderViewType(section);
mCurrentHeader = getSectionHeaderView(section, mCurrentHeader);
ensureHaveHeaderLayout(mCurrentHeader);
mCurrentHeaderViewType = viewType;
mHeaderOffset = 0.0 f;
for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) {
if (mAdapter.isSectionHeader(i)) {
View ChildView = getChildAt(i - firstVisibleItem);
float ChildViewTop = ChildView.getTop();
float ChildViewHeight = ChildView.getMeasuredHeight();
ChildView.setVisibility(VISIBLE);
if (ChildViewHeight >= ChildViewTop && ChildViewTop > 0 ) {
mHeaderOffset = ChildViewTop - ChildViewHeight;
} else if (ChildViewTop <= 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 ) {
return ;
}
int saveCount = canvas.save();
canvas.translate(0 , mHeaderOffset);
canvas.clipRect(0 , 0 , getWidth(), mCurrentHeader.getMeasuredHeight());
mCurrentHeader.draw(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) {
boolean shouldLayout = section != mCurrentSection || oldView == null ;
View view = mAdapter.getSectionHeaderView(section, oldView, this );
if (shouldLayout) {
ensureHaveHeaderLayout(view);
mCurrentSection = section;
}
return view;
}
private void ensureHaveHeaderLayout (View header) {
if (header.isLayoutRequested()) {
int widthSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), mWidthMode);
int heightSpec;
ViewGroup.LayoutParams layoutParams = header.getLayoutParams();
if (layoutParams != null && layoutParams.height > 0 ) {
heightSpec = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
} else {
heightSpec = MeasureSpec.makeMeasureSpec(0 , MeasureSpec.UNSPECIFIED);
}
header.measure(widthSpec, heightSpec);
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 {
private ImageView lv_customize_item_image;
private TextView lv_customize_item_text;
}
class HeaderViewHolder {
private TextView lv_customize_item_header_text;
}
}
最后一步,MainActivity
所有的都准备好了,布局,Adapter,最后让我们在MainActivity中实现吧~
声明所需的变量:
private ListView lv_left;
private LeftAdapter leftAdapter;
private List<String> leftStr;
private List<Boolean> flagArray;
private HaveHeaderListView lv_right;
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 ;
}
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 = 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 {
private ListView lv_left;
private LeftAdapter leftAdapter;
private List<String> leftStr;
private List<Boolean> flagArray;
private HaveHeaderListView lv_right;
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 ;
}
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 = 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,群里面有很多大神的,而且很热情,很热心的,大家不懂的可以问的。
到这里车开完了~~~送你到终点站源码地址 ,欢迎各位吐槽……