自定义ViewGroup实现HorizontalScrollView的效果

自定义ViewGroup主要有三个步骤:
1. 实现onLayout进行布局
2. 实现onMeasure测量
3. 支持margin
4. 实现弹性滑动
5. 解决滑动冲突

class CustomViewByViewGroup(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int):
        ViewGroup(context, attrs, defStyleAttr, defStyleRes){

    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int):this(context, attrs, defStyleAttr, 0)
    constructor(context: Context, attrs: AttributeSet):this(context, attrs, 0, 0)
    constructor(context: Context): this(context, null, 0, 0)

    var lastX: Float = 0f
    var lastY: Float = 0f

    /**===========================================================
     * 1. 继承ViewGroup必须实现onLayout方法
     *============================================================*/
    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        var childLeft = paddingLeft //需要处理padding
        for(i in 0 until childCount){
            val childView = getChildAt(i)
            if(childView.visibility != View.GONE){
                val childWidth = childView.measuredWidth

                //2. 额外处理margin属性
                val childLayoutParams = childView.layoutParams as MarginLayoutParams
                childLeft += childLayoutParams.leftMargin
                childView.layout(childLeft,
                        childLayoutParams.topMargin + paddingTop,
                        childLeft + childWidth,
                        childLayoutParams.topMargin  + paddingTop + childView.measuredHeight) //一定要根据margin处理好四个顶点坐标
                childLeft += childWidth + childLayoutParams.rightMargin
            }
        }
    }

    /**=====================================================================
     * 2. 定义ViewGroup的布局测量过程(也需要额外处理margin)
     *=======================================================================*/
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
        val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
        val heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)

        var measureWidth = 0
        var measureHeight = 0

        //2. 需要测量所有子View!
        measureChildren(widthMeasureSpec, heightMeasureSpec)

        //3. 本身宽高的模式均为wrap_content, 需要根据子View来获得
        if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            for(i in 0 until childCount){
                val childView = getChildAt(i)
                measureWidth += childView.measuredWidth //测量出总宽度

                //6. 处理marigin
                val childLayoutParams = childView.layoutParams as MarginLayoutParams
                measureWidth += childLayoutParams.leftMargin + childLayoutParams.rightMargin

                val totalCurChildHeight = childView.measuredHeight + childLayoutParams.topMargin + childLayoutParams.bottomMargin
                if(totalCurChildHeight > measureHeight){
                    measureHeight = totalCurChildHeight //选取子View中高度最大的
                }
            }
            //7. 处理padding
            measureWidth += paddingLeft + paddingRight
            measureHeight += paddingTop + paddingBottom
            setMeasuredDimension(measureWidth, measureHeight)
        }
        //4. 仅有高度是wrap_content
        else if(heightSpecMode == MeasureSpec.AT_MOST){
            //获取所有子View最大的高度,宽度直接用给定的尺寸
            for(i in 0 until childCount){
                val childView = getChildAt(i)

                // 处理高度(wrap_content)上marigin
                val childLayoutParams = childView.layoutParams as MarginLayoutParams

                val totalCurChildHeight = childView.measuredHeight + childLayoutParams.topMargin + childLayoutParams.bottomMargin
                if(totalCurChildHeight > measureHeight){
                    measureHeight = totalCurChildHeight //选取子View中高度最大的
                }
            }
            measureHeight += paddingTop + paddingBottom //处理高度的padding
            setMeasuredDimension(widthSpecSize, measureHeight)
        }
        //5. 仅有宽度是wrap_content
        else if(widthSpecMode == MeasureSpec.AT_MOST){
            for(i in 0 until childCount){
                val childView = getChildAt(i)
                measureWidth += childView.measuredWidth

                //  处理宽度(wrap_content)上marigin
                val childLayoutParams = childView.layoutParams as MarginLayoutParams
                measureWidth += childLayoutParams.leftMargin + childLayoutParams.rightMargin
            }
            measureWidth += paddingLeft + paddingRight            //  处理宽度的padding
            setMeasuredDimension(measureWidth, heightSpecSize)//高度直接用给定的尺寸
        }
    }

    /**===============================================================
     * 1. 要支持Margin功能,必须要重写方法,并实现自己LayoutParams
     *=================================================================*/
    override fun generateDefaultLayoutParams() = MyLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
    override fun generateLayoutParams(attrs: AttributeSet) = MyLayoutParams(context, attrs)
    override fun generateLayoutParams(p: LayoutParams): MyLayoutParams{
        when(p){
            is LayoutParams -> return MyLayoutParams(p)
            is MarginLayoutParams ->  return MyLayoutParams(p)
            else -> return MyLayoutParams(p)
        }
    }

    open class MyLayoutParams : MarginLayoutParams {
        constructor(c: Context, attrs: AttributeSet) : super(c, attrs)
        constructor(width: Int, height: Int) : super(width, height)
        constructor(p: ViewGroup.LayoutParams) : super(p) {}
        constructor(source: ViewGroup.MarginLayoutParams) : super(source)
    }

    /**==============================================================
     * 3. 滑动所需的scroller
     *==============================================================*/

    val mScroller: Scroller
    init {
        mScroller = Scroller(context)
    }
    override fun computeScroll() {
        if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.currX, mScroller.currY)
            invalidate()
        }
    }
    fun smoothScrollTo(destX: Int, destY: Int){
        mScroller.startScroll(scrollX, scrollY, destX - scrollX, destY - scrollY, 500)
        invalidate()
    }
    var curChildIndex = 0

    /**==============================================================
     * 4. 左右滑动+页面切换(类似HorizontalScrollView)
     *==============================================================*/
    override fun onTouchEvent(event: MotionEvent): Boolean {
        val curX = event.x
        val curY = event.y

        when(event.action){
        //1. View跟随手指滑动
            MotionEvent.ACTION_MOVE -> {
                val deltaX = curX - lastX
                scrollBy(-deltaX.toInt(), 0)
            }
        //2. 滑动一定距离后进行页面弹性切换
            MotionEvent.ACTION_UP -> {
                //3. 滑动的距离需要为 scrollx - 当前View左边所有View的总宽度 = distance
                var totalLeftChildrenWidth = 0
                for(i in 0 until curChildIndex){
                    // 计算出加上子View测量宽+左右margin=总child长度
                    val childView = getChildAt(i)
                    val childLayoutParams = childView.layoutParams as MarginLayoutParams
                    totalLeftChildrenWidth += childView.measuredWidth + childLayoutParams.leftMargin + childLayoutParams.rightMargin
                }
                val distance = scrollX - totalLeftChildrenWidth

                //4. 比较距离和当前View的宽度,来判断是否切换页面
                val curChildView = getChildAt(curChildIndex)
                val childLayoutParams = curChildView.layoutParams as MarginLayoutParams
                val curChildWidth = curChildView.measuredWidth + childLayoutParams.leftMargin + childLayoutParams.rightMargin
                if(Math.abs(distance) > curChildWidth / 3){
                    if(distance < 0 && curChildIndex > 0){ //向左滑动(最左面页面不会向左滑动)
                        curChildIndex--
                    }else if(distance > 0 && (curChildIndex < childCount - 1)){ //向右滑动
                        curChildIndex++
                    }
                }

                //5. 滑动到当前Index表示的页面(Scroller弹性滑动)
                var totalLeftChildWidth = 0
                for(i in 0 until curChildIndex){
                    // 计算除了当前View的左侧所有View的总宽度
                    val childView = getChildAt(i)
                    val childLayoutParams = childView.layoutParams as MarginLayoutParams
                    totalLeftChildWidth += childView.measuredWidth + childLayoutParams.leftMargin + childLayoutParams.rightMargin
                }
                smoothScrollTo(totalLeftChildWidth,0)
            }
            else -> {
            }
        }
        lastX = curX
        lastY = curY
        return true
    }

    /**==============================================================
     * 5. 处理滑动冲突(水平方向的就直接拦截,竖直方向的给子View)
     *==============================================================*/
    var downX = 0f
    var downY = 0f
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        var isIntercept = super.onInterceptTouchEvent(ev)
        when(ev.action){
            MotionEvent.ACTION_DOWN ->{
                downX = ev.x
                downY = ev.y
                isIntercept = false
            }
            MotionEvent.ACTION_MOVE ->{
                val deltaX = ev.x - downX
                val deltaY = ev.y - downY
                isIntercept = (Math.abs(deltaX) >= Math.abs(deltaY)) //deltaX大,表示左右滑动,直接返回true
            }
            MotionEvent.ACTION_UP ->{
                isIntercept = false
            }
        }
        /**===========================================
         * 注意:
         * 1. 页面切换后,lastX/Y还是切换时刻的值,
         * 2. 拦截后直接跳到onTouchEvent的ACTION_MOVE执行
         * 3. 错误的lastX会导致滑动错乱,因此进入MOVE处理前,这里要记录最新的lastX/Y
         *=============================================*/
        lastX = ev.x
        lastY = ev.y
        return isIntercept
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猎羽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值