Android-自定义View-实战分析

447 阅读4分钟
  每一次的积累都可以让你有很大的成长,随着年龄的成长,社会的压力,家里的压力,工作的压力,以及自己
  给自已的驱动,都会让自己越来越难受,但是挺住,加油兄弟们,慢慢的,都会好的,真的,都会好的。

之前我们讲解了自定义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的方式,来实现网格布局。大家可以想象我为什么要用泛型,以及为什么要继承,以及为什么要缓存,这个地方还是值得学习一下的。