package com.plattysoft.leonids; import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.AnimationDrawable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.util.DisplayMetrics; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import com.plattysoft.leonids.initializers.AccelerationInitializer; import com.plattysoft.leonids.initializers.ParticleInitializer; import com.plattysoft.leonids.initializers.RotationInitiazer; import com.plattysoft.leonids.initializers.RotationSpeedInitializer; import com.plattysoft.leonids.initializers.ScaleInitializer; import com.plattysoft.leonids.initializers.SpeeddByComponentsInitializer; import com.plattysoft.leonids.initializers.SpeeddModuleAndRangeInitializer; import com.plattysoft.leonids.modifiers.AlphaModifier; import com.plattysoft.leonids.modifiers.ParticleModifier; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.Timer; import java.util.TimerTask; public class ParticleSystem { private static final long TIMMERTASK_INTERVAL = 50; private int mMaxParticles; private Random mRandom; private ArrayList<Particle> mParticles; private final ArrayList<Particle> mActiveParticles = new ArrayList<>(); private long mTimeToLive; private long mCurrentTime = 0; private float mParticlesPerMilisecond; private int mActivatedParticles; private long mEmitingTime; private List<ParticleModifier> mModifiers; private List<ParticleInitializer> mInitializers; private ValueAnimator mAnimator; private Timer mTimer; private float mDpToPxScale; private int mEmiterXMin; private int mEmiterXMax; private int mEmiterYMin; private int mEmiterYMax; private boolean mIgnorePositionInParent; private OnAnimationEndedListener mListener; private ParticleField mField; public ParticleSystem(ParticleField field, Resources res, int maxParticles, long timeToLive) { mRandom = new Random(); setParticleField(field); mModifiers = new ArrayList<>(); mInitializers = new ArrayList<>(); mMaxParticles = maxParticles; // Create the particles mParticles = new ArrayList<>(); mTimeToLive = timeToLive; DisplayMetrics displayMetrics = res.getDisplayMetrics(); mDpToPxScale = (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT); } private ParticleSystem(Resources res, int maxParticles, long timeToLive, ViewGroup parentView) { this(new ParticleFieldView(parentView.getContext()).initController(parentView), res, maxParticles, timeToLive); } private void setParticleField(ParticleField field) { mField = field; } /** * Creates a particle system with the given parameters * * @param maxParticles The maximum number of particles * @param drawableRedId The drawable resource to use as particle (supports Bitmaps and Animations) * @param timeToLive The time to live for the particles * @param parentView The parent ViewGroup to add the particle field to. */ public ParticleSystem(Resources res, int maxParticles, int drawableRedId, long timeToLive, ViewGroup parentView) { this(res, maxParticles, res.getDrawable(drawableRedId), timeToLive, parentView); } /** * Utility constructor that receives a Drawable. A valid Resources object is required and a * valid parent ViewGroup. * * @param res A resources object associated with the relevant context. * @param maxParticles The maximum number of particles * @param drawable The drawable to use as particle (supports Bitmaps and Animations) * @param timeToLive The time to live for the particles * @param parentView The parent ViewGroup to add the ParticleField to. */ public ParticleSystem(Resources res, int maxParticles, Drawable drawable, long timeToLive, ViewGroup parentView) { this(res, maxParticles, timeToLive, parentView); initParticles(drawable); } public void initParticles(Drawable drawable) { if (drawable instanceof BitmapDrawable) { Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); for (int i = 0; i < mMaxParticles; i++) { mParticles.add(new Particle(bitmap)); } } else if (drawable instanceof AnimationDrawable) { AnimationDrawable animation = (AnimationDrawable) drawable; for (int i = 0; i < mMaxParticles; i++) { mParticles.add(new AnimatedParticle(animation)); } } // else { // Not supported, no particles are being created // } } public float dpToPx(float dp) { return dp * mDpToPxScale; } /** * Adds a modifier to the Particle system, it will be executed on each update. * * @param modifier modifier to be added to the ParticleSystem */ public ParticleSystem addModifier(ParticleModifier modifier) { mModifiers.add(modifier); return this; } public ParticleSystem setSpeedRange(float speedMin, float speedMax) { mInitializers.add(new SpeeddModuleAndRangeInitializer(dpToPx(speedMin), dpToPx(speedMax), 0, 360)); return this; } /** * Initializes the speed range and angle range of emitted particles. Angles are in degrees * and non negative: * 0 meaning to the right, 90 to the bottom,... in clockwise orientation. Speed is non * negative and is described in pixels per millisecond. * * @param speedMin The minimum speed to emit particles. * @param speedMax The maximum speed to emit particles. * @param minAngle The minimum angle to emit particles in degrees. * @param maxAngle The maximum angle to emit particles in degrees. * @return This. */ public ParticleSystem setSpeedModuleAndAngleRange(float speedMin, float speedMax, int minAngle, int maxAngle) { // else emitting from top (270°) to bottom (90°) range would not be possible if someone // entered minAngle = 270 and maxAngle=90 since the module would swap the values while (maxAngle < minAngle) { maxAngle += 360; } mInitializers.add(new SpeeddModuleAndRangeInitializer(dpToPx(speedMin), dpToPx(speedMax), minAngle, maxAngle)); return this; } /** * Initializes the speed components ranges that particles will be emitted. Speeds are * measured in density pixels per millisecond. * * @param speedMinX The minimum speed in x direction. * @param speedMaxX The maximum speed in x direction. * @param speedMinY The minimum speed in y direction. * @param speedMaxY The maximum speed in y direction. * @return This. */ public ParticleSystem setSpeedByComponentsRange(float speedMinX, float speedMaxX, float speedMinY, float speedMaxY) { mInitializers.add(new SpeeddByComponentsInitializer(dpToPx(speedMinX), dpToPx(speedMaxX), dpToPx(speedMinY), dpToPx(speedMaxY))); return this; } /** * Initializes the initial rotation range of emitted particles. The rotation angle is * measured in degrees with 0° being no rotation at all and 90° tilting the image to the right. * * @param minAngle The minimum tilt angle. * @param maxAngle The maximum tilt angle. * @return This. */ public ParticleSystem setInitialRotationRange(int minAngle, int maxAngle) { mInitializers.add(new RotationInitiazer(minAngle, maxAngle)); return this; } /** * Initializes the scale range of emitted particles. Will scale the images around their * center multiplied with the given scaling factor. * * @param minScale The minimum scaling factor * @param maxScale The maximum scaling factor. * @return This. */ public ParticleSystem setScaleRange(float minScale, float maxScale) { mInitializers.add(new ScaleInitializer(minScale, maxScale)); return this; } /** * Initializes the rotation speed of emitted particles. Rotation speed is measured in degrees * per second. * * @param rotationSpeed The rotation speed. * @return This. */ public ParticleSystem setRotationSpeed(float rotationSpeed) { mInitializers.add(new RotationSpeedInitializer(rotationSpeed, rotationSpeed)); return this; } /** * Initializes the rotation speed range for emitted particles. The rotation speed is measured * in degrees per second and can be positive or negative. * * @param minRotationSpeed The minimum rotation speed. * @param maxRotationSpeed The maximum rotation speed. * @return This. */ public ParticleSystem setRotationSpeedRange(float minRotationSpeed, float maxRotationSpeed) { mInitializers.add(new RotationSpeedInitializer(minRotationSpeed, maxRotationSpeed)); return this; } /** * Initializes the acceleration range and angle range of emitted particles. The acceleration * components in x and y direction are controlled by the acceleration angle. The acceleration * is measured in density pixels per square millisecond. The angle is measured in degrees * with 0° pointing to the right and going clockwise. * * @param minAcceleration * @param maxAcceleration * @param minAngle * @param maxAngle * @return */ public ParticleSystem setAccelerationModuleAndAndAngleRange(float minAcceleration, float maxAcceleration, int minAngle, int maxAngle) { mInitializers.add(new AccelerationInitializer(dpToPx(minAcceleration), dpToPx(maxAcceleration), minAngle, maxAngle)); return this; } /** * Initializes the acceleration for emitted particles with the given angle. Acceleration is * measured in pixels per square millisecond. The angle is measured in degrees with 0° * meaning to the right and orientation being clockwise. The angle controls the acceleration * direction. * * @param acceleration The acceleration. * @param angle The acceleration direction. * @return This. */ public ParticleSystem setAcceleration(float acceleration, int angle) { mInitializers.add(new AccelerationInitializer(acceleration, acceleration, angle, angle)); return this; } public ParticleSystem setStartTime(int time) { mCurrentTime = time; return this; } /** * Configures a fade out for the particles when they disappear * * @param milisecondsBeforeEnd fade out duration in milliseconds * @param interpolator the interpolator for the fade out (default is linear) */ public ParticleSystem setFadeOut(long milisecondsBeforeEnd, Interpolator interpolator) { mModifiers.add(new AlphaModifier(255, 0, mTimeToLive - milisecondsBeforeEnd, mTimeToLive, interpolator)); return this; } /** * Configures a fade out for the particles when they disappear * * @param duration fade out duration in milliseconds */ public ParticleSystem setFadeOut(long duration) { return setFadeOut(duration, new LinearInterpolator()); } /** * Starts emiting particles from a specific view. If at some point the number goes over the amount of particles availabe on create * no new particles will be created * * @param emiter View from which center the particles will be emited * @param gravity Which position among the view the emission takes place * @param particlesPerSecond Number of particles per second that will be emited (evenly distributed) * @param emitingTime time the emiter will be emiting particles */ public void emitWithGravity(View emiter, int gravity, int particlesPerSecond, int emitingTime) { // Setup emiter configureEmiter(emiter, gravity); startEmitting(particlesPerSecond, emitingTime); } /** * Starts emiting particles from a specific view. If at some point the number goes over the amount of particles availabe on create * no new particles will be created * * @param emiter View from which center the particles will be emited * @param particlesPerSecond Number of particles per second that will be emited (evenly distributed) * @param emitingTime time the emiter will be emiting particles */ public void emit(View emiter, int particlesPerSecond, int emitingTime) { emitWithGravity(emiter, Gravity.CENTER, particlesPerSecond, emitingTime); } /** * Starts emiting particles from a specific view. If at some point the number goes over the amount of particles availabe on create * no new particles will be created * * @param emiter View from which center the particles will be emited * @param particlesPerSecond Number of particles per second that will be emited (evenly distributed) */ public void emit(View emiter, int particlesPerSecond) { // Setup emiter emitWithGravity(emiter, Gravity.CENTER, particlesPerSecond); } /** * Starts emiting particles from a specific view. If at some point the number goes over the amount of particles availabe on create * no new particles will be created * * @param emiter View from which center the particles will be emited * @param gravity Which position among the view the emission takes place * @param particlesPerSecond Number of particles per second that will be emited (evenly distributed) */ public void emitWithGravity(View emiter, int gravity, int particlesPerSecond) { // Setup emiter configureEmiter(emiter, gravity); startEmitting(particlesPerSecond); } private void startEmitting(int particlesPerSecond) { mActivatedParticles = 0; mParticlesPerMilisecond = particlesPerSecond / 1000f; mField.getParticleController().prepareEmitting(mActiveParticles); mEmitingTime = -1; // Meaning infinite updateParticlesBeforeStartTime(particlesPerSecond); mTimer = new Timer(); mTimer.schedule(new TimerTask() { @Override public void run() { onUpdate(mCurrentTime); mCurrentTime += TIMMERTASK_INTERVAL; } }, 0, TIMMERTASK_INTERVAL); } public void emit(int emitterX, int emitterY, int particlesPerSecond, long emittingTime) { configureEmiter(emitterX, emitterY); startEmitting(particlesPerSecond, emittingTime); } private void configureEmiter(int emitterX, int emitterY) { // We configure the emitter based on the window location to fix the offset of action bar // if present mEmiterXMin = mIgnorePositionInParent ? emitterX : emitterX - mField.getParticleController().getPositionInParentX(); mEmiterXMax = mEmiterXMin; mEmiterYMin = mIgnorePositionInParent ? emitterY : emitterY - mField.getParticleController().getPositionInParentY(); mEmiterYMax = mEmiterYMin; } private void startEmitting(int particlesPerSecond, long emittingTime) { mActivatedParticles = 0; mParticlesPerMilisecond = particlesPerSecond / 1000f; mField.getParticleController().prepareEmitting(mActiveParticles); updateParticlesBeforeStartTime(particlesPerSecond); mEmitingTime = emittingTime; startAnimator(new LinearInterpolator(), emittingTime + mTimeToLive); } public void emit(int emitterX, int emitterY, int particlesPerSecond) { configureEmiter(emitterX, emitterY); startEmitting(particlesPerSecond); } public void updateEmitPoint(int emitterX, int emitterY) { configureEmiter(emitterX, emitterY); } /** * Launches particles in one Shot * * @param emiter View from which center the particles will be emited * @param numParticles number of particles launched on the one shot */ public void oneShot(View emiter, int numParticles) { oneShot(emiter, numParticles, new LinearInterpolator()); } /** * Launches particles in one Shot using a special Interpolator * * @param emiter View from which center the particles will be emited * @param numParticles number of particles launched on the one shot * @param interpolator the interpolator for the time */ public void oneShot(View emiter, int numParticles, Interpolator interpolator) { configureEmiter(emiter, Gravity.CENTER); mActivatedParticles = 0; mEmitingTime = mTimeToLive; // We create particles based in the parameters for (int i = 0; i < numParticles && i < mMaxParticles; i++) { activateParticle(0); } mField.getParticleController().prepareEmitting(mActiveParticles); // We start a property animator that will call us to do the update // Animate from 0 to timeToLiveMax startAnimator(interpolator, mTimeToLive); } private void startAnimator(Interpolator interpolator, long animnationTime) { mAnimator = ValueAnimator.ofInt(0, (int) animnationTime); mAnimator.setDuration(animnationTime); mAnimator.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int miliseconds = (Integer) animation.getAnimatedValue(); onUpdate(miliseconds); } }); mAnimator.addListener(new AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { cleanupAnimation(); onAnimationEnded(false); } @Override public void onAnimationCancel(Animator animation) { cleanupAnimation(); onAnimationEnded(true); } }); mAnimator.setInterpolator(interpolator); mAnimator.start(); } public void onAnimationEnded(boolean cancelled) { if (mListener != null) { mListener.onParticleAnimationEnded(this, cancelled); } } public void clearInitializers() { mInitializers.clear(); } public List<Particle> getActiveParticles() { return mActiveParticles; } public interface OnAnimationEndedListener { void onParticleAnimationEnded(ParticleSystem system, boolean cancelled); } public void setOnAnimationEndedListener(OnAnimationEndedListener listener) { mListener = listener; } private void configureEmiter(View emiter, int gravity) { // It works with an emision range int[] location = new int[2]; emiter.getLocationInWindow(location); int parentX = mField.getParticleController().getPositionInParentX(); int parentY = mField.getParticleController().getPositionInParentY(); // Check horizontal gravity and set range if (hasGravity(gravity, Gravity.LEFT)) { mEmiterXMin = location[0] - parentX; mEmiterXMax = mEmiterXMin; } else if (hasGravity(gravity, Gravity.RIGHT)) { mEmiterXMin = location[0] + emiter.getWidth() - parentX; mEmiterXMax = mEmiterXMin; } else if (hasGravity(gravity, Gravity.CENTER_HORIZONTAL)) { mEmiterXMin = location[0] + emiter.getWidth() / 2 - parentX; mEmiterXMax = mEmiterXMin; } else { // All the range mEmiterXMin = location[0] - parentX; mEmiterXMax = location[0] + emiter.getWidth() - parentX; } // Now, vertical gravity and range if (hasGravity(gravity, Gravity.TOP)) { mEmiterYMin = location[1] - parentY; mEmiterYMax = mEmiterYMin; } else if (hasGravity(gravity, Gravity.BOTTOM)) { mEmiterYMin = location[1] + emiter.getHeight() - parentY; mEmiterYMax = mEmiterYMin; } else if (hasGravity(gravity, Gravity.CENTER_VERTICAL)) { mEmiterYMin = location[1] + emiter.getHeight() / 2 - parentY; mEmiterYMax = mEmiterYMin; } else { // All the range mEmiterYMin = location[1] - parentY; mEmiterYMax = location[1] + emiter.getHeight() - parentY; } } private boolean hasGravity(int gravity, int gravityToCheck) { return (gravity & gravityToCheck) == gravityToCheck; } private void activateParticle(long delay) { Particle p = mParticles.remove(0); p.init(); // Initialization goes before configuration, scale is required before can be configured properly for (int i = 0; i < mInitializers.size(); i++) { mInitializers.get(i).initParticle(p, mRandom); } int particleX = getFromRange(mEmiterXMin, mEmiterXMax); int particleY = getFromRange(mEmiterYMin, mEmiterYMax); p.configure(mTimeToLive, particleX, particleY); p.activate(delay, mModifiers); mActiveParticles.add(p); mActivatedParticles++; } private int getFromRange(int minValue, int maxValue) { if (minValue == maxValue) { return minValue; } return mRandom.nextInt(maxValue - minValue) + minValue; } private void onUpdate(long miliseconds) { while (((mEmitingTime > 0 && miliseconds < mEmitingTime) || mEmitingTime == -1) && // This point should emit !mParticles.isEmpty() && // We have particles in the pool mActivatedParticles < mParticlesPerMilisecond * miliseconds) { // and we are under the number of particles that should be launched // Activate a new particle activateParticle(miliseconds); } synchronized (mActiveParticles) { for (int i = 0; i < mActiveParticles.size(); i++) { boolean active = mActiveParticles.get(i).update(miliseconds); if (!active) { Particle p = mActiveParticles.remove(i); i--; // Needed to keep the index at the right position mParticles.add(p); } } } mField.getParticleController().onUpdate(); } private void cleanupAnimation() { mField.getParticleController().onCleanup(this); mParticles.addAll(mActiveParticles); } /** * Stops emitting new particles, but will draw the existing ones until their timeToLive expire * For an cancellation and stop drawing of the particles, use cancel instead. */ public void stopEmitting() { // The time to be emiting is the current time (as if it was a time-limited emiter mEmitingTime = mCurrentTime; } /** * Cancels the particle system and all the animations. * To stop emitting but animate until the end, use stopEmitting instead. */ public void cancel() { if (mAnimator != null && mAnimator.isRunning()) { mAnimator.cancel(); } if (mTimer != null) { mTimer.cancel(); mTimer.purge(); cleanupAnimation(); } } private void updateParticlesBeforeStartTime(int particlesPerSecond) { if (particlesPerSecond == 0) { return; } long currentTimeInMs = mCurrentTime / 1000; long framesCount = currentTimeInMs / particlesPerSecond; if (framesCount == 0) { return; } long frameTimeInMs = mCurrentTime / framesCount; for (int i = 1; i <= framesCount; i++) { onUpdate(frameTimeInMs * i + 1); } } /** * Allows to emit at exact coordinates within the parent container without offsetting by the * coordinates of the containers view. Useful if the emitting is done in the container's * coordinate system and the offset is not known. * Will not ignore position in parent when emitting from a certain view. */ public void setIgnorePositionInParent() { mIgnorePositionInParent = true; } }