• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.airbnb.lottie.utils;
2 
3 import android.content.Context;
4 import android.content.res.Resources;
5 import android.graphics.Bitmap;
6 import android.graphics.Canvas;
7 import android.graphics.Color;
8 import android.graphics.Matrix;
9 import android.graphics.Paint;
10 import android.graphics.Path;
11 import android.graphics.PathMeasure;
12 import android.graphics.PointF;
13 import android.graphics.RectF;
14 import android.os.Build;
15 import android.provider.Settings;
16 
17 import androidx.annotation.Nullable;
18 
19 import com.airbnb.lottie.L;
20 import com.airbnb.lottie.animation.LPaint;
21 import com.airbnb.lottie.animation.content.TrimPathContent;
22 import com.airbnb.lottie.animation.keyframe.FloatKeyframeAnimation;
23 
24 import java.io.Closeable;
25 import java.io.InterruptedIOException;
26 import java.net.ProtocolException;
27 import java.net.SocketException;
28 import java.net.UnknownHostException;
29 import java.net.UnknownServiceException;
30 import java.nio.channels.ClosedChannelException;
31 
32 import javax.net.ssl.SSLException;
33 
34 public final class Utils {
35   public static final int SECOND_IN_NANOS = 1000000000;
36 
37   /**
38    * Wrap in Local Thread is necessary for prevent race condition in multi-threaded mode
39    */
40   private static final ThreadLocal<PathMeasure> threadLocalPathMeasure = new ThreadLocal<PathMeasure>() {
41     @Override
42     protected PathMeasure initialValue() {
43       return new PathMeasure();
44     }
45   };
46 
47   private static final ThreadLocal<Path> threadLocalTempPath = new ThreadLocal<Path>() {
48     @Override
49     protected Path initialValue() {
50       return new Path();
51     }
52   };
53 
54   private static final ThreadLocal<Path> threadLocalTempPath2 = new ThreadLocal<Path>() {
55     @Override
56     protected Path initialValue() {
57       return new Path();
58     }
59   };
60 
61   private static final ThreadLocal<float[]> threadLocalPoints = new ThreadLocal<float[]>() {
62     @Override
63     protected float[] initialValue() {
64       return new float[4];
65     }
66   };
67 
68   private static final float INV_SQRT_2 = (float) (Math.sqrt(2) / 2.0);
69 
Utils()70   private Utils() {
71   }
72 
createPath(PointF startPoint, PointF endPoint, PointF cp1, PointF cp2)73   public static Path createPath(PointF startPoint, PointF endPoint, PointF cp1, PointF cp2) {
74     Path path = new Path();
75     path.moveTo(startPoint.x, startPoint.y);
76 
77     if (cp1 != null && cp2 != null && (cp1.length() != 0 || cp2.length() != 0)) {
78       path.cubicTo(
79           startPoint.x + cp1.x, startPoint.y + cp1.y,
80           endPoint.x + cp2.x, endPoint.y + cp2.y,
81           endPoint.x, endPoint.y);
82     } else {
83       path.lineTo(endPoint.x, endPoint.y);
84     }
85     return path;
86   }
87 
closeQuietly(Closeable closeable)88   public static void closeQuietly(Closeable closeable) {
89     if (closeable != null) {
90       try {
91         closeable.close();
92       } catch (RuntimeException rethrown) {
93         throw rethrown;
94       } catch (Exception ignored) {
95         // Ignore.
96       }
97     }
98   }
99 
getScale(Matrix matrix)100   public static float getScale(Matrix matrix) {
101     final float[] points = threadLocalPoints.get();
102 
103     points[0] = 0;
104     points[1] = 0;
105     // Use 1/sqrt(2) so that the hypotenuse is of length 1.
106     points[2] = INV_SQRT_2;
107     points[3] = INV_SQRT_2;
108     matrix.mapPoints(points);
109     float dx = points[2] - points[0];
110     float dy = points[3] - points[1];
111 
112     return (float) Math.hypot(dx, dy);
113   }
114 
hasZeroScaleAxis(Matrix matrix)115   public static boolean hasZeroScaleAxis(Matrix matrix) {
116     final float[] points = threadLocalPoints.get();
117 
118     points[0] = 0;
119     points[1] = 0;
120     // Random numbers. The only way these should map to the same thing as 0,0 is if the scale is 0.
121     points[2] = 37394.729378f;
122     points[3] = 39575.2343807f;
123     matrix.mapPoints(points);
124     return points[0] == points[2] || points[1] == points[3];
125   }
126 
applyTrimPathIfNeeded(Path path, @Nullable TrimPathContent trimPath)127   public static void applyTrimPathIfNeeded(Path path, @Nullable TrimPathContent trimPath) {
128     if (trimPath == null || trimPath.isHidden()) {
129       return;
130     }
131     float start = ((FloatKeyframeAnimation) trimPath.getStart()).getFloatValue();
132     float end = ((FloatKeyframeAnimation) trimPath.getEnd()).getFloatValue();
133     float offset = ((FloatKeyframeAnimation) trimPath.getOffset()).getFloatValue();
134     applyTrimPathIfNeeded(path, start / 100f, end / 100f, offset / 360f);
135   }
136 
applyTrimPathIfNeeded( Path path, float startValue, float endValue, float offsetValue)137   public static void applyTrimPathIfNeeded(
138       Path path, float startValue, float endValue, float offsetValue) {
139     L.beginSection("applyTrimPathIfNeeded");
140     final PathMeasure pathMeasure = threadLocalPathMeasure.get();
141     final Path tempPath = threadLocalTempPath.get();
142     final Path tempPath2 = threadLocalTempPath2.get();
143 
144     pathMeasure.setPath(path, false);
145 
146     float length = pathMeasure.getLength();
147     if (startValue == 1f && endValue == 0f) {
148       L.endSection("applyTrimPathIfNeeded");
149       return;
150     }
151     if (length < 1f || Math.abs(endValue - startValue - 1) < .01) {
152       L.endSection("applyTrimPathIfNeeded");
153       return;
154     }
155     float start = length * startValue;
156     float end = length * endValue;
157     float newStart = Math.min(start, end);
158     float newEnd = Math.max(start, end);
159 
160     float offset = offsetValue * length;
161     newStart += offset;
162     newEnd += offset;
163 
164     // If the trim path has rotated around the path, we need to shift it back.
165     if (newStart >= length && newEnd >= length) {
166       newStart = MiscUtils.floorMod(newStart, length);
167       newEnd = MiscUtils.floorMod(newEnd, length);
168     }
169 
170     if (newStart < 0) {
171       newStart = MiscUtils.floorMod(newStart, length);
172     }
173     if (newEnd < 0) {
174       newEnd = MiscUtils.floorMod(newEnd, length);
175     }
176 
177     // If the start and end are equals, return an empty path.
178     if (newStart == newEnd) {
179       path.reset();
180       L.endSection("applyTrimPathIfNeeded");
181       return;
182     }
183 
184     if (newStart >= newEnd) {
185       newStart -= length;
186     }
187 
188     tempPath.reset();
189     pathMeasure.getSegment(
190         newStart,
191         newEnd,
192         tempPath,
193         true);
194 
195     if (newEnd > length) {
196       tempPath2.reset();
197       pathMeasure.getSegment(
198           0,
199           newEnd % length,
200           tempPath2,
201           true);
202       tempPath.addPath(tempPath2);
203     } else if (newStart < 0) {
204       tempPath2.reset();
205       pathMeasure.getSegment(
206           length + newStart,
207           length,
208           tempPath2,
209           true);
210       tempPath.addPath(tempPath2);
211     }
212     path.set(tempPath);
213     L.endSection("applyTrimPathIfNeeded");
214   }
215 
216   @SuppressWarnings("SameParameterValue")
isAtLeastVersion(int major, int minor, int patch, int minMajor, int minMinor, int minPatch)217   public static boolean isAtLeastVersion(int major, int minor, int patch, int minMajor, int minMinor, int
218       minPatch) {
219     if (major < minMajor) {
220       return false;
221     } else if (major > minMajor) {
222       return true;
223     }
224 
225     if (minor < minMinor) {
226       return false;
227     } else if (minor > minMinor) {
228       return true;
229     }
230 
231     return patch >= minPatch;
232   }
233 
hashFor(float a, float b, float c, float d)234   public static int hashFor(float a, float b, float c, float d) {
235     int result = 17;
236     if (a != 0) {
237       result = (int) (31 * result * a);
238     }
239     if (b != 0) {
240       result = (int) (31 * result * b);
241     }
242     if (c != 0) {
243       result = (int) (31 * result * c);
244     }
245     if (d != 0) {
246       result = (int) (31 * result * d);
247     }
248     return result;
249   }
250 
dpScale()251   public static float dpScale() {
252     return Resources.getSystem().getDisplayMetrics().density;
253   }
254 
getAnimationScale(Context context)255   public static float getAnimationScale(Context context) {
256     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
257       return Settings.Global.getFloat(context.getContentResolver(),
258           Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f);
259     } else {
260       //noinspection deprecation
261       return Settings.System.getFloat(context.getContentResolver(),
262           Settings.System.ANIMATOR_DURATION_SCALE, 1.0f);
263     }
264   }
265 
266   /**
267    * Resize the bitmap to exactly the same size as the specified dimension, changing the aspect ratio if needed.
268    * Returns the original bitmap if the dimensions already match.
269    */
resizeBitmapIfNeeded(Bitmap bitmap, int width, int height)270   public static Bitmap resizeBitmapIfNeeded(Bitmap bitmap, int width, int height) {
271     if (bitmap.getWidth() == width && bitmap.getHeight() == height) {
272       return bitmap;
273     }
274     Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);
275     bitmap.recycle();
276     return resizedBitmap;
277   }
278 
279   /**
280    * From http://vaibhavblogs.org/2012/12/common-java-networking-exceptions/
281    */
isNetworkException(Throwable e)282   public static boolean isNetworkException(Throwable e) {
283     return e instanceof SocketException || e instanceof ClosedChannelException ||
284         e instanceof InterruptedIOException || e instanceof ProtocolException ||
285         e instanceof SSLException || e instanceof UnknownHostException ||
286         e instanceof UnknownServiceException;
287   }
288 
saveLayerCompat(Canvas canvas, RectF rect, Paint paint)289   public static void saveLayerCompat(Canvas canvas, RectF rect, Paint paint) {
290     saveLayerCompat(canvas, rect, paint, Canvas.ALL_SAVE_FLAG);
291   }
292 
saveLayerCompat(Canvas canvas, RectF rect, Paint paint, int flag)293   public static void saveLayerCompat(Canvas canvas, RectF rect, Paint paint, int flag) {
294     L.beginSection("Utils#saveLayer");
295     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
296       // This method was deprecated in API level 26 and not recommended since 22, but its
297       // 2-parameter replacement is only available starting at API level 21.
298       canvas.saveLayer(rect, paint, flag);
299     } else {
300       canvas.saveLayer(rect, paint);
301     }
302     L.endSection("Utils#saveLayer");
303   }
304 
305   /**
306    * For testing purposes only. DO NOT USE IN PRODUCTION.
307    */
308   @SuppressWarnings("unused")
renderPath(Path path)309   public static Bitmap renderPath(Path path) {
310     RectF bounds = new RectF();
311     path.computeBounds(bounds, false);
312     Bitmap bitmap = Bitmap.createBitmap((int) bounds.right, (int) bounds.bottom, Bitmap.Config.ARGB_8888);
313     Canvas canvas = new Canvas(bitmap);
314     Paint paint = new LPaint();
315     paint.setAntiAlias(true);
316     paint.setColor(Color.BLUE);
317     canvas.drawPath(path, paint);
318     return bitmap;
319   }
320 }
321