前言:
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
效果图
参考:
1、Android内存优化(使用SparseArray和ArrayMap代替HashMap)
2、为RecyclerView打造通用Adapter 让RecyclerView更加好用
3、实现RecyclerView的item点击事件的内部监听器