为了不枯燥的学习kotlin,我们当然要搞点事情啦,手动@启舰大佬,看过大佬的QQ气泡实现后我有感而生,不行我要kotlin来搞一个,于是就搞了
class Ball : View {
//可以被设置的属性
var color: Int = DEFAULT_COLOR
set(value) {
field = value
invalidate()
}
//显示的文本
var text: String? = null
set(value) {
if (value != null) {
//超过三个字符报异常
if (value.length > 3) throw IllegalArgumentException("text.length should in 0..3,now is ${value.length}")
//非数字和加号报异常
if (!value.matches("^[0-9+]*\$".toRegex())) throw IllegalArgumentException("text can only be number or '+'")
}
field = value
invalidate()
}
var textColor = DEFAULT_TEXT_COLOR
set(value) {
field = value
invalidate()
}
var radius = DEFAULT_RADIUS
set(value) {
field = value
newRadius = field
newDragRadius = field
invalidate()
}
var textSize = radius
set(value) {
field = value
invalidate()
}
private var _explode: (View) -> Unit = {}
//爆炸回调
fun onExplode(e: (View) -> Unit) {
_explode = e
}
private var newRadius = radius //不动圆实时半径
private var newDragRadius = radius
private var startPoint = Point()//不动圆圆心
private var pressPoint = Point()//触摸点
private var isStart = false//是否开始拖动
private val paint by lazy { Paint() }
private var isDead = false//气泡是否爆炸
private var textY = 0
get() {
val fm = paint.fontMetricsInt
if (!isStart) return startPoint.y - fm.descent + (fm.bottom - fm.top) / 2 else return pressPoint.y - fm.descent + (fm.bottom - fm.top) / 2
}
constructor(context: Context?) : super(context)
//构造器,从xml中获得各属性值
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
val typeArray = context.obtainStyledAttributes(attrs, R.styleable.Ball)
color = typeArray.getColor(R.styleable.Ball_color, DEFAULT_COLOR)
text = typeArray.getString(R.styleable.Ball_text)
textColor = typeArray.getColor(R.styleable.Ball_textColor, DEFAULT_TEXT_COLOR)
radius = typeArray.getInteger(R.styleable.Ball_radius, DEFAULT_RADIUS.toInt()).toFloat()
textSize = typeArray.getInteger(R.styleable.Ball_textSize, radius.toInt()).toFloat()
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
paint.style = Paint.Style.FILL
paint.isAntiAlias = true
paint.textAlign = Paint.Align.CENTER
}
override fun onDraw(canvas: Canvas) {
if (!isDead) {
if (!isDisConnect) {
paint.color = color
canvas.drawCircle(startPoint.x.toFloat(), startPoint.y.toFloat(), newRadius, paint)
}
if (isStart) {
paint.color = color
canvas.drawCircle(pressPoint.x.toFloat(), pressPoint.y.toFloat(), newDragRadius, paint)
if (!isDisConnect) {
canvas.drawPath(path, paint)
}
}
if (text != null) {
paint.color = textColor
paint.textSize = textSize
canvas.drawText(text, if (!isStart) startPoint.x.toFloat() else pressPoint.x.toFloat(), textY.toFloat(), paint)
}
}
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
ACTION_DOWN -> {
pressPoint.x = event.x.toInt()
pressPoint.y = event.y.toInt()
if (isInCircle(pressPoint)) {
isStart = true
}
}
ACTION_MOVE -> {
if (isStart) {
pressPoint.x = event.x.toInt()
pressPoint.y = event.y.toInt()
calRadius(pressPoint)
}
}
ACTION_UP -> {
if (isStart) {
if (isDisConnect) {
isDead = true
isStart = false
_explode(this)
} else reBound()
}
}
}
invalidate()
super.onTouchEvent(event)
return true
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(getMeasuredLen(widthMeasureSpec, true), getMeasuredLen(heightMeasureSpec, false))
}
private fun getMeasuredLen(len: Int, isWidth: Boolean): Int {
val specMode = MeasureSpec.getMode(len)
val specSize = MeasureSpec.getSize(len)
val padding = if (isWidth) paddingLeft + paddingRight else paddingBottom + paddingTop
var measuredLen: Int
if (specMode == MeasureSpec.EXACTLY) {
measuredLen = specSize
} else {
measuredLen = if (isWidth) padding + DEFAULT_WIDTH else padding + DEFAULT_HEIGHT
if (specMode == MeasureSpec.AT_MOST) {
measuredLen = Math.min(measuredLen, specSize)
}
}
if (isWidth) startPoint.x = measuredLen / 2 else startPoint.y = measuredLen / 2
return measuredLen
}
private fun isInCircle(pressPoint: Point): Boolean {
return pressPoint disTo startPoint < radius
}
private fun calRadius(pressPoint: Point) {
newRadius = (radius - 0.1 * (pressPoint disTo startPoint)).toFloat()
//newDragRadius = (radius + 0.1 * (pressPoint disTo startPoint)).toFloat()
}
private var path = Path()
get() {
val dx = (pressPoint.x - startPoint.x).toDouble()
val dy = (pressPoint.y - startPoint.y).toDouble()
//计算角度
val angle = Math.atan(dy / dx)
//起始点偏移
val spOffsetX = newRadius * Math.sin(angle)
val spOffsetY = newRadius * Math.cos(angle)
//按压点偏移
val ppOffsetX = newDragRadius * Math.sin(angle)
val ppOffsetY = newDragRadius * Math.cos(angle)
val point0 = Point((startPoint.x + spOffsetX).toInt(), (startPoint.y - spOffsetY).toInt())
val point3 = Point((startPoint.x - spOffsetX).toInt(), (startPoint.y + spOffsetY).toInt())
val point1 = Point((pressPoint.x + ppOffsetX).toInt(), (pressPoint.y - ppOffsetY).toInt())
val point2 = Point((pressPoint.x - ppOffsetX).toInt(), (pressPoint.y + ppOffsetY).toInt())
val controlPoint = Point((pressPoint.x + startPoint.x) / 2, (pressPoint.y + startPoint.y) / 2)
field.reset()
field.moveTo(point0.x.toFloat(), point0.y.toFloat())
field.quadTo(controlPoint.x.toFloat(), controlPoint.y.toFloat(), point1.x.toFloat(), point1.y.toFloat())
field.lineTo(point2.x.toFloat(), point2.y.toFloat())
field.quadTo(controlPoint.x.toFloat(), controlPoint.y.toFloat(), point3.x.toFloat(), point3.y.toFloat())
field.lineTo(point0.x.toFloat(), point0.y.toFloat())
return field
}
private var isDisConnect: Boolean = false
get() = newRadius < 10
companion object {
val DEFAULT_WIDTH = 80
val DEFAULT_HEIGHT = 80
val DEFAULT_RADIUS = 50F
val DEFAULT_COLOR = 0xff2196f2.toInt()
val DEFAULT_TEXT_COLOR = Color.WHITE
}
fun reBound() {
val animtor = ValueAnimator.ofObject(PointEvaluator(), pressPoint, startPoint)
animtor.addUpdateListener {
animator ->
pressPoint = animator.getAnimatedValue() as Point
calRadius(pressPoint)
invalidate()
}
animtor.addListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(p0: Animator?) {
}
override fun onAnimationCancel(p0: Animator?) {
}
override fun onAnimationStart(p0: Animator?) {
}
override fun onAnimationEnd(p0: Animator?) {
isStart = false
}
})
animtor.duration = 300
animtor.interpolator = MyOverShootInterpolator()
animtor.start()
}
//初始化,这里可以重新设置一些属性
fun reset(set: Ball.() -> Unit = {}) {
this.set()
isDead = false
isStart = false
isDisConnect = false
newRadius = radius
newDragRadius = radius
invalidate()
}
class PointEvaluator : TypeEvaluator {
override fun evaluate(p0: Float, p1: Point, p2: Point): Point {
fun cal(start: Int, end: Int): Int {
return (start + p0 * (end - start)).toInt()
}
return Point(cal(p1.x, p2.x), cal(p1.y, p2.y))
}
}
//回弹插值器
class MyOverShootInterpolator(val factor: Double) : Interpolator {
constructor() : this(0.3)
override fun getInterpolation(p0: Float): Float {
return (Math.pow(2.0, (-5 * p0).toDouble()) * Math.sin((p0 - factor / 4) * (2 * Math.PI) / factor) + 1).toFloat()
}
}
infix fun Point.disTo(a: Point): Int {
return Math.sqrt(((this.x - a.x) * (this.x - a.x) + (this.y - a.y) * (this.y - a.y)).toDouble()).toInt()
}
}
var color: Int = DEFAULT_COLOR
set(value) {
field = value
invalidate()
}
上面的可设置的属性都有类似这样的写法,这就是kotlin中的自定义setter,这样设置后,只要取这个变量的值就会自动调用这个set函数,那么field又是什么鬼,他就是color本身,其实field就是跳过set函数直接访问color本身,这样就避免了set中set的无线循环。里面还有个invalidate函数,这就只是为了设置后能马上更新。
这个就厉害了
x轴居中是很好解决的,如下
paint.textAlign = Paint.Align.CENTER
y轴就费事了,怎么居中都没用,然后百度找到了这么一个方法
private var textY = 0
get() {
val fm = paint.fontMetricsInt
if (!isStart) return startPoint.y - fm.descent + (fm.bottom - fm.top) / 2 else return pressPoint.y - fm.descent + (fm.bottom - fm.top) / 2
}
这个,启舰大佬写的非常详细了,要看的话还是看大佬的好
仔细看就发现,ondraw中都没有getpath( ),其他地方也没有,但是路径却是实时计算的,这就要说setter的搭档了
canvas.drawPath(path, paint)
没有getpath( ),只有path变量,同样,取变量值的时候都会调用get函数
private var path = Path()
get() {...}
这里有这样的写法
pressPoint disTo startPoint//中缀表达式,返回两点距离
事实上代码中有这么一个函数,他给Point类添加了一个函数,这样就可以不修改Point本身而达到直接调用的效果
fun Point.disTo(a: Point): Int {
return Math.sqrt(((this.x - a.x) * (this.x - a.x) + (this.y - a.y) * (this.y - a.y)).toDouble()).toInt()
}
但是像上面这样写还是不能用中缀表达式的写法,在fun前加上infix关键字就可以实现了,66的
这大概是最好玩的地方了,其实就是插值器的效果,Android官方有提供回弹插值器,但是只会弹一次,不够有趣,于是我重新写了一个回弹插值器
//回弹插值器
class MyOverShootInterpolator(val factor: Double) : Interpolator {
constructor() : this(0.3)
override fun getInterpolation(p0: Float): Float {
return (Math.pow(2.0, (-5 * p0).toDouble()) * Math.sin((p0 - factor / 4) * (2 * Math.PI) / factor) + 1).toFloat()
}
}
关键就在这里,根据上一个进度p0计算出下一个进度
Math.sqrt(((this.x - a.x) * (this.x - a.x) + (this.y - a.y) * (this.y - a.y)).toDouble()).toInt()
就这样吧,细节都在启舰大佬