Android自定义View 实现窗帘控件

需求分析

在这里插入图片描述

这里只作简单介绍,最后会分享源码地址

  1. 窗帘分为三部分,上面的窗帘杆,杆下面的窗帘布,以及布中间的滑块,实现还是蛮简单的,我们可以通过自定义view把这个窗帘画出来
  2. 窗帘杆是一个上面是圆角,下面是直角的矩形,窗帘的叶子是上面直角,下面圆角的矩形,这里我们可以通过canvas.drawRoundRect来进行圆角矩形的绘制,然后叠加在一起显示。
  3. 但是进行自定义view的绘制时,需要避免重复绘制 因此直接通过canvas.drawRoundRect来绘制在叠加在一起是不行,所以这里我是采用Path.addRoundRect方法来实现
  4. 这里提供了一个自定义属性curtain_type来设置这个窗帘时单开帘还是双开帘

实现

定义好所需要的自定义view属性,这样可以支持定制化的颜色以及滑块图标

<declare-styleable name="CurtainView">
        <!-- 窗帘最小范围 -->
        <attr name="min" format="integer" />
        <!-- 窗帘最大范围 -->
        <attr name="max" format="integer" />
        <!-- 窗帘当前进度 -->
        <attr name="progress" format="integer" />
        <!-- 最小进度为窗帘两边的距离,应该要大于thumb一半的宽度-->
        <attr name="min_progress" format="float" />
        <!-- 动画时长 -->
        <attr name="duration" format="integer" />
        <!-- 窗帘杆的颜色 -->
        <attr name="curtain_rod_color" format="color" />
        <!-- 窗帘杆的高度 -->
        <attr name="curtain_rod_height" format="dimension" />
        <!-- 窗帘叶子的颜色 -->
        <attr name="curtain_leaves_color" format="color" />
        <!-- 窗帘滑块 -->
        <attr name="curtain_thumb" format="reference" />
        <!-- 窗帘类型 单开帘还是双开帘 -->
        <attr name="curtain_type" format="boolean" />
    </declare-styleable>

编写自定义View-CurtainView

val obtainStyledAttributes =
                context.obtainStyledAttributes(attributeSet, R.styleable.CurtainView)
        setMin(obtainStyledAttributes.getInt(R.styleable.CurtainView_min, mMin))
        setMax(obtainStyledAttributes.getInt(R.styleable.CurtainView_max, mMax))
        setDurtain(obtainStyledAttributes.getInt(R.styleable.CurtainView_duration, mDuration))
        setProgress(obtainStyledAttributes.getInt(R.styleable.CurtainView_progress, mProgress))
        setMinProgress(
                obtainStyledAttributes.getFloat(
                        R.styleable.CurtainView_min_progress,
                        minProgress
                )
        )
        setProgressColor(
                obtainStyledAttributes.getInt(
                        R.styleable.CurtainView_curtain_leaves_color,
                        mProgressColor
                )
        )
        setRodColor(
                obtainStyledAttributes.getInt(
                        R.styleable.CurtainView_curtain_rod_color,
                        mRodColor
                )
        )
        setRodHeight(
                obtainStyledAttributes.getDimension(
                        R.styleable.CurtainView_curtain_rod_height,
                        rodHeight
                )
        )
        setThumb(obtainStyledAttributes.getDrawable(R.styleable.CurtainView_curtain_thumb))
        setDouble(obtainStyledAttributes.getBoolean(R.styleable.CurtainView_curtain_type, isDoubled))
        obtainStyledAttributes.recycle()
        mPaint.strokeCap = Paint.Cap.ROUND
        mPaint.style = Paint.Style.FILL
        rodPath = Path()//这里就是绘制窗帘杆所需的Path
        leftRect = RectF()
        rightRect = RectF()
        thumbRect = Rect()
        leftPath = Path()//窗帘叶左边路径
        rightPath = Path()//窗帘叶右边路径

将定义的自定义属性添加设置进来,并初始化好一些需要用到的东西,如画笔,所需的矩形、路径等.实际上难点主要在窗帘位置的计算,ondraw方法反而很简单

mPaint.color = mProgressColor
        canvas.drawPath(leftPath!!, mPaint)
        if (isDoubled) {
            canvas.drawPath(rightPath!!, mPaint)
        }
        mPaint.color = mRodColor
        canvas.drawPath(rodPath, mPaint)
        mThumbDrawable?.apply {
            bounds = thumbRect!!
            draw(canvas)
        }

因为窗帘杆的大小只需要计算一次,不需要修改,因此我们直接在onSizeChanged计算好就行了

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        mWidth = (w + paddingLeft + paddingRight).toFloat()
        mHeight = (h + paddingTop + paddingBottom).toFloat()
        middleLine = (mWidth / 2).toInt()
        thumbHeightHalf = mThumbDrawable!!.intrinsicHeight / 2
        //顺时针方向添加圆角
        rodPath.addRoundRect(
                RectF(0F, 0F, mWidth, rodHeight),
                floatArrayOf(radius, radius, radius, radius, 0F, 0F, 0F, 0F),
                Path.Direction.CCW
        )
        computeProgressRect()
    }

thumbHeightHalf 是我们设置滑块的高度的一半,然后主要是窗帘叶位置的计算,代码如下所示,矩形的大小根据设置的mMax,mProgress ,minProgress的三个值来决定

/**
     * 矩形的大小根据设置的 minProgress,mMax,mProgress 的三个值来决定
     */
    private fun computeProgressRect() {

        var rectMargin = if (!isDoubled) {
            (mWidth - minProgress) / (mMax - mMin) * mProgress + minProgress
        } else {
            (middleLine - minProgress) / (mMax - mMin) * mProgress + minProgress
        }
        if (isDoubled) {
            //矩形边距不能超过中线
            if (rectMargin > middleLine) {
                rectMargin = middleLine.toFloat()
            }
        }
        leftRect?.let {
            it.set(
                    0F, rodHeight,
                    rectMargin, mHeight
            )
            leftPath?.apply {
                //每次设置路径前都需要先重置,否则不会生效
                reset()
                addRoundRect(
                        it,
                        floatArrayOf(0F, 0F, 0F, 0F, radius, radius, radius, radius),
                        Path.Direction.CCW
                )
            }
        }
        if (isDoubled) {
            rectMargin =
                    mWidth - minProgress - (mWidth - minProgress - middleLine) / (mMax - mMin) * mProgress
            if (rectMargin < middleLine) {
                rectMargin = middleLine.toFloat()
            }

            rightRect?.let {
                it.set(
                        rectMargin,
                        rodHeight,
                        mWidth,
                        mHeight
                )
                rightPath?.apply {
                    //每次设置路径前都需要先重置,否则不会生效
                    reset()
                    addRoundRect(
                            it,
                            floatArrayOf(0F, 0F, 0F, 0F, radius, radius, radius, radius),
                            Path.Direction.CCW
                    )
                }
            }
        }

        if (isDoubled) {
            thumbRect?.set(
                    (leftRect!!.right - thumbHeightHalf).roundToInt(),
                    ((mHeight - rodHeight) / 2 - thumbHeightHalf + rodHeight).toInt(),
                    (leftRect!!.right + thumbHeightHalf).roundToInt(),
                    ((mHeight - rodHeight) / 2 + thumbHeightHalf + rodHeight).toInt()
            )
        } else {
            //单开帘时需要判断滑块是否超出view的宽度
            thumbRect?.set(
                    if (leftRect!!.right > mWidth - thumbHeightHalf) {
                        (leftRect!!.right - thumbHeightHalf * 2).roundToInt()
                    } else {
                        (leftRect!!.right - thumbHeightHalf).roundToInt()
                    },
                    ((mHeight - rodHeight) / 2 - thumbHeightHalf + rodHeight).toInt(),
                    if (leftRect!!.right > mWidth - thumbHeightHalf) {
                        (leftRect!!.right).roundToInt()
                    } else {
                        (leftRect!!.right + thumbHeightHalf).roundToInt()
                    },
                    ((mHeight - rodHeight) / 2 + thumbHeightHalf + rodHeight).toInt()
            )
        }

    }

mProgress 需要根据手指滑动计算出来这里重写onTouchEvent方法

override fun onTouchEvent(event: MotionEvent): Boolean {
        if (!isEnabled) {
            return false
        }
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                moved = false
                //只有手指在滑块中才能滑动
                mThumbDrawable?.let {
                    if (it.bounds.contains(event.x.toInt(), event.y.toInt())) {
                        isTouch = true
                        if (null != mOnProgressChangeListener) {
                            mOnProgressChangeListener!!.onStartTrackingTouch(this)
                        }
                    }
                }
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                if (null != mOnProgressChangeListener && moved) {
                    isTouch = false
                    mOnProgressChangeListener!!.onStopTrackingTouch(this)
                }
            }

            MotionEvent.ACTION_MOVE -> {
                var x = event.x
                if (x > minProgress && isTouch) {
                    if (isDoubled) {
                        if (x > middleLine) {
                            x = middleLine.toFloat()
                        }
                    } else {
                        if (x > mWidth) {
                            x = mWidth
                        }
                    }
                    moved = true
                    val ctrlProgress = if (isDoubled) {
                        ((x - minProgress) * (mMax - mMin) / (middleLine - minProgress)).toInt()
                    } else {
                        ((x - minProgress) * (mMax - mMin) / (mWidth - minProgress)).toInt()
                    }
                    if (ctrlProgress != mProgress) {
                        mProgress = ctrlProgress
                        computeProgressRect()//滑动时计算矩形,然后刷新
                        postInvalidate()
                        if (null != mOnProgressChangeListener) {
                            mOnProgressChangeListener!!.onProgressChanged(this, ctrlProgress, true)
                        }
                    }

                }
            }
        }
        return true
    }

实现了上面两步之后,现在的窗帘就可以根据手指滑动而移动了
然后我们给这个View添加上属性动画,当外部设置进度时,也能够进行窗帘的移动了

if (valueAnimator != null && valueAnimator!!.isRunning) {
            valueAnimator!!.cancel()
        }
        valueAnimator = ValueAnimator.ofInt(oldProgress, mProgress)
                .apply {
                    addUpdateListener { animation: ValueAnimator ->
                        mProgress = animation.animatedValue as Int
                        if (mProgress in mMin..mMax) {
                            computeProgressRect()
                            invalidate()
                        }
                    }
                }
        valueAnimator!!.duration = mDuration.toLong()
        valueAnimator!!.start()

到这里就已经实现了一个可以跟随手指滑动而移动,并可以自定义窗帘杆、叶子、滑块以及控制是单开还是双开的窗帘了。

最后提供库的使用,以及源码地址,欢迎Star

使用

  1. 将JitPack存储库添加到您的构建文件
	allprojects {
		repositories {
			...
			maven { url 'https://jitpack.io' }
		}
	}
  1. 添加依赖
dependencies {
	        implementation 'com.github.zhuwang0926:CurtainView:1.0.1'
	}
  1. xml
<com.hnkj.curtainview.CurtainView
            android:id="@+id/curtain"
            android:layout_width="300dp"
            android:layout_height="253dp"
            android:layout_marginTop="20dp"
            app:curtain_leaves_color="@color/curtain_leaves_color"
            app:curtain_rod_color="@color/curtain_rod_color"
            app:curtain_rod_height="40dp"
            app:curtain_thumb="@drawable/drag"
            app:curtain_type="true"
            app:duration="2500"
            app:max="100"
            app:min="0"
            app:min_progress="83"
            app:progress="100" />

源码中也有使用方法,可以直接查看
源码地址链接: 点击这里,欢迎Star

2022.07.04

- 添加新版本v1.0.2,支持设置图片作为窗帘的叶子,具体修改访问上面的github地址

CSDN下载地址

哈哈,终于写完了,感谢看我文章的人,有帮助的,点个赞,给个Star啊

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值