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