Android从入门到放弃——常见控件三

今天我们来看2个列表的布局,一个是ListView,一个是RecyclerView.

ListView:以垂直方向布局的列表

实现代码

xml布局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="10dp">

    <ListView
        android:id="@+id/acMainAListView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

activity代码

class AActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        acMainAListView.adapter = MyListViewAdapter(getDataList(), this)
    }

    private fun getDataList(): List<String> {
        val dataList = mutableListOf<String>()
        for (i in 0 until 50) {
            dataList.add("我是第:${i + 1}项")
        }
        return dataList
    }
}

Adapter代码

class MyListViewAdapter extends BaseAdapter {

    private List<String> dataList;
    private LayoutInflater inflater;

    public MyListViewAdapter(List<String> dataList, Context context) {
        this.dataList = dataList;
        inflater = LayoutInflater.from(context);
    }

    @Override
    public int getCount() {
        return dataList == null || dataList.isEmpty() ? 0 : dataList.size();
    }

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

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        MyViewHolder holder;
        if (convertView == null) {
            convertView = inflater.inflate(R.layout.layout_list_item, parent, false);
            holder = new MyViewHolder();
            holder.tvMsg = convertView.findViewById(R.id.layoutListItemTvMsg);
            convertView.setTag(holder);
        } else {
            holder = (MyViewHolder) convertView.getTag();
        }
        holder.tvMsg.setText(dataList.get(position));
        return convertView;
    }

    private class MyViewHolder {
        TextView tvMsg;
    }
}

代码讲解

首先我们需要在xml中布局listView,代码很简单,直接放置listView就可以了。

然后我们就需要最重要的一步,在代码中给listView设置适配器adapter。

什么是适配器?简单说就是我们想要让列表的每一项怎么显示?大家应该能想到了,其实适配器就是一个布局,布局的是列表的每一项的样式。

这里我们需要自定义一个adapter,只需要继承BaseAdapter即可,其中 有几个需要重写的方法:

  • int getCount()  获取当前总共有多少项
  • Object getItem(int position)  获取position对于的项
  • long getItemId(int position) 获取position对于项的id
  • View getView(int position, View convertView, ViewGroup parent) 获取position对于项的View视图

前面3个方法都比较好理解,我们重点来看看第4个getView方法。可以看到这个方法的参数,除了position之外还有2个,一个View,一个ViewGroup。

根据参数名称我们可以大概知道其代表的意思,convertView表示的当前position的视图,如果当前position是反复滑动的时候再次出现的,那么这个view是有值的,第二个是代表的当前项View的父视图,即listView。

大家想象一种情况,如果我们的列表有几百上千个,那么一屏肯定是显示不完的,那么就要上下拉来查看更多和返回查看之前的。如果我们在getView里面都是新建一个View,重新创建对象那么肯定是会很耗内存的。这里怎么办呢?就需要我们的convertView了。

看看代码里面是怎么写的:

MyViewHolder holder;
        if (convertView == null) {
            convertView = inflater.inflate(R.layout.layout_list_item, parent, false);
            holder = new MyViewHolder();
            holder.tvMsg = convertView.findViewById(R.id.layoutListItemTvMsg);
            convertView.setTag(holder);
        } else {
            holder = (MyViewHolder) convertView.getTag();
        }
        holder.tvMsg.setText(dataList.get(position));
        return convertView;

首先我们定义了一个ViewHolder,但是并没有new对象,这个Viewholder里面放的就是一个TextView。

然后我们就判断了convertView是不是为空,如果为空那么就表示当前的position是第一屏第一次出现,为什么说第一屏第一次出现待会我们再说,然后我们就通过layoutInflate来加载一个布局,注意,这里加载布局的时候我们把parent传给了加载方法。加载了当前需要的view之后我们再new的ViewHolder,并且通过findViewById获取的textview,第二个重要的点就在后面了,我们使用convertView的setTag方法把当前ViewHolder设置进去了。

如果convertView不为空,那么说明当前的view是复用的之前已经加载到内存里面的视图,还记得刚刚设置的tag吗? 这里我们只需要取出tag就能拿到ViewHolder了,而不用在通过new来创建对象了。

然后我们在重新设置TextView的文本。

这样我们就能通过view的复用来消除内存的异常了,从而大大的节省内存。

这里再介绍另外一个非常有用的方法,int getItemViewType(int position),这个方法的作用是通过position来返回当前View的类型。试想一下,我们刚刚这样的处理列表每一项的样式都是一样的,那如果我想要在一些特殊的位置有不同的布局呢?就可以使用这个方法了,只需要在getView中调用此方法来判断是什么类型再加载不同的view即可。

@Override
    public int getItemViewType(int position) {
        return position % 2;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Log.d(getClass().getName(), "ZLog getView :" + convertView + "  " + parent);
        MyViewHolder holder;
        if (convertView == null) {
            convertView = inflater.inflate(R.layout.layout_list_item, parent, false);
            holder = new MyViewHolder();
            holder.tvMsg = convertView.findViewById(R.id.layoutListItemTvMsg);
            holder.rootView = convertView;
            convertView.setTag(holder);
        } else {
            holder = (MyViewHolder) convertView.getTag();
        }
        if (getItemViewType(position) == 0) {
            holder.rootView.setBackgroundColor(Color.parseColor("#CCCCCC"));
        } else {
            holder.rootView.setBackgroundColor(Color.WHITE);
        }
        holder.tvMsg.setText(dataList.get(position));
        return convertView;
    }

这里简单实现了一个效果,通过getItemViewType来设备view的背景

下面一个问题,怎么进行交互?怎么知道用户点击了哪一项?

这个很简单,跟我们设置view的点击事件一样,listView有一个setOnItemClickListener方法来处理点击操作。

acMainAListView.setOnItemClickListener { parent, view, position, id ->
            Toast.makeText(this, "点击了第${position}项", Toast.LENGTH_SHORT).show()
        }

不过使用这里有一个需要注意的地方,如果我们需要使用setOnItemClickListener方法,那么在每一项的布局中不能使用button。因为如果在子项里面使用了button之后,setOnItemClickListener就无效了。

RecyclerView:一个各方面都更厉害的列表控件

如果要使用它,我们首先要做一个导包的操作:

或者通过IDE来添加:

搜索出来后直接点击OK就可以了。

xml布局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="10dp">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/acMainAListView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

activity代码:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        acMainAListView.adapter = MyRecyclerViewAdapter(getDataList(), this)
        acMainAListView.layoutManager = LinearLayoutManager(this)
    }

    private fun getDataList(): List<String> {
        val dataList = mutableListOf<String>()
        for (i in 0 until 50) {
            dataList.add("我是第:${i + 1}项")
        }
        return dataList
    }

MyRecyclerViewAdapter代码:

class MyRecyclerViewAdapter(val dataList: List<String>, context: Context) : RecyclerView.Adapter<ViewHolder>() {

    private val layoutInflater = LayoutInflater.from(context)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
        ViewHolder(layoutInflater.inflate(R.layout.layout_list_item, parent, false))

    override fun getItemCount(): Int = dataList.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.textView.text = dataList[position]
    }

}

class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {

    val textView = view.findViewById<TextView>(R.id.layoutListItemTvMsg)
}

布局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"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:padding="10dp">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher" />

    <TextView
        android:id="@+id/layoutListItemTvMsg"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="20dp" />


</LinearLayout>

看到adapter的代码有没有感觉代码少了好多……好幸福,得益于recyclerview的优秀设计,我们不需要想在listview中去自己判断是不是要复用view这些东西了,我们只需要关注自己的业务逻辑。

recyclerview的adapter需要继承RecyclerView.Adapter。它需要泛型参数,即我们的ViewHolder,我们在下面定义了一个ViewHolder类,继承自RecyclerView.ViewHolder,它的构造方法需要一个View,即每个item的视图View。

adapter本身也只需要重新onCreateViewHolder/getItemCount/onBindViewHolder3个方法。

onCreateViewHolder即获取我们自定义的ViewHolder,我们在这里加载需要的布局。

getItemCount返回列表数量

onBindViewHolder为处理视图数据的方法,这里系统给我们带回了ViewHolder和position。

大家应该也注意到在activity中设置adapter后,我们还设置了一个acMainAListView.layoutManager = LinearLayoutManager(this)。这里开始体现了RecyclerView的强大了。我们这里设置的layoutManager是LinearLayout,默认是垂直方向的,也可以设置水平方向,

acMainAListView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)

这样就是水平方向的滑动。如果是网格呢?简单

acMainAListView.layoutManager = GridLayoutManager(this, 3)

网格同样也是可以设置方向的

acMainAListView.layoutManager = GridLayoutManager(this, 2, GridLayoutManager.HORIZONTAL, false)

是不是很方便?

还有一种更厉害的布局,大家是不是在一些展示图片类的应用上看到过所谓瀑布流的布局?即每个item的宽高都不同。

我们稍微改一下代码,让每项的内容不同,达到每个布局大小不一样的效果

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        acMainAListView.adapter = MyRecyclerViewAdapter(getDataList(), this)
        acMainAListView.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
    }

    private fun getDataList(): List<String> {
        val dataList = mutableListOf<String>()
        for (i in 0 until 50) {
            val sb = StringBuilder()
            val size = Random().nextInt(50)
            for (j in 1 until size) {
                sb.append(j)
            }
            dataList.add("我是第:${i + 1}项,${sb.toString()}")
        }
        return dataList
    }

修改的地方有2点,第一个是getDataList,显示的文本随机增加了内容。第二个是layoutManager,改成了StaggeredGridLayoutManager。我们来看看效果:

因为我们随机的对显示内容进行了修改,所以每个item要显示的都不一样,这样看到的效果就是每个item的高度都不同。

下面是比较麻烦的一点,你会发现recyclerview没有类似setItemClickListener这样的方法。确实,recyclerview没有这样的方法,并不是故意不支持,而且Google特意这样做的,其实像listView的setOnItemClickListener方法在实际应用的时候用的并不多。一般的布局都是很复杂的,所以用到这个方法的机会也不多。那recyclerview怎么来监听点击事件呢?

这里介绍1种,自定义一个接口,然后在adapter的onBindViewHolder中对要响应点击的控件设置onClickListener,再在onclick方法中回调接口。下面看代码

首先定义我们需要的接口

interface IRecyclerViewClick {

    fun onItemClick(position: Int)

}

adapter中传递接口并且在onClick中调用

class MyRecyclerViewAdapter(val dataList: List<String>, context: Context, val itemClick: IRecyclerViewClick) :
    RecyclerView.Adapter<ViewHolder>() {

    private val layoutInflater = LayoutInflater.from(context)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
        ViewHolder(layoutInflater.inflate(R.layout.layout_list_item, parent, false))

    override fun getItemCount(): Int = dataList.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.textView.text = dataList[position]
        holder.textView.setOnClickListener {
            itemClick.onItemClick(position)
        }
    }

}

activity

 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        acMainAListView.adapter = MyRecyclerViewAdapter(getDataList(), this, object : IRecyclerViewClick {
            override fun onItemClick(position: Int) {
                Toast.makeText(this@AActivity, "点击了第$position  项", Toast.LENGTH_SHORT).show()
            }
        })
        acMainAListView.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
    }

这里就差不多讲完了这种2列表的处理,当然目前主推的还是第二种Recyclerview。还有很多没有讲到的地方后续会在以后应用的地方在慢慢讲解。大家也都可以动手试试。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值