| package com.airbnb.lottie; |
| |
| import android.animation.Animator; |
| import android.animation.ValueAnimator; |
| import android.content.Context; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.ColorFilter; |
| import android.graphics.Matrix; |
| import android.graphics.PixelFormat; |
| import android.graphics.Typeface; |
| import android.graphics.drawable.Drawable; |
| import android.os.Build; |
| import android.support.annotation.FloatRange; |
| import android.support.annotation.IntRange; |
| import android.support.annotation.NonNull; |
| import android.support.annotation.Nullable; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.animation.LinearInterpolator; |
| |
| import com.airbnb.lottie.manager.FontAssetManager; |
| import com.airbnb.lottie.manager.ImageAssetManager; |
| import com.airbnb.lottie.model.layer.CompositionLayer; |
| import com.airbnb.lottie.model.layer.Layer; |
| import com.airbnb.lottie.utils.LottieValueAnimator; |
| |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Set; |
| |
| /** |
| * This can be used to show an lottie animation in any place that would normally take a drawable. |
| * If there are masks or mattes, then you MUST call {@link #recycleBitmaps()} when you are done |
| * or else you will leak bitmaps. |
| * <p> |
| * It is preferable to use {@link com.airbnb.lottie.LottieAnimationView} when possible because it |
| * handles bitmap recycling and asynchronous loading |
| * of compositions. |
| */ |
| @SuppressWarnings({"WeakerAccess", "unused"}) public class LottieDrawable extends Drawable implements Drawable.Callback { |
| private static final String TAG = LottieDrawable.class.getSimpleName(); |
| private boolean systemAnimationsAreDisabled; |
| |
| private interface LazyCompositionTask { |
| void run(LottieComposition composition); |
| } |
| |
| private final Matrix matrix = new Matrix(); |
| private LottieComposition composition; |
| private final LottieValueAnimator animator = new LottieValueAnimator(); |
| private float speed = 1f; |
| private float scale = 1f; |
| |
| private final Set<ColorFilterData> colorFilterData = new HashSet<>(); |
| private final ArrayList<LazyCompositionTask> lazyCompositionTasks = new ArrayList<>(); |
| @Nullable private ImageAssetManager imageAssetManager; |
| @Nullable private String imageAssetsFolder; |
| @Nullable private ImageAssetDelegate imageAssetDelegate; |
| @Nullable private FontAssetManager fontAssetManager; |
| @Nullable FontAssetDelegate fontAssetDelegate; |
| @Nullable TextDelegate textDelegate; |
| private boolean enableMergePaths; |
| @Nullable private CompositionLayer compositionLayer; |
| private int alpha = 255; |
| private boolean performanceTrackingEnabled; |
| |
| public LottieDrawable() { |
| animator.setRepeatCount(0); |
| animator.setInterpolator(new LinearInterpolator()); |
| animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
| @Override public void onAnimationUpdate(ValueAnimator animation) { |
| if (compositionLayer != null) { |
| compositionLayer.setProgress(animator.getProgress()); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Returns whether or not any layers in this composition has masks. |
| */ |
| public boolean hasMasks() { |
| return compositionLayer != null && compositionLayer.hasMasks(); |
| } |
| |
| /** |
| * Returns whether or not any layers in this composition has a matte layer. |
| */ |
| public boolean hasMatte() { |
| return compositionLayer != null && compositionLayer.hasMatte(); |
| } |
| |
| public boolean enableMergePathsForKitKatAndAbove() { |
| return enableMergePaths; |
| } |
| |
| /** |
| * Enable this to get merge path support for devices running KitKat (19) and above. |
| * |
| * Merge paths currently don't work if the the operand shape is entirely contained within the |
| * first shape. If you need to cut out one shape from another shape, use an even-odd fill type |
| * instead of using merge paths. |
| */ |
| public void enableMergePathsForKitKatAndAbove(boolean enable) { |
| if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { |
| Log.w(TAG, "Merge paths are not supported pre-Kit Kat."); |
| return; |
| } |
| enableMergePaths = enable; |
| if (composition != null) { |
| buildCompositionLayer(); |
| } |
| } |
| |
| /** |
| * If you use image assets, you must explicitly specify the folder in assets/ in which they are |
| * located because bodymovin uses the name filenames across all compositions (img_#). |
| * Do NOT rename the images themselves. |
| * |
| * If your images are located in src/main/assets/airbnb_loader/ then call |
| * `setImageAssetsFolder("airbnb_loader/");`. |
| * |
| * |
| * If you use LottieDrawable directly, you MUST call {@link #recycleBitmaps()} when you |
| * are done. Calling {@link #recycleBitmaps()} doesn't have to be final and {@link LottieDrawable} |
| * will recreate the bitmaps if needed but they will leak if you don't recycle them. |
| */ |
| public void setImagesAssetsFolder(@Nullable String imageAssetsFolder) { |
| this.imageAssetsFolder = imageAssetsFolder; |
| } |
| |
| @Nullable public String getImageAssetsFolder() { |
| return imageAssetsFolder; |
| } |
| |
| /** |
| * If you have image assets and use {@link LottieDrawable} directly, you must call this yourself. |
| * |
| * Calling recycleBitmaps() doesn't have to be final and {@link LottieDrawable} |
| * will recreate the bitmaps if needed but they will leak if you don't recycle them. |
| * |
| */ |
| public void recycleBitmaps() { |
| if (imageAssetManager != null) { |
| imageAssetManager.recycleBitmaps(); |
| } |
| } |
| |
| /** |
| * @return True if the composition is different from the previously set composition, false otherwise. |
| */ |
| public boolean setComposition(LottieComposition composition) { |
| if (this.composition == composition) { |
| return false; |
| } |
| |
| clearComposition(); |
| this.composition = composition; |
| setSpeed(speed); |
| setScale(scale); |
| updateBounds(); |
| buildCompositionLayer(); |
| applyColorFilters(); |
| |
| // We copy the tasks to a new ArrayList so that if this method is called from multiple threads, |
| // then there won't be two iterators iterating and removing at the same time. |
| Iterator<LazyCompositionTask> it = new ArrayList<>(lazyCompositionTasks).iterator(); |
| while (it.hasNext()) { |
| LazyCompositionTask t = it.next(); |
| t.run(composition); |
| it.remove(); |
| } |
| lazyCompositionTasks.clear(); |
| |
| composition.setPerformanceTrackingEnabled(performanceTrackingEnabled); |
| |
| animator.forceUpdate(); |
| |
| return true; |
| } |
| |
| public void setPerformanceTrackingEnabled(boolean enabled) { |
| performanceTrackingEnabled = enabled; |
| if (composition != null) { |
| composition.setPerformanceTrackingEnabled(enabled); |
| } |
| } |
| |
| @Nullable |
| public PerformanceTracker getPerformanceTracker() { |
| if (composition != null) { |
| return composition.getPerformanceTracker(); |
| } |
| return null; |
| } |
| |
| private void buildCompositionLayer() { |
| compositionLayer = new CompositionLayer( |
| this, Layer.Factory.newInstance(composition), composition.getLayers(), composition); |
| } |
| |
| private void applyColorFilters() { |
| if (compositionLayer == null) { |
| return; |
| } |
| |
| for (ColorFilterData data : colorFilterData) { |
| compositionLayer.addColorFilter(data.layerName, data.contentName, data.colorFilter); |
| } |
| } |
| |
| private void clearComposition() { |
| recycleBitmaps(); |
| compositionLayer = null; |
| imageAssetManager = null; |
| invalidateSelf(); |
| } |
| |
| @Override public void invalidateSelf() { |
| final Callback callback = getCallback(); |
| if (callback != null) { |
| callback.invalidateDrawable(this); |
| } |
| } |
| |
| @Override public void setAlpha(@IntRange(from = 0, to = 255) int alpha) { |
| this.alpha = alpha; |
| } |
| |
| @Override public int getAlpha() { |
| return alpha; |
| } |
| |
| @Override public void setColorFilter(@Nullable ColorFilter colorFilter) { |
| throw new UnsupportedOperationException("Use addColorFilter instead."); |
| } |
| |
| /** |
| * Add a color filter to specific content on a specific layer. |
| * @param layerName name of the layer where the supplied content name lives |
| * @param contentName name of the specific content that the color filter is to be applied |
| * @param colorFilter the color filter, null to clear the color filter |
| */ |
| public void addColorFilterToContent(String layerName, String contentName, |
| @Nullable ColorFilter colorFilter) { |
| addColorFilterInternal(layerName, contentName, colorFilter); |
| } |
| |
| /** |
| * Add a color filter to a whole layer |
| * @param layerName name of the layer that the color filter is to be applied |
| * @param colorFilter the color filter, null to clear the color filter |
| */ |
| public void addColorFilterToLayer(String layerName, @Nullable ColorFilter colorFilter) { |
| addColorFilterInternal(layerName, null, colorFilter); |
| } |
| |
| /** |
| * Add a color filter to all layers |
| * @param colorFilter the color filter, null to clear all color filters |
| */ |
| public void addColorFilter(ColorFilter colorFilter) { |
| addColorFilterInternal(null, null, colorFilter); |
| } |
| |
| /** |
| * Clear all color filters on all layers and all content in the layers |
| */ |
| public void clearColorFilters() { |
| colorFilterData.clear(); |
| addColorFilterInternal(null, null, null); |
| } |
| |
| /** |
| * Private method to capture all color filter additions. |
| * There are 3 different behaviors here. |
| * 1. layerName is null. All layers supporting color filters will apply the passed in color filter |
| * 2. layerName is not null, contentName is null. This will apply the passed in color filter |
| * to the whole layer |
| * 3. layerName is not null, contentName is not null. This will apply the pass in color filter |
| * to a specific composition content. |
| */ |
| private void addColorFilterInternal(@Nullable String layerName, @Nullable String contentName, |
| @Nullable ColorFilter colorFilter) { |
| final ColorFilterData data = new ColorFilterData(layerName, contentName, colorFilter); |
| if (colorFilter == null && colorFilterData.contains(data)) { |
| colorFilterData.remove(data); |
| } else { |
| colorFilterData.add(new ColorFilterData(layerName, contentName, colorFilter)); |
| } |
| |
| if (compositionLayer == null) { |
| return; |
| } |
| |
| compositionLayer.addColorFilter(layerName, contentName, colorFilter); |
| } |
| |
| @Override public int getOpacity() { |
| return PixelFormat.TRANSLUCENT; |
| } |
| |
| @Override public void draw(@NonNull Canvas canvas) { |
| L.beginSection("Drawable#draw"); |
| if (compositionLayer == null) { |
| return; |
| } |
| |
| float scale = this.scale; |
| float extraScale = 1f; |
| float maxScale = getMaxScale(canvas); |
| if (scale > maxScale) { |
| scale = maxScale; |
| extraScale = this.scale / scale; |
| } |
| |
| if (extraScale > 1) { |
| // This is a bit tricky... |
| // We can't draw on a canvas larger than ViewConfiguration.get(context).getScaledMaximumDrawingCacheSize() |
| // which works out to be roughly the size of the screen because Android can't generate a |
| // bitmap large enough to render to. |
| // As a result, we cap the scale such that it will never be wider/taller than the screen |
| // and then only render in the top left corner of the canvas. We then use extraScale |
| // to scale up the rest of the scale. However, since we rendered the animation to the top |
| // left corner, we need to scale up and translate the canvas to zoom in on the top left |
| // corner. |
| canvas.save(); |
| float halfWidth = composition.getBounds().width() / 2f; |
| float halfHeight = composition.getBounds().height() / 2f; |
| float scaledHalfWidth = halfWidth * scale; |
| float scaledHalfHeight = halfHeight * scale; |
| |
| canvas.translate( |
| getScale() * halfWidth - scaledHalfWidth, |
| getScale() * halfHeight - scaledHalfHeight); |
| canvas.scale(extraScale, extraScale, scaledHalfWidth, scaledHalfHeight); |
| } |
| |
| matrix.reset(); |
| matrix.preScale(scale, scale); |
| compositionLayer.draw(canvas, matrix, alpha); |
| L.endSection("Drawable#draw"); |
| |
| if (extraScale > 1) { |
| canvas.restore(); |
| } |
| } |
| |
| void systemAnimationsAreDisabled() { |
| systemAnimationsAreDisabled = true; |
| animator.systemAnimationsAreDisabled(); |
| } |
| |
| public void loop(boolean loop) { |
| animator.setRepeatCount(loop ? ValueAnimator.INFINITE : 0); |
| } |
| |
| public boolean isLooping() { |
| return animator.getRepeatCount() == ValueAnimator.INFINITE; |
| } |
| |
| public boolean isAnimating() { |
| return animator.isRunning(); |
| } |
| |
| public void playAnimation() { |
| playAnimation(true); |
| } |
| |
| public void resumeAnimation() { |
| // Reset if they try to resume from the end of the animation |
| // or if system animations are disabled. |
| // If they are disabled then LottieValueAnimator will have it jump to its |
| // max progress. |
| playAnimation( |
| animator.getAnimatedFraction() == animator.getMaxProgress() || |
| systemAnimationsAreDisabled); |
| } |
| |
| private void playAnimation(final boolean resetProgress) { |
| if (compositionLayer == null) { |
| lazyCompositionTasks.add(new LazyCompositionTask() { |
| @Override public void run(LottieComposition composition) { |
| playAnimation(resetProgress); |
| } |
| }); |
| return; |
| } |
| if (resetProgress) { |
| animator.start(); |
| } else { |
| animator.resumeAnimation(); |
| } |
| } |
| |
| public void playAnimation(final int startFrame, final int endFrame) { |
| if (composition == null) { |
| lazyCompositionTasks.add(new LazyCompositionTask() { |
| @Override public void run(LottieComposition composition) { |
| playAnimation(startFrame, endFrame); |
| } |
| }); |
| return; |
| } |
| playAnimation(startFrame / composition.getDurationFrames(), |
| endFrame / composition.getDurationFrames()); |
| } |
| |
| public void playAnimation(@FloatRange(from = 0f, to = 1f) float startProgress, |
| @FloatRange(from = 0f, to = 1f) float endProgress) { |
| animator.updateValues(startProgress, endProgress); |
| animator.setCurrentPlayTime(0); |
| setProgress(startProgress); |
| playAnimation(false); |
| } |
| |
| public void resumeReverseAnimation() { |
| reverseAnimation(false); |
| } |
| |
| public void reverseAnimation() { |
| float progress = getProgress(); |
| reverseAnimation(true); |
| } |
| |
| private void reverseAnimation(final boolean resetProgress) { |
| if (compositionLayer == null) { |
| lazyCompositionTasks.add(new LazyCompositionTask() { |
| @Override public void run(LottieComposition composition) { |
| reverseAnimation(resetProgress); |
| } |
| }); |
| return; |
| } |
| float progress = animator.getProgress(); |
| animator.reverse(); |
| if (resetProgress || getProgress() == 1f) { |
| animator.setProgress(animator.getMinProgress()); |
| } else { |
| animator.setProgress(progress); |
| } |
| } |
| |
| public void setMinFrame(final int minFrame) { |
| if (composition == null) { |
| lazyCompositionTasks.add(new LazyCompositionTask() { |
| @Override public void run(LottieComposition composition) { |
| setMinFrame(minFrame); |
| } |
| }); |
| return; |
| } |
| setMinProgress(minFrame / composition.getDurationFrames()); |
| } |
| |
| public void setMinProgress(float minProgress) { |
| animator.setMinProgress(minProgress); |
| } |
| |
| public void setMaxFrame(final int maxFrame) { |
| if (composition == null) { |
| lazyCompositionTasks.add(new LazyCompositionTask() { |
| @Override public void run(LottieComposition composition) { |
| setMaxFrame(maxFrame); |
| } |
| }); |
| return; |
| } |
| setMaxProgress(maxFrame / composition.getDurationFrames()); |
| } |
| |
| public void setMaxProgress(float maxProgress) { |
| animator.setMaxProgress(maxProgress); |
| } |
| |
| public void setMinAndMaxFrame(int minFrame, int maxFrame) { |
| setMinFrame(minFrame); |
| setMaxFrame(maxFrame); |
| } |
| |
| public void setMinAndMaxProgress(float minProgress, float maxProgress) { |
| setMinProgress(minProgress); |
| setMaxProgress(maxProgress); |
| } |
| |
| public void setSpeed(float speed) { |
| this.speed = speed; |
| animator.setIsReversed(speed < 0); |
| |
| if (composition != null) { |
| animator.setDuration((long) (composition.getDuration() / Math.abs(speed))); |
| } |
| } |
| |
| public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { |
| animator.setProgress(progress); |
| if (compositionLayer != null) { |
| compositionLayer.setProgress(progress); |
| } |
| } |
| |
| public float getProgress() { |
| return animator.getProgress(); |
| } |
| |
| /** |
| * Set the scale on the current composition. The only cost of this function is re-rendering the |
| * current frame so you may call it frequent to scale something up or down. |
| * |
| * The smaller the animation is, the better the performance will be. You may find that scaling an |
| * animation down then rendering it in a larger ImageView and letting ImageView scale it back up |
| * with a scaleType such as centerInside will yield better performance with little perceivable |
| * quality loss. |
| */ |
| public void setScale(float scale) { |
| this.scale = scale; |
| updateBounds(); |
| } |
| |
| /** |
| * Use this if you can't bundle images with your app. This may be useful if you download the |
| * animations from the network or have the images saved to an SD Card. In that case, Lottie |
| * will defer the loading of the bitmap to this delegate. |
| */ |
| public void setImageAssetDelegate( |
| @SuppressWarnings("NullableProblems") ImageAssetDelegate assetDelegate) { |
| this.imageAssetDelegate = assetDelegate; |
| if (imageAssetManager != null) { |
| imageAssetManager.setDelegate(assetDelegate); |
| } |
| } |
| |
| /** |
| * Use this to manually set fonts. |
| */ |
| public void setFontAssetDelegate( |
| @SuppressWarnings("NullableProblems") FontAssetDelegate assetDelegate) { |
| this.fontAssetDelegate = assetDelegate; |
| if (fontAssetManager != null) { |
| fontAssetManager.setDelegate(assetDelegate); |
| } |
| } |
| |
| public void setTextDelegate(@SuppressWarnings("NullableProblems") TextDelegate textDelegate) { |
| this.textDelegate = textDelegate; |
| } |
| |
| @Nullable public TextDelegate getTextDelegate() { |
| return textDelegate; |
| } |
| |
| public boolean useTextGlyphs() { |
| return textDelegate == null && composition.getCharacters().size() > 0; |
| } |
| |
| public float getScale() { |
| return scale; |
| } |
| |
| public LottieComposition getComposition() { |
| return composition; |
| } |
| |
| private void updateBounds() { |
| if (composition == null) { |
| return; |
| } |
| float scale = getScale(); |
| setBounds(0, 0, (int) (composition.getBounds().width() * scale), |
| (int) (composition.getBounds().height() * scale)); |
| } |
| |
| public void cancelAnimation() { |
| lazyCompositionTasks.clear(); |
| animator.cancel(); |
| } |
| |
| public void addAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) { |
| animator.addUpdateListener(updateListener); |
| } |
| |
| public void removeAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) { |
| animator.removeUpdateListener(updateListener); |
| } |
| |
| public void addAnimatorListener(Animator.AnimatorListener listener) { |
| animator.addListener(listener); |
| } |
| |
| public void removeAnimatorListener(Animator.AnimatorListener listener) { |
| animator.removeListener(listener); |
| } |
| |
| @Override public int getIntrinsicWidth() { |
| return composition == null ? -1 : (int) (composition.getBounds().width() * getScale()); |
| } |
| |
| @Override public int getIntrinsicHeight() { |
| return composition == null ? -1 : (int) (composition.getBounds().height() * getScale()); |
| } |
| |
| /** |
| * Allows you to modify or clear a bitmap that was loaded for an image either automatically |
| * through {@link #setImagesAssetsFolder(String)} or with an {@link ImageAssetDelegate}. |
| * |
| * @return the previous Bitmap or null. |
| */ |
| @Nullable |
| public Bitmap updateBitmap(String id, @Nullable Bitmap bitmap) { |
| ImageAssetManager bm = getImageAssetManager(); |
| if (bm == null) { |
| Log.w(L.TAG, "Cannot update bitmap. Most likely the drawable is not added to a View " + |
| "which prevents Lottie from getting a Context."); |
| return null; |
| } |
| Bitmap ret = bm.updateBitmap(id, bitmap); |
| invalidateSelf(); |
| return ret; |
| } |
| |
| @Nullable public Bitmap getImageAsset(String id) { |
| ImageAssetManager bm = getImageAssetManager(); |
| if (bm != null) { |
| return bm.bitmapForId(id); |
| } |
| return null; |
| } |
| |
| private ImageAssetManager getImageAssetManager() { |
| if (getCallback() == null) { |
| // We can't get a bitmap since we can't get a Context from the callback. |
| return null; |
| } |
| |
| if (imageAssetManager != null && !imageAssetManager.hasSameContext(getContext())) { |
| imageAssetManager.recycleBitmaps(); |
| imageAssetManager = null; |
| } |
| |
| if (imageAssetManager == null) { |
| imageAssetManager = new ImageAssetManager(getCallback(), |
| imageAssetsFolder, imageAssetDelegate, composition.getImages()); |
| } |
| |
| return imageAssetManager; |
| } |
| |
| @Nullable public Typeface getTypeface(String fontFamily, String style) { |
| FontAssetManager assetManager = getFontAssetManager(); |
| if (assetManager != null) { |
| return assetManager.getTypeface(fontFamily, style); |
| } |
| return null; |
| } |
| |
| private FontAssetManager getFontAssetManager() { |
| if (getCallback() == null) { |
| // We can't get a bitmap since we can't get a Context from the callback. |
| return null; |
| } |
| |
| if (fontAssetManager == null) { |
| fontAssetManager = new FontAssetManager(getCallback(), fontAssetDelegate); |
| } |
| |
| return fontAssetManager; |
| } |
| |
| @Nullable private Context getContext() { |
| Callback callback = getCallback(); |
| if (callback == null) { |
| return null; |
| } |
| |
| if (callback instanceof View) { |
| return ((View) callback).getContext(); |
| } |
| return null; |
| } |
| |
| /** |
| * These Drawable.Callback methods proxy the calls so that this is the drawable that is |
| * actually invalidated, not a child one which will not pass the view's validateDrawable check. |
| */ |
| @Override public void invalidateDrawable(@NonNull Drawable who) { |
| Callback callback = getCallback(); |
| if (callback == null) { |
| return; |
| } |
| callback.invalidateDrawable(this); |
| } |
| |
| @Override public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { |
| Callback callback = getCallback(); |
| if (callback == null) { |
| return; |
| } |
| callback.scheduleDrawable(this, what, when); |
| } |
| |
| @Override public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { |
| Callback callback = getCallback(); |
| if (callback == null) { |
| return; |
| } |
| callback.unscheduleDrawable(this, what); |
| } |
| |
| /** |
| * If the composition is larger than the canvas, we have to use a different method to scale it up. |
| * See the comments in {@link #draw(Canvas)} for more info. |
| */ |
| private float getMaxScale(@NonNull Canvas canvas) { |
| float maxScaleX = canvas.getWidth() / (float) composition.getBounds().width(); |
| float maxScaleY = canvas.getHeight() / (float) composition.getBounds().height(); |
| return Math.min(maxScaleX, maxScaleY); |
| } |
| |
| private static class ColorFilterData { |
| |
| final String layerName; |
| @Nullable final String contentName; |
| @Nullable final ColorFilter colorFilter; |
| |
| ColorFilterData(@Nullable String layerName, @Nullable String contentName, |
| @Nullable ColorFilter colorFilter) { |
| this.layerName = layerName; |
| this.contentName = contentName; |
| this.colorFilter = colorFilter; |
| } |
| |
| @Override public int hashCode() { |
| int hashCode = 17; |
| if (layerName != null) { |
| hashCode = hashCode * 31 * layerName.hashCode(); |
| } |
| |
| if (contentName != null) { |
| hashCode = hashCode * 31 * contentName.hashCode(); |
| } |
| return hashCode; |
| } |
| |
| @Override public boolean equals(Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| |
| if (!(obj instanceof ColorFilterData)) { |
| return false; |
| } |
| |
| final ColorFilterData other = (ColorFilterData) obj; |
| |
| return hashCode() == other.hashCode() && colorFilter == other.colorFilter; |
| |
| } |
| } |
| } |