ListView
ListView的基本使用涉及到3个部分:数据集合、Adapter和ListView的用法。数据集合为ListView提供了统一的数据维护和管理,可以是一个数组或者一个集合对象。Adapter提供了数据集合和ListView的一个联系通道,将布局文件inflate出View对象,并将集合中的当前元素的数据填入布局文件的组件中,并作为ListView的一个列表项,从而实现ListView的基本使用。
三者的关系如下图所示:
ListView与Adapter与数据集合
Adapter的基本定义位于ListAdapter中,在ListAdapter的基础上扩展出BaseAdapter子类,同时以BaseAdapter为基础,派生出ArrayAdapter、SimpleAdapter、SpinnerAdapte和CursorAdapter等常用类。ArrayAdapter 功能最简单,SimpleAdapter 适用于稍微复杂的变更需求不多的场景,CursorAdapter 主要针对游标集合。更多的时候,我们其实需要自定义BaseAdapter才能满足我们的业务需求。
Adapter是一个接口,其中定义了一些基本的方法:
public interface Adapter {
int getCount();
Object getItem(int position);
long getItemId(int position);
View getView(int position, View convertView, ViewGroup parent);
int getItemViewType(int position);
int getViewTypeCount();
}
ListView简单实用
1 . 显示ListView的Activity的.xml布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".listview.ListViewNormalActivity">
<Button
android:id="@+id/normalActivity_btn1"
android:layout_width="match_parent"
android:layout_height="40dp"
android:onClick="clickForList"
android:text="添加数据" />
<Button
android:id="@+id/normalActivity_btn2"
android:layout_width="match_parent"
android:layout_height="40dp"
android:onClick="clickForList"
android:text="删除数据" />
<ListView
android:id="@+id/normalActivity_lv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:divider="#123"
android:dividerHeight="1dp"
android:headerDividersEnabled="true"
android:footerDividersEnabled="true"
android:listSelector="@color/colorAccent"
android:stackFromBottom="true"
android:transcriptMode="alwaysScroll"
tools:listitem="@layout/item_normal_layout" />
</LinearLayout>
2 . 显示ListView的Activity页面的代码
public class ListViewNormalActivity extends AppCompatActivity {
private static final String TAG = ListViewNormalActivity.class.getSimpleName();
private ListView mListView;
private ArrayList<String> mList = new ArrayList<>();
private NormalAdapter mNormalAdapter;
//跳转到该页面的方法;
public static void toListViewNormalActivity(Context context) {
Intent intent = new Intent(context, ListViewNormalActivity.class);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list_view_normal);
initView();
}
//初始化数据;
private void initView() {
mListView = findViewById(R.id.normalActivity_lv);
for (int i = 0; i < 30; i++) {
String content = "hello " + (i + 1) + " world";
mList.add(content);
}
mNormalAdapter = new NormalAdapter(this, mList);
mListView.setAdapter(mNormalAdapter);
//设置HeaderView;
mListView.addHeaderView(new View(this));
//设置FooterView;
mListView.addFooterView(new View(this));
//Item的点击事件;
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Log.e(TAG, "onItemClick position:" + position);
Log.e(TAG, "onItemClick content:" + mList.get(position));
Toast.makeText(ListViewNormalActivity.this, "点击的位置是:" + (position + 1), Toast.LENGTH_SHORT).show();
}
});
//Item的长按事件;
mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
Log.e(TAG, "onItemLongClick position:" + (position));
Toast.makeText(ListViewNormalActivity.this, "长按的位置是:" + (position), Toast.LENGTH_SHORT).show();
//返回false表示会再触发下onItemClick方法,返回true表示就不会再触发onItemClick方法;
return true;
}
});
}
public void clickForList(View view) {
switch (view.getId()) {
case R.id.normalActivity_btn1:
Log.e(TAG, "点击了添加数据...");
mList.add("这是新添加的数据 " + (mList.size() + 1));
//使用notifyDataSetChanged()方法更新发生了变化的数据;
mNormalAdapter.notifyDataSetChanged();
break;
case R.id.normalActivity_btn2:
Log.e(TAG, "点击了删除数据...");
mList.remove(0);
mNormalAdapter.notifyDataSetChanged();
break;
default:
break;
}
}
}
3 . 使用的Adapter的布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/itemNormal_iv"
android:layout_width="100dp"
android:layout_height="100dp"
android:scaleType="centerInside"
tools:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/itemNormal_tv"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginLeft="20dp"
android:gravity="center_vertical"
tools:text="hello world" />
</LinearLayout>
4 . Adapter的代码
public class NormalAdapter extends BaseAdapter {
private static final String TAG = NormalAdapter.class.getSimpleName();
private Context mContext;
private List<String> mList;
private LayoutInflater mLayoutInflater;
public NormalAdapter(Context context, List<String> list) {
mContext = context;
mList = list;
mLayoutInflater = LayoutInflater.from(mContext);
}
@Override
public int getCount() {
return mList.size();
}
@Override
public Object getItem(int position) {
return mList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder = null;
if (convertView == null) {
//使用布局装饰器对象LayoutInflater的inflate()方法把一个XML转换成View;
convertView = mLayoutInflater.inflate(R.layout.item_normal_layout, null);
viewHolder = new ViewHolder();
viewHolder.imageView = convertView.findViewById(R.id.itemNormal_iv);
viewHolder.textView = convertView.findViewById(R.id.itemNormal_tv);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.imageView.setImageDrawable(mContext.getResources().getDrawable(R.mipmap.ic_launcher));
viewHolder.textView.setText(mList.get(position));
return convertView;
}
static class ViewHolder {
private ImageView imageView;
private TextView textView;
}
}
使用ListView的方法简单用法就是上面,初始化ListView之后,初始化作为ListView的数据的List,把List数据作为参数初始化Adapter,然后使用ListView的setAdapter()方法把Adapter和ListVie绑定起来。
ListView及BaseAdapter的常用属性和方法
ListView属性
- transcriptMode:该属性设置当数据发生变化时,ListView是否会滚动到ListView的底部;可取值:disable、normal、alwaysScroll,默认值是disable;
disable就是禁用,不会在数据发生变化之后进行ListView的滚动;normal指如果当前最后一个Item在ListView的显示范围之内,ListView在发生了数据变化之后会滚动到底部,否则不会滚动到底部;alwaysScroll指ListView在发生数据变化之后会强制滚动到底部;
setTranscriptMode(int mode):方法可取值ListView.TRANSCRIPT_MODE_DISABLED、ListView.TRANSCRIPT_MODE_NORMAL、ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL;
如果,我们是在数据List的底部添加数据,那么滚动到ListView底部会正好看到我们添加的数据,但是如果我们在List的顶部添加数据,滚动到ListView底部就不是我们的预期了,所以要根据业务来设置该属性; - listSelector:设置当Item被点击时的背景颜色;
- divider:设置分割线,可以是颜色也可以是Drawable,ListView有默认的分割线,但是如果设置了这个属性,那么必须设置dividerHeight这个属性,否则会不显示分割线;
- dividerHeight:设置分割线的高度;
- stackFromBottom:设置数据显示List的顶部数据还是底部数据;为true则会直接滚动到ListView的底部;
- scrollbars:设置是否显示滚动条,可取值vertical、horizontal、none;
对ListView来说,它只能垂直滚动,将scrollbars设置成horizontal或者none效果都是一样的,也就是不会出现滚动条。所以如果不希望ListView显示滚动条,就将scrollbars设置成none。此外,如果scrollbars设置成none,那么其他的滚动条相关的配置都不会有效果。 - headerDividersEnabled:设置第一行item的顶部是否显示分割线,true为显示,false为不显示,默认为true。但是显示条件是要设置headerView;如果我们项目中不需要设置headerView,但是我们可以设置一个空的;
mListView.addHeaderView(new View(this));
- footerDividersEnabled:设置最后一行item的底部是否显示分割线,true为显示,false为不显示,默认为true。但是显示条件是要设置footerView;如果我们项目中不需要设置footerView,但是我们可以设置一个空的;
mListView.addFooterView(new View(this));
ListView方法
- getFirstVisiblePosition():获取第一个可见的item;
- getLastVisiblePosition():获取最后一个可见的item;
- getItemAtPosition():获取item数据集;
- getChildAt():获取位于position位置的item;
- getAdapter():获取ListView的适配器;
BaseAdapter方法
notifyDataSetChanged():对ListView的所有item进行刷新;
ListView常用的适配器
ListView配合使用的适配器有多个类型,但是我们一般使用BaseAdapter这个适配器。
BaseAdapter的主要方法:
1 . getCount():适配器中数据集中数据的个数;
2 . getItem(int position):获取数据集中与指定索引对应的数据项;
3 . getItemId(int position):获取指定行对应的ID;
4 . getView(int position,View convertView,ViewGroup parent):获取每一个Item的显示内容;
我们一般使用BaseAdapter作为ListView的适配器,我们可以在BaseAdapter中进行一些优化。
1 . 复用convertView减少对item的绘制(涉及ListView的缓存和显示机制,需要时候显示,划出屏幕时候被回收到缓存。在对convertView的判空方法中,是为了避免反复使用inflate()方法,这个方法还是很耗时的);
2 . 使用ViewHolder减少findViewById()查找控件的次数;
ListView常用的场景
判断上滑和下滑
我们可以使用ListView的OnScrollListener接口来判断ListView是向上滑动还是在向下滑动。在这个接口中,有两个方法:滚动状态发生变化的方法onScrollStateChanged()和ListView滚动时调用的方法onScroll()方法。
在滚动状态发生改变的onScrollStateChanged()方法中,有三种状态:
(1) . SCROLL_STATE_TOUCH_SCROLL:手指按下移动的状态,触摸滑动;
(2) . SCROLL_STATE_FLING:惯性滚动的状态,滑翔;
(3) . SCROLL_STATE_IDLE:静止状态,就是滚动已经停止了;
如下:
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
Log.e(TAG, "onScrollStateChanged...scrollState:" + scrollState);
switch (scrollState) {
case AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
index = view.getLastVisiblePosition();
break;
case AbsListView.OnScrollListener.SCROLL_STATE_IDLE:
int firstIndex = view.getFirstVisiblePosition();
int lastIndex = view.getLastVisiblePosition();
Log.e(TAG, "index:" + index + "--firstIndex:" + firstIndex + "--lastIndex:" + lastIndex + "--count:" + view.getCount());
if (lastIndex >= index && firstIndex != 0) {
Log.e(TAG, "向上滑动了...");
if (lastIndex + 1 == view.getCount()) {
Log.e(TAG, "滑动到了底部...");
}
} else {
Log.e(TAG, "向下滑动了...");
if (firstIndex == 0) {
Log.e(TAG, "滑动到了顶部...");
}
}
break;
default:
break;
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
设置HeaderView和FooterView
给ListView添加HeaderView
//获取HeaderView的布局;
View headerView = LayoutInflater.from(this).inflate(R.layout.item_header_view, null);
//将HeaderView布局添加到ListView中;
mListView.addHeaderView(headerView);
//获取HeaderView中的控件;
TextView textViewHeader = headerView.findViewById(R.id.headerView_tv);
给ListView添加FooterView
View footerView = LayoutInflater.from(this).inflate(R.layout.item_footer_view, null);
mListView.addFooterView(footerView);
TextView textViewFooter = footerView.findViewById(R.id.footerView_tv);
设置多布局item
ListView可以显示多个不同布局类型的item的。使用的是getViewTypeCount()和getItemViewType()这两个方法。我们在getViewTypeCount()方法中设置能显示几种不同的布局类型,在getItemViewType()方法中根据项目中的业务逻辑来设置在不同的position的位置显示哪种布局类型,在getView()方法中来设置如何显示不同的布局并设置要显示的数据。
比如,我们显示两种不同的布局类型。首先设置要显示的两种布局类型的值。
private int ITEM_TYPE_FIRST = 0;
private int ITEM_TYPE_SECOND = 1;
然后设置要显示的不同布局类型的总数量,在这里我们例子中只显示两种布局类型;
@Override
public int getViewTypeCount() {
return 2;
}
接下来设置在position的位置显示哪种布局,在这里我们简单地根据position的位置来做了显示不同布局,实际开发中,我们可能会根据获取到的List中的某个position的某个属性值来确定显示某种布局类型;
@Override
public int getItemViewType(int position) {
if (position % 3 == 0) {
return ITEM_TYPE_FIRST;
} else {
return ITEM_TYPE_SECOND;
}
}
最后,我们在getView()方法中来设置不同布局类型的使用和数据的显示;
@Override
public View getView(int position, View convertView, ViewGroup parent) {
int currentType = getItemViewType(position);
if (currentType == ITEM_TYPE_FIRST) {
//第一种类型;
ViewHolder1 viewHolder1;
if (convertView == null) {
viewHolder1 = new ViewHolder1();
convertView = mLayoutInflater.inflate(R.layout.item_type1, null);
viewHolder1.mTextViewTitle = convertView.findViewById(R.id.item_type1_tv);
viewHolder1.mTextViewContent = convertView.findViewById(R.id.item_type1_tv2);
convertView.setTag(viewHolder1);
} else {
viewHolder1 = (ViewHolder1) convertView.getTag();
}
viewHolder1.mTextViewTitle.setText("first title:" + mList.get(position));
viewHolder1.mTextViewContent.setText("this is first content " + position);
} else {
//第二种类型;
ViewHolder2 viewHolder2;
if (convertView == null) {
viewHolder2 = new ViewHolder2();
convertView = mLayoutInflater.inflate(R.layout.item_type2, null);
viewHolder2.mImageView = convertView.findViewById(R.id.item_type2_iv);
viewHolder2.mTextViewMessage = convertView.findViewById(R.id.item_type2_tv);
convertView.setTag(viewHolder2);
} else {
viewHolder2 = (ViewHolder2) convertView.getTag();
}
viewHolder2.mTextViewMessage.setText("this is second title:" + mList.get(position));
}
return convertView;
}
static class ViewHolder1 {
private TextView mTextViewTitle;
private TextView mTextViewContent;
}
static class ViewHolder2 {
private ImageView mImageView;
private TextView mTextViewMessage;
}
设置ListView滚动道某个指定的位置
我们可以通过setSelection(int position)方法来设置让ListView滚动到指定的position的位置。
//设置ListView滚动到指定位置,这里我们设置把ListView滚动到position下标为15的item的位置;
mListView.setSelection(15);
设置自定义的快速滚动条
设置自定义的快速滚动条:在application的theme中添加以下两行代码:
<item name="android:fastScrollThumbDrawable">@mipmap/icon_scrollbar</item>//设置自定义的滚动条图片;
<item name="android:fastScrollTrackDrawable">@null</item>//设置滚动条轨迹;
在ListView的xml文件中设置以下属性:
android:scrollbars="none"//取消先前默认的滚动条;
android:fastScrollEnabled="true"//设置是否启用快速滚动条;
android:fastScrollAlwaysVisible="true"//设置快速滚动条是否一直显示;
记录ListView先前滚动的位置
实现原理:记录下ListView在页面内可视区域的位置,然后存储到SharedPreferences中,然后在再次进入页面时读取位置,滑动到先前的位置;
//记录位置;
@Override
protected void onPause() {
super.onPause();
Log.e("===MockingJay===", "onPause.....");
ListView listView = findViewById(R.id.listViewActivity_lv1);
int pos = listView.getFirstVisiblePosition();
SPUtil.putInt(this, "sp_position", pos);
}
//获取位置并滑动到该位置,进行延迟操作,否则可能不会实现效果;
final int position = SPUtil.getInt(this, "sp_position", 0);
mListViewActivityLv1.postDelayed(new Runnable() {
@Override
public void run() {
mListViewActivityLv1.setSelection(position);
}
}, 300 + 50);
ScrollView中嵌套ListView
ScrollView中嵌套ListView会造成ListView的item显示不全的问题。这个问题有两种解决方法,第一种是不改变ListView,但是在代码中对ListView的高度进行测量计算和设置;第二种方法是自定义一个控件,继承自ListView,重写onMeasure()方法,对ListView的高度进行重新测量;
第一种方法
private void initView() {
mListView = findViewById(R.id.scrollView_listView_lv);
for (int i = 0; i < 30; i++) {
String content = "hello " + i + " world";
mList.add(content);
}
mAdapter = new NormalAdapter(this, mList);
mListView.setAdapter(mAdapter);
setListViewHeightOnChildren(mListView);
}
//在这个方法中重新测量ListView的高度;
private void setListViewHeightOnChildren(ListView listView) {
ListAdapter adapter = listView.getAdapter();
if (adapter == null) {
return;
}
int totalHeight = 0;
for (int i = 0; i < adapter.getCount(); i++) {
View listItem = adapter.getView(i, null, listView);
listItem.measure(0, 0);
totalHeight += listItem.getMeasuredHeight();
}
ViewGroup.LayoutParams params = listView.getLayoutParams();
params.height = totalHeight + (listView.getDividerHeight() * adapter.getCount() - 1);
// params.height += 5;
listView.setLayoutParams(params);
}
第二种方法
public class ScrollViewListView extends ListView {
private static final String TAG = ScrollViewListView.class.getSimpleName();
public ScrollViewListView(Context context) {
super(context);
}
public ScrollViewListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ScrollViewListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int spec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, spec);
}
}
ListView中的图片错位情况
需要完善
ListView中的图片优化
需要完善
ListView的item中含有CheckBox选择的情况
需要完善