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