Kotlin-Android开发之RecyclerView+StickyListHeadersListView(粘列表标题控件)实现商品类别和商品的左右联动

前言:现在大部分app都有分类的这一功能,分类的需求一般都是左边是标题右边是内容,点击左边的标题会跳到相应的内容,滑动右边的内容会对应相应的标题,我们称为左右联动,早期的时候实现是两个listview,后期的时候则是RecycleView代替了ListView,今天我们用RecyclerView+StickyListHeadersListView更好的实现这一个需求。(注意:代码使用kotlin语言)


我们先看一下我们要实现的预览图


1.RecycleView介绍:是Android一个更强大的控件,其不仅可以实现和ListView同样的效果,还有优化了ListView中的各种不足。使用方法可以参考我的RecycleView系列博客(以下都是java代码开发的):

1)《Android开发之RecyclerView的基本使用(实现常用的4种效果)

2)《Android开发之RecyclerView实现点击事件和长按事件

3)《Android开发之RecyclerView的间隔线处理

4)《Android开发之RecyclerView添加头部和底部

5)《Android开发之实现滑动RecyclerView,浮动按钮的显示和隐藏(一)

6)《Android开发之实现滑动RecyclerView,浮动按钮的显示和隐藏(二)

7)《Android开发之RecyclerView的交互动画(实现拖拽和删除)

2.StickyListHeadersListView介绍:顾名思义,就是粘列表标题,如下图所示


实现思路:整体分左边的RecycleView和右边的StickyListHeadersListView,通过点击左边item所在的position获得此typeId,通过遍历得到右边商品第一个位置的position,然后调用StickyListHeadersListView中setSelection()方法,就能联动右边的商品。滑动右边的商品,对StickyListHeadersListView的滑动监听,通过对比记录的左侧选中的position的位置和当前的position的对比,如果不一致,需要切换类别。下面开始代码实现。


1.包的结构图和json数据的结构图

注意json数据放在res下的raw包中了,用InputStream按行读取txt文件即可。

2.需要添加的依赖

  //stickylistheaders
  implementation 'se.emilsjolander:stickylistheaders:2.7.0'
  //design
  implementation 'com.android.support:design:28.0.0'
  //gson
  implementation 'com.google.code.gson:gson:2.2.4'

3.通过json数据建立对应的bean对象

GoodsTypeInfo.kt
class GoodsTypeInfo {
    var id: Int = 0//商品类型id
    var name: String = ""//商品类型名称
    var info: String = ""//特价信息
    var list: List<GoodsInfo> = listOf()//商品列表
}
GoodsInfo.kt
class GoodsInfo {
    var id: Int = 0//商品id
    var name: String = ""//商品名称
    var icon: String = ""//商品图片
    var form: String = ""//组成
    var monthSaleNum: Int = 0//月销售量
    var isBargainPrice: Boolean = false//特价
    var isNew: Boolean = false//是否是新产品
    var newPrice: String = ""//新价
    var oldPrice: Int = 0//原价
    var sellerId: Int = 0
    //此商品属于那一个类别id以及类别名称
    var typeId: Int = 0
    var typeName: String = ""
    var count: Int =0
}

4.需要两个Adapter,分别对应左边类别的adapter和右边商品的adapter

GoodsTypeRvAdapter.kt
import android.graphics.Color
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.fly.recyclerviewdemo.ui.MainActivity
import com.fly.recyclerviewdemo.R
import com.fly.recyclerviewdemo.beans.GoodsTypeInfo

class GoodsTypeRvAdapter(val mainActivity: MainActivity, val goodsTypeList: List<GoodsTypeInfo>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val goodsTypeItemHolder = holder as GoodsTypeItemHolder
        goodsTypeItemHolder.bindData(goodsTypeList.get(position), position)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val itemView = LayoutInflater.from(mainActivity).inflate(R.layout.item_type, parent, false)
        return GoodsTypeItemHolder(itemView)
    }

    override fun getItemCount(): Int {
        return goodsTypeList.size
    }

    var selectPosition = 0 //选中的位置
    inner class GoodsTypeItemHolder(val item: View) : RecyclerView.ViewHolder(item) {
        val tvType: TextView
        var mPosition: Int = 0
        lateinit var goodsTypeInfo: GoodsTypeInfo

        init {
            tvType = item.findViewById(R.id.type)
            item.setOnClickListener {
                selectPosition = mPosition
                notifyDataSetChanged()
                //step2:右侧列表跳转到该类型中第一个商品
                val typeId = goodsTypeInfo.id
                //遍历所有商品,找到此position
                val position = mainActivity.getGoodsPositionByTypeId(typeId)
                mainActivity.slhlv.setSelection(position)
            }
        }

        fun bindData(goodsTypeInfo: GoodsTypeInfo, position: Int) {
            mPosition = position
            this.goodsTypeInfo = goodsTypeInfo
            if (position == selectPosition) {
                //选中的为白底加粗黑字,
                item.setBackgroundColor(Color.WHITE)
                tvType.setTextColor(Color.BLACK)
                tvType.setTypeface(Typeface.DEFAULT_BOLD)
            } else {
                //未选中是灰色背景 普通字体
                item.setBackgroundColor(Color.parseColor("#b9dedcdc"))
                tvType.setTextColor(Color.GRAY)
                tvType.setTypeface(Typeface.DEFAULT)
            }
            tvType.text = goodsTypeInfo.name
        }
    }
}

对应的 item_type.xml布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:clickable="true"
    android:layout_height="wrap_content"
    android:minHeight="80dp"
    android:background="#b9dedcdc"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:padding="5dp"
        android:id="@+id/type"
        android:text="种类1">
    </TextView>

</LinearLayout>

右边的adapter:

GoodsAdapter.kt
import android.graphics.Color
import android.graphics.Paint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.TextView
import com.fly.recyclerviewdemo.ui.MainActivity
import com.fly.recyclerviewdemo.R
import com.fly.recyclerviewdemo.beans.GoodsInfo
import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter

class GoodsAdapter(val mainActivity: MainActivity, val goodsList: List<GoodsInfo>) : BaseAdapter(), StickyListHeadersAdapter {

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        var itemView: View
        val goodsItemHolder: GoodsItemHolder
        if (convertView == null) {
            itemView = LayoutInflater.from(mainActivity).inflate(R.layout.item_goods, parent, false)
            goodsItemHolder = GoodsItemHolder(itemView)
            itemView.tag = goodsItemHolder
        } else {
            itemView = convertView
            goodsItemHolder = convertView.tag as GoodsItemHolder
        }
        goodsItemHolder.bindData(goodsList.get(position))
        return itemView
    }

    override fun getItem(position: Int): Any {
        return goodsList.get(position)
    }

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    override fun getCount(): Int {
        return goodsList.size
    }


    override fun getHeaderId(position: Int): Long {
        val goodsInfo: GoodsInfo = goodsList.get(position)
        return goodsInfo.typeId.toLong()
    }

    override fun getHeaderView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val goodsInfo: GoodsInfo = goodsList.get(position)
        val typeName = goodsInfo.typeName
        val textView: TextView = LayoutInflater.from(mainActivity).inflate(R.layout.item_type_header, parent, false) as TextView
        textView.text = typeName
        textView.setTextColor(Color.BLACK)
        return textView
    }

    inner class GoodsItemHolder(itemView: View) {
        val tvName: TextView
        val tvForm: TextView
        val tvMonthSale: TextView
        val tvNewPrice: TextView
        val tvOldPrice: TextView
        val tvCount: TextView

        init {
            tvName = itemView.findViewById(R.id.tv_name)
            tvForm = itemView.findViewById(R.id.tv_form)
            tvMonthSale = itemView.findViewById(R.id.tv_month_sale)
            tvNewPrice = itemView.findViewById(R.id.tv_newprice)
            tvOldPrice = itemView.findViewById(R.id.tv_oldprice)
            tvCount = itemView.findViewById(R.id.tv_count)
        }

        fun bindData(goodsInfo: GoodsInfo) {
            tvName.text = goodsInfo.name
            tvForm.text = goodsInfo.form
            tvMonthSale.text = "月售${goodsInfo.monthSaleNum}份"
            tvNewPrice.text = goodsInfo.newPrice
            tvOldPrice.text = "¥${goodsInfo.oldPrice}"
            tvOldPrice.paint.flags = Paint.STRIKE_THRU_TEXT_FLAG
            tvCount.text = goodsInfo.count.toString()
        }
    }
}

item_goods.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal" android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <ImageView
        android:src="@mipmap/ic_launcher_round"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_margin="10dp" />
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:maxLines="2"
            android:textSize="16sp"
            android:textStyle="bold"
            android:textColor="#000"
            android:text="尖椒土豆丝"
            />
        <TextView
            android:id="@+id/tv_form"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:maxLines="2"
            android:textSize="10sp"
            android:layout_marginTop="2dp"
            android:text="尖椒土豆丝+千叶豆腐+时蔬"
            />
        <TextView
            android:id="@+id/tv_month_sale"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:maxLines="1"
            android:textSize="12sp"
            android:layout_marginTop="2dp"
            android:text="月售137份"
            />
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="5dp">
            <TextView
                android:id="@+id/tv_newprice"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="16sp"
                android:text="¥30"
                android:textStyle="bold"
                android:textColor="#be5048"
                />
            <TextView
                android:id="@+id/tv_oldprice"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="13sp"
                android:text="¥60"
                android:layout_toRightOf="@id/tv_newprice"
                android:layout_marginLeft="10dp"
                android:textStyle="bold"
                android:textColor="#8f8d8d"
                android:layout_alignBottom="@id/tv_newprice"
                />

            <LinearLayout
                android:gravity="right"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:orientation="horizontal">

                <TextView
                    android:id="@+id/tv_count"
                    android:layout_width="20dp"
                    android:layout_height="20dp"
                    android:textSize="14sp"
                    android:text="0"
                    android:gravity="center"
                    android:textColor="#000"
                    android:visibility="invisible"
                    android:layout_gravity="center_vertical"
                    />
            </LinearLayout>
        </RelativeLayout>
    </LinearLayout>

</LinearLayout>
item_type_header.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:textSize="12sp"
    android:padding="5dp"
    android:background="#c6c5c5"
    android:textColor="#fd262525"
    android:textStyle="bold"
    android:text="标题">
</TextView>

5.在activity中进行这两个控件的使用。

MainActivity.kt
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.AbsListView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.fly.recyclerviewdemo.utils.DataUtil
import com.fly.recyclerviewdemo.R
import com.fly.recyclerviewdemo.adapter.GoodsAdapter
import com.fly.recyclerviewdemo.adapter.GoodsTypeRvAdapter
import com.fly.recyclerviewdemo.beans.GoodsInfo
import com.fly.recyclerviewdemo.beans.GoodsTypeInfo
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import org.json.JSONObject
import se.emilsjolander.stickylistheaders.StickyListHeadersListView

class MainActivity : AppCompatActivity() {

    val allTypeGoodsList : ArrayList<GoodsInfo> = arrayListOf()
    var goodstypeList: List<GoodsTypeInfo> = arrayListOf()
    lateinit var rvGoodsType: RecyclerView
    lateinit var slhlv: StickyListHeadersListView
    lateinit var goodsAdapter: GoodsAdapter
    lateinit var goodsTypeAdapter : GoodsTypeRvAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        getDatas()
        rvGoodsType = findViewById(R.id.rv_goods_type)
        slhlv = findViewById(R.id.slhlv)
        goodsAdapter = GoodsAdapter(this, allTypeGoodsList)
        slhlv.adapter = goodsAdapter
        rvGoodsType.layoutManager = LinearLayoutManager(this!!)
        goodsTypeAdapter = GoodsTypeRvAdapter(this, goodstypeList)
        rvGoodsType.adapter = goodsTypeAdapter

        slhlv.setOnScrollListener(object  : AbsListView.OnScrollListener{

            override fun onScroll(view: AbsListView?, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) {
                //先找出旧的类别
                val oldPosition = goodsTypeAdapter.selectPosition

                val newTypeId = allTypeGoodsList.get(firstVisibleItem).typeId
                //把新的id找到它对应的position
                val newPositon = getTypePositionByTypeId(newTypeId)
                //当newPositon与旧的不同时,证明需要切换类别了
                if(newPositon!=oldPosition){
                    goodsTypeAdapter.selectPosition = newPositon
                    goodsTypeAdapter.notifyDataSetChanged()
                }
            }

            override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) {

            }
        })

    }

    private fun getDatas() {
        var json : String= DataUtil.readFromRaw(
            this,
            R.raw.goodsdata
        );
        val gson = Gson()
        val jsoObj = JSONObject(json)
        val allStr = jsoObj.getString("list")
        goodstypeList = gson.fromJson(allStr, object : TypeToken<List<GoodsTypeInfo>>() {}.type)
        Log.e("business", "该商家一共有" + goodstypeList.size + "个类别商品")
        for( i in 0 until  goodstypeList.size){
            val goodsTypeInfo = goodstypeList.get(i)
            val aTypeList:List<GoodsInfo> = goodsTypeInfo.list
            for(j in 0 until  aTypeList.size){
                val goodsInfo = aTypeList.get(j)
                //建立双向绑定关系
                goodsInfo.typeName = goodsTypeInfo.name
                goodsInfo.typeId = goodsTypeInfo.id
            }
            allTypeGoodsList.addAll(aTypeList)
        }
    }

    //根据类别id找到其在左侧列表中的position
    fun getTypePositionByTypeId(newTypeId: Int):Int {
        var position = -1 //-1表示未找到
        for(i in 0 until  goodstypeList.size){
            val goodsTypeInfo = goodstypeList.get(i)
            if(goodsTypeInfo.id == newTypeId){
                position = i
                break;
            }
        }
        return position
    }

    //根据商品类别id找到此类别第一个商品的位置
    fun getGoodsPositionByTypeId(typeId: Int): Int {
        var position = -1 //-1表示未找到
        for(j in 0 until  allTypeGoodsList.size){
            val goodsInfo = allTypeGoodsList.get(j)
            if(goodsInfo.typeId == typeId){
                position = j
                break;
            }
        }
        return position
    }

}

对应的布局代码

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

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

    <se.emilsjolander.stickylistheaders.StickyListHeadersListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/slhlv"/>
</LinearLayout>

6.读取raw下的txt文件代码

DataUtil.java
import android.content.Context;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

/**
 * Created by Fly on 2019/1/10/010.
 */

public class DataUtil {

    /**
     * 从raw中按行读取txt
     */
    public static String readFromRaw(Context context,int id) {
        try {
            InputStream is = context.getResources().openRawResource(id);
            InputStreamReader reader = new InputStreamReader(is);
            BufferedReader bufferedReader = new BufferedReader(reader);
            StringBuffer buffer = new StringBuffer("");
            String str;
            while ((str = bufferedReader.readLine()) != null) {
                buffer.append(str);
                buffer.append("\n");
            }
            return buffer.toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "没有读取到";
    }
}

7.json数据

点击下载goodsdata.json

8.运行效果见上图,没截成动图见谅。


最后说句话,大家加油!

发布了106 篇原创文章 · 获赞 74 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/qq_32306361/article/details/103978247