Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Любой Android разработчик работал с кнопками, поэтому видел ripple эффект и всю его красоту.
Иногда хочется реализовать что-нибудь кастомное нежели стандартные вещи, которые уже предоставляются компонентами Material Design.
Поэтому я решил написать наследник AppCompatImageView
и сделать для него свой ripple эффект с минимальным количеством кода.
Сразу выкладываю код:
private class Watcher {
private val observers = mutableListOf<() -> Unit>()
fun tellAbout() = observers.forEach { it.invoke() }
fun replace(observer: () -> Unit) {
if (observers.isNotEmpty()) {
observers.clear()
}
observers.add(observer)
}
}
private class Delay {
fun waitForSomeInterval() {
val interval: Long = 30000
val start = System.nanoTime()
var end: Long = 0
do {
end = System.nanoTime()
} while (start + interval >= end)
}
}
class RippleImageButton @JvmOverloads constructor(
ctx: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatImageView(ctx, attrs, defStyleAttr) {
private var radius: Float = 0f
private var initial: Float = 0f
private val step = 0.09f
private var pointX = 0f
private var pointY = 0f
private var rippleColor = Color.argb(60, 33, 33, 150)
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = rippleColor }
init {
isClickable = true
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
radius = w * 2f
}
private val watcher = Watcher()
private var isRunning = false
private val uiHandler = Handler(Looper.getMainLooper())
private val threadPool = Executors.newSingleThreadExecutor()
private val submits = mutableListOf<Future<*>>()
private val rippleRunnable = Runnable {
initial = 0f
if (isRunning) {
return@Runnable
}
isRunning = true
while (isRunning) {
initial += step
if (initial > radius) {
break
}
uiHandler.post { invalidate() }
Delay().waitForSomeInterval()
}
uiHandler.post {
isRunning = false
watcher.tellAbout()
invalidate()
}
}
private fun stopLast() {
isRunning = false
submits.forEach { it.cancel(true) }
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
pointX = event.x
pointY = event.y
watcher.replace { }
stopLast()
uiHandler.postDelayed({
submits.add(threadPool.submit(rippleRunnable))
}, 1L)
}
MotionEvent.ACTION_UP -> {
if (isRunning) {
watcher.replace {
initial = 0f
invalidate()
}
} else {
initial = 0f
invalidate()
}
}
}
return true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(pointX, pointY, initial, paint)
}
}
Прошу прощения, что без пояснений, добавлю если кто-нибудь не разберется :)
Основная проблема заключается в создании маленькой длительности и возможности отменять предыдущий ripple эффект при повторном нажатии.
RippleImageButton
далеко не является идеальным и не соотвествует Material Design стандартам, я лишь хотел показать возможность такого варианта.
Всем хорошего кода :)