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