Любой Android разработчик работал с кнопками, поэтому видел ripple эффект и всю его красоту.

Иногда хочется реализовать что-нибудь кастомное нежели стандартные вещи, которые уже предоставляются компонентами Material Design.

Поэтому я решил написать наследник AppCompatImageView и сделать для него свой ripple эффект с минимальным количеством кода.

Здесь вы можете посмотреть как это выглядит.

Сразу выкладываю код:

private class RippleAnimator(private var radius: Float, private var delay: Long) {

    private val animators = mutableListOf<ValueAnimator>()
    private var isRunning: Boolean = false

    fun changeRippleRadius(radius: Float) {
        this.radius = radius
    }

    fun changeDelay(delay: Long) {
        this.delay = delay
    }

    fun start(updateListener: ValueAnimator.AnimatorUpdateListener) {
        val anim = ValueAnimator.ofFloat(0f, radius)
        anim.addUpdateListener(updateListener)

        anim.doOnStart { isRunning = true }
        anim.doOnEnd { isRunning = false }
        anim.duration = delay

        animators.firstOrNull()?.cancel()
        animators.clear()

        animators.add(anim)
        anim.start()
    }

    fun stop(after: () -> Unit) {
        if (isRunning) {
            animators.firstOrNull()?.doOnEnd {
                after()
            }
        } else {
            after()
        }
    }
}

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 var pointX = 0f
    private var pointY = 0f


    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)

    private val rippleAnimator = RippleAnimator(0f, 300L)

    init {
        isClickable = true
        changeRippleColor(Color.GREEN)
    }

    fun changeRippleDuration(duration: Long) {
        rippleAnimator.changeDelay(duration)
    }

    fun changeRippleColor(color: Int) {
        paint.color = Color.argb(90, Color.red(color), Color.green(color), Color.blue(color))
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        rippleAnimator.changeRippleRadius(w * 2f)
        radius = w * 2f
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {

        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                pointX = event.x
                pointY = event.y

                rippleAnimator.start {
                    initial = it.animatedValue as Float
                    invalidate()
                }
            }
            MotionEvent.ACTION_UP -> {
                rippleAnimator.stop {
                    initial = 0f
                    invalidate()
                }
            }
        }
        return true
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawCircle(pointX, pointY, initial, paint)

    }

}

Прошу прощения, что без пояснений, добавлю если кто-нибудь не разберется :)

Основная проблема заключается в создании маленькой длительности и возможности отменять предыдущий ripple эффект при повторном нажатии.

RippleImageButton далеко не является идеальным и не соотвествует Material Design стандартам, я лишь хотел показать возможность такого варианта.

Всем хорошего кода :)

Комментарии (11)


  1. etozhesano
    13.09.2021 21:26
    +2

    Не хватает примера для наглядности. Мне например впадлу создавать нулевой проект для проверки


  1. shaman4d
    13.09.2021 22:42
    +1

    Ну скрикаст хоть бы записали - любопытно сначала на результат ведь посмотреть!


  1. vzhilin
    14.09.2021 00:03
    +2

    Пара вопросов:

    1. Вас не смущает горячий цикл в Delay?

    2. Почему в Delay задержка всего 30 микросекунд? C какой частотой кадров будет проигрываться ваша анимация?


    1. KiberneticWorm Автор
      14.09.2021 15:51

      Я вынес анимацию на другой поток, а обновление UI осуществляется только на главном


      1. KiberneticWorm Автор
        14.09.2021 16:01

        30 миллисекунд я подобрал экспериметнально, задержка осуществляется между увеличением шага радиуса ripple эффекта.


        1. vzhilin
          14.09.2021 16:33
          +1

          val interval: Long = 30000
          val start = System.nanoTime()
          var end: Long = 0
          do {
            end = System.nanoTime()
          } while (start + interval >= end)

          Нет, ну смотрите: 1 наносекунда это 10e-9 секунд. 30 000 * 10e-9 получаем 30 * 10e-6. Это 30 микросекунд.


      1. vzhilin
        14.09.2021 16:37
        +1

        do {
          end = System.nanoTime()
        } while (start + interval >= end)

        Хорошо, но другой поток разве не будет непрерывано вызывать метод System.nanoTime(), эффективно превращая заряд батареи в тепло?


        1. lorc
          14.09.2021 21:20

          А потом говорят что андроид тормозит и жрет батарею...


          1. KiberneticWorm Автор
            15.09.2021 15:17
            +1

            Да, я ошибся 30 микросекунд, что касается другого потока, я думаю можно найти решение получше, нежели тупо использовать тяжелый поток для какого то простого эффекта.


        1. KiberneticWorm Автор
          16.09.2021 15:51

          Я поменял код, сейчас стало гораздо понятнее, можно менять цвет и задержку Ripple эффекта


  1. kazinaki
    15.09.2021 15:15

    А чем вас не устраивает

    "?attr/selectableItemBackground"

    и

    "?attr/selectableItemBackgroundBorderless"

    ?