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"//设置快速滚动条是否一直显示;

项目地址https://github.com/loveyoyo/SimpleDemo/blob/master/app/src/main/java/com/kang/demo/widget/ListViewActivity.java

记录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选择的情况

需要完善