每一次的积累都可以让你有很大的成长,随着年龄的成长,社会的压力,家里的压力,工作的压力,以及自己
给自已的驱动,都会让自己越来越难受,但是挺住,加油兄弟们,慢慢的,都会好的,真的,都会好的。
之前我们讲解了自定义View需要的一些基础知识,接下来我会用我写的自定义View(使用在我们的项目中了),来分析,顺便串一下我们之前学的知识。这里我就没有写源码分析了,比如LinearLayout,原理都是一样的,大家应该也是可以看懂的,我们只需要关注onMeasure和onLayout就行。
这个自定义View可以实现GridLayout的功能,把指定的宽度进行等分,实现网格布局。但是我这里是没有考虑Margin的。
open class GridFlowLayout : ViewGroup {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
companion object {
private const val DEFAULT_COUNT_EACH_LINE = 3
}
var itemCountEachLine = DEFAULT_COUNT_EACH_LINE
var itemHorizontalDistance = 0 // 网格布局,横向网格的每一个View之间的间距
var itemVerticalDistance = 0 // 网格布局,竖向网格的每一个View之间的间距
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 根据MeasureSpec获取尺寸和测量模式
val sizeWidth = MeasureSpec.getSize(widthMeasureSpec)
val sizeHeight = MeasureSpec.getSize(heightMeasureSpec)
val modeHeight = MeasureSpec.getMode(heightMeasureSpec)
var childView: View
// 既然要实现等分,计算出单行一个网格需要占据的宽度。
val eachChildWidth = (sizeWidth - paddingLeft - paddingRight - (itemCountEachLine - 1) * itemHorizontalDistance) / itemCountEachLine
var totalHeight = 0 // 自view累计的总高度,就是当前ViewGroup的高度
var itemLineIndex = 0 // 每一行View的索引
var itemLineMaxHeight = 0 // 一行中View中,最高的View的高度,那么这一行的高度,以最高的View的高度为准
for (i in 0 until childCount) {
++itemLineIndex
childView = getChildAt(i)
val lp = childView.layoutParams
// 首次测量,就是为了得到高度,此时的宽度可能就是不准确的,因为我们要一行的View均分一行的 // 宽度,实现网格布局,更具父View的约束,可以算出当前View的预期高度
measureChild(childView, widthMeasureSpec, heightMeasureSpec)
// 均分,改变child view的宽度,实现网格布局,那么在确定宽度的父View中,子View的测量模 // 式就是exactly
childView.layoutParams = childView.layoutParams.apply {
width = eachChildWidth
}
// 第二次测量是为了,用新的约束,让子View自己重新测量一次
// 你虽然手动改变了布局参数,为了让当前子View生效,需要重新用约束测量一下
// 获取宽度的MeasureSpec
val childWidthMeasureSpec = if (lp.width == LayoutParams.MATCH_PARENT) {
val width = Math.max(0, measuredWidth - paddingLeft - paddingRight)
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
} else {
getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, lp.width)
}
// 获取高度的MeasureSpec
val childHeightMeasureSpec = if (lp.height == FrameLayout.LayoutParams.MATCH_PARENT) {
val height = Math.max(0, measuredHeight - paddingTop - paddingBottom)
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
} else {
getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, lp.height)
}
// 让子View重新测量一下,有人问可能为什么,当前View可能是一个ViewGroup,改变了其布局,
// 内部的子View会受到影响,需要把这个View整体自己重新测量一下
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
if (itemLineIndex == itemCountEachLine) {
itemLineMaxHeight = Math.max(childView.measuredHeight, itemLineMaxHeight)
// 需要统计高度
totalHeight += itemLineMaxHeight
itemLineMaxHeight = 0
itemLineIndex = 0
} else {
// 计算最高高度
itemLineMaxHeight = Math.max(childView.measuredHeight, itemLineMaxHeight)
}
}
// 如果没有换行,需要补加最后一个高度
if (itemLineIndex < itemCountEachLine) {
totalHeight += itemLineMaxHeight
}
val lineCount = if (childCount % itemCountEachLine == 0) childCount / itemCountEachLine else childCount / itemCountEachLine + 1
val measHeight = if (modeHeight == MeasureSpec.EXACTLY) sizeHeight else totalHeight + paddingTop + paddingBottom + (lineCount - 1) * itemVerticalDistance
// 把计算的累计高度设置进去
setMeasuredDimension(sizeWidth, measHeight)
}
// 具体的放置子View的过程,不用过多赘述,很好理解。
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var left = paddingLeft
var top = paddingTop
var maxCountEachLine = 0
var childView: View
for (i in 0 until childCount) {
childView = getChildAt(i)
++maxCountEachLine
childView.layout(left, top, left + childView.measuredWidth, top + childView.measuredHeight)
left += childView.width + itemHorizontalDistance
if (maxCountEachLine >= itemCountEachLine) {
// 当前行数量足够了,需要换行了
left = paddingLeft
maxCountEachLine = 0
top += childView.measuredHeight + itemVerticalDistance
}
}
}
}
小结:这里,我们需要关注的就是MeasureSpec的生成,以及ViewGroup的子View的测量,最终确定ViewGroup的尺寸,中间就经历了几次测量,要理解其内涵,为什么要多次测量,LinearLayout也是如此。
同时,基于这个组件,为了外界使用方便,我进行了二次封装,模拟RecyclerView,大家有兴趣也可以看一下。
abstract class CacheContainer<T, R : CacheContainer.BaseItemViewHolder<T>> : GridFlowLayout {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
// 当前展示列表的数据holder(和ViewGroup对应的子View顺序是一样的)
private val showItemViewHolderList by lazy { mutableListOf<R>() }
// 缓存的数据holder
private val cacheItemViewHolderList by lazy { mutableListOf<R>() }
// 缓存数据,外部可直接刷新
private var dataList: MutableList<T>? = null
fun bindData(newDataList: MutableList<T>?) {
bindData(newDataList, null)
}
/**
* 填充数据
* 外部只需要调用,不用关心bindData内部的逻辑
*
* 支持局部控件加载
*/
fun bindData(newDataList: MutableList<T>?, payloads: MutableList<Any>? = null) {
// 判断数据源
if (newDataList == null || newDataList.isEmpty()) {
// 这个时候,把显示的View缓存起来,同时清空显示的View
for (itemViewHolder in showItemViewHolderList) {
cacheItemViewHolderList.add(itemViewHolder)
}
showItemViewHolderList.clear()
removeAllViews()
return
}
// 填充数据Holder
inflateAllViews(showItemViewHolderList, newDataList)
// 最后一层校验,理论上大小是一样的
if (showItemViewHolderList.size == newDataList.size) {
// 绑定数据源
dataList = newDataList
// 逐步添加View并且渲染数据
for ((index, itemViewHolder) in showItemViewHolderList.withIndex()) {
itemViewHolder.update(index, newDataList[index], payloads)
}
}
}
/**
* 数据全部刷新
*/
fun notifyDataSetChanged(payloads: MutableList<Any>? = null) {
bindData(dataList, payloads)
}
/**
* 实现item数据的刷新
*/
fun notifyItemChanged(position: Int, payloads: MutableList<Any>? = null) {
dataList?.let { list ->
if (position < 0 || position >= showItemViewHolderList.size || position >= list.size) return
showItemViewHolderList[position].update(position, list[position], payloads)
}
}
/**
* 准备好好ViewHolder
*/
private fun inflateAllViews(showDataList: MutableList<R>, newDataList: MutableList<T>) {
var diffCount = showDataList.size - newDataList.size
var itemViewHolder: R
if (diffCount > 0) {
// 当前数量需要减少
for (i in 0 until diffCount) {
itemViewHolder = showItemViewHolderList.removeAt(showItemViewHolderList.size - 1)
removeViewAt(childCount - 1)
cacheItemViewHolderList.add(itemViewHolder)
}
} else if (diffCount < 0) {
// 表明当前数量不够,需要增加
diffCount = -diffCount
// 先查找缓存
if (diffCount <= cacheItemViewHolderList.size) {
// 缓存数量足够,就先取缓存
for (i in 0 until diffCount) {
itemViewHolder = cacheItemViewHolderList.removeAt(cacheItemViewHolderList.size - 1)
addView(itemViewHolder.itemView)
showItemViewHolderList.add(itemViewHolder)
}
} else {
// 缓存数量不足,先取缓存,后面再直接创建
for (cacheItemViewHolder in cacheItemViewHolderList) {
addView(cacheItemViewHolder.itemView)
showItemViewHolderList.add(cacheItemViewHolder)
}
// 剩下的创建出来
for (i in 0 until (diffCount - cacheItemViewHolderList.size)) {
itemViewHolder = createItemViewHolder(LayoutInflater.from(context).inflate(getItemLayoutId(), this, false))
addView(itemViewHolder.itemView)
showItemViewHolderList.add(itemViewHolder)
}
// 缓存取出之后,缓存列表先清空
cacheItemViewHolderList.clear()
}
}
requestLayout()
}
/**
* 由外部继承
*/
open class BaseItemViewHolder<T>(val itemView: View) {
open fun update(position: Int, item: T?) {}
open fun update(position: Int, item: T?, payloads: MutableList<Any>?) {
update(position, item)
}
}
/**
* 每一个子View的layout id
*/
abstract fun getItemLayoutId(): Int
/**
* 获取holder,也是有具体外部创建
*/
abstract fun createItemViewHolder(itemView: View): R
}
代码就不过多讲解,就通过泛型,类似RecyclerView的方式,来实现网格布局。大家可以想象我为什么要用泛型,以及为什么要继承,以及为什么要缓存,这个地方还是值得学习一下的。