Reputation: 2627
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
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:
Upvotes: 5