kotlin学习之QQ消息气泡简单实现
来源:互联网 发布:杭州大树网络金融 编辑:程序博客网 时间:2024/04/29 06:45
kotlin学习之QQ消息气泡简单实现
为了不枯燥的学习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<Point> { 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()
附上函数图
就这样吧,细节都在启舰大佬
阅读全文
0 0
- kotlin学习之QQ消息气泡简单实现
- 实现类似QQ气泡消息的样式
- 类似QQ拖动气泡删除消息的气泡实现
- 【HTML5】简单实现QQ聊天气泡效果
- Android之QQ聊天气泡对话实现
- Android自定义View仿QQ消息拖拽气泡实现
- (十三)QQ 消息气泡
- CSS 消息气泡实现
- 实现QQ聊天气泡效果
- 仿QQ消息气泡拖拽效果
- 仿照qq聊天,包含气泡消息发送
- BezierDemo源码解析-实现qq消息气泡拖拽消失的效果
- 安卓仿手机QQ消息BadgeView气泡跟随手指移动,并实现进出动画效果。
- Java Swing实现的仿QQ气泡消息聊天窗口效果
- 贝塞尔曲线实现QQ未读消息气泡拖拽效果
- 仿QQ拖动删除未读消息个数气泡之二
- 微信小程序之『仿 QQ 消息气泡拖拽消失』
- iOS 类似微信,QQ聊天界面的气泡聊天简单实现Demo
- 从入门到入门-Spring Boot-属性配置3
- post提交的数据有哪几种编码格式?能否通过URL参数获取用户账户密码
- 布谷鸟算法详细讲解
- 大并发架构-Keepalived
- logger4j总结
- kotlin学习之QQ消息气泡简单实现
- java中正则表达式的使用
- 认真学习php面向对象-5
- js事件
- webview里面的图片不显示
- Splay复杂度的证明
- TELE POJ
- windows可视化编程(6)
- linux-配置java,mysql