使用Kotlin实现一个自定义View

kotlin出来老久了,项目中一直用不上,光学习吧!学了又会忘,怎么办?
试试用kotlin自定义一个View玩玩吧!kotlin这是大趋势,我们得紧跟技术前沿!
碰巧网上看到一个自定义View,名叫StickySwitch;让我们来看看重点实现代码吧!


StickySwitch 介绍

官方网站是这么介绍的:美丽的开关小部件与粘性动画。话不多说,上图。今天只研究kotlin语法及动画效果实现。

针对该自定义View内容分析:

  • 背景是一个带圆角的矩形
  • 左右两边各有两张图片,两段文字
  • 当左右两边各被选中的情况下,有蓝色圆标注
  • 当点击自定义View时作切换操作时,有几个过渡动画:
    • 左右滚动的一个流体动画
    • 左右两边字体大小变化及透明度变化(4个动画)
    • 滚动圆的一个带反弹效果

源码研究

kotlin 继承

自定义一个View必先继承自View,这继承方法如下,和java还是很有区别的。

1
class StickySwitch : View { //java 使用extends代替:

枚举状态

  • 动画类型(两种状态:中间矩形和圆角)
    1
    2
    3
    4
    enum class AnimationType { // 相比 java来说 enum AnimationType{} 多了一个class
    LINE,
    CURVED
    }
  • View可见性(三种状态:可见、占位不可见、不占位不可见)
    1
    2
    3
    4
    5
    enum class TextVisibility {
    VISIBLE,
    INVISIBLE,
    GONE
    }

定义属性

  • 这里的属性有
    • 图片资源
    • 文本内容
    • 文本颜色
    • 文本可见性
    • 动画时间
    • 绘制的画笔

下面是一个示例:

1
2
3
4
5
var leftIcon: Drawable? = null
set(drawable) { // 这是该字段的set方法,在此方法中,作了重新绘制功能
field = drawable
invalidate()
}

逻辑实现

获取自定义view的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private fun init(attrs: AttributeSet?, defStyleAttr: Int = 0, defStyleRes: Int = 0) {

val typedArray = context.obtainStyledAttributes(attrs, R.styleable.StickySwitch, defStyleAttr, defStyleRes)

// left switch icon
leftIcon = typedArray.getDrawable(R.styleable.StickySwitch_ss_leftIcon)
leftText = typedArray.getString(R.styleable.StickySwitch_ss_leftText) ?: leftText

// right switch icon
rightIcon = typedArray.getDrawable(R.styleable.StickySwitch_ss_rightIcon)
rightText = typedArray.getString(R.styleable.StickySwitch_ss_rightText) ?: rightText

// icon size
iconSize = typedArray.getDimensionPixelSize(R.styleable.StickySwitch_ss_iconSize, iconSize)
iconPadding = typedArray.getDimensionPixelSize(R.styleable.StickySwitch_ss_iconPadding, iconPadding)

// saved text size
textSize = typedArray.getDimensionPixelSize(R.styleable.StickySwitch_ss_textSize, textSize)
selectedTextSize = typedArray.getDimensionPixelSize(R.styleable.StickySwitch_ss_selectedTextSize, selectedTextSize)

// current text size
leftTextSize = selectedTextSize.toFloat()
rightTextSize = textSize.toFloat()

// slider background color
sliderBackgroundColor = typedArray.getColor(R.styleable.StickySwitch_ss_sliderBackgroundColor, sliderBackgroundColor)

// switch color
switchColor = typedArray.getColor(R.styleable.StickySwitch_ss_switchColor, switchColor)

// text color
textColor = typedArray.getColor(R.styleable.StickySwitch_ss_textColor, textColor)

// animation duration
animationDuration = typedArray.getInt(R.styleable.StickySwitch_ss_animationDuration, animationDuration.toInt()).toLong()

//animation type
animationType = AnimationType.values()[typedArray.getInt(R.styleable.StickySwitch_ss_animationType, AnimationType.LINE.ordinal)]

// text visibility
textVisibility = TextVisibility.values()[typedArray.getInt(R.styleable.StickySwitch_ss_textVisibility, TextVisibility.VISIBLE.ordinal)]

typedArray.recycle()
}

绘制逻辑分析

  • 画圆矩形,该矩形基本位置固定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// icon margin 图标各方位的间距
val iconMarginLeft = iconPadding
val iconMarginBottom = iconPadding
val iconMarginRight = iconPadding
val iconMarginTop = iconPadding

// icon width, height 图标的宽度、高度
val iconWidth = iconSize
val iconHeight = iconSize

// circle Radius 圆的半径
val sliderRadius = iconMarginTop + iconHeight / 2f
val circleRadius = iconMarginTop + iconHeight / 2f

sliderBackgroundPaint.color = sliderBackgroundColor
sliderBackgroundRect.set(0f, 0f, measuredWidth.toFloat(), (iconMarginTop + iconHeight + iconMarginBottom).toFloat())
canvas?.drawRoundRect(sliderBackgroundRect, sliderRadius, sliderRadius, sliderBackgroundPaint)//该处?表示可为null
  • 画滚动的圆的逻辑思路
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 这里表示animatePercent的值在0.0 到0.5的范围之内
// 这里的开关应该是用于判断球滚动的方向
val isBeforeHalf = animatePercent in 0.0..0.5

// 中间矩形的宽度
val widthSpace = measuredWidth - circleRadius * 2


第一个圆的坐标范围:
x =(circleRadius,circleRadius+widthSpace * Math.min(1.0, animatePercent * 2))
y = circleRadius

第二个圆的坐标范围:
x =(circleRadius,Math.abs(0.5 - animatePercent) * 2))
y = circleRadius

canvas?.drawCircle(ocX.toFloat(), ocY, evaluateBounceRate(ocRadius).toFloat(), switchBackgroundPaint)
canvas?.drawCircle(ccX.toFloat(), ccY, evaluateBounceRate(ccRadius).toFloat(), switchBackgroundPaint)

animatePercent 的值控制由该动画控制:
private fun getLiquidAnimator(newCheckedState: Boolean): Animator {
val liquidAnimator = ValueAnimator.ofFloat(animatePercent.toFloat(), if (newCheckedState) 1f else 0f)
liquidAnimator.duration = animationDuration
liquidAnimator.interpolator = AccelerateInterpolator()
liquidAnimator.addUpdateListener { animatePercent = (it.animatedValue as Float).toDouble() }
return liquidAnimator
}

newCheckedState 的值由触摸事件控制:
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (isEnabled.not() || isClickable.not()) return false

when (event?.action) {
ACTION_UP -> {
isSwitchOn = isSwitchOn.not()
animateCheckState(isSwitchOn)
notifySelectedChange()
}
}

return super.onTouchEvent(event)
}

isSwitchOn 是一个变量,代表两种状态,可以外部手动更改并执行绘制:
// switch Status
// false : left status
// true : right status
private var isSwitchOn = false
set(value) {
field = value
invalidate()
}

这里是执行切换动画的总阀门
private fun animateCheckState(newCheckedState: Boolean) {
this.animatorSet = AnimatorSet()
if (animatorSet != null) {
animatorSet?.playTogether(
getLiquidAnimator(newCheckedState),
leftTextSizeAnimator(newCheckedState),
rightTextSizeAnimator(newCheckedState),
leftTextAlphaAnimator(newCheckedState),
rightTextAlphaAnimator(newCheckedState),
getBounceAnimator()
)
animatorSet?.start()
}
}
  • 绘制左右icon
    仅仅贴出一半代码,isSwitchOn值为true的时候为153透明度,否则为255

    1
    2
    3
    4
    5
    6
    7
    leftIcon?.run {
    canvas?.save()
    setBounds(iconMarginLeft, iconMarginTop, iconMarginLeft + iconWidth, iconMarginTop + iconHeight)
    alpha = if (isSwitchOn) 153 else 255
    draw(canvas)
    canvas?.restore()
    }
  • 绘制左右文字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// draw left text

leftTextPaint.textSize = leftTextSize

canvas?.save()
canvas?.drawText(leftText, leftTextX.toFloat(), leftTextY.toFloat(), leftTextPaint)
canvas?.restore()

//字体动画控制逻辑
private fun leftTextSizeAnimator(newCheckedState: Boolean): Animator {
val toTextSize = if (newCheckedState) textSize else selectedTextSize
val textSizeAnimator = ValueAnimator.ofFloat(leftTextSize, toTextSize.toFloat())//字体变化的范围
textSizeAnimator.interpolator = AccelerateDecelerateInterpolator() //加减速控制器
textSizeAnimator.startDelay = animationDuration / 3 //延迟1/3的时间
textSizeAnimator.duration = animationDuration - (animationDuration / 3) // 播放2/3的时间
textSizeAnimator.addUpdateListener { leftTextSize = (it.animatedValue as Float) } //监听数值变化,然后设置字体大小
return textSizeAnimator
}
  • 绘制两个圆直接的连接矩形,另一种情况,我看不出什么效果。

    1
    2
    3
    4
    5
    6
    if (animationType == AnimationType.LINE) {
    val rectT = circleRadius - circleRadius / 2
    val rectB = circleRadius + circleRadius / 2
    canvas?.drawCircle(ccX.toFloat(), ccY, evaluateBounceRate(ccRadius).toFloat(), switchBackgroundPaint)
    canvas?.drawRect(rectL.toFloat(), rectT, rectR.toFloat(), rectB, switchBackgroundPaint)
    }
  • 圆滚动到边缘之后回弹动画

    1
    2
    3
    4
    5
    6
    7
    8
    private fun getBounceAnimator(): Animator {
    val animator = ValueAnimator.ofFloat(1f, 0.9f, 1f)//从1到0.9倍,再到1
    animator.duration = (animationDuration * 0.41).toLong() //播放回弹动画 0.41个动画时间
    animator.startDelay = animationDuration //延迟时间,动画进行结束
    animator.interpolator = DecelerateInterpolator() //减速插值器
    animator.addUpdateListener { animateBounceRate = (it.animatedValue as Float).toDouble() }
    return animator
    }

    调用地方

    1
    2
    canvas?.drawCircle(ocX.toFloat(), ocY, evaluateBounceRate(ocRadius).toFloat(), switchBackgroundPaint) 
    private fun evaluateBounceRate(value: Double): Double = value * animateBounceRate

@JvmOverloads 注解介绍

源码中发现一堆没调用的代码,但@JvmOverloads注解可以讲讲

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@JvmOverloads
fun setDirection(direction: Direction, isAnimate: Boolean = true, shouldTriggerSelected: Boolean = true) {
val newSwitchState = when (direction) {
Direction.LEFT -> false
Direction.RIGHT -> true
}

if (newSwitchState != isSwitchOn) {

isSwitchOn = newSwitchState

// cancel animation when if animate is running
animatorSet?.cancel()

// when isAnimate is false not showing liquid animation
if (isAnimate) animateCheckState(isSwitchOn) else changeCheckState(isSwitchOn)
if (shouldTriggerSelected) {
notifySelectedChange()
}
}
}

以上方法如果不加该注解,则表示方法,是没有默认值的:

1
fun setDirection(direction: Direction, isAnimate: Boolean, shouldTriggerSelected: Boolean)

如果加上该注解,则表示表示重构方法:相当于申明有以下三个方法

1
2
3
fun setDirection(direction: Direction, isAnimate: Boolean = true, shouldTriggerSelected: Boolean = true)
fun setDirection(direction: Direction, isAnimate: Boolean = true)
fun setDirection(direction: Direction)

照理说,这个方法是提供给外部切换状态用的。