东莞网页制作免费网站制作,厦门比较有名的设计公司,做网站的人,oa管理系统软件文章目录 前言效果图思路方式一#xff1a;通过xml布局来实现方式二#xff1a;通过ItemDecoration方式来实现 实现步骤1、数据项格式2、左侧列表适配器3、右侧列表适配器4、头部及悬浮头部绘制4.1头部偏移高度为要绘制xml布局的高度--getItemOffsets()4.2 绘制固定头部--onD… 文章目录 前言效果图思路方式一通过xml布局来实现方式二通过ItemDecoration方式来实现 实现步骤1、数据项格式2、左侧列表适配器3、右侧列表适配器4、头部及悬浮头部绘制4.1头部偏移高度为要绘制xml布局的高度--getItemOffsets()4.2 绘制固定头部--onDraw()4.3 绘制悬浮头部-onDrawOver() 总结参考文章 前言 做过的功能一定要总结因为过段时间你就忘记了哈哈哈 最近在做功能分类页功能我看了下这不和我之前做的美团购物车功能差不多么然后就再看了遍之前写的文章并看了下底下的评论不得不说当时实现的方式确实复杂搞得我都有点懵所以就打算优化下当时实现的方式。 效果图
先上张最后实现的效果图吧
思路
有两种方式
方式一通过xml布局来实现
右侧每个标题加下面的分组列表为一个ItemView直接在该ItemView的xml布局内绘制好即可悬浮头部是直接在右侧整个RecyclerView上方和他重合绘制一个固定的头部布局即可
看下图即可明白该如何来实现主要的内容是在xml来直接设置好头部及悬浮头部位置布局 具体的实现可以参考Android 仿京东、拼多多商品分类页这篇文章
优点
悬浮头部和itemView的头部均可点击实现便捷
缺点
每个ItemView的头部样式都一样不可以来动态的更改
方式二通过ItemDecoration方式来实现
固定头部通过onDraw()方法来绘制悬浮头部通过onDrawOver()方法绘制。
这种方法在Android购物车效果实现(RecyclerView悬浮头部实现)中使用过原理差不多但是
当时写的比较复杂主要麻烦在两点
1、数据项格式太复杂之前实现的方式是将数据进行整合后将右侧所有的子项形成一个集合然后用一个RecyclerView来展示这样导致左右联动右侧滑动找左侧父id时很麻烦。
2、绘制悬浮头部和各组的标题头时是在onDraw()和onDrawOver()中来绘制的对于简单的TextView还可以但是对于一些复杂的头部的话绘制就比较复杂尤其是不太擅长的小白那就更别说了。
改善点
1、使用源数据的分组结构左右两侧的数据均使用同一集合右侧列表的ItemView由RecyclerView组成这样实现了右侧的数据分组而不再是将数据分开后再重新分组。这样做可以使左右联动更方便左右联动只需各自将相同位置的ItemView项展示出来即可。
2、组标题和悬浮头部的绘制使用xml加载布局并在onDraw()和onDrawOver()中绘制可以实现复杂头部简单加载
难点
如何使用xml布局来连续绘制到Canvas里在onDrawOver()中如何绘制实现悬浮头部
优点
可以动态给每个ItemView都设置不一样的头部布局切换头部布局和悬浮头部很方便解耦直接替换就好
缺点
悬浮头部和子项头部都不能点击
实现步骤
这里主要介绍下使用ItemDecoration的方式来绘制分组头部布局的实现方法。
1、数据项格式
这里数据使用Android 仿京东、拼多多商品分类页内提供的数据格式如下 2、左侧列表适配器
增加点击事件当点击position位置时让右侧recyclerView的position项滑动到顶部即可
leftAdapter.setLeftClickListener(object : LeftAdapter.LeftClickListener {override fun onItemClick(position: Int) {var layoutManager binding.rightRcy.layoutManager as LinearLayoutManager//将position该位置的itemView移动到第一项layoutManager.scrollToPositionWithOffset(position, 0)}
})3、右侧列表适配器
增加滑动监听实现两个功能
当滑动时实时获取右侧第一个可见项所在的位置position同时将左侧RecyclerView的position项选中当滑动到底部且无法下滑时将左侧RecyclerView的最后一项选中后续可以增加如果左侧选中项位置太低将其滑动到上方来的操作
binding.rightRcy.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 (!recyclerView.canScrollVertically(1)) {leftAdapter.setSelectedNum(dataList.size-1)}//右侧列表可以滑动else {val rightLayoutManager binding.rightRcy.layoutManager as LinearLayoutManagerval position rightLayoutManager.findFirstVisibleItemPosition()leftAdapter.setSelectedNum(position)}
}
})4、头部及悬浮头部绘制
4.1头部偏移高度为要绘制xml布局的高度–getItemOffsets()
override fun getItemOffsets(outRect: Rect,view: View,parent: RecyclerView,state: RecyclerView.State,
) {super.getItemOffsets(outRect, view, parent, state)if (headTitleView null) {headTitleView LayoutInflater.from(parent.context).inflate(R.layout.head_itemview, null, false)val width parent.layoutManager?.width?:0headTitleView?.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED))}headTitleView?.let {//距离ItemView的上方偏移topHeight高度outRect.top it.measuredHeight}
}这里我们首先需要加载headTitleView布局然后获取该布局的高度最后通过outRect.top来偏移该布局的高度后面我们在onDraw()和onDrawOver()方法里分别绘制头部和悬浮头部。
注意 View的宽高属性在measure()方法调用之前都是默认值不反映实际情况。 measure()方法是用来测量View的大小的它会根据父容器传递的限制条件(例如这里的width和height参数)来确定View的实际宽高。 所以在获取View的宽高之前需要先调用measure()方法否则得到的只是默认值不符合实际需要调用它之后才能保证后续的宽高数据是准确的。 这里使用layoutManager来获取recyclerview的宽度因为在此处直接调用parent.width 或parent.measuredWidth方法获取到的宽度均为0 getItemOffsets在RecyclerView完成布局和测量前调用这时measuredWidth还没准备好所以获取到的宽度为0layoutManager可以获取到RecyclerView的宽高限制条件spec知道RecyclerView的宽高限制所以只能通过layoutManager.width获取宽度measuredWidth无效 这里的头部布局如下建议在最外层套一层 ?xml version1.0 encodingutf-8?
FrameLayout xmlns:androidhttp://schemas.android.com/apk/res/androidandroid:layout_widthmatch_parentandroid:layout_heightwrap_contentRelativeLayoutandroid:layout_widthmatch_parentandroid:layout_height50dpandroid:backgrounddrawable/sp_headtitleandroid:layout_gravitycenter_horizontalImageViewandroid:layout_width129dpandroid:layout_heightmatch_parentandroid:layout_alignParentRighttrueandroid:srcmipmap/ic_bgandroid:scaleTypefitXY/TextViewandroid:idid/tvTitleandroid:layout_widthmatch_parentandroid:layout_heightmatch_parentandroid:gravitycenterandroid:text头部标题android:textColorcolor/blackandroid:textSize18sp //RelativeLayout
/FrameLayout如果按如下方式写否则会出现下面的情况 ?xml version1.0 encodingutf-8?
RelativeLayout xmlns:androidhttp://schemas.android.com/apk/res/androidandroid:layout_widthmatch_parentandroid:layout_height50dpandroid:layout_gravitycenter_horizontalandroid:backgrounddrawable/sp_headtitleImageViewandroid:layout_width129dpandroid:layout_heightmatch_parentandroid:layout_alignParentRighttrueandroid:scaleTypefitXYandroid:srcmipmap/ic_bg /TextViewandroid:idid/tvTitleandroid:layout_widthmatch_parentandroid:layout_heightmatch_parentandroid:gravitycenterandroid:text头部标题android:textColorcolor/blackandroid:textSize18sp /
/RelativeLayout 宽高设置 val width parent.layoutManager?.width?:0
headTitleView?.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
)宽度 因为我们头部布局的父容器为match_parent且我们想绘制的宽度为占满右侧RecyclerView的宽度所以这里View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)用View.MeasureSpec.EXACTLY代表精确模式将其设定为我们获取到的RecyclerView的宽度即可。 其实使用View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.AT_MOST)也可以代表子View宽度不确定但是最大为我们测量的RecyclerView的width即可。 高度 因为我们加载headTitleView后需要通过measure()方法测量后才可用所以此时我们并不知道它的具体高度所以不能用EXACTLY或AT_MOST模式所以使用UNSPECIFIED代表父容器不对子View有限制子View要多大给多大
4.2 绘制固定头部–onDraw()
/*** 绘制头部*/
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {super.onDraw(c, parent, state)val childCount parent.childCountfor (i in 0 until childCount) {val child parent.getChildAt(i)val bottom child.topheadTitleView?.let {val top bottom - it.measuredHeightval itemView parent.getChildAt(i)val position parent.getChildAdapterPosition(itemView)//获取该位置的标题名称val groupTitleName titleDataList[position].toUpperCase()//设置标题内容it.findViewByIdTextView(R.id.tvTitle).text groupTitleName// 保存 Canvas 的状态c.save()// 平移 Canvas使 View 绘制在正确位置c.translate(0f, top.toFloat())it.layout(0, top, parent.measuredWidth, bottom)it.draw(c)c.restore()}}
}具体的头部绘制的位置可参考前两篇文章Android购物车效果实现(RecyclerView悬浮头部实现)
自定义ItemDecoration分割线的高度、颜色、偏移看完这个你就懂了
这里主要讲下注意事项 设置title的名字要在draw()方法之前不然你都绘制了还在那设置名字没有意义 因为右侧每个ItemView都是一组数据该ItemView布局由一个RecyclerView构成所以需要给每个ItemView都绘制头部布局getItemOffsets() 是针对每一个 ItemView而 onDraw()方法却是针对 RecyclerView 本身所以在onDraw()方法中需要遍历屏幕上可见的ItemView来循环绘制。 这里在绘制前分别调用了translate()、layout()方法 刚开始是直接调用 headTitleView.draw(canvas)但发现并没有绘制出来这是因为我们没有将Canvas平移到指定位置直接绘制的话头部View会默认绘制在Canvas的(0,0)坐标点而我们期望它绘制在ItemView的顶部适当位置。通过translate平移和layout重新布局,可以重用同一个头部View来绘制不同Item的头部避免重复创建View。 这里在绘制前和绘制后分别调用了c.save()和c.restore()方法 保存Canvas状态可以防止绘制操作对Canvas产生影响绘制完成后恢复状态可以保证不污染Canvas。
4.3 绘制悬浮头部-onDrawOver() override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {super.onDrawOver(c, parent, state)val itemView parent.getChildAt(0)val position parent.getChildAdapterPosition(itemView)var titleName titleDataList[position].toUpperCase()val left 0val right parent.measuredWidth//默认的指定高度var height headTitleView?.measuredHeight ?: 0//当前ItemView的底部var bottom itemView.bottomif (bottomheight){heightbottom}headTitleView?.let {it.findViewByIdTextView(R.id.tvTitle).text titleNamec.save()// 平移 Canvas使 View 绘制在正确位置c.translate(0f, (height-it.measuredHeight).toFloat())it.layout(left, height-it.measuredHeight, right, height)it.draw(c)c.restore()}}通过不断改变绘制的顶部和底部位置来实现被顶出的动画效果这里不再详细阐述具体可看Android购物车效果实现(RecyclerView悬浮头部实现)的第4小节 具体的绘制和onDraw()方法中的绘制流程一致。
总结
其实主要还是ItemDecoration相关的内容相比较Android购物车效果实现(RecyclerView悬浮头部实现)的内容不同点在于优化了数据项的分组使用和头部绘制使用xml两个地方所以说做功能前还是要先考虑考虑数据该如何使用不然会增加很多工作量。
如果本文对你有帮助请别忘记三连如果有不恰当的地方也请提出来下篇文章见。
参考文章
MeasureSpec讲解
DividerItemDecoration.java
Android 仿京东、拼多多商品分类页