/* * This is the source code of Telegram for Android v. 5.x.x. * It is licensed under GNU GPL v. 2 or later. * You should have received a copy of the license in this archive (see LICENSE). * * Copyright Nikolai Kudashov, 2013-2018. */ package org.telegram.ui.Components; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Animatable; import android.graphics.drawable.BitmapDrawable; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; import android.view.HapticFeedbackConstants; import android.view.View; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.DispatchQueue; import org.telegram.messenger.DispatchQueuePool; import org.telegram.messenger.DispatchQueuePoolBackground; import org.telegram.messenger.FileLog; import org.telegram.messenger.ImageReceiver; import org.telegram.messenger.R; import org.telegram.messenger.Utilities; import org.telegram.messenger.utils.BitmapsCache; import org.telegram.ui.ActionBar.Theme; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.concurrent.CountDownLatch; public class RLottieDrawable extends BitmapDrawable implements Animatable, BitmapsCache.Cacheable { public boolean skipFrameUpdate; public static native long create(String src, String json, int w, int h, int[] params, boolean precache, int[] colorReplacement, boolean limitFps, int fitzModifier); protected static native long createWithJson(String json, String name, int[] params, int[] colorReplacement); public static native void destroy(long ptr); private static native void setLayerColor(long ptr, String layer, int color); private static native void replaceColors(long ptr, int[] colorReplacement); public static native int getFrame(long ptr, int frame, Bitmap bitmap, int w, int h, int stride, boolean clear); protected final int width; protected final int height; protected final int[] metaData = new int[3]; protected int timeBetweenFrames; protected int customEndFrame = -1; protected boolean playInDirectionOfCustomEndFrame; private int[] newReplaceColors; private int[] pendingReplaceColors; private HashMap newColorUpdates = new HashMap<>(); private volatile HashMap pendingColorUpdates = new HashMap<>(); private HashMap vibrationPattern; private boolean resetVibrationAfterRestart = false; private boolean allowVibration = true; private WeakReference frameReadyCallback; protected WeakReference onFinishCallback; private int finishFrame; private View currentParentView; private ArrayList parentViews = new ArrayList<>(); protected int isDice; protected int diceSwitchFramesCount = -1; protected int autoRepeat = 1; protected int autoRepeatPlayCount; protected long autoRepeatTimeout; private long lastFrameTime; protected volatile boolean nextFrameIsLast; protected Runnable cacheGenerateTask; protected Runnable loadFrameTask; protected volatile Bitmap renderingBitmap; protected volatile Bitmap nextRenderingBitmap; protected volatile Bitmap backgroundBitmap; protected boolean waitingForNextTask; protected CountDownLatch frameWaitSync; protected boolean destroyWhenDone; private boolean decodeSingleFrame; private boolean singleFrameDecoded; private boolean forceFrameRedraw; private boolean applyingLayerColors; protected int currentFrame; private boolean shouldLimitFps; private float scaleX = 1.0f; private float scaleY = 1.0f; private boolean applyTransformation; private boolean needScale; private final RectF dstRect = new RectF(); private RectF dstRectBackground; private Paint backgroundPaint; protected static final Handler uiHandler = new Handler(Looper.getMainLooper()); protected volatile boolean isRunning; protected volatile boolean isRecycled; protected volatile long nativePtr; protected volatile long secondNativePtr; protected boolean loadingInBackground; protected boolean secondLoadingInBackground; protected boolean destroyAfterLoading; protected int secondFramesCount; protected volatile boolean setLastFrame; private boolean fallbackCache; private boolean invalidateOnProgressSet; private boolean isInvalid; private boolean doNotRemoveInvalidOnFrameReady; private static ThreadLocal readBufferLocal = new ThreadLocal<>(); private static ThreadLocal bufferLocal = new ThreadLocal<>(); private static final DispatchQueuePool loadFrameRunnableQueue = new DispatchQueuePool(4); public static DispatchQueue lottieCacheGenerateQueue; File file; boolean precache; private Runnable onAnimationEndListener; private Runnable onFrameReadyRunnable; private View masterParent; NativePtrArgs args; protected Runnable uiRunnableNoFrame = new Runnable() { @Override public void run() { loadFrameTask = null; decodeFrameFinishedInternal(); if (onFrameReadyRunnable != null) { onFrameReadyRunnable.run(); } } }; protected Runnable uiRunnable = new Runnable() { @Override public void run() { singleFrameDecoded = true; invalidateInternal(); decodeFrameFinishedInternal(); if (onFrameReadyRunnable != null) { onFrameReadyRunnable.run(); } } }; long startTime; boolean generatingCache; private Runnable uiRunnableGenerateCache = new Runnable() { @Override public void run() { if (!isRecycled && !destroyWhenDone && canLoadFrames() && cacheGenerateTask == null) { startTime = System.currentTimeMillis(); generatingCache = true; lottieCacheGenerateQueue.postRunnable(cacheGenerateTask = () -> { BitmapsCache bitmapsCacheFinal = bitmapsCache; if (bitmapsCacheFinal != null) { bitmapsCacheFinal.createCache(); } uiHandler.post(uiRunnableCacheFinished); }); } } }; private Runnable uiRunnableCacheFinished = new Runnable() { @Override public void run() { cacheGenerateTask = null; generatingCache = false; decodeFrameFinishedInternal(); } }; BitmapsCache bitmapsCache; int generateCacheFramePointer; public static void createCacheGenQueue() { lottieCacheGenerateQueue = new DispatchQueue("cache generator queue"); } protected void checkRunningTasks() { if (cacheGenerateTask != null) { lottieCacheGenerateQueue.cancelRunnable(cacheGenerateTask); cacheGenerateTask = null; } if (!hasParentView() && nextRenderingBitmap != null && loadFrameTask != null) { loadFrameTask = null; nextRenderingBitmap = null; } } protected void decodeFrameFinishedInternal() { if (destroyWhenDone) { checkRunningTasks(); if (loadFrameTask == null && cacheGenerateTask == null && nativePtr != 0) { destroy(nativePtr); nativePtr = 0; if (secondNativePtr != 0) { destroy(secondNativePtr); secondNativePtr = 0; } } } if ((nativePtr == 0 || fallbackCache) && secondNativePtr == 0 && bitmapsCache == null) { recycleResources(); return; } waitingForNextTask = true; if (!hasParentView()) { stop(); } scheduleNextGetFrame(); } protected void recycleResources() { ArrayList bitmapToRecycle = new ArrayList<>(); bitmapToRecycle.add(renderingBitmap); bitmapToRecycle.add(nextRenderingBitmap); renderingBitmap = null; backgroundBitmap = null; AndroidUtilities.recycleBitmaps(bitmapToRecycle); if (onAnimationEndListener != null) { onAnimationEndListener = null; } invalidateInternal(); } public void setOnFinishCallback(Runnable callback, int frame) { if (callback != null) { onFinishCallback = new WeakReference<>(callback); finishFrame = frame; } else if (onFinishCallback != null) { onFinishCallback = null; } } private boolean genCacheSend; protected Runnable loadFrameRunnable = new Runnable() { private long lastUpdate = 0; @Override public void run() { if (isRecycled) { return; } if (!canLoadFrames() || isDice == 2 && secondNativePtr == 0) { if (frameWaitSync != null) { frameWaitSync.countDown(); } uiHandler.post(uiRunnableNoFrame); return; } if (backgroundBitmap == null) { try { backgroundBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); } catch (Throwable e) { FileLog.e(e); } } if (backgroundBitmap != null) { try { if (!pendingColorUpdates.isEmpty()) { for (HashMap.Entry entry : pendingColorUpdates.entrySet()) { setLayerColor(nativePtr, entry.getKey(), entry.getValue()); } pendingColorUpdates.clear(); } } catch (Exception ignore) { } if (pendingReplaceColors != null && nativePtr != 0) { replaceColors(nativePtr, pendingReplaceColors); pendingReplaceColors = null; } try { int result = 0; int framesPerUpdates = shouldLimitFps ? 2 : 1; if (precache && bitmapsCache != null && (!fallbackCache || !generatingCache)) { try { result = bitmapsCache.getFrame(currentFrame / framesPerUpdates, backgroundBitmap); } catch (Exception e) { FileLog.e(e); } } else { if (fallbackCache) { final long now = SystemClock.elapsedRealtime(); if (lastUpdate > 0) { framesPerUpdates = Math.max(1, Math.min(4, Math.round((now - lastUpdate) / 16f))); if (currentFrame + framesPerUpdates > (customEndFrame >= 0 ? customEndFrame : metaData[0])) { framesPerUpdates = (customEndFrame >= 0 ? customEndFrame : metaData[0]) - currentFrame; } } lastUpdate = now; } long ptrToUse; if (isDice == 1) { ptrToUse = nativePtr; } else if (isDice == 2) { ptrToUse = secondNativePtr; if (setLastFrame) { currentFrame = secondFramesCount - 1; } } else { ptrToUse = nativePtr; } result = getFrame(ptrToUse, currentFrame, backgroundBitmap, width, height, backgroundBitmap.getRowBytes(), true); } if (bitmapsCache != null && bitmapsCache.needGenCache()) { if (!genCacheSend) { genCacheSend = true; uiHandler.post(uiRunnableGenerateCache); } if (!fallbackCache) { result = -1; } } if (result == -1) { uiHandler.post(uiRunnableNoFrame); if (frameWaitSync != null) { frameWaitSync.countDown(); } return; } nextRenderingBitmap = backgroundBitmap; if (isDice == 1) { if (currentFrame + framesPerUpdates < (diceSwitchFramesCount == -1 ? metaData[0] : diceSwitchFramesCount)) { currentFrame += framesPerUpdates; } else { currentFrame = 0; nextFrameIsLast = false; if (secondNativePtr != 0) { isDice = 2; } if (resetVibrationAfterRestart) { vibrationPattern = null; resetVibrationAfterRestart = false; } } } else if (isDice == 2) { if (currentFrame + framesPerUpdates < secondFramesCount) { currentFrame += framesPerUpdates; } else { nextFrameIsLast = true; autoRepeatPlayCount++; } } else { if (customEndFrame >= 0 && playInDirectionOfCustomEndFrame) { if (currentFrame > customEndFrame) { if (currentFrame - framesPerUpdates >= customEndFrame) { currentFrame -= framesPerUpdates; nextFrameIsLast = false; } else { nextFrameIsLast = true; checkDispatchOnAnimationEnd(); } } else { if (currentFrame + framesPerUpdates < customEndFrame) { currentFrame += framesPerUpdates; nextFrameIsLast = false; } else { nextFrameIsLast = true; checkDispatchOnAnimationEnd(); } } } else { if (currentFrame + framesPerUpdates < (customEndFrame >= 0 ? customEndFrame : metaData[0])) { if (autoRepeat == 3) { nextFrameIsLast = true; autoRepeatPlayCount++; } else { currentFrame += framesPerUpdates; nextFrameIsLast = false; } } else if (autoRepeat == 1) { currentFrame = 0; nextFrameIsLast = false; if (resetVibrationAfterRestart) { vibrationPattern = null; resetVibrationAfterRestart = false; } } else if (autoRepeat == 2) { currentFrame = 0; nextFrameIsLast = true; autoRepeatPlayCount++; if (resetVibrationAfterRestart) { vibrationPattern = null; resetVibrationAfterRestart = false; } } else { nextFrameIsLast = true; checkDispatchOnAnimationEnd(); } } } } catch (Exception e) { FileLog.e(e); } } uiHandler.post(uiRunnable); if (frameWaitSync != null) { frameWaitSync.countDown(); } } }; public RLottieDrawable(File file, int w, int h, BitmapsCache.CacheOptions cacheOptions, boolean limitFps) { this(file, w, h, cacheOptions, limitFps, null, 0); } public RLottieDrawable(File file, int w, int h, BitmapsCache.CacheOptions cacheOptions, boolean limitFps, int[] colorReplacement, int fitzModifier) { width = w; height = h; shouldLimitFps = limitFps; this.precache = cacheOptions != null; this.fallbackCache = cacheOptions != null && cacheOptions.fallback; getPaint().setFlags(Paint.FILTER_BITMAP_FLAG); this.file = file; if (precache && lottieCacheGenerateQueue == null) { createCacheGenQueue(); } if (precache) { bitmapsCache = new BitmapsCache(file, this, cacheOptions, w, h); args = new NativePtrArgs(); args.file = file.getAbsoluteFile(); args.json = null; args.colorReplacement = colorReplacement; args.fitzModifier = fitzModifier; nativePtr = create(file.getAbsolutePath(), null, w, h, metaData, precache, colorReplacement, shouldLimitFps, fitzModifier); if (fallbackCache) { if (nativePtr == 0) { file.delete(); } } else { destroy(nativePtr); nativePtr = 0; } } else { nativePtr = create(file.getAbsolutePath(), null, w, h, metaData, precache, colorReplacement, shouldLimitFps, fitzModifier); if (nativePtr == 0) { file.delete(); } } if (shouldLimitFps && metaData[1] < 60) { shouldLimitFps = false; } timeBetweenFrames = Math.max(shouldLimitFps ? 33 : 16, (int) (1000.0f / metaData[1])); } public RLottieDrawable(File file, String json, int w, int h, BitmapsCache.CacheOptions options, boolean limitFps, int[] colorReplacement, int fitzModifier) { width = w; height = h; shouldLimitFps = limitFps; this.precache = options != null; getPaint().setFlags(Paint.FILTER_BITMAP_FLAG); if (precache && lottieCacheGenerateQueue == null) { createCacheGenQueue(); } if (precache) { bitmapsCache = new BitmapsCache(file, this, options, w, h); args = new NativePtrArgs(); args.file = file.getAbsoluteFile(); args.json = json; args.colorReplacement = colorReplacement; args.fitzModifier = fitzModifier; nativePtr = create(file.getAbsolutePath(), json, w, h, metaData, precache, colorReplacement, shouldLimitFps, fitzModifier); if (fallbackCache) { if (nativePtr == 0) { file.delete(); } } else { if (nativePtr != 0) { destroy(nativePtr); } nativePtr = 0; } } else { nativePtr = create(file.getAbsolutePath(), json, w, h, metaData, precache, colorReplacement, shouldLimitFps, fitzModifier); if (nativePtr == 0) { file.delete(); } } if (shouldLimitFps && metaData[1] < 60) { shouldLimitFps = false; } timeBetweenFrames = Math.max(shouldLimitFps ? 33 : 16, (int) (1000.0f / metaData[1])); } public RLottieDrawable(int rawRes, String name, int w, int h) { this(rawRes, name, w, h, true, null); } public RLottieDrawable(String diceEmoji, int w, int h) { width = w; height = h; isDice = 1; String jsonString; if ("\uD83C\uDFB2".equals(diceEmoji)) { jsonString = readRes(null, R.raw.diceloop); diceSwitchFramesCount = 60; } else if ("\uD83C\uDFAF".equals(diceEmoji)) { jsonString = readRes(null, R.raw.dartloop); } else { jsonString = null; } getPaint().setFlags(Paint.FILTER_BITMAP_FLAG); if (TextUtils.isEmpty(jsonString)) { timeBetweenFrames = 16; return; } nativePtr = createWithJson(jsonString, "dice", metaData, null); timeBetweenFrames = Math.max(16, (int) (1000.0f / metaData[1])); } private void checkDispatchOnAnimationEnd() { if (onAnimationEndListener != null) { onAnimationEndListener.run(); onAnimationEndListener = null; } } public void setOnAnimationEndListener(Runnable onAnimationEndListener) { this.onAnimationEndListener = onAnimationEndListener; } public boolean isDice() { return isDice != 0; } public boolean setBaseDice(File path) { if (nativePtr != 0 || loadingInBackground) { return true; } String jsonString = readRes(path, 0); if (TextUtils.isEmpty(jsonString)) { return false; } loadingInBackground = true; Utilities.globalQueue.postRunnable(() -> { nativePtr = createWithJson(jsonString, "dice", metaData, null); AndroidUtilities.runOnUIThread(() -> { loadingInBackground = false; if (!secondLoadingInBackground && destroyAfterLoading) { recycle(); return; } timeBetweenFrames = Math.max(16, (int) (1000.0f / metaData[1])); scheduleNextGetFrame(); invalidateInternal(); }); }); return true; } public boolean hasBaseDice() { return nativePtr != 0 || loadingInBackground; } public boolean setDiceNumber(File path, boolean instant) { if (secondNativePtr != 0 || secondLoadingInBackground) { return true; } String jsonString = readRes(path, 0); if (TextUtils.isEmpty(jsonString)) { return false; } if (instant && nextRenderingBitmap == null && renderingBitmap == null && loadFrameTask == null) { isDice = 2; setLastFrame = true; } secondLoadingInBackground = true; Utilities.globalQueue.postRunnable(() -> { if (destroyAfterLoading) { AndroidUtilities.runOnUIThread(() -> { secondLoadingInBackground = false; if (!loadingInBackground && destroyAfterLoading) { recycle(); } }); return; } int[] metaData2 = new int[3]; secondNativePtr = createWithJson(jsonString, "dice", metaData2, null); AndroidUtilities.runOnUIThread(() -> { secondLoadingInBackground = false; if (!secondLoadingInBackground && destroyAfterLoading) { recycle(); return; } secondFramesCount = metaData2[0]; timeBetweenFrames = Math.max(16, (int) (1000.0f / metaData2[1])); scheduleNextGetFrame(); invalidateInternal(); }); }); return true; } public RLottieDrawable(int rawRes, String name, int w, int h, boolean startDecode, int[] colorReplacement) { width = w; height = h; autoRepeat = 0; String jsonString = readRes(null, rawRes); if (TextUtils.isEmpty(jsonString)) { return; } getPaint().setFlags(Paint.FILTER_BITMAP_FLAG); nativePtr = createWithJson(jsonString, name, metaData, colorReplacement); timeBetweenFrames = Math.max(16, (int) (1000.0f / metaData[1])); if (startDecode) { setAllowDecodeSingleFrame(true); } } public static String readRes(File path, int rawRes) { int totalRead = 0; byte[] readBuffer = readBufferLocal.get(); if (readBuffer == null) { readBuffer = new byte[64 * 1024]; readBufferLocal.set(readBuffer); } InputStream inputStream = null; try { if (path != null) { inputStream = new FileInputStream(path); } else { inputStream = ApplicationLoader.applicationContext.getResources().openRawResource(rawRes); } int readLen; byte[] buffer = bufferLocal.get(); if (buffer == null) { buffer = new byte[4096]; bufferLocal.set(buffer); } while ((readLen = inputStream.read(buffer, 0, buffer.length)) >= 0) { if (readBuffer.length < totalRead + readLen) { byte[] newBuffer = new byte[readBuffer.length * 2]; System.arraycopy(readBuffer, 0, newBuffer, 0, totalRead); readBuffer = newBuffer; readBufferLocal.set(readBuffer); } if (readLen > 0) { System.arraycopy(buffer, 0, readBuffer, totalRead, readLen); totalRead += readLen; } } } catch (Throwable e) { return null; } finally { try { if (inputStream != null) { inputStream.close(); } } catch (Throwable ignore) { } } return new String(readBuffer, 0, totalRead); } public int getCurrentFrame() { return currentFrame; } public int getCustomEndFrame() { return customEndFrame; } public long getDuration() { return (long) (metaData[0] / (float) metaData[1] * 1000); } public void setPlayInDirectionOfCustomEndFrame(boolean value) { playInDirectionOfCustomEndFrame = value; } public boolean setCustomEndFrame(int frame) { if (customEndFrame == frame || frame > metaData[0]) { return false; } customEndFrame = frame; return true; } public int getFramesCount() { return metaData[0]; } public void addParentView(ImageReceiver parent) { if (parent == null) { return; } parentViews.add(parent); } public void removeParentView(ImageReceiver parent) { if (parent == null) { return; } parentViews.remove(parent); } protected boolean hasParentView() { return !parentViews.isEmpty() || masterParent != null || getCallback() != null; } protected void invalidateInternal() { for (int i = 0; i < parentViews.size(); i++) { parentViews.get(i).invalidate(); } if (masterParent != null) { masterParent.invalidate(); } if (getCallback() != null) { invalidateSelf(); } } public void setAllowDecodeSingleFrame(boolean value) { decodeSingleFrame = value; if (decodeSingleFrame) { scheduleNextGetFrame(); } } public void recycle() { isRunning = false; isRecycled = true; checkRunningTasks(); if (loadingInBackground || secondLoadingInBackground) { destroyAfterLoading = true; } else if (loadFrameTask == null && cacheGenerateTask == null && !generatingCache) { if (nativePtr != 0) { destroy(nativePtr); nativePtr = 0; } if (secondNativePtr != 0) { destroy(secondNativePtr); secondNativePtr = 0; } if (bitmapsCache != null) { bitmapsCache.recycle(); bitmapsCache = null; } recycleResources(); } else { destroyWhenDone = true; } } public void setAutoRepeat(int value) { if (autoRepeat == 2 && value == 3 && currentFrame != 0) { return; } autoRepeat = value; } public void setAutoRepeatTimeout(long timeout) { autoRepeatTimeout = timeout; } @Override protected void finalize() throws Throwable { try { recycle(); } finally { super.finalize(); } } @Override public int getOpacity() { return PixelFormat.TRANSPARENT; } @Override public void start() { if (isRunning || autoRepeat >= 2 && autoRepeatPlayCount != 0 || customEndFrame == currentFrame) { return; } isRunning = true; if (invalidateOnProgressSet) { isInvalid = true; if (loadFrameTask != null) { doNotRemoveInvalidOnFrameReady = true; } } scheduleNextGetFrame(); invalidateInternal(); } public boolean restart() { if (autoRepeat < 2 || autoRepeatPlayCount == 0) { return false; } autoRepeatPlayCount = 0; autoRepeat = 2; start(); return true; } public void setVibrationPattern(HashMap pattern) { vibrationPattern = pattern; } public boolean hasVibrationPattern() { return vibrationPattern != null; } public void beginApplyLayerColors() { applyingLayerColors = true; } public void commitApplyLayerColors() { if (!applyingLayerColors) { return; } applyingLayerColors = false; if (!isRunning && decodeSingleFrame) { if (currentFrame <= 2) { currentFrame = 0; } nextFrameIsLast = false; singleFrameDecoded = false; if (!scheduleNextGetFrame()) { forceFrameRedraw = true; } } invalidateInternal(); } public void replaceColors(int[] colors) { newReplaceColors = colors; requestRedrawColors(); } public void setLayerColor(String layerName, int color) { newColorUpdates.put(layerName, color); requestRedrawColors(); } private void requestRedrawColors() { if (!applyingLayerColors && !isRunning && decodeSingleFrame) { if (currentFrame <= 2) { currentFrame = 0; } nextFrameIsLast = false; singleFrameDecoded = false; if (!scheduleNextGetFrame()) { forceFrameRedraw = true; } } invalidateInternal(); } protected boolean scheduleNextGetFrame() { return scheduleNextGetFrame(false); } protected boolean scheduleNextGetFrame(boolean allowGroupedUpdateLocal) { if (loadFrameTask != null || nextRenderingBitmap != null || !canLoadFrames() || loadingInBackground || destroyWhenDone || !isRunning && (!decodeSingleFrame || decodeSingleFrame && singleFrameDecoded)) { return false; } if (generatingCache) { // return false; } if (!newColorUpdates.isEmpty()) { pendingColorUpdates.putAll(newColorUpdates); newColorUpdates.clear(); } if (newReplaceColors != null) { pendingReplaceColors = newReplaceColors; newReplaceColors = null; } loadFrameTask = loadFrameRunnable; if (allowGroupedUpdateLocal && shouldLimitFps) { DispatchQueuePoolBackground.execute(loadFrameTask); } else { loadFrameRunnableQueue.execute(loadFrameTask); } return true; } public boolean isHeavyDrawable() { return isDice == 0; } @Override public void stop() { isRunning = false; } public void setCurrentFrame(int frame) { setCurrentFrame(frame, true); } public void setCurrentFrame(int frame, boolean async) { setCurrentFrame(frame, async, false); } public void setCurrentFrame(int frame, boolean async, boolean resetFrame) { if (frame < 0 || frame > metaData[0] || (currentFrame == frame && !resetFrame)) { return; } currentFrame = frame; nextFrameIsLast = false; singleFrameDecoded = false; if (invalidateOnProgressSet) { isInvalid = true; if (loadFrameTask != null) { doNotRemoveInvalidOnFrameReady = true; } } if ((!async || resetFrame) && waitingForNextTask && nextRenderingBitmap != null) { backgroundBitmap = nextRenderingBitmap; nextRenderingBitmap = null; loadFrameTask = null; waitingForNextTask = false; } if (!async) { if (loadFrameTask == null) { frameWaitSync = new CountDownLatch(1); } } if (resetFrame && !isRunning) { isRunning = true; } if (scheduleNextGetFrame(false)) { if (!async) { try { frameWaitSync.await(); } catch (Exception e) { FileLog.e(e); } frameWaitSync = null; } } else { forceFrameRedraw = true; } invalidateSelf(); } public boolean isCacheFallbacked() { return fallbackCache; } public void setProgressMs(long ms) { int frameNum = (int) ((Math.max(0, ms) / timeBetweenFrames) % metaData[0]); setCurrentFrame(frameNum, true, true); } public void setProgress(float progress) { setProgress(progress, true); } public void setProgress(float progress, boolean async) { if (progress < 0.0f) { progress = 0.0f; } else if (progress > 1.0f) { progress = 1.0f; } setCurrentFrame((int) (metaData[0] * progress), async); } public void setCurrentParentView(View view) { currentParentView = view; } @Override public boolean isRunning() { return isRunning; } @Override public int getIntrinsicHeight() { return height; } @Override public int getIntrinsicWidth() { return width; } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); applyTransformation = true; } private void setCurrentFrame(long now, long timeDiff, long timeCheck, boolean force) { backgroundBitmap = renderingBitmap; renderingBitmap = nextRenderingBitmap; nextRenderingBitmap = null; if (isDice == 2) { if (onFinishCallback != null && currentFrame - 1 >= finishFrame) { Runnable runnable = onFinishCallback.get(); if (runnable != null) { runnable.run(); } onFinishCallback = null; } } if (nextFrameIsLast) { stop(); } loadFrameTask = null; if (doNotRemoveInvalidOnFrameReady) { doNotRemoveInvalidOnFrameReady = false; } else if (isInvalid) { isInvalid = false; } singleFrameDecoded = true; waitingForNextTask = false; if (AndroidUtilities.screenRefreshRate <= 60) { lastFrameTime = now; } else { lastFrameTime = now - Math.min(16, timeDiff - timeCheck); } if (force && forceFrameRedraw) { singleFrameDecoded = false; forceFrameRedraw = false; } if (isDice == 0) { if (onFinishCallback != null && currentFrame >= finishFrame) { Runnable runnable = onFinishCallback.get(); if (runnable != null) { runnable.run(); } } } scheduleNextGetFrame(true); } @Override public void draw(Canvas canvas) { drawInternal(canvas, false, 0); } public void drawInBackground(Canvas canvas, float x, float y, float w, float h, int alpha) { if (dstRectBackground == null) { dstRectBackground = new RectF(); backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); backgroundPaint.setFilterBitmap(true); } backgroundPaint.setAlpha(alpha); dstRectBackground.set(x, y, x + w, y + h); drawInternal(canvas, true, 0); } public void drawInternal(Canvas canvas, boolean drawInBackground, long time) { if (!canLoadFrames() || destroyWhenDone) { return; } if (!drawInBackground) { updateCurrentFrame(time, false); } RectF rect = drawInBackground ? dstRectBackground : dstRect; Paint paint = drawInBackground ? backgroundPaint : getPaint(); if (paint.getAlpha() == 0) { return; } if (!isInvalid && renderingBitmap != null) { float scaleX, scaleY; boolean needScale; if (!drawInBackground) { rect.set(getBounds()); if (applyTransformation) { this.scaleX = rect.width() / width; this.scaleY = rect.height() / height; applyTransformation = false; this.needScale = !(Math.abs(rect.width() - width) < AndroidUtilities.dp(1) && Math.abs(rect.height() - height) < AndroidUtilities.dp(1)); } scaleX = this.scaleX; scaleY = this.scaleY; needScale = this.needScale; } else { scaleX = rect.width() / width; scaleY = rect.height() / height; needScale = !(Math.abs(rect.width() - width) < AndroidUtilities.dp(1) && Math.abs(rect.height() - height) < AndroidUtilities.dp(1)); } if (!needScale) { canvas.drawBitmap(renderingBitmap, rect.left, rect.top, paint); } else { canvas.save(); canvas.translate(rect.left, rect.top); canvas.scale(scaleX, scaleY); canvas.drawBitmap(renderingBitmap, 0, 0, paint); canvas.restore(); } if (isRunning && !drawInBackground) { invalidateInternal(); } } } public void updateCurrentFrame(long time, boolean updateInBackground) { long now = time == 0 ? System.currentTimeMillis() : time; long timeDiff = now - lastFrameTime; int timeCheck; if (AndroidUtilities.screenRefreshRate <= 60 || (updateInBackground && AndroidUtilities.screenRefreshRate <= 80)) { timeCheck = timeBetweenFrames - 6; } else { timeCheck = timeBetweenFrames; } if (isRunning) { if (renderingBitmap == null && nextRenderingBitmap == null) { scheduleNextGetFrame(true); } else if (nextRenderingBitmap != null && (renderingBitmap == null || (timeDiff >= timeCheck && !skipFrameUpdate))) { if (vibrationPattern != null && currentParentView != null && allowVibration) { Integer force = vibrationPattern.get(currentFrame - 1); if (force != null) { currentParentView.performHapticFeedback(force == 1 ? HapticFeedbackConstants.LONG_PRESS : HapticFeedbackConstants.KEYBOARD_TAP, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } } setCurrentFrame(now, timeDiff, timeCheck, false); } } else if ((forceFrameRedraw || decodeSingleFrame && timeDiff >= timeCheck) && nextRenderingBitmap != null) { setCurrentFrame(now, timeDiff, timeCheck, true); } } public void setAllowVibration(boolean allow) { allowVibration = allow; } public void resetVibrationAfterRestart(boolean value) { resetVibrationAfterRestart = value; } @Override public int getMinimumHeight() { return height; } @Override public int getMinimumWidth() { return width; } public Bitmap getRenderingBitmap() { return renderingBitmap; } public Bitmap getNextRenderingBitmap() { return nextRenderingBitmap; } public Bitmap getBackgroundBitmap() { return backgroundBitmap; } public Bitmap getAnimatedBitmap() { if (renderingBitmap != null) { return renderingBitmap; } else if (nextRenderingBitmap != null) { return nextRenderingBitmap; } return null; } public boolean hasBitmap() { return !isRecycled && (renderingBitmap != null || nextRenderingBitmap != null) && !isInvalid; } public void setInvalidateOnProgressSet(boolean value) { invalidateOnProgressSet = value; } public boolean isGeneratingCache() { return cacheGenerateTask != null; } public void setOnFrameReadyRunnable(Runnable onFrameReadyRunnable) { this.onFrameReadyRunnable = onFrameReadyRunnable; } public boolean isLastFrame() { return currentFrame == getFramesCount() - 1; } long generateCacheNativePtr; @Override public void prepareForGenerateCache() { generateCacheNativePtr = create(args.file.toString(), args.json, width, height, new int[3], false, args.colorReplacement, false, args.fitzModifier); if (generateCacheNativePtr == 0) { file.delete(); } } @Override public int getNextFrame(Bitmap bitmap) { if (generateCacheNativePtr == 0) { return -1; } int framesPerUpdates = shouldLimitFps ? 2 : 1; int result = getFrame(generateCacheNativePtr, generateCacheFramePointer, bitmap, width, height, bitmap.getRowBytes(), true); if (result == -5) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } return getNextFrame(bitmap); } generateCacheFramePointer += framesPerUpdates; if (generateCacheFramePointer > metaData[0]) { return 0; } return 1; } @Override public void releaseForGenerateCache() { if (generateCacheNativePtr != 0) { destroy(generateCacheNativePtr); generateCacheNativePtr = 0; } } @Override public Bitmap getFirstFrame(Bitmap bitmap) { long nativePtr = create(args.file.toString(), args.json, width, height, new int[3], false, args.colorReplacement, false, args.fitzModifier); if (nativePtr == 0) { return bitmap; } getFrame(nativePtr, 0, bitmap, width, height, bitmap.getRowBytes(), true); destroy(nativePtr); return bitmap; } void setMasterParent(View parent) { masterParent = parent; } public boolean canLoadFrames() { if (precache) { return bitmapsCache != null || fallbackCache; } else { return nativePtr != 0; } } private class NativePtrArgs { public int[] colorReplacement; public int fitzModifier; File file; String json; } }