自定义View学习三(MRecyclerView)

MRecyclerView是一个简化RecyclerView使用的库,它提供了动态添加Header和Footer、内置通用Adapter和ViewHolder、加载更多监听等功能。本文详细解析了MRecyclerView的实现,包括MViewHolder、MRecyclerViewAdapter的使用,以及如何在XML和代码中配置。此外,还介绍了其对外公开的接口和自定义属性。测试部分展示了只使用Header的效果,并给出了实际项目中的应用示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言:

RecyclerView在项目中的使用之频繁不用我再去过多的强调了。每一次我们使用RecyclerView的时候总要为RecyclerView写适配器,写 ViewHolder,并为Adapter转换数据等。如果我们需要为RecyclerView再添加Header,Footer,加载更多监听等,我们又不免要再写近百行代码。
可不可以不做这些?当然可以!下面请看MRecyclerView


MRecyclerView功能

已实现

  • 1、动态的添加Header,Footer;

  • 2、内置通用Adapter,不需要我们再写Adapter;

  • 3、内置通用ViewHolder,不需要再写ViewHolder;

  • 4、加载更多监听(支持GridLayoutManager和LinearLayoutManager,支持横向和纵向)

  • 5、Header,Item,Footer点击事件监听

将要添加的功能

  • 1、提供默认的Item进入动画

  • 2、丰富加载更多动画


MRecyclerView解析

内置通用Adapter和ViewHolder实现

MViewHolder
    //ViewHolder作用:复用已经加载过的item,减少绑定布局时间
    /**
     * 通用ViewHolder功能:将不同布局里面的view数量以及类型泛化,打造一个更为通用的ViewHolder
     */
inner class MViewHolder(val itemsView: View, val listener: MOnClickListener?) : RecyclerView.ViewHolder(itemsView) {
        private var views = SparseArray<View>()
        //通过在layout布局里的Id获取控件

        init {
            if (listener != null) {
                itemsView.setOnClickListener {
                    listener.onClick(itemsView)
                    //如果listener是ItemListener,由于我们对于ItemListener通常要多使用一项position,所以在此设定
                    if (listener is OnItemClickedListener) {
                        //如果有header,我们需要将位置减去一之后再返回
                        if (hasHeader) {
                            listener.onClick(adapterPosition - 1)
                        } else {
                            listener.onClick(adapterPosition)
                        }

                    }
                }
            }
        }

        fun getView(viewId: Int): View? {
            var view = views.get(viewId)
            if (view == null) {
                view = itemsView.findViewById(viewId)
                views.put(viewId, view)
            }
            return view
        }
    }
MRecyclerViewAdapter

    //数据显示适配器
    private inner class MRecyclerViewAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

        /**根据不同的ViewType,为MViewHolder传入不同的layout,从而构造不同类型的holder
         *现在没有对异常进行捕获
         *
         * 对于点击事件监听的设置:Header,Footer的点击事件监听在onCreateViewHolder里面设置
         * 但是NormalType的点击事件在onBindViewHolder里面设置,这样安排的主要原因是我们点击Item时,想要得到的回执数据主要是position
         */
        override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
            when (viewType) {
                HEADER -> {
                    return buildHolder(parent, headerRes, headerClickedListener)
                }
                FOOTER -> {
                    if (footerRes != NO_LAYOUT_VALUE)
                        return buildHolder(parent, footerRes, footerClickedListener)
                    else //如果存在Footer类型,但却没有Footer布局文件的话,那么我们使用这个holder的构造器
                        return buildHolder(getDefaultLoadMoreView(),footerClickedListener)
                }
            }
            return buildHolder(parent, itemRes, itemClickedListener)
        }




        override fun getItemCount(): Int {
            var ret = dataSize
            if (hasHeader) ret++
            //如果dataSize为0,且设置了加载更多监听,那么不显示footer
            if (dataSize != 0 || loadMoreListener != null){
                if (hasFooter) ret++
            }
            return ret
        }

        //数据绑定,按照resId为View设定数值,我们在此要向外提供设定数值方法,因为像ImageView的图片加载,
        // 我们往往是通过Glide等第三方图片加载框架
        override fun onBindViewHolder(holder: ViewHolder?, position: Int) {

            //如果没有提供bindServer,那么抛出异常
            if (bindDataServer == null) {
                throw NoBindDataServerException("You should provide a server binding data to viewHolder!")
            } else {
                holder as MViewHolder
                /**
                 * 当存在header的情况下,position包含了header,所以再根据position去取值得时候就会发生数组越界的问题
                 * 这个时候要将position的值减去1(对于header的position,它的值为-1,这一点我们需要注意)
                 */

                var positionModify = position
                if (hasHeader) positionModify = position - 1
                //这个应该需要修改,因为我们已经把数据提交到MRecyclerView内部了,这样写的话我们就使用不到我们提供的数据了。
                bindDataServer?.OnBindData(holder, positionModify, getItemViewType(position))
            }

        }

        //因为可能有Footer和Header,所以需要重写这个函数
        override fun getItemViewType(position: Int): Int {
            if (position == 0 && hasHeader) return HEADER
            if (position == itemCount - 1 && hasFooter) return FOOTER
            return NORMAL
        }

    }

加载更多监听实现

对于设置onLoadMoreListener处理
        //如果设置有加载更多的监听,那么在此注册加载更多监听
        if (loadMoreListener != null) {
            this.addOnScrollListener(object : RecyclerView.OnScrollListener() {
                override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
                    super.onScrollStateChanged(recyclerView, newState)
                }

                override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
                    super.onScrolled(recyclerView, dx, dy)
                    if (isBottom()) {
                        loadMoreListener!!.loadMore()
                    }
                }
            })
        }
加载更多监听的辅助函数:isBottom() 和 getOrientation()
    /**
     * 包含了纵向和横向检查
     * computeVerticalScrollExtent()是当前屏幕显示的区域高度
     * computeVerticalScrollOffset() 是当前屏幕之前滑过的距离
     * computeVerticalScrollRange()是整个View控件的高度。
     */
    private fun isBottom(): Boolean {
        /**
         * 数据到达底部分为两种情况:横向到达底部和纵向到达底部
         */
        if (getOrientation(layoutManager) == LinearLayoutManager.VERTICAL) {
            return computeVerticalScrollExtent() + computeVerticalScrollOffset() >= computeVerticalScrollRange()
        } else {
            return computeHorizontalScrollExtent() + computeHorizontalScrollOffset() >= computeHorizontalScrollRange()
        }
    }

    /**
     * 利用反射得到布局的orientation
     */
    private fun getOrientation(layoutManager: RecyclerView.LayoutManager): Int {

        var mOrientation = 0
        val clazz: Class<*>?
        try {
            //GridlayoutManager也是继承子LinearLayoutManager
            clazz = Class.forName("android.support.v7.widget.LinearLayoutManager")
            val field = clazz!!.getDeclaredField("mOrientation")
            field.isAccessible = true
            mOrientation = field.getInt(layoutManager)
        } catch (e: ClassNotFoundException) {
            e.printStackTrace()
        } catch (e: IllegalAccessException) {
            e.printStackTrace()
        } catch (e: NoSuchFieldException) {
            e.printStackTrace()
        }
        return mOrientation
    }

对外公开的接口

    interface MOnClickListener {  //定义这个主要是为了使用泛型,不然的话在我们需要添加很多额外的参数
        fun onClick(view: View?)
    }

    //RecyclerView里面Item点击事件监听接口
    interface OnItemClickedListener : MOnClickListener {
        fun onClick(position: Int)
    }

    //item长按事件监听
    interface OnItemLongClickedListener {
        fun onLongClicked(position: Int)
    }

    //header点击的监听(不知道能不能实现内部具体的监听)
    interface OnHeaderClickListener : MOnClickListener {

    }

    //footer点击事件监听
    interface OnFooterClickedListener : MOnClickListener {

    }

    //为数据的绑定提供外部接口,从而得到一个通用的RecyclerView
    interface BindDataService {
        /**
         * 这个函数的有两个最主要的功能
         * 一:在MRecyclerView之外为Item里面的子View设定数值
         * 二:在MRecyclerView之外为Item里面的子View设定点击监听等
         */
        fun OnBindData(holder: MViewHolder?, position: Int, type: Int)
    }

    interface OnLoadMoreListener {
        fun loadMore()
    }

自定义属性

    <declare-styleable name="MRecyclerView">

        <!--item的布局文件-->
        <attr name="itemLayout" format="reference"/>
        <!--header的布局文件-->
        <attr name="headerLayout" format="reference"/>
        <!--footer的布局文件-->
        <attr name="footerLayout" format="reference"/>

    </declare-styleable>

MRecyclerView的使用

XML文件中的使用

    <com.example.xiaojun.kotlin_try.mlibrary.MRecyclerView
        android:id="@+id/songLocalRecyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:headerLayout="@layout/song_list_play"
        app:itemLayout="@layout/song_show_item"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
    </com.example.xiaojun.kotlin_try.mlibrary.MRecyclerView>
header : song_list_play.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/playAll"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="48dp">

    <ImageView
        android:id="@+id/songListPlayIcon"
        android:layout_marginLeft="@dimen/activity_horizontal_margin"
        android:layout_centerVertical="true"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:src="@drawable/song_list_play"/>

    <TextView
        android:id="@+id/songListPlayTip"
        android:layout_centerVertical="true"
        android:layout_marginLeft="@dimen/activity_horizontal_margin"
        android:layout_toRightOf="@id/songListPlayIcon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/black"
        android:textSize="17sp"
        android:text="@string/playAll"/>

    <TextView
        android:id="@+id/songListCapacity"
        android:layout_centerVertical="true"
        android:layout_toRightOf="@id/songListPlayTip"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="(共45首)"/>


    <LinearLayout
        android:id="@+id/mutiChoose"
        android:orientation="horizontal"
        android:layout_centerVertical="true"
        android:layout_alignParentRight="true"
        android:layout_marginRight="@dimen/activity_horizontal_margin"
        android:gravity="center_vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <ImageView
            android:layout_width="22dp"
            android:layout_height="22dp"
            android:src="@drawable/menu_muti_choose"/>

        <TextView

            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/black"
            android:text="@string/muti_choose"/>

    </LinearLayout>

    <View
        android:layout_alignParentBottom="true"
        android:layout_width="match_parent"
        android:layout_height="0.1px"
        android:background="@color/grey"/>
</RelativeLayout>
item : song_show_item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="48dp">

    <TextView
        android:id="@+id/songOrder"
        android:layout_width="35dp"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_centerVertical="true"
        android:visibility="visible"
        android:text="1"/>

    <TextView
        android:id="@+id/songTitle"
        android:layout_toRightOf="@id/songOrder"
        android:layout_marginTop="8dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:lines="1"
        android:ellipsize="end"
        android:textSize="16sp"
        android:textColor="@color/black"
        android:text="Need you know"/>

    <TextView
        android:id="@+id/songDetail"
        android:layout_toRightOf="@id/songOrder"
        android:layout_below="@id/songTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:lines="1"
        android:ellipsize="end"
        android:textSize="10sp"
        android:text="Lady Antebelum-iTunes Session"/>


    <ImageView
        android:id="@+id/songOperatorMore"
        android:layout_alignParentRight="true"
        android:layout_marginRight="@dimen/activity_horizontal_margin"
        android:layout_centerVertical="true"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:src="@drawable/more"/>

</RelativeLayout>

代码中的使用

说明:

下面的代码是我直接从工程里面截取出来的,看着可能有些突兀,所以在此我解释一下。

1、initView()的工作是对RecyclerView做一些基本设置(和我们自己定义的内容无关):

1、设置LayoutManager
2、解决RecyclerView嵌套ScrollView的滑动冲突

2、onSuccess()的工作是MRecyclerView的具体使用,下面的代码的工作如下:

1、设置想要显示数据的条目数
2、设置View和Data的具体绑定规则
3、Header与Item的点击事件监听
4、RecyclerView就绪,显示 

initView()
    override fun initView() {
        super.initView()
        recyclerView = mView!!.findViewById(R.id.songLocalRecyclerView)

        val layoutManager =  LinearLayoutManager(activity, LinearLayout.VERTICAL,false)
        layoutManager.isSmoothScrollbarEnabled = true
        layoutManager.isAutoMeasureEnabled = true
        recyclerView!!.layoutManager = layoutManager
        recyclerView!!.addItemDecoration(RecyclerViewItemSpace(2,20,0))
        recyclerView!!.setHasFixedSize(true)
        recyclerView!!.isNestedScrollingEnabled = false
    }

onSuccess()
    override fun onSuccess() {
        super.onSuccess()
        songList = mPresenter.submitData()

        recyclerView?.setDataSize(songList.size)
        recyclerView?.setBindDataServer(object :MRecyclerView.BindDataService{  //设置具体的绑定
            @SuppressLint("SetTextI18n")
            override fun OnBindData(holder: MRecyclerView.MViewHolder?, position: Int, type:Int) {
                when (type){
                    MRecyclerView.NORMAL->{
                        (holder?.getView(R.id.songOrder) as TextView).text = position.toString()
                        (holder.getView(R.id.songTitle) as TextView).text = songList[position].title
                        var detail = ""
                        detail = songList[position].artist+" - "+ songList[position].album
                        (holder.getView(R.id.songDetail) as TextView).text = detail
                    }
                    MRecyclerView.HEADER->{
                        (holder?.getView(R.id.songListCapacity) as TextView).text = "共("+songList.size.toString()+")首"
                    }

                    MRecyclerView.FOOTER->{

                    }

                }
            }
        })
        //header 点击监听
        recyclerView?.setOnHeaderClickedListener(object :MRecyclerView.OnHeaderClickListener{
            override fun onClick(view: View?) {
                Log.e("MusicLocal","HeaderClicked!"+view?.id)
                if (view == null) return
                when (view.id){
                    R.id.mutiChoose->{
                        Toast.makeText(activity,"you clicked mutiChoose",Toast.LENGTH_SHORT).show()
                    }
                    R.id.playAll->{
                        //播放全部
                    }
                }
            }
        })
        recyclerView?.setOnItemClickedListener(object :MRecyclerView.OnItemClickedListener{
            override fun onClick(position: Int,view: View?) {
                val event = PlayListChangedEvent(songList, position)
                EventBus.getDefault().post(event)
                val intent = Intent(activity, MusicPlayActivity::class.java)
                startActivity(intent)
            }
        })
        recyclerView?.show()
        Log.e("localMusic","success"+ songList.size)
    }

测试:

目前只是使用了Header,并没有使用Footer

效果图

这里写图片描述


链接:MRecyclerView源代


参考:

1、Android内存优化(使用SparseArray和ArrayMap代替HashMap)

2、为RecyclerView打造通用Adapter 让RecyclerView更加好用

3、实现RecyclerView的item点击事件的内部监听器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值