需求分析
这里只作简单介绍,最后会分享源码地址
- 窗帘分为三部分,上面的窗帘杆,杆下面的窗帘布,以及布中间的滑块,实现还是蛮简单的,我们可以通过自定义view把这个窗帘画出来
- 窗帘杆是一个上面是圆角,下面是直角的矩形,窗帘的叶子是上面直角,下面圆角的矩形,这里我们可以通过canvas.drawRoundRect来进行圆角矩形的绘制,然后叠加在一起显示。
- 但是进行自定义view的绘制时,需要避免重复绘制 因此直接通过canvas.drawRoundRect来绘制在叠加在一起是不行,所以这里我是采用Path.addRoundRect方法来实现
- 这里提供了一个自定义属性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
使用
- 将JitPack存储库添加到您的构建文件
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
- 添加依赖
dependencies {
implementation 'com.github.zhuwang0926:CurtainView:1.0.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地址
哈哈,终于写完了,感谢看我文章的人,有帮助的,点个赞,给个Star啊