Ambran
Ambran

Reputation: 2627

A simple implementation of Flying/Floating/Bubbling Hearts animation

I've been asked to create a "flying hearts" animation in the style of what you could experience on Instagram or FB Messenger. The idea is that this will be a click reaction to a post or the likes. The original implementation has a counter attached to count the number of clicks, but thought it's not relevant here.

I think it turned out nice so sharing it here. Hope it can be helpful to others.

Upvotes: 1

Views: 2303

Answers (1)

Ambran
Ambran

Reputation: 2627

fragment_main layout:

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/clone_container"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.7">

        <ImageView
            android:id="@+id/heart_image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_heart" />
    </FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

MainFragment:

package com.example.flyinghearts

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.graphics.Path
import android.graphics.RectF
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.fragment.app.Fragment
import com.example.flyinghearts.databinding.FragmentMainBinding
import kotlin.random.Random


class MainFragment : Fragment() {

    companion object {
        fun newInstance() = MainFragment()
    }

    private lateinit var binding: FragmentMainBinding

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = FragmentMainBinding.inflate(inflater)

        return binding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        binding.heartImage.setOnClickListener {
            heartOnClick()
        }
    }

    private fun heartOnClick() {
        // Disable clips on all parent generations
        disableAllParentsClip(binding.heartImage)

        // Create clone
        val imageClone = cloneImage()

        // Animate
        animateFlying(imageClone)
        animateFading(imageClone)
    }

    private fun cloneImage(): ImageView {
        val clone = ImageView(context)
        clone.layoutParams = binding.heartImage.layoutParams
        clone.setImageDrawable(binding.heartImage.drawable)
        binding.cloneContainer.addView(clone)
        return clone
    }

    private fun animateFlying(image: ImageView) {
        val x = 0f
        val y = 0f
        val r = Random.nextInt(1000, 5000)
        val angle = 25f

        val path = Path().apply {
            when (r % 2) {
                0 -> arcTo(RectF(x, y - r, x + 2 * r, y + r), 180f, angle)
                else -> arcTo(RectF(x - 2 * r, y - r, x, y + r), 0f, -angle)
            }
        }

        ObjectAnimator.ofFloat(image, View.X, View.Y, path).apply {
            duration = 1000
            start()
        }
    }

    private fun animateFading(image: ImageView) {
        image.animate()
            .alpha(0f)
            .setDuration(1000)
            .setListener(object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator) {
                    binding.cloneContainer.removeView(image)
                }
            })
    }

    private fun disableAllParentsClip(view: View) {
        var view = view
        view.parent?.let {
            while (view.parent is ViewGroup) {
                val viewGroup = view.parent as ViewGroup
                viewGroup.clipChildren = false
                viewGroup.clipToPadding = false
                view = viewGroup
            }
        }
    }
}

Result:

Click her for a video

enter image description here

Upvotes: 5

Related Questions