Android 实现一个类似Excel表格似的效果 2

忽有故人心上过,回首山河便是秋。

本demo地址 https://github.com/yuqianglianshou/ExcelList

效果 由于放GIF图比较卡,这里放几张静态图,也是一目了然。





一个类Excel表格似的数据展示如何实现?

横向可滚可以用HorizontalScrollview实现,纵向可滚可用RecycelerView实现,可 横向 纵向 都能滚怎么弄?
下面我来一步步分析实现。

  1. 组合 HorizontalScrollview 和 RecycelerView,rv的item布局使用 HorizontalScrollview 实现横向滑动 。
    item 布局如下:
<?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="40dp"
    android:background="#b2d235"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:padding="5dp">

    <TextView
        android:id="@+id/tv_name"
        style="@style/Cells"
        android:text="姓名" />

    <HorizontalScrollView
        android:id="@+id/headerHorizontalScrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="none">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/tv_a"
                style="@style/Cells"
                android:text="性别" />

            <TextView
                android:id="@+id/tv_b"
                style="@style/Cells"
                android:text="年龄" />

            <TextView
                android:id="@+id/tv_c"
                style="@style/Cells"
                android:text="出生日期" />

            <TextView
                android:id="@+id/tv_d"
                style="@style/Cells"
                android:text="家庭住址" />

            <TextView
                android:id="@+id/tv_e"
                style="@style/Cells"
                android:text="电话号码" />

            <TextView
                android:id="@+id/tv_f"
                style="@style/Cells"
                android:text="房?" />

            <TextView
                android:id="@+id/tv_g"
                style="@style/Cells"
                android:text="车?" />
        </LinearLayout>
    </HorizontalScrollView>


</LinearLayout>

@style/Cells 如下:

    <style name="Cells">
        <item name="android:layout_width">@dimen/dimen_width_80</item>
        <item name="android:layout_height">match_parent</item>
        <item name="android:gravity">center_vertical</item>
        <item name="android:padding">5dp</item>
        <item name="android:textSize">14sp</item>
    </style>

此布局的实现效果,第一个单元格固定,后面7个可以滑动。
完成rv设置,adapter设置,实现出来的效果是 纵向滑动没问题,横向滑动则为 每个item各自为营,单独的都可以滑动,缺少了Excel表格的 “联动” 效果。

  1. 实现 联动 效果。
    思路是 固定一个 item 布局 作为 头布局,rv中每个 item 的 HorizontalScrollView 监听头布局中 HorizontalScrollView 的滚动,跟随一起滚动,且item中的 HorizontalScrollView 不可滚动,这样的话,rv失去左右滚动效果(避免各自为营的问题),头布局滚动时rv中所有item会一起滚动。
    首先拦截rv中 HorizontalScrollView 的滚动,方法是 在 HorizontalScrollView 外包裹一层拦截事件的 InterceptLinearLayout ,这样就不会响应到 HorizontalScrollView 的滚动; 其次 HorizontalScrollView 可以添加观察者,实现随一而动的效果,关于如何添加观察者,一会请看 adapter 的实现。

HorizontalScrollView 可添加观察者的实现 CanObserverHorizontalScrollView.kt

import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.widget.HorizontalScrollView

/**
 *
 *@author : lq
 *@date   : 2020/12/28
 *@desc   : 可添加观察者的 HorizontalScrollView
 *
 */
private const val TAG = "lq"
class CanObserverHorizontalScrollView : HorizontalScrollView {

    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(
        context,
        attrs,
        defStyle
    )

    //定义观察者 集合
    var mScrollViewObserver = ScrollViewObserver()

    override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) {
        super.onScrollChanged(l, t, oldl, oldt)
        /*
		 * 当自己 滚动条移动后,引发 滚动事件。通知给所有观察者。
		 */
        if (mScrollViewObserver != null) {
            mScrollViewObserver.notifyOnScrollChanged(l, t, oldl, oldt)
        }

    }

    /*
     * 订阅 本控件 的 滚动条变化事件
     * */
    fun addScrollChangedListener(listener: ScrollViewObserver.OnScrollChangedListener) {
        mScrollViewObserver.addScrollChangedListener(listener)
    }

    /*
     * 取消 订阅 本控件 的 滚动条变化事件
     * */
    fun removeScrollChangedListener(listener: ScrollViewObserver.OnScrollChangedListener) {
        mScrollViewObserver.removeScrollChangedListener(listener)
    }


    /**
     *
     *@author : lq
     *@date   : 2020/12/29
     *@desc   : scrollview  的观察者
     *
     */
    class ScrollViewObserver {
        var mList: ArrayList<OnScrollChangedListener> = ArrayList()

        //添加一个监听者
        fun addScrollChangedListener(listener: OnScrollChangedListener) {
//            Log.i(TAG, "addScrollChangedListener: "+listener.hashCode())
            mList.add(listener)
        }

        //移除一个监听者
        fun removeScrollChangedListener(listener: OnScrollChangedListener) {
//            Log.i(TAG, "removeScrollChangedListener: "+listener.hashCode())
            mList.remove(listener)
        }

        //滚动 传递给所有 监听者
        fun notifyOnScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) {
            if (mList == null || mList.size == 0) {
                return
            }
            var iterator = mList.iterator()
            while (iterator.hasNext()) {
                var listener = iterator.next()
                if (listener != null) {
                    listener.onScrollChanged(l, t, oldl, oldt)
                } else {
                    iterator.remove()
                }
            }

            Log.i("lq", "notifyOnScrollChanged: mList.size == " + mList.size)
        }


        /**
         *
         *@author : lq
         *@date   : 2020/12/29
         *@desc   : 当发生了滚动事件时
         *
         */
        interface OnScrollChangedListener {
            fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int)
        }


    }

}

拦截事件的 LinearLayout 实现 InterceptLinearLayout.kt

/**
 *
 *@author : lq
 *@date   : 2020/12/28
 *@desc   : 拦截处理事件,事件不向子控件传递
 *  不响应内部的子控件事件
 */
class InterceptLinearLayout : LinearLayout {
    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        // true 为拦截
        return true
    }
}

新的 rv的item布局 item_rv.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="40dp"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:padding="5dp">

    <TextView
        android:id="@+id/tv_name"
        style="@style/Cells"
        android:background="@drawable/list_item_color_bg"
        android:text="Column1" />

<!--    拦截事件传递到子控件-->
    <com.lq.excellist.InterceptLinearLayout
        android:id="@+id/linearLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/list_item_color_bg">

        <HorizontalScrollView
            android:id="@+id/horizontalScrollView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scrollbars="none">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="horizontal">

                <TextView
                    android:id="@+id/tv_a"
                    style="@style/Cells"
                    android:text="Column2" />

                <TextView
                    android:id="@+id/tv_b"
                    style="@style/Cells"
                    android:text="Column3" />

                <TextView
                    android:id="@+id/tv_c"
                    style="@style/Cells"
                    android:text="Column4" />

                <TextView
                    android:id="@+id/tv_d"
                    style="@style/Cells"
                    android:text="Column5" />

                <TextView
                    android:id="@+id/tv_e"
                    style="@style/Cells"
                    android:text="Column6" />

                <TextView
                    android:id="@+id/tv_f"
                    style="@style/Cells"
                    android:text="Column7" />

                <TextView
                    android:id="@+id/tv_g"
                    style="@style/Cells"
                    android:text="Column8" />
            </LinearLayout>
        </HorizontalScrollView>
    </com.lq.excellist.InterceptLinearLayout>

</LinearLayout>

头布局 item_rv_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="40dp"
    android:background="#b2d235"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:padding="5dp">

    <TextView
        android:id="@+id/tv_name"
        style="@style/Cells"
        android:text="姓名" />


<!--    可添加观察者的HorizontalScrollView-->
    <com.lq.excellist.CanObserverHorizontalScrollView
        android:id="@+id/headerHorizontalScrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="none">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/tv_a"
                style="@style/Cells"
                android:text="性别" />

            <TextView
                android:id="@+id/tv_b"
                style="@style/Cells"
                android:text="年龄" />

            <TextView
                android:id="@+id/tv_c"
                style="@style/Cells"
                android:text="出生日期" />

            <TextView
                android:id="@+id/tv_d"
                style="@style/Cells"
                android:text="家庭住址" />

            <TextView
                android:id="@+id/tv_e"
                style="@style/Cells"
                android:text="电话号码" />

            <TextView
                android:id="@+id/tv_f"
                style="@style/Cells"
                android:text="房?" />

            <TextView
                android:id="@+id/tv_g"
                style="@style/Cells"
                android:text="车?" />
        </LinearLayout>
    </com.lq.excellist.CanObserverHorizontalScrollView>


</LinearLayout>

最终的布局文件 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="vertical">

    <!--  表头-->
    <include layout="@layout/item_rv_header" />

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

</LinearLayout>

到目前为止,我们已经实现了部分效果,可纵向滑动,手指在 头布局 的 HorizontalScrollView 可滑动并可带动 rv 中所有 item 一起滑动,实现了 联动 效果,还有最后一个问题,手指在rv上没有实现左右滑动,这个通过重写rv的 OnTouchListener 事件,将 此 事件 分发给头布局的 HorizontalScrollView ,与 其一起处理事件,

        rv.setOnTouchListener { v, event ->
            //当在rv 上touch时,将事件分发给 表头的 scrollview 处理
            headerHorizontalScrollView.onTouchEvent(event)
            //如果删掉下面这行代码,rv的上下滚动效果会失效
            onTouchEvent(event)
        }

所以最终的 MainActivity.kt是

package com.lq.excellist

import android.annotation.SuppressLint
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.item_rv_header.*
import java.util.*


/**
 * 原理:
 * 将rv的touch事件交给表头的scrollview处理并监听表头的scrollview,表头的scrollview滚动(滑动表头的scrollview 或 滑动rv),通知监听者(rv中所有的scrollview)一起滚动。
 *
 * 1,表头 和 内容 作为两个部分 单独处理。
 * 2,将整个 recyclerview 的touch事件交给表头的 scrollview 处理。
 * 3,将recyclerview的每个item中的scrollview添加为头部scrollview的观察者,使之跟随头部scrollview一起滚动。
 *
 * 表头布局 item_rv_header 和 rv 的 item 布局 item_rv 区别,item_rv 布局在 horizontalscrollview 外 包裹了一层拦截事件的LinearLayout,
 * 即InterceptLinearLayout,用于拦截每个item中scrollview的滚动事件。否则会看到item中每个scrollview都可以单独滚动。item_rv_header 中的 horizontalscrollview 使用的是自定义的可以添加观察者的
 * horizontalscrollview,用于通知所有观察者一起滑动。
 *
 *
 */
class MainActivity : AppCompatActivity() {


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


        rv.setOnTouchListener { v, event ->
            //当在rv 上touch时,将事件分发给 表头的 scrollview 处理
            headerHorizontalScrollView.onTouchEvent(event)
            //如果删掉下面这行代码,rv的上下滚动效果会失效
            onTouchEvent(event)
        }

//        设置布局管理器
        rv.layoutManager = LinearLayoutManager(this)

        val myAdapter = MyAdapter(this, R.layout.item_rv, headerHorizontalScrollView)

        rv.adapter = myAdapter

//        设置数据
        myAdapter.setData(getData())


    }

    /**
     * 模拟数据
     */
    private fun getData(): List<DataBean> {
        val list = ArrayList<DataBean>()
        for (i in 0..119) {
            list.add(
                DataBean(
                    "name $i",
                    "A_ $i", "B_ $i", "C_ $i", "D_ $i", "E_ $i", "F_ $i", "G_ $i"
                )
            )
        }
        return list
    }

}

MyAdapter.kt 是

package com.lq.excellist

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.HorizontalScrollView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.item_rv.view.*

/**
 *
 *@author : lq
 *@date   : 2020/12/29
 *@desc   :
 *
 */
class MyAdapter(
    private val context: Context,
    private val resource: Int,
    private val headScrollView: CanObserverHorizontalScrollView
) : RecyclerView.Adapter<MyAdapter.MyViewHoder>() {
    //数据集合
    private var mList = ArrayList<DataBean>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHoder {
        return MyViewHoder(LayoutInflater.from(parent.context).inflate(resource, parent, false))
    }

    override fun onBindViewHolder(holder: MyViewHoder, position: Int) {

        val itemView = holder.itemView
        val bean = mList.get(position)

        //赋值
        itemView.tv_name.text = bean.name
        itemView.tv_a.text = bean.data1
        itemView.tv_b.text = bean.data2
        itemView.tv_c.text = bean.data3
        itemView.tv_d.text = bean.data4
        itemView.tv_e.text = bean.data5
        itemView.tv_f.text = bean.data6
        itemView.tv_g.text = bean.data7

        //item 第一个单元格可点击
        itemView.tv_name.setOnClickListener {
            Toast.makeText(context, "守得云开见月明 " + bean.name, Toast.LENGTH_SHORT).show()
        }

        //注意:放开点击事件 会有 左右滑动难以响应的问题
//        itemView.linearLayout.setOnClickListener {
//            Toast.makeText(context,"守得云开见月明 linearLayout",Toast.LENGTH_SHORT).show()
//        }

        //解决 监听 无限增多问题
        if (itemView.tag == null) {
            //将rv item 中的scrollview 添加为 头布局 中 scrollview 的观察者,使每一个item的滚动跟随 头部 的滚动
            val listener = OnScrollChangedListenerImp(itemView.horizontalScrollView)
            headScrollView.addScrollChangedListener(listener)

            itemView.tag = listener
        }


    }

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


    fun setData(list: List<DataBean>) {
        mList.clear()
        mList = list as ArrayList<DataBean>
        notifyDataSetChanged()
    }

    class MyViewHoder(itemView: View) : RecyclerView.ViewHolder(itemView)

    /**
     * 滚动监听  的 实现
     */
    internal class OnScrollChangedListenerImp(var mScrollViewArg: HorizontalScrollView) :
        CanObserverHorizontalScrollView.ScrollViewObserver.OnScrollChangedListener {

        override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) {
            mScrollViewArg.smoothScrollTo(l, t)
        }
    }

}

DataBean.kt 为

package com.lq.excellist

/**
 *
 *@author : lq
 *@date   : 2020/12/29
 *@desc   :
 *
 */
data class DataBean(
    val name:String,
    val data1:String,
    val data2:String,
    val data3:String,
    val data4:String,
    val data5:String,
    val data6:String,
    val data7:String
)

到此,思路与代码全部在上面了,现在还遗留有一个问题,就是rv的item点击事件无法实现,目前能做到的是item的第一个单元格可响应点击,其他的控件如果设置点击监听会与滚动监听冲突。所以这个作为浏览数据用可以,流畅度很好,但是要添加item点击事件是有问题的,注意使用场景。以前写过一个 ListView + HorizontalScrollView 的实现是没这个问题的,不过代码老旧很多地方需要重写优化。








转载请注明:劉清揚的博客 » Android 实现一个类似Excel表格似的效果 2