diff --git a/android-gif-drawable/build.gradle b/android-gif-drawable/build.gradle index 51ee8888..6674aaa6 100644 --- a/android-gif-drawable/build.gradle +++ b/android-gif-drawable/build.gradle @@ -10,8 +10,8 @@ android { compileSdkVersion versions.compileSdk compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } defaultConfig { diff --git a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/AllocationByteCountTest.java b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/AllocationByteCountTest.java deleted file mode 100644 index 1788666f..00000000 --- a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/AllocationByteCountTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package pl.droidsonroids.gif; - -import android.content.res.Resources; -import androidx.test.platform.app.InstrumentationRegistry; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import pl.droidsonroids.gif.test.R; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(AndroidJUnit4.class) -public class AllocationByteCountTest { - - @Test - public void allocationByteCountIsConsistent() throws Exception { - final Resources resources = InstrumentationRegistry.getInstrumentation().getContext().getResources(); - final GifDrawable drawable = new GifDrawable(resources, R.raw.test); - final GifAnimationMetaData metaData = new GifAnimationMetaData(resources, R.raw.test); - - assertThat(drawable.getFrameByteCount() + metaData.getAllocationByteCount()).isEqualTo(drawable.getAllocationByteCount()); - assertThat(metaData.getDrawableAllocationByteCount(null, 1)).isEqualTo(drawable.getAllocationByteCount()); - assertThat(metaData.getDrawableAllocationByteCount(drawable, 1)).isEqualTo(drawable.getAllocationByteCount()); - } -} diff --git a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/AllocationByteCountTest.kt b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/AllocationByteCountTest.kt new file mode 100644 index 00000000..e2cb2398 --- /dev/null +++ b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/AllocationByteCountTest.kt @@ -0,0 +1,28 @@ +package pl.droidsonroids.gif + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import pl.droidsonroids.gif.test.R + +@RunWith(AndroidJUnit4::class) +class AllocationByteCountTest { + + @Test + fun allocationByteCountIsConsistent() { + val resources = getInstrumentation().context.resources + + val drawable = GifDrawable(resources, R.raw.test) + + val metaData = GifAnimationMetaData(resources, R.raw.test) + + assertThat(drawable.frameByteCount + metaData.allocationByteCount) + .isEqualTo(drawable.allocationByteCount) + assertThat(metaData.getDrawableAllocationByteCount(null, 1)) + .isEqualTo(drawable.allocationByteCount) + assertThat(metaData.getDrawableAllocationByteCount(drawable, 1)) + .isEqualTo(drawable.allocationByteCount) + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/ErrnoMessageTest.java b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/ErrnoMessageTest.java deleted file mode 100644 index 68401760..00000000 --- a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/ErrnoMessageTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package pl.droidsonroids.gif; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; - -import java.io.File; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -@RunWith(AndroidJUnit4.class) -public class ErrnoMessageTest { - - @Rule - public ExpectedException mExpectedException = ExpectedException.none(); - @Rule - public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); - - @Test - public void errnoMessageAppendedToOpenFailed() throws Exception { - mExpectedException.expect(GifIOException.class); - mExpectedException.expectMessage("GifError 101: Failed to open given input: No such file or directory"); - final File nonExistentFile = new File(mTemporaryFolder.getRoot(), "non-existent"); - new GifDrawable(nonExistentFile); - } - - @Test - public void errnoMessageAppendedToReadFailed() throws Exception { - mExpectedException.expect(GifIOException.class); - mExpectedException.expectMessage("GifError 102: Failed to read from given input: Is a directory"); - new GifDrawable(mTemporaryFolder.getRoot()); - } - -} diff --git a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/ErrnoMessageTest.kt b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/ErrnoMessageTest.kt new file mode 100644 index 00000000..645674bf --- /dev/null +++ b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/ErrnoMessageTest.kt @@ -0,0 +1,40 @@ +package pl.droidsonroids.gif + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import pl.droidsonroids.gif.GifIOException +import java.io.File + +@RunWith(AndroidJUnit4::class) +class ErrnoMessageTest { + + @get:Rule + var mExpectedException = ExpectedException.none() + + @get:Rule + var mTemporaryFolder = TemporaryFolder() + + @Test + fun errnoMessageAppendedToOpenFailed() { + mExpectedException.expect(GifIOException::class.java) + + mExpectedException.expectMessage("GifError 101: Failed to open given input: No such file or directory") + + val nonExistentFile = File(mTemporaryFolder.root, "non-existent") + + GifDrawable(nonExistentFile) + } + + @Test + fun errnoMessageAppendedToReadFailed() { + mExpectedException.expect(GifIOException::class.java) + + mExpectedException.expectMessage("GifError 102: Failed to read from given input: Is a directory") + + GifDrawable(mTemporaryFolder.root) + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifDrawableAssert.java b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifDrawableAssert.java deleted file mode 100644 index a8031d1f..00000000 --- a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifDrawableAssert.java +++ /dev/null @@ -1,23 +0,0 @@ -package pl.droidsonroids.gif; - -import android.graphics.drawable.Drawable; - -import org.assertj.core.api.AbstractAssert; -import org.assertj.core.api.Assertions; - -class GifDrawableAssert extends AbstractAssert { - - private GifDrawableAssert(final GifDrawable actual) { - super(actual, GifDrawableAssert.class); - } - - static GifDrawableAssert assertThat(final Drawable actual) { - Assertions.assertThat(actual).isInstanceOf(GifDrawable.class); - return new GifDrawableAssert((GifDrawable) actual); - } - - GifDrawableAssert hasLoopCountEqualTo(final int loopCount) { - Assertions.assertThat(actual.getLoopCount()).isEqualTo(loopCount); - return this; - } -} diff --git a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifDrawableAssert.kt b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifDrawableAssert.kt new file mode 100644 index 00000000..9f02fc95 --- /dev/null +++ b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifDrawableAssert.kt @@ -0,0 +1,22 @@ +package pl.droidsonroids.gif + +import android.graphics.drawable.Drawable +import org.assertj.core.api.AbstractAssert +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import pl.droidsonroids.gif.GifDrawable + +internal class GifDrawableAssert private constructor(actual: GifDrawable) : + AbstractAssert(actual, GifDrawableAssert::class.java) { + fun hasLoopCountEqualTo(loopCount: Int): GifDrawableAssert { + assertThat(actual!!.loopCount).isEqualTo(loopCount) + return this + } + + companion object { + fun assertThat(actual: Drawable): GifDrawableAssert { + Assertions.assertThat(actual).isInstanceOf(GifDrawable::class.java) + return GifDrawableAssert(actual as GifDrawable) + } + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifDrawableExceptionTest.java b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifDrawableExceptionTest.java deleted file mode 100644 index 96836fe0..00000000 --- a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifDrawableExceptionTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package pl.droidsonroids.gif; - -import android.content.res.Resources; -import androidx.test.platform.app.InstrumentationRegistry; - -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import pl.droidsonroids.gif.test.R; - -import static org.hamcrest.CoreMatchers.containsString; - -@RunWith(AndroidJUnit4.class) -public class GifDrawableExceptionTest { - @Rule - public ExpectedException expectedException = ExpectedException.none(); - private GifDrawable gifDrawable; - - @Before - public void setUp() throws Exception { - final Resources resources = InstrumentationRegistry.getInstrumentation().getContext().getResources(); - gifDrawable = new GifDrawable(resources, R.raw.test); - } - - @After - public void tearDown() { - gifDrawable.recycle(); - } - - @Test - public void frameIndexOutOfBoundsMessageContainsRange() throws Exception { - final int numberOfFrames = gifDrawable.getNumberOfFrames(); - final int invalidFrameIndex = numberOfFrames + 10; - expectedException.expectMessage(containsString(Integer.toString(numberOfFrames))); - expectedException.expect(IndexOutOfBoundsException.class); - gifDrawable.getFrameDuration(invalidFrameIndex); - } - - @Test - public void exceptionThrownWhenPixelsArrayTooSmall() throws Exception { - expectedException.expect(ArrayIndexOutOfBoundsException.class); - gifDrawable.getPixels(new int[0]); - } - - @Test - public void exceptionThrownWhenPixelCoordinateXOutOfRange() throws Exception { - expectedException.expect(IllegalArgumentException.class); - gifDrawable.getPixel(gifDrawable.getIntrinsicWidth(), 0); - } - - @Test - public void exceptionThrownWhenPixelCoordinateYOutOfRange() throws Exception { - expectedException.expect(IllegalArgumentException.class); - gifDrawable.getPixel(0, gifDrawable.getIntrinsicHeight()); - } -} diff --git a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifDrawableExceptionTest.kt b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifDrawableExceptionTest.kt new file mode 100644 index 00000000..c8c309c9 --- /dev/null +++ b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifDrawableExceptionTest.kt @@ -0,0 +1,60 @@ +package pl.droidsonroids.gif + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.hamcrest.CoreMatchers +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException +import org.junit.runner.RunWith +import pl.droidsonroids.gif.test.R + +@RunWith(AndroidJUnit4::class) +class GifDrawableExceptionTest { + + @get:Rule + var expectedException = ExpectedException.none() + + private lateinit var gifDrawable: GifDrawable + + @Before + fun setUp() { + val resources = InstrumentationRegistry.getInstrumentation().context.resources + gifDrawable = GifDrawable(resources, R.raw.test) + } + + @After + fun tearDown() { + gifDrawable.recycle() + } + + @Test + fun frameIndexOutOfBoundsMessageContainsRange() { + val numberOfFrames = gifDrawable.numberOfFrames + val invalidFrameIndex = numberOfFrames + 10 + + expectedException.expectMessage(CoreMatchers.containsString(numberOfFrames.toString())) + expectedException.expect(IndexOutOfBoundsException::class.java) + gifDrawable.getFrameDuration(invalidFrameIndex) + } + + @Test + fun exceptionThrownWhenPixelsArrayTooSmall() { + expectedException.expect(ArrayIndexOutOfBoundsException::class.java) + gifDrawable.getPixels(IntArray(0)) + } + + @Test + fun exceptionThrownWhenPixelCoordinateXOutOfRange() { + expectedException.expect(IllegalArgumentException::class.java) + gifDrawable.getPixel(gifDrawable.intrinsicWidth, 0) + } + + @Test + fun exceptionThrownWhenPixelCoordinateYOutOfRange() { + expectedException.expect(IllegalArgumentException::class.java) + gifDrawable.getPixel(0, gifDrawable.intrinsicHeight) + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifViewDescriptorTest.java b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifViewDescriptorTest.java deleted file mode 100644 index c0b100f9..00000000 --- a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifViewDescriptorTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package pl.droidsonroids.gif; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import androidx.test.platform.app.InstrumentationRegistry; -import android.view.LayoutInflater; -import android.view.View; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import pl.droidsonroids.gif.test.R; - -import static pl.droidsonroids.gif.GifDrawableAssert.assertThat; - -@RunWith(AndroidJUnit4.class) -public class GifViewDescriptorTest { - - private static final int IMAGE_BUTTON_LOOP_COUNT = 2; - private static final int IMAGE_VIEW_LOOP_COUNT = 3; - private static final int TEXT_VIEW_LOOP_COUNT = 4; - private View rootView; - - @Before - public void setUp() { - final Context context = InstrumentationRegistry.getInstrumentation().getContext(); - rootView = LayoutInflater.from(context).inflate(R.layout.attrributes, null, false); - } - - @Test - public void loopCountSetOnGifImageButton() { - final GifImageButton view = rootView.findViewById(R.id.imageButton); - assertThat(view.getBackground()).hasLoopCountEqualTo(IMAGE_BUTTON_LOOP_COUNT); - assertThat(view.getDrawable()).hasLoopCountEqualTo(IMAGE_BUTTON_LOOP_COUNT); - } - - @Test - public void loopCountSetOnGifImageView() { - final GifImageView view = rootView.findViewById(R.id.imageView); - assertThat(view.getBackground()).hasLoopCountEqualTo(IMAGE_VIEW_LOOP_COUNT); - assertThat(view.getDrawable()).hasLoopCountEqualTo(IMAGE_VIEW_LOOP_COUNT); - } - - @Test - public void loopCountSetOnGifTextView() { - final GifTextView view = rootView.findViewById(R.id.textView); - - assertThat(view.getBackground()).hasLoopCountEqualTo(TEXT_VIEW_LOOP_COUNT); - for (final Drawable drawable : view.getCompoundDrawablesRelative()) { - assertThat(drawable).hasLoopCountEqualTo(TEXT_VIEW_LOOP_COUNT); - } - } - -} diff --git a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifViewDescriptorTest.kt b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifViewDescriptorTest.kt new file mode 100644 index 00000000..4c8d4f7d --- /dev/null +++ b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/GifViewDescriptorTest.kt @@ -0,0 +1,56 @@ +package pl.droidsonroids.gif + +import android.view.LayoutInflater +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import pl.droidsonroids.gif.GifDrawableAssert.Companion.assertThat +import pl.droidsonroids.gif.test.R + +@RunWith(AndroidJUnit4::class) +class GifViewDescriptorTest { + + private lateinit var rootView: View + + @Before + fun setUp() { + val context = InstrumentationRegistry.getInstrumentation().context + rootView = LayoutInflater.from(context).inflate(R.layout.attrributes, null, false) + } + + @Test + fun loopCountSetOnGifImageButton() { + val view = rootView.findViewById(R.id.imageButton) + + assertThat(view.background).hasLoopCountEqualTo(IMAGE_BUTTON_LOOP_COUNT) + assertThat(view.drawable).hasLoopCountEqualTo(IMAGE_BUTTON_LOOP_COUNT) + } + + @Test + fun loopCountSetOnGifImageView() { + val view = rootView.findViewById(R.id.imageView) + + assertThat(view.background).hasLoopCountEqualTo(IMAGE_VIEW_LOOP_COUNT) + assertThat(view.drawable).hasLoopCountEqualTo(IMAGE_VIEW_LOOP_COUNT) + } + + @Test + fun loopCountSetOnGifTextView() { + val view = rootView.findViewById(R.id.textView) + + assertThat(view.background).hasLoopCountEqualTo(TEXT_VIEW_LOOP_COUNT) + + for (drawable in view.compoundDrawablesRelative) { + assertThat(drawable).hasLoopCountEqualTo(TEXT_VIEW_LOOP_COUNT) + } + } + + companion object { + private const val IMAGE_BUTTON_LOOP_COUNT = 2 + private const val IMAGE_VIEW_LOOP_COUNT = 3 + private const val TEXT_VIEW_LOOP_COUNT = 4 + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/InputStreamTest.java b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/InputStreamTest.java deleted file mode 100644 index 9e4c3763..00000000 --- a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/InputStreamTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package pl.droidsonroids.gif; - -import android.content.res.AssetFileDescriptor; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.ByteArrayInputStream; -import java.io.FileInputStream; -import java.io.InputStream; - -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import pl.droidsonroids.gif.test.R; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(AndroidJUnit4.class) -public class InputStreamTest { - - @Test - public void gifDrawableCreatedFromInputStream() throws Exception { - final AssetFileDescriptor assetFileDescriptor = InstrumentationRegistry.getInstrumentation() - .getContext().getResources().openRawResourceFd(R.raw.test); - final byte[] buffer = new byte[(int) assetFileDescriptor.getDeclaredLength()]; - final FileInputStream inputStream = assetFileDescriptor.createInputStream(); - final int bufferedByteCount = inputStream.read(buffer); - inputStream.close(); - assetFileDescriptor.close(); - assertThat(bufferedByteCount).isEqualTo(buffer.length); - - final InputStream responseStream = new ByteArrayInputStream(buffer); - - final GifDrawable gifDrawable = new GifDrawable(responseStream); - assertThat(gifDrawable.getError()).isEqualTo(GifError.NO_ERROR); - assertThat(gifDrawable.getIntrinsicWidth()).isEqualTo(278); - assertThat(gifDrawable.getIntrinsicHeight()).isEqualTo(183); - } -} diff --git a/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/InputStreamTest.kt b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/InputStreamTest.kt new file mode 100644 index 00000000..1f83c1aa --- /dev/null +++ b/android-gif-drawable/src/androidTest/java/pl/droidsonroids/gif/InputStreamTest.kt @@ -0,0 +1,38 @@ +package pl.droidsonroids.gif + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import pl.droidsonroids.gif.test.R +import java.io.ByteArrayInputStream +import java.io.InputStream + +@RunWith(AndroidJUnit4::class) +class InputStreamTest { + + @Test + fun gifDrawableCreatedFromInputStream() { + val assetFileDescriptor = InstrumentationRegistry.getInstrumentation() + .context.resources.openRawResourceFd(R.raw.test) + + val buffer = ByteArray(assetFileDescriptor.declaredLength.toInt()) + + val inputStream = assetFileDescriptor.createInputStream() + + val bufferedByteCount = inputStream.read(buffer) + inputStream.close() + assetFileDescriptor.close() + + assertThat(bufferedByteCount).isEqualTo(buffer.size) + + val responseStream: InputStream = ByteArrayInputStream(buffer) + + val gifDrawable = GifDrawable(responseStream) + + assertThat(gifDrawable.error).isEqualTo(GifError.NO_ERROR) + assertThat(gifDrawable.intrinsicWidth).isEqualTo(278) + assertThat(gifDrawable.intrinsicHeight).isEqualTo(183) + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/c/bitmap.c b/android-gif-drawable/src/main/c/bitmap.c index 2f88f63c..4e762d72 100644 --- a/android-gif-drawable/src/main/c/bitmap.c +++ b/android-gif-drawable/src/main/c/bitmap.c @@ -55,7 +55,7 @@ void unlockPixels(JNIEnv *env, jobject jbitmap) { } __unused JNIEXPORT jlong JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_renderFrame(JNIEnv *env, jclass __unused handleClass, jlong gifInfo, jobject jbitmap) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_renderFrame(JNIEnv *env, jclass __unused handleClass, jlong gifInfo, jobject jbitmap) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL) return -1; diff --git a/android-gif-drawable/src/main/c/control.c b/android-gif-drawable/src/main/c/control.c index f10c3a34..0716e4ef 100644 --- a/android-gif-drawable/src/main/c/control.c +++ b/android-gif-drawable/src/main/c/control.c @@ -12,7 +12,7 @@ bool reset(GifInfo *info) { } __unused JNIEXPORT jboolean JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_reset(JNIEnv *__unused env, jclass __unused class, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_reset(JNIEnv *__unused env, jclass __unused class, jlong gifInfo) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info != NULL && reset(info)) { return JNI_TRUE; @@ -21,7 +21,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_reset(JNIEnv *__unused env, jclass __u } __unused JNIEXPORT void JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_setSpeedFactor(JNIEnv __unused *env, jclass __unused handleClass, +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_setSpeedFactor(JNIEnv __unused *env, jclass __unused handleClass, jlong gifInfo, jfloat factor) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL) { @@ -82,7 +82,7 @@ uint_fast32_t seek(GifInfo *info, uint_fast32_t desiredIndex, void *pixels) { } __unused JNIEXPORT void JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_seekToTime(JNIEnv *env, jclass __unused handleClass, +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_seekToTime(JNIEnv *env, jclass __unused handleClass, jlong gifInfo, jint desiredPos, jobject jbitmap) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL) { @@ -110,7 +110,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_seekToTime(JNIEnv *env, jclass __unused } __unused JNIEXPORT void JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_seekToFrame(JNIEnv *env, jclass __unused handleClass, +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_seekToFrame(JNIEnv *env, jclass __unused handleClass, jlong gifInfo, jint desiredIndex, jobject jbitmap) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL) { @@ -125,7 +125,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_seekToFrame(JNIEnv *env, jclass __unused } __unused JNIEXPORT void JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_saveRemainder(JNIEnv *__unused env, jclass __unused handleClass, +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_saveRemainder(JNIEnv *__unused env, jclass __unused handleClass, jlong gifInfo) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL || info->lastFrameRemainder != -1 || info->currentIndex == info->gifFilePtr->ImageCount || @@ -137,7 +137,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_saveRemainder(JNIEnv *__unused env, jcl } __unused JNIEXPORT jlong JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_restoreRemainder(JNIEnv *__unused env, +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_restoreRemainder(JNIEnv *__unused env, jclass __unused handleClass, jlong gifInfo) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL || info->lastFrameRemainder == -1 || info->gifFilePtr->ImageCount == 1 || diff --git a/android-gif-drawable/src/main/c/dispose.c b/android-gif-drawable/src/main/c/dispose.c index 12d5db24..fe3f896c 100644 --- a/android-gif-drawable/src/main/c/dispose.c +++ b/android-gif-drawable/src/main/c/dispose.c @@ -1,7 +1,7 @@ #include "gif.h" __unused JNIEXPORT void JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_free(JNIEnv *env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_free(JNIEnv *env, jclass __unused handleClass, jlong gifInfo) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL) { return; diff --git a/android-gif-drawable/src/main/c/gif.c b/android-gif-drawable/src/main/c/gif.c index 337b75db..e1f6c486 100644 --- a/android-gif-drawable/src/main/c/gif.c +++ b/android-gif-drawable/src/main/c/gif.c @@ -132,7 +132,7 @@ int directByteBufferRewind(GifInfo *info) { } __unused JNIEXPORT jlong JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_openFile(JNIEnv *env, jclass __unused class, jstring jfname) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_openFile(JNIEnv *env, jclass __unused class, jstring jfname) { if (isSourceNull(jfname, env)) { return NULL_GIF_INFO; } @@ -161,7 +161,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_openFile(JNIEnv *env, jclass __unused cl } __unused JNIEXPORT jlong JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_openByteArray(JNIEnv *env, jclass __unused class, jbyteArray bytes) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_openByteArray(JNIEnv *env, jclass __unused class, jbyteArray bytes) { if (isSourceNull(bytes, env)) { return NULL_GIF_INFO; } @@ -195,7 +195,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_openByteArray(JNIEnv *env, jclass __unus } __unused JNIEXPORT jlong JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_openDirectByteBuffer(JNIEnv *env, jclass __unused class, jobject buffer) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_openDirectByteBuffer(JNIEnv *env, jclass __unused class, jobject buffer) { jbyte *bytes = (*env)->GetDirectBufferAddress(env, buffer); jlong capacity = (*env)->GetDirectBufferCapacity(env, buffer); if (bytes == NULL || capacity <= 0) { @@ -235,7 +235,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_openDirectByteBuffer(JNIEnv *env, jclass } __unused JNIEXPORT jlong JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_openStream(JNIEnv *env, jclass __unused class, jobject stream) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_openStream(JNIEnv *env, jclass __unused class, jobject stream) { jbyteArray bufferArray = (*env)->NewByteArray(env, STREAM_BUFFER_SIZE); if (bufferArray == NULL) { throwException(env, OUT_OF_MEMORY_ERROR, OOME_MESSAGE); @@ -310,7 +310,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_openStream(JNIEnv *env, jclass __unused } __unused JNIEXPORT jint JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_extractNativeFileDescriptor(JNIEnv *env, jclass __unused handleClass, jobject fileDescriptor, jboolean closeOriginalDescriptor) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_extractNativeFileDescriptor(JNIEnv *env, jclass __unused handleClass, jobject fileDescriptor, jboolean closeOriginalDescriptor) { if (isSourceNull(fileDescriptor, env)) { return -1; } @@ -334,12 +334,12 @@ Java_pl_droidsonroids_gif_GifInfoHandle_extractNativeFileDescriptor(JNIEnv *env, } __unused JNIEXPORT jint JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_createTempNativeFileDescriptor(JNIEnv __unused *env, jclass __unused handleClass) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_createTempNativeFileDescriptor(JNIEnv __unused *env, jclass __unused handleClass) { return eventfd(0, 0); } __unused JNIEXPORT jlong JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_openNativeFileDescriptor(JNIEnv *env, jclass __unused handleClass, jint fd, jlong offset) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_openNativeFileDescriptor(JNIEnv *env, jclass __unused handleClass, jint fd, jlong offset) { if (lseek64(fd, offset, SEEK_SET) != -1) { FILE *file = fdopen(fd, "rb"); if (file == NULL) { diff --git a/android-gif-drawable/src/main/c/init.c b/android-gif-drawable/src/main/c/init.c index 142e0ae3..fed3044c 100644 --- a/android-gif-drawable/src/main/c/init.c +++ b/android-gif-drawable/src/main/c/init.c @@ -85,7 +85,7 @@ void setGCBDefaults(GraphicsControlBlock *gcb) { } __unused JNIEXPORT void JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_setOptions(__unused JNIEnv *env, jclass __unused class, jlong gifInfo, jchar sampleSize, jboolean isOpaque) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_setOptions(__unused JNIEnv *env, jclass __unused class, jlong gifInfo, jchar sampleSize, jboolean isOpaque) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL) { return; diff --git a/android-gif-drawable/src/main/c/metadata.c b/android-gif-drawable/src/main/c/metadata.c index 937aa94b..612c54d9 100644 --- a/android-gif-drawable/src/main/c/metadata.c +++ b/android-gif-drawable/src/main/c/metadata.c @@ -1,7 +1,7 @@ #include "gif.h" __unused JNIEXPORT jstring JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_getComment(JNIEnv *env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_getComment(JNIEnv *env, jclass __unused handleClass, jlong gifInfo) { GifInfo *const info = ((GifInfo *) (intptr_t) gifInfo); if (info == NULL) { return NULL; @@ -10,7 +10,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_getComment(JNIEnv *env, jclass __unused } __unused JNIEXPORT jboolean JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_isAnimationCompleted(JNIEnv __unused *env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_isAnimationCompleted(JNIEnv __unused *env, jclass __unused handleClass, jlong gifInfo) { GifInfo *info = ((GifInfo *) (intptr_t) gifInfo); if (info != NULL && info->loopCount != 0 && info->currentLoop == info->loopCount) { return JNI_TRUE; @@ -19,7 +19,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_isAnimationCompleted(JNIEnv __unused *en } __unused JNIEXPORT jint JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_getLoopCount(JNIEnv __unused *env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_getLoopCount(JNIEnv __unused *env, jclass __unused handleClass, jlong gifInfo) { GifInfo *const info = ((GifInfo *) (intptr_t) gifInfo); if (info == NULL) { return 0; @@ -28,7 +28,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_getLoopCount(JNIEnv __unused *env, jclas } __unused JNIEXPORT void JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_setLoopCount(JNIEnv __unused *env, jclass __unused handleClass, jlong gifInfo, jchar loopCount) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_setLoopCount(JNIEnv __unused *env, jclass __unused handleClass, jlong gifInfo, jchar loopCount) { GifInfo *const info = ((GifInfo *) (intptr_t) gifInfo); if (info != NULL) { info->loopCount = loopCount; @@ -36,7 +36,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_setLoopCount(JNIEnv __unused *env, jclas } __unused JNIEXPORT jint JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_getDuration(JNIEnv *__unused env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_getDuration(JNIEnv *__unused env, jclass __unused handleClass, jlong gifInfo) { GifInfo *const info = ((GifInfo *) (intptr_t) gifInfo); if (info == NULL) { return 0; @@ -49,7 +49,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_getDuration(JNIEnv *__unused env, jclas } __unused JNIEXPORT jlong JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_getSourceLength(JNIEnv __unused *env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_getSourceLength(JNIEnv __unused *env, jclass __unused handleClass, jlong gifInfo) { GifInfo *const info = ((GifInfo *) (intptr_t) gifInfo); if (info == NULL) { return -1; @@ -58,7 +58,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_getSourceLength(JNIEnv __unused *env, jc } __unused JNIEXPORT jint JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_getCurrentPosition(JNIEnv *__unused env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_getCurrentPosition(JNIEnv *__unused env, jclass __unused handleClass, jlong gifInfo) { GifInfo *const info = ((GifInfo *) (intptr_t) gifInfo); if (info == NULL) { return 0; @@ -86,7 +86,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_getCurrentPosition(JNIEnv *__unused env, } __unused JNIEXPORT jlong JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_getMetadataByteCount(JNIEnv *__unused env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_getMetadataByteCount(JNIEnv *__unused env, jclass __unused handleClass, jlong gifInfo) { GifInfo *const info = ((GifInfo *) (intptr_t) gifInfo); if (info == NULL) { return 0; @@ -99,7 +99,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_getMetadataByteCount(JNIEnv *__unused e } __unused JNIEXPORT jlong JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_getAllocationByteCount(JNIEnv *__unused env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_getAllocationByteCount(JNIEnv *__unused env, jclass __unused handleClass, jlong gifInfo) { GifInfo *const info = ((GifInfo *) (intptr_t) gifInfo); if (info == NULL) { return 0; @@ -142,7 +142,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_getAllocationByteCount(JNIEnv *__unused } __unused JNIEXPORT jint JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_getNativeErrorCode(JNIEnv *__unused env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_getNativeErrorCode(JNIEnv *__unused env, jclass __unused handleClass, jlong gifInfo) { GifInfo *const info = ((GifInfo *) (intptr_t) gifInfo); if (info == NULL) { return 0; @@ -151,7 +151,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_getNativeErrorCode(JNIEnv *__unused env } __unused JNIEXPORT jint JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_getCurrentLoop(JNIEnv __unused *env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_getCurrentLoop(JNIEnv __unused *env, jclass __unused handleClass, jlong gifInfo) { GifInfo *const info = ((GifInfo *) (intptr_t) gifInfo); if (info == NULL) { return 0; @@ -160,7 +160,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_getCurrentLoop(JNIEnv __unused *env, jcl } __unused JNIEXPORT jint JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_getCurrentFrameIndex(JNIEnv __unused *env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_getCurrentFrameIndex(JNIEnv __unused *env, jclass __unused handleClass, jlong gifInfo) { GifInfo *const info = ((GifInfo *) (intptr_t) gifInfo); if (info == NULL) { return -1; @@ -169,7 +169,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_getCurrentFrameIndex(JNIEnv __unused *en } __unused JNIEXPORT jlongArray JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_getSavedState(JNIEnv *env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_getSavedState(JNIEnv *env, jclass __unused handleClass, jlong gifInfo) { GifInfo *const info = ((GifInfo *) (intptr_t) gifInfo); if (info == NULL) { return NULL; @@ -228,7 +228,7 @@ jint restoreSavedState(GifInfo *info, JNIEnv *env, jlongArray state, void *pixel } __unused JNIEXPORT jint JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_restoreSavedState(JNIEnv *env, jclass __unused handleClass, +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_restoreSavedState(JNIEnv *env, jclass __unused handleClass, jlong gifInfo, jlongArray state, jobject jbitmap) { GifInfo *const info = ((GifInfo *) (intptr_t) gifInfo); void *pixels; @@ -241,13 +241,13 @@ Java_pl_droidsonroids_gif_GifInfoHandle_restoreSavedState(JNIEnv *env, jclass __ } __unused JNIEXPORT jint JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_getFrameDuration(__unused JNIEnv *env, jclass __unused handleClass, jlong gifInfo, jint index) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_getFrameDuration(__unused JNIEnv *env, jclass __unused handleClass, jlong gifInfo, jint index) { GifInfo *const info = ((GifInfo *) (intptr_t) gifInfo); return info == NULL ? 0 : (jint) info->controlBlock[index].DelayTime; } __unused JNIEXPORT jboolean JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_isOpaque(__unused JNIEnv *env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_isOpaque(__unused JNIEnv *env, jclass __unused handleClass, jlong gifInfo) { GifInfo *const info = ((GifInfo *) (intptr_t) gifInfo); if (info != NULL && info->isOpaque) { return JNI_TRUE; @@ -256,7 +256,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_isOpaque(__unused JNIEnv *env, jclass __ } __unused JNIEXPORT jint JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_getWidth(__unused JNIEnv *env, jclass __unused class, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_getWidth(__unused JNIEnv *env, jclass __unused class, jlong gifInfo) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL) { return 0; @@ -265,7 +265,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_getWidth(__unused JNIEnv *env, jclass __ } __unused JNIEXPORT jint JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_getHeight(__unused JNIEnv *env, jclass __unused class, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_getHeight(__unused JNIEnv *env, jclass __unused class, jlong gifInfo) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL) { return 0; @@ -274,7 +274,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_getHeight(__unused JNIEnv *env, jclass _ } __unused JNIEXPORT jint JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_getNumberOfFrames(__unused JNIEnv *env, jclass __unused class, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_getNumberOfFrames(__unused JNIEnv *env, jclass __unused class, jlong gifInfo) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL) { return 0; diff --git a/android-gif-drawable/src/main/c/opengl.c b/android-gif-drawable/src/main/c/opengl.c index a4cb4bff..de356d08 100755 --- a/android-gif-drawable/src/main/c/opengl.c +++ b/android-gif-drawable/src/main/c/opengl.c @@ -9,7 +9,7 @@ typedef struct { } TexImageDescriptor; __unused JNIEXPORT void JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_glTexImage2D(JNIEnv *__unused unused, jclass __unused handleClass, jlong gifInfo, jint target, jint level) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_glTexImage2D(JNIEnv *__unused unused, jclass __unused handleClass, jlong gifInfo, jint target, jint level) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL || info->frameBufferDescriptor == NULL) { return; @@ -24,7 +24,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_glTexImage2D(JNIEnv *__unused unused, jc } __unused JNIEXPORT void JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_glTexSubImage2D(JNIEnv *__unused env, jclass __unused handleClass, jlong gifInfo, jint target, jint level) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_glTexSubImage2D(JNIEnv *__unused env, jclass __unused handleClass, jlong gifInfo, jint target, jint level) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL || info->frameBufferDescriptor == NULL) { return; @@ -89,7 +89,7 @@ static void releaseTexImageDescriptor(GifInfo *info, JNIEnv *env) { } __unused JNIEXPORT void JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_initTexImageDescriptor(JNIEnv *env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_initTexImageDescriptor(JNIEnv *env, jclass __unused handleClass, jlong gifInfo) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL) { return; @@ -115,7 +115,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_initTexImageDescriptor(JNIEnv *env, jcla } __unused JNIEXPORT void JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_startDecoderThread(JNIEnv *env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_startDecoderThread(JNIEnv *env, jclass __unused handleClass, jlong gifInfo) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL) { return; @@ -140,7 +140,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_startDecoderThread(JNIEnv *env, jclass _ } __unused JNIEXPORT void JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_stopDecoderThread(JNIEnv *env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_stopDecoderThread(JNIEnv *env, jclass __unused handleClass, jlong gifInfo) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL || info->frameBufferDescriptor == NULL) { return; @@ -149,7 +149,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_stopDecoderThread(JNIEnv *env, jclass __ } __unused JNIEXPORT void JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_seekToFrameGL(__unused JNIEnv *env, jclass __unused handleClass, jlong gifInfo, jint desiredIndex) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_seekToFrameGL(__unused JNIEnv *env, jclass __unused handleClass, jlong gifInfo, jint desiredIndex) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL) { return; diff --git a/android-gif-drawable/src/main/c/surface.c b/android-gif-drawable/src/main/c/surface.c index 5e90da84..610d642f 100644 --- a/android-gif-drawable/src/main/c/surface.c +++ b/android-gif-drawable/src/main/c/surface.c @@ -56,7 +56,7 @@ static void releaseSurfaceDescriptor(GifInfo *info, JNIEnv *env) { } __unused JNIEXPORT void JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_bindSurface(JNIEnv *env, jclass __unused handleClass, jlong gifInfo, +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_bindSurface(JNIEnv *env, jclass __unused handleClass, jlong gifInfo, jobject jsurface, jlongArray savedState) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; SurfaceDescriptor *descriptor = info->frameBufferDescriptor; @@ -239,7 +239,7 @@ Java_pl_droidsonroids_gif_GifInfoHandle_bindSurface(JNIEnv *env, jclass __unused } __unused JNIEXPORT void JNICALL -Java_pl_droidsonroids_gif_GifInfoHandle_postUnbindSurface(JNIEnv *env, jclass __unused handleClass, jlong gifInfo) { +Java_pl_droidsonroids_gif_GifInfoHandle_00024Companion_postUnbindSurface(JNIEnv *env, jclass __unused handleClass, jlong gifInfo) { GifInfo *info = (GifInfo *) (intptr_t) gifInfo; if (info == NULL || info->frameBufferDescriptor == NULL) { return; diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/AnimationListener.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/AnimationListener.java deleted file mode 100644 index b0c0d644..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/AnimationListener.java +++ /dev/null @@ -1,13 +0,0 @@ -package pl.droidsonroids.gif; - -/** - * Interface which can be used to run some code when particular animation event occurs. - */ -public interface AnimationListener { - /** - * Called when a single loop of the animation is completed. - * - * @param loopNumber 0-based number of the completed loop, 0 for infinite animations - */ - void onAnimationCompleted(int loopNumber); -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/AnimationListener.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/AnimationListener.kt new file mode 100644 index 00000000..610728f0 --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/AnimationListener.kt @@ -0,0 +1,13 @@ +package pl.droidsonroids.gif + +/** + * Interface which can be used to run some code when particular animation event occurs. + */ +interface AnimationListener { + /** + * Called when a single loop of the animation is completed. + * + * @param loopNumber 0-based number of the completed loop, 0 for infinite animations + */ + fun onAnimationCompleted(loopNumber: Int) +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/ConditionVariable.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/ConditionVariable.java deleted file mode 100644 index a7c5f1de..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/ConditionVariable.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package pl.droidsonroids.gif; - -class ConditionVariable { - private volatile boolean mCondition; - - synchronized void set(boolean state) { - if (state) { - open(); - } else { - close(); - } - } - - synchronized void open() { - boolean old = mCondition; - mCondition = true; - if (!old) { - this.notify(); - } - } - - synchronized void close() { - mCondition = false; - } - - synchronized void block() throws InterruptedException { - while (!mCondition) { - this.wait(); - } - } -} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/ConditionVariable.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/ConditionVariable.kt new file mode 100644 index 00000000..b8112d5b --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/ConditionVariable.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pl.droidsonroids.gif + +import java.util.concurrent.locks.Condition +import java.util.concurrent.locks.Lock +import java.util.concurrent.locks.ReentrantLock + +class ConditionVariable { + @Volatile + private var mCondition: Boolean = false + private val lock: Lock = ReentrantLock() + private val condition: Condition = lock.newCondition() + + fun set(state: Boolean) { + lock.lock() + try { + if (state) { + open() + } else { + close() + } + } finally { + lock.unlock() + } + } + + fun open() { + lock.lock() + try { + val old = mCondition + mCondition = true + if (!old) { + condition.signal() + } + } finally { + lock.unlock() + } + } + + fun close() { + mCondition = false + } + + fun block() { + lock.lock() + try { + while (!mCondition) { + condition.await() + } + } finally { + lock.unlock() + } + } +} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifAnimationMetaData.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifAnimationMetaData.java deleted file mode 100644 index ff3575a2..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifAnimationMetaData.java +++ /dev/null @@ -1,316 +0,0 @@ -package pl.droidsonroids.gif; - -import android.content.ContentResolver; -import android.content.res.AssetFileDescriptor; -import android.content.res.AssetManager; -import android.content.res.Resources; -import android.net.Uri; -import android.os.Build; -import android.os.Parcel; -import android.os.Parcelable; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.IOException; -import java.io.InputStream; -import java.io.Serializable; -import java.nio.ByteBuffer; -import java.util.Locale; - -import androidx.annotation.DrawableRes; -import androidx.annotation.IntRange; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RawRes; -import pl.droidsonroids.gif.annotations.Beta; - -/** - * Lightweight version of {@link pl.droidsonroids.gif.GifDrawable} used to retrieve metadata of GIF only, - * without having to allocate the memory for its pixels. - */ -public class GifAnimationMetaData implements Serializable, Parcelable { - private static final long serialVersionUID = 5692363926580237325L; - - private final int mLoopCount; - private final int mDuration; - private final int mHeight; - private final int mWidth; - private final int mImageCount; - private final long mPixelsBytesCount; - private final long mMetadataBytesCount; - - /** - * Retrieves from resource. - * - * @param res Resources to read from - * @param id resource id - * @throws android.content.res.Resources.NotFoundException if the given ID does not exist. - * @throws java.io.IOException when opening failed - * @throws NullPointerException if res is null - */ - public GifAnimationMetaData(@NonNull Resources res, @RawRes @DrawableRes int id) throws Resources.NotFoundException, IOException { - this(res.openRawResourceFd(id)); - } - - /** - * Retrieves metadata from asset. - * - * @param assets AssetManager to read from - * @param assetName name of the asset - * @throws IOException when opening failed - * @throws NullPointerException if assets or assetName is null - */ - public GifAnimationMetaData(@NonNull AssetManager assets, @NonNull String assetName) throws IOException { - this(assets.openFd(assetName)); - } - - /** - * Constructs metadata from given file path.
- * Only metadata is read, no graphic data is decoded here. - * In practice can be called from main thread. However it will violate - * {@link android.os.StrictMode} policy if disk reads detection is enabled.
- * - * @param filePath path to the GIF file - * @throws IOException when opening failed - * @throws NullPointerException if filePath is null - */ - public GifAnimationMetaData(@NonNull String filePath) throws IOException { - this(new GifInfoHandle(filePath)); - } - - /** - * Equivalent to {@code} GifMetadata(file.getPath())} - * - * @param file the GIF file - * @throws IOException when opening failed - * @throws NullPointerException if file is null - */ - public GifAnimationMetaData(@NonNull File file) throws IOException { - this(file.getPath()); - } - - /** - * Retrieves metadata from InputStream. - * InputStream must support marking, IllegalArgumentException will be thrown otherwise. - * - * @param stream stream to read from - * @throws IOException when opening failed - * @throws IllegalArgumentException if stream does not support marking - * @throws NullPointerException if stream is null - */ - public GifAnimationMetaData(@NonNull InputStream stream) throws IOException { - this(new GifInfoHandle(stream)); - } - - /** - * Retrieves metadata from AssetFileDescriptor. - * Convenience wrapper for {@link GifAnimationMetaData#GifAnimationMetaData(FileDescriptor)} - * - * @param afd source - * @throws NullPointerException if afd is null - * @throws IOException when opening failed - */ - public GifAnimationMetaData(@NonNull AssetFileDescriptor afd) throws IOException { - this(new GifInfoHandle(afd)); - } - - /** - * Retrieves metadata from FileDescriptor - * - * @param fd source - * @throws IOException when opening failed - * @throws NullPointerException if fd is null - */ - public GifAnimationMetaData(@NonNull FileDescriptor fd) throws IOException { - this(new GifInfoHandle(fd)); - } - - /** - * Retrieves metadata from byte array.
- * It can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed. - * - * @param bytes raw GIF bytes - * @throws IOException if bytes does not contain valid GIF data - * @throws NullPointerException if bytes are null - */ - public GifAnimationMetaData(@NonNull byte[] bytes) throws IOException { - this(new GifInfoHandle(bytes)); - } - - /** - * Retrieves metadata from {@link ByteBuffer}. Only direct buffers are supported. - * Buffer can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed. - * - * @param buffer buffer containing GIF data - * @throws IOException if buffer does not contain valid GIF data or is indirect - * @throws NullPointerException if buffer is null - */ - public GifAnimationMetaData(@NonNull ByteBuffer buffer) throws IOException { - this(new GifInfoHandle(buffer)); - } - - /** - * Retrieves metadata from {@link android.net.Uri} which is resolved using {@code resolver}. - * {@link android.content.ContentResolver#openAssetFileDescriptor(android.net.Uri, String)} - * is used to open an Uri. - * - * @param uri GIF Uri, cannot be null. - * @param resolver resolver, null is allowed for file:// scheme Uris only - * @throws IOException if resolution fails or destination is not a GIF. - */ - public GifAnimationMetaData(@Nullable ContentResolver resolver, @NonNull Uri uri) throws IOException { - this(GifInfoHandle.openUri(resolver, uri)); - } - - private GifAnimationMetaData(final GifInfoHandle gifInfoHandle) { - mLoopCount = gifInfoHandle.getLoopCount(); - mDuration = gifInfoHandle.getDuration(); - mWidth = gifInfoHandle.getWidth(); - mHeight = gifInfoHandle.getHeight(); - mImageCount = gifInfoHandle.getNumberOfFrames(); - mMetadataBytesCount = gifInfoHandle.getMetadataByteCount(); - mPixelsBytesCount = gifInfoHandle.getAllocationByteCount(); - gifInfoHandle.recycle(); - } - - /** - * @return width od the GIF canvas in pixels - */ - public int getWidth() { - return mWidth; - } - - /** - * @return height od the GIF canvas in pixels - */ - public int getHeight() { - return mHeight; - } - - /** - * @return number of frames in GIF, at least one - */ - public int getNumberOfFrames() { - return mImageCount; - } - - /** - * See {@link GifDrawable#getLoopCount()} - * - * @return loop count, 0 means that animation is infinite - */ - public int getLoopCount() { - return mLoopCount; - } - - /** - * See {@link GifDrawable#getDuration()} - * - * @return duration of of one loop the animation in milliseconds. Result is always multiple of 10. - */ - public int getDuration() { - return mDuration; - } - - /** - * @return true if GIF is animated (has at least 2 frames and positive duration), false otherwise - */ - public boolean isAnimated() { - return mImageCount > 1 && mDuration > 0; - } - - /** - * Like {@link GifDrawable#getAllocationByteCount()} but does not include memory needed for backing {@link android.graphics.Bitmap}. - * {@code Bitmap} in {@code GifDrawable} may be allocated at the time of creation or existing one may be reused if {@link GifDrawableInit#with(GifDrawable)} - * is used. - * This method assumes no subsampling (sample size = 1).
- * To calculate allocation byte count of {@link GifDrawable} created from the same input source {@link #getDrawableAllocationByteCount(GifDrawable, int)} - * can be used. - * - * @return possible size of the memory needed to store pixels excluding backing {@link android.graphics.Bitmap} and assuming no subsampling - */ - public long getAllocationByteCount() { - return mPixelsBytesCount; - } - - /** - * Like {@link #getAllocationByteCount()} but includes also backing {@link android.graphics.Bitmap} and takes sample size into account. - * - * @param oldDrawable optional old drawable to be reused, pass {@code null} if there is no one - * @param sampleSize sample size, pass {@code 1} if not using subsampling - * @return possible size of the memory needed to store pixels - * @throws IllegalArgumentException if sample size out of range - */ - @Beta - public long getDrawableAllocationByteCount(@Nullable GifDrawable oldDrawable, @IntRange(from = 1, to = Character.MAX_VALUE) int sampleSize) { - if (sampleSize < 1 || sampleSize > Character.MAX_VALUE) { - throw new IllegalStateException("Sample size " + sampleSize + " out of range <1, " + Character.MAX_VALUE + ">"); - } - - final int sampleSizeFactor = sampleSize * sampleSize; - final long bufferSize; - if (oldDrawable != null && !oldDrawable.mBuffer.isRecycled()) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - bufferSize = oldDrawable.mBuffer.getAllocationByteCount(); - } else { - bufferSize = oldDrawable.getFrameByteCount(); - } - } else { - bufferSize = (mWidth * mHeight * 4) / sampleSizeFactor; - } - return (mPixelsBytesCount / sampleSizeFactor) + bufferSize; - } - - /** - * See{@link GifDrawable#getMetadataAllocationByteCount()} - * - * @return maximum possible size of the allocated memory needed to store metadata - */ - public long getMetadataAllocationByteCount() { - return mMetadataBytesCount; - } - - @Override - @NonNull - public String toString() { - final String loopCount = mLoopCount == 0 ? "Infinity" : Integer.toString(mLoopCount); - final String suffix = String.format(Locale.ENGLISH, "GIF: size: %dx%d, frames: %d, loops: %s, duration: %d", mWidth, mHeight, mImageCount, loopCount, mDuration); - return isAnimated() ? "Animated " + suffix : suffix; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mLoopCount); - dest.writeInt(mDuration); - dest.writeInt(mHeight); - dest.writeInt(mWidth); - dest.writeInt(mImageCount); - dest.writeLong(mMetadataBytesCount); - dest.writeLong(mPixelsBytesCount); - } - - private GifAnimationMetaData(Parcel in) { - mLoopCount = in.readInt(); - mDuration = in.readInt(); - mHeight = in.readInt(); - mWidth = in.readInt(); - mImageCount = in.readInt(); - mMetadataBytesCount = in.readLong(); - mPixelsBytesCount = in.readLong(); - } - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - public GifAnimationMetaData createFromParcel(Parcel source) { - return new GifAnimationMetaData(source); - } - - public GifAnimationMetaData[] newArray(int size) { - return new GifAnimationMetaData[size]; - } - }; -} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifAnimationMetaData.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifAnimationMetaData.kt new file mode 100644 index 00000000..c8461b3b --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifAnimationMetaData.kt @@ -0,0 +1,297 @@ +package pl.droidsonroids.gif + +import android.content.ContentResolver +import android.content.res.AssetFileDescriptor +import android.content.res.AssetManager +import android.content.res.Resources +import android.net.Uri +import android.os.Build +import android.os.Parcel +import android.os.Parcelable +import android.os.Parcelable.Creator +import androidx.annotation.DrawableRes +import androidx.annotation.IntRange +import androidx.annotation.RawRes +import pl.droidsonroids.gif.GifAnimationMetaData +import pl.droidsonroids.gif.GifInfoHandle.Companion.openUri +import pl.droidsonroids.gif.annotations.Beta +import java.io.File +import java.io.FileDescriptor +import java.io.IOException +import java.io.InputStream +import java.io.Serializable +import java.nio.ByteBuffer +import java.util.Locale +import kotlin.jvm.Throws + +/** + * Lightweight version of [pl.droidsonroids.gif.GifDrawable] used to retrieve metadata of GIF only, + * without having to allocate the memory for its pixels. + */ +class GifAnimationMetaData : Serializable, Parcelable { + /** + * See [GifDrawable.getLoopCount] + * + * @return loop count, 0 means that animation is infinite + */ + val loopCount: Int + + /** + * See [GifDrawable.getDuration] + * + * @return duration of one loop the animation in milliseconds. Result is always multiple of 10. + */ + val duration: Int + + /** + * @return height of the GIF canvas in pixels + */ + val height: Int + + /** + * @return width of the GIF canvas in pixels + */ + val width: Int + + /** + * @return number of frames in GIF, at least one + */ + val numberOfFrames: Int + + /** + * Like [GifDrawable.getAllocationByteCount] but does not include memory needed for backing [android.graphics.Bitmap]. + * `Bitmap` in `GifDrawable` may be allocated at the time of creation or existing one may be reused if [GifDrawableInit.with] + * is used. + * This method assumes no subsampling (sample size = 1).

+ * To calculate allocation byte count of [GifDrawable] created from the same input source [.getDrawableAllocationByteCount] + * can be used. + * + * @return possible size of the memory needed to store pixels excluding backing [android.graphics.Bitmap] and assuming no subsampling + */ + val allocationByteCount: Long + + /** + * See[GifDrawable.getAllocationByteCount] ()} + * + * @return maximum possible size of the allocated memory needed to store metadata + */ + val metadataAllocationByteCount: Long + + /** + * Retrieves from resource. + * + * @param res Resources to read from + * @param id resource id + * @throws android.content.res.Resources.NotFoundException if the given ID does not exist. + * @throws java.io.IOException when opening failed + * @throws NullPointerException if res is null + */ + @Throws(IOException::class, NullPointerException::class) + constructor(res: Resources, @RawRes @DrawableRes id: Int) : this(res.openRawResourceFd(id)) + + /** + * Retrieves metadata from asset. + * + * @param assets AssetManager to read from + * @param assetName name of the asset + * @throws IOException when opening failed + * @throws NullPointerException if assets or assetName is null + */ + @Throws(IOException::class, NullPointerException::class) + constructor(assets: AssetManager, assetName: String) : this(assets.openFd(assetName)) + + /** + * Constructs metadata from given file path.

+ * Only metadata is read, no graphic data is decoded here. + * In practice can be called from main thread. However it will violate + * [android.os.StrictMode] policy if disk reads detection is enabled.

+ * + * @param filePath path to the GIF file + * @throws IOException when opening failed + * @throws NullPointerException if filePath is null + */ + @Throws(IOException::class, NullPointerException::class) + constructor(filePath: String) : this(GifInfoHandle(filePath)) + + /** + * Equivalent to `GifMetadata(file.getPath())` + * + * @param file the GIF file + * @throws IOException when opening failed + * @throws NullPointerException if file is null + */ + @Throws(IOException::class, NullPointerException::class) + constructor(file: File) : this(file.path) + + /** + * Retrieves metadata from InputStream. + * InputStream must support marking, IllegalArgumentException will be thrown otherwise. + * + * @param stream stream to read from + * @throws IOException when opening failed + * @throws IllegalArgumentException if stream does not support marking + * @throws NullPointerException if stream is null + */ + @Throws(IOException::class, NullPointerException::class, IllegalArgumentException::class) + constructor(stream: InputStream) : this(GifInfoHandle(stream)) + + /** + * Retrieves metadata from AssetFileDescriptor. + * Convenience wrapper for [GifAnimationMetaData.GifAnimationMetaData] + * + * @param afd source + * @throws NullPointerException if afd is null + * @throws IOException when opening failed + */ + @Throws(IOException::class, NullPointerException::class) + constructor(afd: AssetFileDescriptor) : this(GifInfoHandle(afd)) + + /** + * Retrieves metadata from FileDescriptor + * + * @param fd source + * @throws IOException when opening failed + * @throws NullPointerException if fd is null + */ + @Throws(IOException::class, NullPointerException::class) + constructor(fd: FileDescriptor) : this(GifInfoHandle(fd)) + + /** + * Retrieves metadata from byte array.

+ * It can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed. + * + * @param bytes raw GIF bytes + * @throws IOException if bytes does not contain valid GIF data + * @throws NullPointerException if bytes are null + */ + @Throws(IOException::class, NullPointerException::class) + constructor(bytes: ByteArray) : this(GifInfoHandle(bytes)) + + /** + * Retrieves metadata from [ByteBuffer]. Only direct buffers are supported. + * Buffer can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed. + * + * @param buffer buffer containing GIF data + * @throws IOException if buffer does not contain valid GIF data or is indirect + * @throws NullPointerException if buffer is null + */ + @Throws(IOException::class, NullPointerException::class) + constructor(buffer: ByteBuffer) : this(GifInfoHandle(buffer)) + + /** + * Retrieves metadata from [android.net.Uri] which is resolved using `resolver`. + * [android.content.ContentResolver.openAssetFileDescriptor] + * is used to open an Uri. + * + * @param uri GIF Uri, cannot be null. + * @param resolver resolver, null is allowed for file:// scheme Uris only + * @throws IOException if resolution fails or destination is not a GIF. + */ + @Throws(IOException::class) + constructor(resolver: ContentResolver?, uri: Uri) : this( + openUri( + resolver, uri + ) + ) + + private constructor(gifInfoHandle: GifInfoHandle) { + loopCount = gifInfoHandle.loopCount + duration = gifInfoHandle.duration + width = gifInfoHandle.width + height = gifInfoHandle.height + numberOfFrames = gifInfoHandle.numberOfFrames + metadataAllocationByteCount = gifInfoHandle.metadataAllocationByteCount + allocationByteCount = gifInfoHandle.allocationByteCount + gifInfoHandle.recycle() + } + + /** + * @return true if GIF is animated (has at least 2 frames and positive duration), false otherwise + */ + val isAnimated: Boolean + get() = numberOfFrames > 1 && duration > 0 + + /** + * Like [.getAllocationByteCount] but includes also backing [android.graphics.Bitmap] and takes sample size into account. + * + * @param oldDrawable optional old drawable to be reused, pass `null` if there is no one + * @param sampleSize sample size, pass `1` if not using subsampling + * @return possible size of the memory needed to store pixels + * @throws IllegalArgumentException if sample size out of range + */ + @Throws(IllegalArgumentException::class) + @Beta + fun getDrawableAllocationByteCount( + oldDrawable: GifDrawable?, + @IntRange( + from = 1, + to = Character.MAX_VALUE.code.toLong() + ) sampleSize: Int + ): Long { + check(!(sampleSize < 1 || sampleSize > Character.MAX_VALUE.code)) { "Sample size " + sampleSize + " out of range <1, " + Character.MAX_VALUE + ">" } + val sampleSizeFactor = sampleSize * sampleSize + val bufferSize: Long = if (oldDrawable != null && !oldDrawable.mBuffer.isRecycled) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + oldDrawable.mBuffer.allocationByteCount.toLong() + } else { + oldDrawable.frameByteCount.toLong() + } + } else { + (width * height * 4 / sampleSizeFactor).toLong() + } + return allocationByteCount / sampleSizeFactor + bufferSize + } + + override fun toString(): String { + val loopCount = if (loopCount == 0) "Infinity" else loopCount.toString() + val suffix = String.format( + Locale.ENGLISH, + "GIF: size: %dx%d, frames: %d, loops: %s, duration: %d", + width, + height, + numberOfFrames, + loopCount, + duration + ) + return if (isAnimated) "Animated $suffix" else suffix + } + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeInt(loopCount) + dest.writeInt(duration) + dest.writeInt(height) + dest.writeInt(width) + dest.writeInt(numberOfFrames) + dest.writeLong(metadataAllocationByteCount) + dest.writeLong(allocationByteCount) + } + + private constructor(parcel: Parcel) { + loopCount = parcel.readInt() + duration = parcel.readInt() + height = parcel.readInt() + width = parcel.readInt() + numberOfFrames = parcel.readInt() + metadataAllocationByteCount = parcel.readLong() + allocationByteCount = parcel.readLong() + } + + companion object { + private const val serialVersionUID = 5692363926580237325L + + @JvmField + val CREATOR: Creator = object : Creator { + override fun createFromParcel(source: Parcel): GifAnimationMetaData { + return GifAnimationMetaData(source) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } +} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDecoder.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDecoder.java deleted file mode 100644 index 3f3dec8f..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDecoder.java +++ /dev/null @@ -1,171 +0,0 @@ -package pl.droidsonroids.gif; - -import android.graphics.Bitmap; - -import androidx.annotation.IntRange; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.io.IOException; - -/** - * GifDecoder allows lightweight access to GIF frames, without wrappers like Drawable or View. - * {@link Bitmap} with size equal to or greater than size of the GIF is needed. - * For access only metadata (size, number of frames etc.) without pixels see {@link GifAnimationMetaData}. - */ -public class GifDecoder { - //TODO extract common container - private final GifInfoHandle mGifInfoHandle; - - /** - * Constructs new GifDecoder. - * Equivalent of {@link #GifDecoder(InputSource, GifOptions)} with null {@code options} - * - * @param inputSource source - * @throws IOException when creation fails - */ - public GifDecoder(@NonNull final InputSource inputSource) throws IOException { - this(inputSource, null); - } - - /** - * Constructs new GifDecoder - * - * @param inputSource source - * @param options null-ok; options controlling subsampling and opacity - * @throws IOException when creation fails - */ - public GifDecoder(@NonNull final InputSource inputSource, @Nullable final GifOptions options) throws IOException { - mGifInfoHandle = inputSource.open(); - if (options != null) { - mGifInfoHandle.setOptions(options.inSampleSize, options.inIsOpaque); - } - } - - /** - * See {@link GifDrawable#getComment()} - * - * @return GIF comment - */ - public String getComment() { - return mGifInfoHandle.getComment(); - } - - /** - * See {@link GifDrawable#getLoopCount()} - * - * @return loop count, 0 means that animation is infinite - */ - public int getLoopCount() { - return mGifInfoHandle.getLoopCount(); - } - - /** - * See {@link GifDrawable#getInputSourceByteCount()} - * - * @return number of bytes backed by input source or -1 if it is unknown - */ - public long getSourceLength() { - return mGifInfoHandle.getSourceLength(); - } - - /** - * See {@link GifDrawable#seekTo(int)} - * - * @param position position to seek to in milliseconds - * @param buffer the frame buffer - * @throws IllegalArgumentException if {@code position < 0 }or {@code buffer} is recycled - */ - public void seekToTime(@IntRange(from = 0, to = Integer.MAX_VALUE) final int position, @NonNull final Bitmap buffer) { - checkBuffer(buffer); - mGifInfoHandle.seekToTime(position, buffer); - } - - /** - * See {@link GifDrawable#seekToFrame(int)} - * - * @param frameIndex position to seek to in milliseconds - * @param buffer the frame buffer - * @throws IllegalArgumentException if {@code frameIndex < 0} or {@code buffer} is recycled - */ - public void seekToFrame(@IntRange(from = 0, to = Integer.MAX_VALUE) final int frameIndex, @NonNull final Bitmap buffer) { - checkBuffer(buffer); - mGifInfoHandle.seekToFrame(frameIndex, buffer); - } - - /** - * See {@link GifDrawable#getAllocationByteCount()} - * - * @return possible size of the memory needed to store pixels of this object - */ - public long getAllocationByteCount() { - return mGifInfoHandle.getAllocationByteCount(); - } - - /** - * See {@link GifDrawable#getFrameDuration(int)} - * - * @param index index of the frame - * @return duration of the given frame in milliseconds - * @throws IndexOutOfBoundsException if {@code index < 0 || index >= } - */ - public int getFrameDuration(@IntRange(from = 0) int index) { - return mGifInfoHandle.getFrameDuration(index); - } - - /** - * See {@link GifDrawable#getDuration()} - * - * @return duration of of one loop the animation in milliseconds. Result is always multiple of 10. - */ - public int getDuration() { - return mGifInfoHandle.getDuration(); - } - - /** - * @return width od the GIF canvas in pixels - */ - public int getWidth() { - return mGifInfoHandle.getWidth(); - } - - /** - * @return height od the GIF canvas in pixels - */ - public int getHeight() { - return mGifInfoHandle.getHeight(); - } - - /** - * @return number of frames in GIF, at least one - */ - public int getNumberOfFrames() { - return mGifInfoHandle.getNumberOfFrames(); - } - - /** - * @return true if GIF is animated (has at least 2 frames and positive duration), false otherwise - */ - public boolean isAnimated() { - return mGifInfoHandle.getNumberOfFrames() > 1 && getDuration() > 0; - } - - /** - * See {@link GifDrawable#recycle()} - */ - public void recycle() { - mGifInfoHandle.recycle(); - } - - private void checkBuffer(final Bitmap buffer) { - if (buffer.isRecycled()) { - throw new IllegalArgumentException("Bitmap is recycled"); - } - if (buffer.getWidth() < mGifInfoHandle.getWidth() || buffer.getHeight() < mGifInfoHandle.getHeight()) { - throw new IllegalArgumentException("Bitmap ia too small, size must be greater than or equal to GIF size"); - } - if (buffer.getConfig() != Bitmap.Config.ARGB_8888) { - throw new IllegalArgumentException("Only Config.ARGB_8888 is supported. Current bitmap config: " + buffer.getConfig()); - } - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDecoder.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDecoder.kt new file mode 100644 index 00000000..c1a461fd --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDecoder.kt @@ -0,0 +1,147 @@ +package pl.droidsonroids.gif + +import android.graphics.Bitmap +import androidx.annotation.IntRange +import kotlin.jvm.Throws + +/** + * GifDecoder allows lightweight access to GIF frames, without wrappers like Drawable or View. + * [Bitmap] with size equal to or greater than size of the GIF is needed. + * For access only metadata (size, number of frames etc.) without pixels see [GifAnimationMetaData]. + * + * + * @constructor new GifDecoder + * @param inputSource source + * @param options null-ok; options controlling subsampling and opacity + * @throws IOException when creation fails + * + */ +class GifDecoder @JvmOverloads constructor(inputSource: InputSource, options: GifOptions? = null) { + //TODO extract common container + private val mGifInfoHandle: GifInfoHandle + + init { + mGifInfoHandle = inputSource.open() + if (options != null) { + mGifInfoHandle.setOptions(options.inSampleSize, options.inIsOpaque) + } + } + + /** + * See [GifDrawable.getComment] + * + * @return GIF comment + */ + val comment: String + get() = mGifInfoHandle.comment + + /** + * See [GifDrawable.getLoopCount] + * + * @return loop count, 0 means that animation is infinite + */ + val loopCount: Int + get() = mGifInfoHandle.loopCount + + /** + * See [GifDrawable.getInputSourceByteCount] + * + * @return number of bytes backed by input source or -1 if it is unknown + */ + val sourceLength: Long + get() = mGifInfoHandle.inputSourceByteCount + + /** + * See [GifDrawable.seekTo] + * + * @param position position to seek to in milliseconds + * @param buffer the frame buffer + * @throws IllegalArgumentException if `position < 0 `or `buffer` is recycled + */ + @Throws(IllegalArgumentException::class) + fun seekToTime(@IntRange(from = 0, to = Int.MAX_VALUE.toLong()) position: Int, buffer: Bitmap) { + checkBuffer(buffer) + mGifInfoHandle.seekToTime(position, buffer) + } + + /** + * See [GifDrawable.seekToFrame] + * + * @param frameIndex position to seek to in milliseconds + * @param buffer the frame buffer + * @throws IllegalArgumentException if `frameIndex < 0` or `buffer` is recycled + */ + @Throws(IllegalArgumentException::class) + fun seekToFrame( + @IntRange(from = 0, to = Int.MAX_VALUE.toLong()) frameIndex: Int, + buffer: Bitmap + ) { + checkBuffer(buffer) + mGifInfoHandle.seekToFrame(frameIndex, buffer) + } + + /** + * See [GifDrawable.getAllocationByteCount] + * + * @return possible size of the memory needed to store pixels of this object + */ + val allocationByteCount: Long + get() = mGifInfoHandle.allocationByteCount + + /** + * See [GifDrawable.getFrameDuration] + * + * @param index index of the frame + * @return duration of the given frame in milliseconds + * @throws IndexOutOfBoundsException if `index < 0 || index >= ` + */ + @Throws(IndexOutOfBoundsException::class) + fun getFrameDuration(@IntRange(from = 0) index: Int): Int { + return mGifInfoHandle.getFrameDuration(index) + } + + /** + * See [GifDrawable.getDuration] + * + * @return duration of of one loop the animation in milliseconds. Result is always multiple of 10. + */ + val duration: Int + get() = mGifInfoHandle.duration + + /** + * @return width od the GIF canvas in pixels + */ + val width: Int + get() = mGifInfoHandle.width + + /** + * @return height od the GIF canvas in pixels + */ + val height: Int + get() = mGifInfoHandle.height + + /** + * @return number of frames in GIF, at least one + */ + val numberOfFrames: Int + get() = mGifInfoHandle.numberOfFrames + + /** + * @return true if GIF is animated (has at least 2 frames and positive duration), false otherwise + */ + val isAnimated: Boolean + get() = mGifInfoHandle.numberOfFrames > 1 && duration > 0 + + /** + * See [GifDrawable.recycle] + */ + fun recycle() { + mGifInfoHandle.recycle() + } + + private fun checkBuffer(buffer: Bitmap) { + require(!buffer.isRecycled) { "Bitmap is recycled" } + require(!(buffer.width < mGifInfoHandle.width || buffer.height < mGifInfoHandle.height)) { "Bitmap ia too small, size must be greater than or equal to GIF size" } + require(buffer.config == Bitmap.Config.ARGB_8888) { "Only Config.ARGB_8888 is supported. Current bitmap config: ${buffer.config}" } + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawable.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawable.java deleted file mode 100644 index b3182eca..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawable.java +++ /dev/null @@ -1,1026 +0,0 @@ -package pl.droidsonroids.gif; - -import android.content.ContentResolver; -import android.content.res.AssetFileDescriptor; -import android.content.res.AssetManager; -import android.content.res.ColorStateList; -import android.content.res.Resources; -import android.content.res.Resources.NotFoundException; -import android.graphics.Bitmap; -import android.graphics.BitmapShader; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.ColorFilter; -import android.graphics.Paint; -import android.graphics.PixelFormat; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; -import android.graphics.Rect; -import android.graphics.drawable.Animatable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Build; -import android.os.StrictMode; -import android.os.SystemClock; -import android.widget.MediaController.MediaPlayerControl; - -import androidx.annotation.DrawableRes; -import androidx.annotation.FloatRange; -import androidx.annotation.IntRange; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RawRes; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.util.Locale; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -import pl.droidsonroids.gif.transforms.CornerRadiusTransform; -import pl.droidsonroids.gif.transforms.Transform; - -import static pl.droidsonroids.gif.InvalidationHandler.MSG_TYPE_INVALIDATION; - -/** - * A {@link Drawable} which can be used to hold GIF images, especially animations. - * Basic GIF metadata can also be examined. - * - * @author koral-- - */ -public class GifDrawable extends Drawable implements Animatable, MediaPlayerControl { - final ScheduledThreadPoolExecutor mExecutor; - - volatile boolean mIsRunning = true; - long mNextFrameRenderTime = Long.MIN_VALUE; - - private final Rect mDstRect = new Rect(); - /** - * Paint used to draw on a Canvas - */ - protected final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG); - /** - * Frame buffer, holds current frame. - */ - final Bitmap mBuffer; - final GifInfoHandle mNativeInfoHandle; - final ConcurrentLinkedQueue mListeners = new ConcurrentLinkedQueue<>(); - private ColorStateList mTint; - private PorterDuffColorFilter mTintFilter; - private PorterDuff.Mode mTintMode; - final boolean mIsRenderingTriggeredOnDraw; - final InvalidationHandler mInvalidationHandler; - - private final RenderTask mRenderTask = new RenderTask(this); - private final Rect mSrcRect; - ScheduledFuture mRenderTaskSchedule; - private int mScaledWidth; - private int mScaledHeight; - private Transform mTransform; - - /** - * Creates drawable from resource. - * - * @param res Resources to read from - * @param id resource id (raw or drawable) - * @throws NotFoundException if the given ID does not exist. - * @throws IOException when opening failed - * @throws NullPointerException if res is null - */ - public GifDrawable(@NonNull Resources res, @RawRes @DrawableRes int id) throws NotFoundException, IOException { - this(res.openRawResourceFd(id)); - final float densityScale = GifViewUtils.getDensityScale(res, id); - mScaledHeight = (int) (mNativeInfoHandle.getHeight() * densityScale); - mScaledWidth = (int) (mNativeInfoHandle.getWidth() * densityScale); - } - - /** - * Creates drawable from asset. - * - * @param assets AssetManager to read from - * @param assetName name of the asset - * @throws IOException when opening failed - * @throws NullPointerException if assets or assetName is null - */ - public GifDrawable(@NonNull AssetManager assets, @NonNull String assetName) throws IOException { - this(assets.openFd(assetName)); - } - - /** - * Constructs drawable from given file path.
- * Only metadata is read, no graphic data is decoded here. - * In practice can be called from main thread. However it will violate - * {@link StrictMode} policy if disk reads detection is enabled.
- * - * @param filePath path to the GIF file - * @throws IOException when opening failed - * @throws NullPointerException if filePath is null - */ - public GifDrawable(@NonNull String filePath) throws IOException { - this(new GifInfoHandle(filePath), null, null, true); - } - - /** - * Equivalent to {@code} GifDrawable(file.getPath())} - * - * @param file the GIF file - * @throws IOException when opening failed - * @throws NullPointerException if file is null - */ - public GifDrawable(@NonNull File file) throws IOException { - this(file.getPath()); - } - - /** - * Creates drawable from InputStream. - * InputStream must support marking, IllegalArgumentException will be thrown otherwise. - * - * @param stream stream to read from - * @throws IOException when opening failed - * @throws IllegalArgumentException if stream does not support marking - * @throws NullPointerException if stream is null - */ - public GifDrawable(@NonNull InputStream stream) throws IOException { - this(new GifInfoHandle(stream), null, null, true); - } - - /** - * Creates drawable from AssetFileDescriptor. - * Convenience wrapper for {@link GifDrawable#GifDrawable(FileDescriptor)} - * - * @param afd source - * @throws NullPointerException if afd is null - * @throws IOException when opening failed - */ - public GifDrawable(@NonNull AssetFileDescriptor afd) throws IOException { - this(new GifInfoHandle(afd), null, null, true); - } - - /** - * Creates drawable from FileDescriptor - * - * @param fd source - * @throws IOException when opening failed - * @throws NullPointerException if fd is null - */ - public GifDrawable(@NonNull FileDescriptor fd) throws IOException { - this(new GifInfoHandle(fd), null, null, true); - } - - /** - * Creates drawable from byte array.
- * It can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed. - * - * @param bytes raw GIF bytes - * @throws IOException if bytes does not contain valid GIF data - * @throws NullPointerException if bytes are null - */ - public GifDrawable(@NonNull byte[] bytes) throws IOException { - this(new GifInfoHandle(bytes), null, null, true); - } - - /** - * Creates drawable from {@link ByteBuffer}. Only direct buffers are supported. - * Buffer can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed. - * - * @param buffer buffer containing GIF data - * @throws IOException if buffer does not contain valid GIF data or is indirect - * @throws NullPointerException if buffer is null - */ - public GifDrawable(@NonNull ByteBuffer buffer) throws IOException { - this(new GifInfoHandle(buffer), null, null, true); - } - - /** - * Creates drawable from {@link android.net.Uri} which is resolved using {@code resolver}. - * {@link android.content.ContentResolver#openAssetFileDescriptor(android.net.Uri, String)} - * is used to open an Uri. - * - * @param uri GIF Uri, cannot be null. - * @param resolver resolver used to query {@code uri}, can be null for file:// scheme Uris - * @throws IOException if resolution fails or destination is not a GIF. - */ - public GifDrawable(@Nullable ContentResolver resolver, @NonNull Uri uri) throws IOException { - this(GifInfoHandle.openUri(resolver, uri), null, null, true); - } - - /** - * Creates drawable from {@link InputSource}. - * - * @param inputSource The {@link InputSource} concrete subclass used to construct {@link GifDrawable}. - * @param oldDrawable The old drawable that will be reused to save the memory. Can be null. - * @param executor The executor for rendering tasks. Can be null. - * @param isRenderingTriggeredOnDraw True if rendering of the next frame is scheduled after drawing current one, false otherwise. - * @param options Options controlling various GIF parameters. - * @throws IOException if input source is invalid. - */ - protected GifDrawable(@NonNull InputSource inputSource, - @Nullable GifDrawable oldDrawable, - @Nullable ScheduledThreadPoolExecutor executor, - boolean isRenderingTriggeredOnDraw, - @NonNull GifOptions options) throws IOException { - - this(inputSource.createHandleWith(options), oldDrawable, executor, isRenderingTriggeredOnDraw); - } - - GifDrawable(GifInfoHandle gifInfoHandle, final GifDrawable oldDrawable, ScheduledThreadPoolExecutor executor, boolean isRenderingTriggeredOnDraw) { - mIsRenderingTriggeredOnDraw = isRenderingTriggeredOnDraw; - mExecutor = executor != null ? executor : GifRenderingExecutor.getInstance(); - mNativeInfoHandle = gifInfoHandle; - Bitmap oldBitmap = null; - if (oldDrawable != null) { - synchronized (oldDrawable.mNativeInfoHandle) { - if (!oldDrawable.mNativeInfoHandle.isRecycled() - && oldDrawable.mNativeInfoHandle.getHeight() >= mNativeInfoHandle.getHeight() - && oldDrawable.mNativeInfoHandle.getWidth() >= mNativeInfoHandle.getWidth()) { - oldDrawable.shutdown(); - oldBitmap = oldDrawable.mBuffer; - oldBitmap.eraseColor(Color.TRANSPARENT); - } - } - } - - if (oldBitmap == null) { - mBuffer = Bitmap.createBitmap(mNativeInfoHandle.getWidth(), mNativeInfoHandle.getHeight(), Bitmap.Config.ARGB_8888); - } else { - mBuffer = oldBitmap; - } - mBuffer.setHasAlpha(!gifInfoHandle.isOpaque()); - mSrcRect = new Rect(0, 0, mNativeInfoHandle.getWidth(), mNativeInfoHandle.getHeight()); - mInvalidationHandler = new InvalidationHandler(this); - mRenderTask.doWork(); - mScaledWidth = mNativeInfoHandle.getWidth(); - mScaledHeight = mNativeInfoHandle.getHeight(); - } - - /** - * Frees any memory allocated native way. - * Operation is irreversible. After this call, nothing will be drawn. - * This method is idempotent, subsequent calls have no effect. - * Like {@link android.graphics.Bitmap#recycle()} this is an advanced call and - * is invoked implicitly by finalizer. - */ - public void recycle() { - shutdown(); - mBuffer.recycle(); - } - - private void shutdown() { - mIsRunning = false; - mInvalidationHandler.removeMessages(MSG_TYPE_INVALIDATION); - mNativeInfoHandle.recycle(); - } - - /** - * @return true if drawable is recycled - */ - public boolean isRecycled() { - return mNativeInfoHandle.isRecycled(); - } - - @Override - public void invalidateSelf() { - super.invalidateSelf(); - scheduleNextRender(); - } - - @Override - public int getIntrinsicHeight() { - return mScaledHeight; - } - - @Override - public int getIntrinsicWidth() { - return mScaledWidth; - } - - @Override - public void setAlpha(@IntRange(from = 0, to = 255) int alpha) { - mPaint.setAlpha(alpha); - } - - @Override - public void setColorFilter(@Nullable ColorFilter cf) { - mPaint.setColorFilter(cf); - } - - /** - * See {@link Drawable#getOpacity()} - * - * @return either {@link PixelFormat#TRANSPARENT} or {@link PixelFormat#OPAQUE} - * depending on current {@link Paint} and {@link GifOptions#setInIsOpaque(boolean)} used to construct this Drawable - */ - @SuppressWarnings("deprecation") - @Override - public int getOpacity() { - if (!mNativeInfoHandle.isOpaque() || mPaint.getAlpha() < 255) { - return PixelFormat.TRANSPARENT; - } - return PixelFormat.OPAQUE; - } - - /** - * Starts the animation. Does nothing if GIF is not animated. - * This method is thread-safe. - */ - @Override - public void start() { - synchronized (this) { - if (mIsRunning) { - return; - } - mIsRunning = true; - } - final long lastFrameRemainder = mNativeInfoHandle.restoreRemainder(); - startAnimation(lastFrameRemainder); - } - - void startAnimation(long lastFrameRemainder) { - if (mIsRenderingTriggeredOnDraw) { - mNextFrameRenderTime = 0; - mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0); - } else { - cancelPendingRenderTask(); - mRenderTaskSchedule = mExecutor.schedule(mRenderTask, Math.max(lastFrameRemainder, 0), TimeUnit.MILLISECONDS); - } - } - - /** - * Causes the animation to start over. - * If rewinding input source fails then state is not affected. - * This method is thread-safe. - */ - public void reset() { - mExecutor.execute(new SafeRunnable(this) { - @Override - public void doWork() { - if (mNativeInfoHandle.reset()) { - start(); - } - } - }); - } - - /** - * Stops the animation. Does nothing if GIF is not animated. - * This method is thread-safe. - */ - @Override - public void stop() { - synchronized (this) { - if (!mIsRunning) { - return; - } - mIsRunning = false; - } - - cancelPendingRenderTask(); - mNativeInfoHandle.saveRemainder(); - } - - private void cancelPendingRenderTask() { - if (mRenderTaskSchedule != null) { - mRenderTaskSchedule.cancel(false); - } - mInvalidationHandler.removeMessages(MSG_TYPE_INVALIDATION); - } - - @Override - public boolean isRunning() { - return mIsRunning; - } - - /** - * Returns GIF comment - * - * @return comment or null if there is no one defined in file - */ - @Nullable - public String getComment() { - return mNativeInfoHandle.getComment(); - } - - /** - * Returns loop count previously read from GIF's application extension block. - * Defaults to 1 if there is no such extension. - * - * @return loop count, 0 means that animation is infinite - */ - public int getLoopCount() { - return mNativeInfoHandle.getLoopCount(); - } - - /** - * Sets loop count of the animation. Loop count must be in range {@code <0 ,65535>} - * - * @param loopCount loop count, 0 means infinity - */ - public void setLoopCount(@IntRange(from = 0, to = Character.MAX_VALUE) final int loopCount) { - mNativeInfoHandle.setLoopCount(loopCount); - } - - /** - * @return basic description of the GIF including size and number of frames - */ - @Override - @NonNull - public String toString() { - return String.format(Locale.ENGLISH, "GIF: size: %dx%d, frames: %d, error: %d", - mNativeInfoHandle.getWidth(), mNativeInfoHandle.getHeight(), mNativeInfoHandle.getNumberOfFrames(), mNativeInfoHandle.getNativeErrorCode()); - } - - /** - * @return number of frames in GIF, at least one - */ - public int getNumberOfFrames() { - return mNativeInfoHandle.getNumberOfFrames(); - } - - /** - * Retrieves last error which is also the indicator of current GIF status. - * - * @return current error or {@link GifError#NO_ERROR} if there was no error or drawable is recycled - */ - @NonNull - public GifError getError() { - return GifError.fromCode(mNativeInfoHandle.getNativeErrorCode()); - } - - /** - * An {@link GifDrawable#GifDrawable(Resources, int)} wrapper but returns null - * instead of throwing exception if creation fails. - * - * @param res resources to read from - * @param resourceId resource id - * @return correct drawable or null if creation failed - */ - @Nullable - public static GifDrawable createFromResource(@NonNull Resources res, @RawRes @DrawableRes int resourceId) { - try { - return new GifDrawable(res, resourceId); - } catch (IOException ignored) { - return null; - } - } - - /** - * Sets new animation speed factor.
- * Note: If animation is in progress ({@link #draw(Canvas)}) was already called) - * then effects will be visible starting from the next frame. Duration of the currently rendered - * frame is not affected. - * - * @param factor new speed factor, eg. 0.5f means half speed, 1.0f - normal, 2.0f - double speed - * @throws IllegalArgumentException if factor<=0 - */ - public void setSpeed(@FloatRange(from = 0, fromInclusive = false) final float factor) { - mNativeInfoHandle.setSpeedFactor(factor); - } - - /** - * Equivalent of {@link #stop()} - */ - @Override - public void pause() { - stop(); - } - - /** - * Retrieves duration of one loop of the animation. - * If there is no data (no Graphics Control Extension blocks) 0 is returned. - * Note that one-frame GIFs can have non-zero duration defined in Graphics Control Extension block, - * use {@link #getNumberOfFrames()} to determine if there is one or more frames. - * - * @return duration of of one loop the animation in milliseconds. Result is always multiple of 10. - */ - @Override - public int getDuration() { - return mNativeInfoHandle.getDuration(); - } - - /** - * Retrieves elapsed time from the beginning of a current loop of animation. - * If there is only 1 frame or drawable is recycled 0 is returned. - * - * @return elapsed time from the beginning of a loop in ms - */ - @Override - public int getCurrentPosition() { - return mNativeInfoHandle.getCurrentPosition(); - } - - /** - * Seeks animation to given absolute position (within given loop) and refreshes the canvas.
- * If position is greater than duration of the loop of animation (or whole animation if there is no loop) - * then animation will be sought to the end, no exception will be thrown.
- * NOTE: all frames from current (or first one if seeking backward) to desired one must be rendered sequentially to perform seeking. - * It may take a lot of time if number of such frames is large. - * Method is thread-safe. Decoding is performed in background thread and drawable is invalidated automatically - * afterwards. - * - * @param position position to seek to in milliseconds - * @throws IllegalArgumentException if position<0 - */ - @Override - public void seekTo(@IntRange(from = 0, to = Integer.MAX_VALUE) final int position) { - if (position < 0) { - throw new IllegalArgumentException("Position is not positive"); - } - mExecutor.execute(new SafeRunnable(this) { - @Override - public void doWork() { - mNativeInfoHandle.seekToTime(position, mBuffer); - mGifDrawable.mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0); - } - }); - } - - /** - * Like {@link #seekTo(int)} but performs operation synchronously on current thread - * - * @param position position to seek to in milliseconds - * @throws IllegalArgumentException if position<0 - */ - public void seekToBlocking(@IntRange(from = 0, to = Integer.MAX_VALUE) final int position) { - if (position < 0) { - throw new IllegalArgumentException("Position is not positive"); - } - - synchronized (mNativeInfoHandle) { - mNativeInfoHandle.seekToTime(position, mBuffer); - } - mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0); - } - - /** - * Like {@link #seekTo(int)} but uses index of the frame instead of time. - * If frameIndex exceeds number of frames, seek stops at the end, no exception is thrown. - * - * @param frameIndex index of the frame to seek to (zero based) - * @throws IllegalArgumentException if frameIndex<0 - */ - public void seekToFrame(@IntRange(from = 0, to = Integer.MAX_VALUE) final int frameIndex) { - if (frameIndex < 0) { - throw new IndexOutOfBoundsException("Frame index is not positive"); - } - mExecutor.execute(new SafeRunnable(this) { - @Override - public void doWork() { - mNativeInfoHandle.seekToFrame(frameIndex, mBuffer); - mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0); - } - }); - } - - /** - * Like {@link #seekToFrame(int)} but performs operation synchronously and returns that frame. - * - * @param frameIndex index of the frame to seek to (zero based) - * @return frame at desired index - * @throws IndexOutOfBoundsException if frameIndex<0 - */ - public Bitmap seekToFrameAndGet(@IntRange(from = 0, to = Integer.MAX_VALUE) final int frameIndex) { - if (frameIndex < 0) { - throw new IndexOutOfBoundsException("Frame index is not positive"); - } - final Bitmap bitmap; - synchronized (mNativeInfoHandle) { - mNativeInfoHandle.seekToFrame(frameIndex, mBuffer); - bitmap = getCurrentFrame(); - } - mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0); - return bitmap; - } - - /** - * Like {@link #seekTo(int)} but performs operation synchronously and returns that frame. - * - * @param position position to seek to in milliseconds - * @return frame at desired position - * @throws IndexOutOfBoundsException if position<0 - */ - public Bitmap seekToPositionAndGet(@IntRange(from = 0, to = Integer.MAX_VALUE) final int position) { - if (position < 0) { - throw new IllegalArgumentException("Position is not positive"); - } - final Bitmap bitmap; - synchronized (mNativeInfoHandle) { - mNativeInfoHandle.seekToTime(position, mBuffer); - bitmap = getCurrentFrame(); - } - mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0); - return bitmap; - } - - /** - * Equivalent of {@link #isRunning()} - * - * @return true if animation is running - */ - @Override - public boolean isPlaying() { - return mIsRunning; - } - - /** - * Used by MediaPlayer for secondary progress bars. - * There is no buffer in GifDrawable, so buffer is assumed to be always full. - * - * @return always 100 - */ - @Override - public int getBufferPercentage() { - return 100; - } - - /** - * Checks whether pause is supported. - * - * @return always true, even if there is only one frame - */ - @Override - public boolean canPause() { - return true; - } - - /** - * Checks whether seeking backward can be performed. - * - * @return true if GIF has at least 2 frames - */ - @Override - public boolean canSeekBackward() { - return getNumberOfFrames() > 1; - } - - /** - * Checks whether seeking forward can be performed. - * - * @return true if GIF has at least 2 frames - */ - @Override - public boolean canSeekForward() { - return getNumberOfFrames() > 1; - } - - /** - * Used by MediaPlayer. - * GIFs contain no sound, so 0 is always returned. - * - * @return always 0 - */ - @Override - public int getAudioSessionId() { - return 0; - } - - /** - * Returns the minimum number of bytes that can be used to store pixels of the single frame. - * Returned value is the same for all the frames since it is based on the size of GIF screen. - *

This method should not be used to calculate the memory usage of the bitmap. - * Instead see {@link #getAllocationByteCount()}. - * - * @return the minimum number of bytes that can be used to store pixels of the single frame - */ - public int getFrameByteCount() { - return mBuffer.getRowBytes() * mBuffer.getHeight(); - } - - /** - * Returns size of the memory needed to store pixels of this object. It counts possible length of all frame buffers. - * Returned value may be lower than amount of actually allocated memory if GIF uses dispose to previous method but frame requiring it - * has never been needed yet. Returned value does not change during runtime. - * - * @return possible size of the memory needed to store pixels of this object - */ - public long getAllocationByteCount() { - long byteCount = mNativeInfoHandle.getAllocationByteCount(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - byteCount += mBuffer.getAllocationByteCount(); - } else { - byteCount += getFrameByteCount(); - } - return byteCount; - } - - /** - * Returns the maximum possible size of the allocated memory used to store pixels and metadata of this object. - * It counts length of all frame buffers. Returned value does not change over time. - * - * @return maximum possible size of the allocated memory needed to store metadata of this object - */ - public long getMetadataAllocationByteCount() { - return mNativeInfoHandle.getMetadataByteCount(); - } - - /** - * Returns length of the input source obtained at the opening time or -1 if - * length cannot be determined. Returned value does not change during runtime. - * If GifDrawable is constructed from {@link InputStream} -1 is always returned. - * In case of byte array and {@link ByteBuffer} length is always known. - * In other cases length -1 can be returned if length cannot be determined. - * - * @return number of bytes backed by input source or -1 if it is unknown - */ - public long getInputSourceByteCount() { - return mNativeInfoHandle.getSourceLength(); - } - - /** - * Returns in pixels[] a copy of the data in the current frame. Each value is a packed int representing a {@link Color}. - * - * @param pixels the array to receive the frame's colors - * @throws ArrayIndexOutOfBoundsException if the pixels array is too small to receive required number of pixels - */ - public void getPixels(@NonNull int[] pixels) { - mBuffer.getPixels(pixels, 0, mNativeInfoHandle.getWidth(), 0, 0, mNativeInfoHandle.getWidth(), mNativeInfoHandle.getHeight()); - } - - /** - * Returns the {@link Color} at the specified location. Throws an exception - * if x or y are out of bounds (negative or >= to the width or height - * respectively). The returned color is a non-premultiplied ARGB value. - * - * @param x The x coordinate (0...width-1) of the pixel to return - * @param y The y coordinate (0...height-1) of the pixel to return - * @return The argb {@link Color} at the specified coordinate - * @throws IllegalArgumentException if x, y exceed the drawable's bounds - * @throws IllegalStateException if drawable is recycled - */ - public int getPixel(@IntRange(from = 0) int x, @IntRange(from = 0) int y) { - if (x >= mNativeInfoHandle.getWidth()) { //need to check explicitly because reused bitmap may be larger - throw new IllegalArgumentException("x must be < width"); - } - if (y >= mNativeInfoHandle.getHeight()) { - throw new IllegalArgumentException("y must be < height"); - } - return mBuffer.getPixel(x, y); - } - - @Override - protected void onBoundsChange(Rect bounds) { - mDstRect.set(bounds); - if (mTransform != null) { - mTransform.onBoundsChange(bounds); - } - } - - /** - * Reads and renders new frame if needed then draws last rendered frame. - * - * @param canvas canvas to draw into - */ - @Override - public void draw(@NonNull Canvas canvas) { - final boolean clearColorFilter; - if (mTintFilter != null && mPaint.getColorFilter() == null) { - mPaint.setColorFilter(mTintFilter); - clearColorFilter = true; - } else { - clearColorFilter = false; - } - if (mTransform == null) { - canvas.drawBitmap(mBuffer, mSrcRect, mDstRect, mPaint); - } else { - mTransform.onDraw(canvas, mPaint, mBuffer); - } - if (clearColorFilter) { - mPaint.setColorFilter(null); - } - - } - - private void scheduleNextRender() { - if (mIsRenderingTriggeredOnDraw && mIsRunning && mNextFrameRenderTime != Long.MIN_VALUE) { - final long renderDelay = Math.max(0, mNextFrameRenderTime - SystemClock.uptimeMillis()); - mNextFrameRenderTime = Long.MIN_VALUE; - mExecutor.remove(mRenderTask); - mRenderTaskSchedule = mExecutor.schedule(mRenderTask, renderDelay, TimeUnit.MILLISECONDS); - } - } - - /** - * @return the paint used to render this drawable - */ - @NonNull - public final Paint getPaint() { - return mPaint; - } - - @Override - public int getAlpha() { - return mPaint.getAlpha(); - } - - @Override - public void setFilterBitmap(boolean filter) { - mPaint.setFilterBitmap(filter); - invalidateSelf(); - } - - @SuppressWarnings("deprecation") - @Override - public void setDither(boolean dither) { - mPaint.setDither(dither); - invalidateSelf(); - } - - /** - * Adds a new animation listener - * - * @param listener animation listener to be added, not null - * @throws java.lang.NullPointerException if listener is null - */ - public void addAnimationListener(@NonNull AnimationListener listener) { - mListeners.add(listener); - } - - /** - * Removes an animation listener - * - * @param listener animation listener to be removed - * @return true if listener collection has been modified - */ - public boolean removeAnimationListener(AnimationListener listener) { - return mListeners.remove(listener); - } - - @Override - public ColorFilter getColorFilter() { - return mPaint.getColorFilter(); - } - - /** - * Retrieves a copy of currently buffered frame. - * - * @return current frame - */ - public Bitmap getCurrentFrame() { - final Bitmap copy = mBuffer.copy(mBuffer.getConfig(), mBuffer.isMutable()); - copy.setHasAlpha(mBuffer.hasAlpha()); - return copy; - } - - private PorterDuffColorFilter updateTintFilter(ColorStateList tint, PorterDuff.Mode tintMode) { - if (tint == null || tintMode == null) { - return null; - } - - final int color = tint.getColorForState(getState(), Color.TRANSPARENT); - return new PorterDuffColorFilter(color, tintMode); - } - - @Override - public void setTintList(ColorStateList tint) { - mTint = tint; - mTintFilter = updateTintFilter(tint, mTintMode); - invalidateSelf(); - } - - @Override - public void setTintMode(@Nullable PorterDuff.Mode tintMode) { - mTintMode = tintMode; - mTintFilter = updateTintFilter(mTint, tintMode); - invalidateSelf(); - } - - @Override - protected boolean onStateChange(int[] stateSet) { - if (mTint != null && mTintMode != null) { - mTintFilter = updateTintFilter(mTint, mTintMode); - return true; - } - return false; - } - - @Override - public boolean isStateful() { - return super.isStateful() || (mTint != null && mTint.isStateful()); - } - - /** - * Sets whether this drawable is visible. If rendering of next frame is scheduled on draw current one (the default) then this method - * only calls through to the super class's implementation.
- * Otherwise (if {@link GifDrawableBuilder#setRenderingTriggeredOnDraw(boolean)} was used with true) - * when the drawable becomes invisible, it will pause its animation. A - * subsequent change to visible with restart set to true will - * restart the animation from the first frame. If restart is - * false, the animation will resume from the most recent frame. - * - * @param visible true if visible, false otherwise - * @param restart when visible and rendering is triggered on draw, true to force the animation to restart - * from the first frame - * @return true if the new visibility is different than its previous state - */ - @Override - public boolean setVisible(boolean visible, boolean restart) { - final boolean changed = super.setVisible(visible, restart); - if (!mIsRenderingTriggeredOnDraw) { - if (visible) { - if (restart) { - reset(); - } - if (changed) { - start(); - } - } else if (changed) { - stop(); - } - } - return changed; - } - - /** - * Returns zero-based index of recently rendered frame in given loop or -1 when drawable is recycled. - * - * @return index of recently rendered frame or -1 when drawable is recycled - */ - public int getCurrentFrameIndex() { - return mNativeInfoHandle.getCurrentFrameIndex(); - } - - /** - * Returns zero-based index of currently played animation loop. If animation is infinite or - * drawable is recycled 0 is returned. - * - * @return index of currently played animation loop - */ - public int getCurrentLoop() { - final int currentLoop = mNativeInfoHandle.getCurrentLoop(); - if (currentLoop == 0 || currentLoop < mNativeInfoHandle.getLoopCount()) { - return currentLoop; - } else { - return currentLoop - 1; - } - } - - /** - * Returns whether all animation loops has ended. If drawable is recycled false is returned. - * - * @return true if all animation loops has ended - */ - public boolean isAnimationCompleted() { - return mNativeInfoHandle.isAnimationCompleted(); - } - - /** - * Returns duration of the given frame (in milliseconds). If there is no data (no Graphics - * Control Extension blocks or drawable is recycled) 0 is returned. - * - * @param index index of the frame - * @return duration of the given frame in milliseconds - * @throws IndexOutOfBoundsException if index < 0 or index >= number of frames - */ - public int getFrameDuration(@IntRange(from = 0) final int index) { - return mNativeInfoHandle.getFrameDuration(index); - } - - /** - * Sets the corner radius to be applied when drawing the bitmap. - * Note that changing corner radius will cause replacing current {@link Paint} shader by {@link BitmapShader}. - * Transform set by {@link #setTransform(Transform)} will also be replaced. - * - * @param cornerRadius corner radius or 0 to remove rounding - */ - public void setCornerRadius(@FloatRange(from = 0) final float cornerRadius) { - mTransform = new CornerRadiusTransform(cornerRadius); - mTransform.onBoundsChange(mDstRect); - } - - /** - * @return The corner radius applied when drawing this drawable. 0 when drawable is not rounded. - */ - @FloatRange(from = 0) - public float getCornerRadius() { - if (mTransform instanceof CornerRadiusTransform) { - return ((CornerRadiusTransform) mTransform).getCornerRadius(); - } - return 0; - } - - /** - * Specify a {@link Transform} implementation to customize how the GIF's current Bitmap is drawn. - * - * @param transform new {@link Transform} or null to remove current one - */ - public void setTransform(@Nullable Transform transform) { - mTransform = transform; - if (mTransform != null) { - mTransform.onBoundsChange(mDstRect); - } - } - - /** - * @return The current {@link Transform} implementation that customizes - * how the GIF's current Bitmap is drawn or null if nothing has been set. - */ - @Nullable - public Transform getTransform() { - return mTransform; - } - -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawable.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawable.kt new file mode 100644 index 00000000..e298ab7d --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawable.kt @@ -0,0 +1,985 @@ +package pl.droidsonroids.gif + +import android.content.ContentResolver +import android.content.res.AssetFileDescriptor +import android.content.res.AssetManager +import android.content.res.ColorStateList +import android.content.res.Resources +import android.content.res.Resources.NotFoundException +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.Rect +import android.graphics.drawable.Animatable +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Build +import android.os.SystemClock +import android.widget.MediaController.MediaPlayerControl +import androidx.annotation.DrawableRes +import androidx.annotation.FloatRange +import androidx.annotation.IntRange +import androidx.annotation.RawRes +import pl.droidsonroids.gif.GifDrawable +import pl.droidsonroids.gif.GifError.Companion.fromCode +import pl.droidsonroids.gif.GifInfoHandle.Companion.openUri +import pl.droidsonroids.gif.GifViewUtils.getDensityScale +import pl.droidsonroids.gif.transforms.CornerRadiusTransform +import pl.droidsonroids.gif.transforms.Transform +import java.io.File +import java.io.FileDescriptor +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer +import java.util.Locale +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.ScheduledThreadPoolExecutor +import java.util.concurrent.TimeUnit +import kotlin.jvm.Throws +import kotlin.math.max + +/** + * A [Drawable] which can be used to hold GIF images, especially animations. + * Basic GIF metadata can also be examined. + * + * @author koral-- + */ +class GifDrawable internal constructor( + val mNativeInfoHandle: GifInfoHandle, + oldDrawable: GifDrawable?, + executor: ScheduledThreadPoolExecutor?, + val mIsRenderingTriggeredOnDraw: Boolean +) : Drawable(), Animatable, MediaPlayerControl { + + val mExecutor: ScheduledThreadPoolExecutor + + @Volatile + var mIsRunning = true + var mNextFrameRenderTime = Long.MIN_VALUE + private val mDstRect = Rect() + + /** + * Paint used to draw on a Canvas + */ + private val paint = Paint(Paint.FILTER_BITMAP_FLAG or Paint.DITHER_FLAG) + + /** + * Frame buffer, holds current frame. + */ + var mBuffer: Bitmap + + val mListeners = ConcurrentLinkedQueue() + private var mTint: ColorStateList? = null + private var mTintFilter: PorterDuffColorFilter? = null + private var mTintMode: PorterDuff.Mode? = null + val mInvalidationHandler: InvalidationHandler + private val mRenderTask = RenderTask(this) + private val mSrcRect: Rect + var mRenderTaskSchedule: ScheduledFuture<*>? = null + private var mScaledWidth: Int + private var mScaledHeight: Int + private var mTransform: Transform? = null + + /** + * Creates drawable from resource. + * + * @param res Resources to read from + * @param id resource id (raw or drawable) + * @throws NotFoundException if the given ID does not exist. + * @throws IOException when opening failed + * @throws NullPointerException if res is null + */ + @Throws(NotFoundException::class, IOException::class, NullPointerException::class) + constructor(res: Resources, @RawRes @DrawableRes id: Int) : this(res.openRawResourceFd(id)) { + val densityScale = getDensityScale(res, id) + mScaledHeight = (mNativeInfoHandle.height * densityScale).toInt() + mScaledWidth = (mNativeInfoHandle.width * densityScale).toInt() + } + + /** + * Creates drawable from asset. + * + * @param assets AssetManager to read from + * @param assetName name of the asset + * @throws IOException when opening failed + * @throws NullPointerException if assets or assetName is null + */ + @Throws(IOException::class, NullPointerException::class) + constructor(assets: AssetManager, assetName: String) : this(assets.openFd(assetName)) + + /** + * Constructs drawable from given file path.

+ * Only metadata is read, no graphic data is decoded here. + * In practice can be called from main thread. However it will violate + * [StrictMode] policy if disk reads detection is enabled.

+ * + * @param filePath path to the GIF file + * @throws IOException when opening failed + * @throws NullPointerException if filePath is null + */ + @Throws(IOException::class, NullPointerException::class) + constructor(filePath: String) : this(GifInfoHandle(filePath), null, null, true) + + /** + * Equivalent to `` GifDrawable(file.getPath())} + * + * @param file the GIF file + * @throws IOException when opening failed + * @throws NullPointerException if file is null + */ + @Throws(IOException::class, NullPointerException::class) + constructor(file: File) : this(file.path) + + /** + * Creates drawable from InputStream. + * InputStream must support marking, IllegalArgumentException will be thrown otherwise. + * + * @param stream stream to read from + * @throws IOException when opening failed + * @throws IllegalArgumentException if stream does not support marking + * @throws NullPointerException if stream is null + */ + @Throws(IOException::class, NullPointerException::class, IllegalArgumentException::class) + constructor(stream: InputStream) : this(GifInfoHandle(stream), null, null, true) + + /** + * Creates drawable from AssetFileDescriptor. + * Convenience wrapper for [GifDrawable.GifDrawable] + * + * @param afd source + * @throws NullPointerException if afd is null + * @throws IOException when opening failed + */ + @Throws(IOException::class, NullPointerException::class) + constructor(afd: AssetFileDescriptor) : this(GifInfoHandle(afd), null, null, true) + + /** + * Creates drawable from FileDescriptor + * + * @param fd source + * @throws IOException when opening failed + * @throws NullPointerException if fd is null + */ + @Throws(IOException::class, NullPointerException::class) + constructor(fd: FileDescriptor) : this(GifInfoHandle(fd), null, null, true) + + /** + * Creates drawable from byte array.

+ * It can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed. + * + * @param bytes raw GIF bytes + * @throws IOException if bytes does not contain valid GIF data + * @throws NullPointerException if bytes are null + */ + @Throws(IOException::class, NullPointerException::class) + constructor(bytes: ByteArray) : this(GifInfoHandle(bytes), null, null, true) + + /** + * Creates drawable from [ByteBuffer]. Only direct buffers are supported. + * Buffer can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed. + * + * @param buffer buffer containing GIF data + * @throws IOException if buffer does not contain valid GIF data or is indirect + * @throws NullPointerException if buffer is null + */ + @Throws(IOException::class, NullPointerException::class) + constructor(buffer: ByteBuffer) : this(GifInfoHandle(buffer), null, null, true) + + /** + * Creates drawable from [android.net.Uri] which is resolved using `resolver`. + * [android.content.ContentResolver.openAssetFileDescriptor] + * is used to open an Uri. + * + * @param uri GIF Uri, cannot be null. + * @param resolver resolver used to query `uri`, can be null for file:// scheme Uris + * @throws IOException if resolution fails or destination is not a GIF. + */ + @Throws(IOException::class) + constructor(resolver: ContentResolver?, uri: Uri) : this( + openUri( + resolver, uri + ), null, null, true + ) + + /** + * Creates drawable from [InputSource]. + * + * @param inputSource The [InputSource] concrete subclass used to construct [GifDrawable]. + * @param oldDrawable The old drawable that will be reused to save the memory. Can be null. + * @param executor The executor for rendering tasks. Can be null. + * @param isRenderingTriggeredOnDraw True if rendering of the next frame is scheduled after drawing current one, false otherwise. + * @param options Options controlling various GIF parameters. + * @throws IOException if input source is invalid. + */ + @Throws(IOException::class) + protected constructor( + inputSource: InputSource, + oldDrawable: GifDrawable?, + executor: ScheduledThreadPoolExecutor?, + isRenderingTriggeredOnDraw: Boolean, + options: GifOptions + ) : this( + inputSource.createHandleWith(options), + oldDrawable, + executor, + isRenderingTriggeredOnDraw + ) + + init { + mExecutor = executor ?: GifRenderingExecutor + var oldBitmap: Bitmap? = null + if (oldDrawable != null) { + synchronized(oldDrawable.mNativeInfoHandle) { + if ((!oldDrawable.mNativeInfoHandle.isRecycled + && (oldDrawable.mNativeInfoHandle.height >= mNativeInfoHandle.height + ) && (oldDrawable.mNativeInfoHandle.width >= mNativeInfoHandle.width)) + ) { + oldDrawable.shutdown() + oldBitmap = oldDrawable.mBuffer + oldBitmap!!.eraseColor(Color.TRANSPARENT) + } + } + } + mBuffer = oldBitmap + ?: Bitmap.createBitmap( + mNativeInfoHandle.width, + mNativeInfoHandle.height, + Bitmap.Config.ARGB_8888 + ) + mBuffer.setHasAlpha(!mNativeInfoHandle.isOpaque) + mSrcRect = Rect(0, 0, mNativeInfoHandle.width, mNativeInfoHandle.height) + mInvalidationHandler = InvalidationHandler(this) + mRenderTask.doWork() + mScaledWidth = mNativeInfoHandle.width + mScaledHeight = mNativeInfoHandle.height + } + + /** + * Frees any memory allocated native way. + * Operation is irreversible. After this call, nothing will be drawn. + * This method is idempotent, subsequent calls have no effect. + * Like [android.graphics.Bitmap.recycle] this is an advanced call and + * is invoked implicitly by finalizer. + */ + fun recycle() { + shutdown() + mBuffer.recycle() + } + + private fun shutdown() { + mIsRunning = false + mInvalidationHandler.removeMessages(InvalidationHandler.MSG_TYPE_INVALIDATION) + mNativeInfoHandle.recycle() + } + + val isRecycled: Boolean + /** + * @return true if drawable is recycled + */ + get() = mNativeInfoHandle.isRecycled + + override fun invalidateSelf() { + super.invalidateSelf() + scheduleNextRender() + } + + override fun getIntrinsicHeight(): Int { + return mScaledHeight + } + + override fun getIntrinsicWidth(): Int { + return mScaledWidth + } + + override fun setAlpha(@IntRange(from = 0, to = 255) alpha: Int) { + paint.alpha = alpha + } + + override fun setColorFilter(cf: ColorFilter?) { + paint.colorFilter = cf + } + + /** + * See [Drawable.getOpacity] + * + * @return either [PixelFormat.TRANSPARENT] or [PixelFormat.OPAQUE] + * depending on current [Paint] and [GifOptions#setInIsOpaque] used to construct this Drawable + */ + @Deprecated("Deprecated in Java") + override fun getOpacity(): Int { + return if (!mNativeInfoHandle.isOpaque || paint.alpha < 255) { + PixelFormat.TRANSPARENT + } else PixelFormat.OPAQUE + } + + /** + * Starts the animation. Does nothing if GIF is not animated. + * This method is thread-safe. + */ + override fun start() { + synchronized(this) { + if (mIsRunning) { + return + } + mIsRunning = true + } + val lastFrameRemainder = mNativeInfoHandle.restoreRemainder() + startAnimation(lastFrameRemainder) + } + + fun startAnimation(lastFrameRemainder: Long) { + if (mIsRenderingTriggeredOnDraw) { + mNextFrameRenderTime = 0 + mInvalidationHandler.sendEmptyMessageAtTime( + InvalidationHandler.MSG_TYPE_INVALIDATION, + 0 + ) + } else { + cancelPendingRenderTask() + mRenderTaskSchedule = mExecutor.schedule( + mRenderTask, + max(lastFrameRemainder, 0), + TimeUnit.MILLISECONDS + ) + } + } + + /** + * Causes the animation to start over. + * If rewinding input source fails then state is not affected. + * This method is thread-safe. + */ + fun reset() { + mExecutor.execute(object : SafeRunnable(this) { + override fun doWork() { + if (mNativeInfoHandle.reset()) { + start() + } + } + }) + } + + /** + * Stops the animation. Does nothing if GIF is not animated. + * This method is thread-safe. + */ + override fun stop() { + synchronized(this) { + if (!mIsRunning) { + return + } + mIsRunning = false + } + cancelPendingRenderTask() + mNativeInfoHandle.saveRemainder() + } + + private fun cancelPendingRenderTask() { + if (mRenderTaskSchedule != null) { + mRenderTaskSchedule!!.cancel(false) + } + mInvalidationHandler.removeMessages(InvalidationHandler.MSG_TYPE_INVALIDATION) + } + + override fun isRunning(): Boolean { + return mIsRunning + } + + /** + * Returns GIF comment + * + * @return comment or null if there is no one defined in file + */ + val comment: String + get() = mNativeInfoHandle.comment + + /** + * Returns loop count previously read from GIF's application extension block. + * Defaults to 1 if there is no such extension. + * + * Sets loop count of the animation. Loop count must be in range `<0 ,65535>` + * @param loopCount loop count, 0 means infinity + * + * @return loop count, 0 means that animation is infinite + */ + var loopCount: Int + get() = mNativeInfoHandle.loopCount + set(loopCount) { + mNativeInfoHandle.loopCount = loopCount + } + + /** + * @return basic description of the GIF including size and number of frames + */ + override fun toString(): String { + return String.format( + Locale.ENGLISH, + "GIF: size: %dx%d, frames: %d, error: %d", + mNativeInfoHandle.width, + mNativeInfoHandle.height, + mNativeInfoHandle.numberOfFrames, + mNativeInfoHandle.nativeErrorCode + ) + } + + /** + * @return number of frames in GIF, at least one + */ + val numberOfFrames: Int + get() = mNativeInfoHandle.numberOfFrames + + /** + * Retrieves last error which is also the indicator of current GIF status. + * + * @return current error or [GifError.NO_ERROR] if there was no error or drawable is recycled + */ + val error: GifError + get() = fromCode(mNativeInfoHandle.nativeErrorCode) + + /** + * Sets new animation speed factor.

+ * Note: If animation is in progress ([.draw]) was already called) + * then effects will be visible starting from the next frame. Duration of the currently rendered + * frame is not affected. + * + * @param factor new speed factor, eg. 0.5f means half speed, 1.0f - normal, 2.0f - double speed + * @throws IllegalArgumentException if factor<=0 + */ + @Throws(IllegalArgumentException::class) + fun setSpeed(@FloatRange(from = 0.0, fromInclusive = false) factor: Float) { + mNativeInfoHandle.setSpeedFactor(factor) + } + + /** + * Equivalent of [.stop] + */ + override fun pause() { + stop() + } + + /** + * Retrieves duration of one loop of the animation. + * If there is no data (no Graphics Control Extension blocks) 0 is returned. + * Note that one-frame GIFs can have non-zero duration defined in Graphics Control Extension block, + * use [.getNumberOfFrames] to determine if there is one or more frames. + * + * @return duration of of one loop the animation in milliseconds. Result is always multiple of 10. + */ + override fun getDuration(): Int { + return mNativeInfoHandle.duration + } + + /** + * Retrieves elapsed time from the beginning of a current loop of animation. + * If there is only 1 frame or drawable is recycled 0 is returned. + * + * @return elapsed time from the beginning of a loop in ms + */ + override fun getCurrentPosition(): Int { + return mNativeInfoHandle.currentPosition + } + + /** + * Seeks animation to given absolute position (within given loop) and refreshes the canvas.

+ * If `position` is greater than duration of the loop of animation (or whole animation if there is no loop) + * then animation will be sought to the end, no exception will be thrown.

+ * NOTE: all frames from current (or first one if seeking backward) to desired one must be rendered sequentially to perform seeking. + * It may take a lot of time if number of such frames is large. + * Method is thread-safe. Decoding is performed in background thread and drawable is invalidated automatically + * afterwards. + * + * @param position position to seek to in milliseconds + * @throws IllegalArgumentException if `position`<0 + */ + @Throws(IllegalArgumentException::class) + override fun seekTo(@IntRange(from = 0, to = Int.MAX_VALUE.toLong()) position: Int) { + require(position >= 0) { "Position is not positive" } + mExecutor.execute(object : SafeRunnable(this) { + override fun doWork() { + mNativeInfoHandle.seekToTime(position, mBuffer) + mGifDrawable.mInvalidationHandler.sendEmptyMessageAtTime( + InvalidationHandler.MSG_TYPE_INVALIDATION, + 0 + ) + } + }) + } + + /** + * Like [.seekTo] but performs operation synchronously on current thread + * + * @param position position to seek to in milliseconds + * @throws IllegalArgumentException if `position`<0 + */ + @Throws(IllegalArgumentException::class) + fun seekToBlocking(@IntRange(from = 0, to = Int.MAX_VALUE.toLong()) position: Int) { + require(position >= 0) { "Position is not positive" } + synchronized(mNativeInfoHandle) { mNativeInfoHandle.seekToTime(position, mBuffer) } + mInvalidationHandler.sendEmptyMessageAtTime(InvalidationHandler.MSG_TYPE_INVALIDATION, 0) + } + + /** + * Like [.seekTo] but uses index of the frame instead of time. + * If `frameIndex` exceeds number of frames, seek stops at the end, no exception is thrown. + * + * @param frameIndex index of the frame to seek to (zero based) + * @throws IllegalArgumentException if `frameIndex`<0 + */ + @Throws(IllegalArgumentException::class) + fun seekToFrame(@IntRange(from = 0, to = Int.MAX_VALUE.toLong()) frameIndex: Int) { + if (frameIndex < 0) { + throw IndexOutOfBoundsException("Frame index is not positive") + } + mExecutor.execute(object : SafeRunnable(this) { + override fun doWork() { + mNativeInfoHandle.seekToFrame(frameIndex, mBuffer) + mInvalidationHandler.sendEmptyMessageAtTime( + InvalidationHandler.MSG_TYPE_INVALIDATION, + 0 + ) + } + }) + } + + /** + * Like [.seekToFrame] but performs operation synchronously and returns that frame. + * + * @param frameIndex index of the frame to seek to (zero based) + * @return frame at desired index + * @throws IndexOutOfBoundsException if frameIndex<0 + */ + @Throws(IndexOutOfBoundsException::class) + fun seekToFrameAndGet( + @IntRange( + from = 0, + to = Int.MAX_VALUE.toLong() + ) frameIndex: Int + ): Bitmap { + if (frameIndex < 0) { + throw IndexOutOfBoundsException("Frame index is not positive") + } + val bitmap: Bitmap + synchronized(mNativeInfoHandle) { + mNativeInfoHandle.seekToFrame(frameIndex, mBuffer) + bitmap = currentFrame + } + mInvalidationHandler.sendEmptyMessageAtTime(InvalidationHandler.MSG_TYPE_INVALIDATION, 0) + return bitmap + } + + /** + * Like [.seekTo] but performs operation synchronously and returns that frame. + * + * @param position position to seek to in milliseconds + * @return frame at desired position + * @throws IndexOutOfBoundsException if position<0 + */ + @Throws(IndexOutOfBoundsException::class) + fun seekToPositionAndGet( + @IntRange( + from = 0, + to = Int.MAX_VALUE.toLong() + ) position: Int + ): Bitmap { + require(position >= 0) { "Position is not positive" } + val bitmap: Bitmap + synchronized(mNativeInfoHandle) { + mNativeInfoHandle.seekToTime(position, mBuffer) + bitmap = currentFrame + } + mInvalidationHandler.sendEmptyMessageAtTime(InvalidationHandler.MSG_TYPE_INVALIDATION, 0) + return bitmap + } + + /** + * Equivalent of [.isRunning] + * + * @return true if animation is running + */ + override fun isPlaying(): Boolean { + return mIsRunning + } + + /** + * Used by MediaPlayer for secondary progress bars. + * There is no buffer in GifDrawable, so buffer is assumed to be always full. + * + * @return always 100 + */ + override fun getBufferPercentage(): Int { + return 100 + } + + /** + * Checks whether pause is supported. + * + * @return always true, even if there is only one frame + */ + override fun canPause(): Boolean { + return true + } + + /** + * Checks whether seeking backward can be performed. + * + * @return true if GIF has at least 2 frames + */ + override fun canSeekBackward(): Boolean { + return numberOfFrames > 1 + } + + /** + * Checks whether seeking forward can be performed. + * + * @return true if GIF has at least 2 frames + */ + override fun canSeekForward(): Boolean { + return numberOfFrames > 1 + } + + /** + * Used by MediaPlayer. + * GIFs contain no sound, so 0 is always returned. + * + * @return always 0 + */ + override fun getAudioSessionId(): Int { + return 0 + } + + /** + * Returns the minimum number of bytes that can be used to store pixels of the single frame. + * Returned value is the same for all the frames since it is based on the size of GIF screen. + * + * This method should not be used to calculate the memory usage of the bitmap. + * Instead see [.getAllocationByteCount]. + * + * @return the minimum number of bytes that can be used to store pixels of the single frame + */ + val frameByteCount: Int + get() = mBuffer.rowBytes * mBuffer.height + + /** + * Returns size of the memory needed to store pixels of this object. It counts possible length of all frame buffers. + * Returned value may be lower than amount of actually allocated memory if GIF uses dispose to previous method but frame requiring it + * has never been needed yet. Returned value does not change during runtime. + * + * @return possible size of the memory needed to store pixels of this object + */ + val allocationByteCount: Long + get() { + var byteCount = mNativeInfoHandle.allocationByteCount + byteCount += if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + mBuffer.allocationByteCount.toLong() + } else { + frameByteCount.toLong() + } + return byteCount + } + + /** + * Returns in pixels[] a copy of the data in the current frame. Each value is a packed int representing a [Color]. + * + * @param pixels the array to receive the frame's colors + * @throws ArrayIndexOutOfBoundsException if the pixels array is too small to receive required number of pixels + */ + @Throws(ArrayIndexOutOfBoundsException::class) + fun getPixels(pixels: IntArray) { + mBuffer.getPixels( + pixels, + 0, + mNativeInfoHandle.width, + 0, + 0, + mNativeInfoHandle.width, + mNativeInfoHandle.height + ) + } + + /** + * Returns the [Color] at the specified location. Throws an exception + * if x or y are out of bounds (negative or >= to the width or height + * respectively). The returned color is a non-premultiplied ARGB value. + * + * @param x The x coordinate (0...width-1) of the pixel to return + * @param y The y coordinate (0...height-1) of the pixel to return + * @return The argb [Color] at the specified coordinate + * @throws IllegalArgumentException if x, y exceed the drawable's bounds + * @throws IllegalStateException if drawable is recycled + */ + @Throws(IllegalArgumentException::class, IllegalStateException::class) + fun getPixel(@IntRange(from = 0) x: Int, @IntRange(from = 0) y: Int): Int { + require(x < mNativeInfoHandle.width) { //need to check explicitly because reused bitmap may be larger + "x must be < width" + } + if (y >= mNativeInfoHandle.height) { + throw IllegalArgumentException("y must be < height") + } + return mBuffer.getPixel(x, y) + } + + override fun onBoundsChange(bounds: Rect) { + mDstRect.set(bounds) + if (mTransform != null) { + mTransform!!.onBoundsChange(bounds) + } + } + + /** + * Reads and renders new frame if needed then draws last rendered frame. + * + * @param canvas canvas to draw into + */ + override fun draw(canvas: Canvas) { + val clearColorFilter: Boolean + if (mTintFilter != null && paint.colorFilter == null) { + paint.colorFilter = mTintFilter + clearColorFilter = true + } else { + clearColorFilter = false + } + if (mTransform == null) { + canvas.drawBitmap(mBuffer, mSrcRect, mDstRect, paint) + } else { + mTransform!!.onDraw(canvas, paint, mBuffer) + } + if (clearColorFilter) { + paint.colorFilter = null + } + } + + private fun scheduleNextRender() { + if (mIsRenderingTriggeredOnDraw && mIsRunning && mNextFrameRenderTime != Long.MIN_VALUE) { + val renderDelay = max(0, mNextFrameRenderTime - SystemClock.uptimeMillis()) + mNextFrameRenderTime = Long.MIN_VALUE + mExecutor.remove(mRenderTask) + mRenderTaskSchedule = + mExecutor.schedule(mRenderTask, renderDelay, TimeUnit.MILLISECONDS) + } + } + + override fun getAlpha(): Int { + return paint.alpha + } + + override fun setFilterBitmap(filter: Boolean) { + paint.isFilterBitmap = filter + invalidateSelf() + } + + @Deprecated("Deprecated in Java") + override fun setDither(dither: Boolean) { + paint.isDither = dither + invalidateSelf() + } + + /** + * Adds a new animation listener + * + * @param listener animation listener to be added, not null + * @throws NullPointerException if listener is null + */ + @Throws(NullPointerException::class) + fun addAnimationListener(listener: AnimationListener) { + mListeners.add(listener) + } + + /** + * Removes an animation listener + * + * @param listener animation listener to be removed + * @return true if listener collection has been modified + */ + fun removeAnimationListener(listener: AnimationListener): Boolean { + return mListeners.remove(listener) + } + + override fun getColorFilter(): ColorFilter? { + return paint.colorFilter + } + + /** + * Retrieves a copy of currently buffered frame. + * + * @return current frame + */ + private val currentFrame: Bitmap + get() { + val copy = mBuffer.copy(mBuffer.config, mBuffer.isMutable) + copy.setHasAlpha(mBuffer.hasAlpha()) + return copy + } + + private fun updateTintFilter( + tint: ColorStateList?, + tintMode: PorterDuff.Mode? + ): PorterDuffColorFilter? { + if (tint == null || tintMode == null) { + return null + } + val color = tint.getColorForState(state, Color.TRANSPARENT) + return PorterDuffColorFilter(color, tintMode) + } + + override fun setTintList(tint: ColorStateList?) { + mTint = tint + mTintFilter = updateTintFilter(tint, mTintMode) + invalidateSelf() + } + + override fun setTintMode(tintMode: PorterDuff.Mode?) { + mTintMode = tintMode + mTintFilter = updateTintFilter(mTint, tintMode) + invalidateSelf() + } + + override fun onStateChange(stateSet: IntArray): Boolean { + if (mTint != null && mTintMode != null) { + mTintFilter = updateTintFilter(mTint, mTintMode) + return true + } + return false + } + + override fun isStateful(): Boolean { + return super.isStateful() || mTint != null && mTint!!.isStateful + } + + /** + * Sets whether this drawable is visible. If rendering of next frame is scheduled on draw current one (the default) then this method + * only calls through to the super class's implementation.

+ * Otherwise (if [GifDrawableBuilder.setRenderingTriggeredOnDraw] was used with `true`) + * when the drawable becomes invisible, it will pause its animation. A + * subsequent change to visible with `restart` set to true will + * restart the animation from the first frame. If `restart` is + * false, the animation will resume from the most recent frame. + * + * @param visible true if visible, false otherwise + * @param restart when visible and rendering is triggered on draw, true to force the animation to restart + * from the first frame + * @return true if the new visibility is different than its previous state + */ + override fun setVisible(visible: Boolean, restart: Boolean): Boolean { + val changed = super.setVisible(visible, restart) + if (!mIsRenderingTriggeredOnDraw) { + if (visible) { + if (restart) { + reset() + } + if (changed) { + start() + } + } else if (changed) { + stop() + } + } + return changed + } + + /** + * Returns zero-based index of recently rendered frame in given loop or -1 when drawable is recycled. + * + * @return index of recently rendered frame or -1 when drawable is recycled + */ + val currentFrameIndex: Int + get() = mNativeInfoHandle.currentFrameIndex + + /** + * Returns zero-based index of currently played animation loop. If animation is infinite or + * drawable is recycled 0 is returned. + * + * @return index of currently played animation loop + */ + val currentLoop: Int + get() { + val currentLoop = mNativeInfoHandle.currentLoop + return if (currentLoop == 0 || currentLoop < mNativeInfoHandle.loopCount) { + currentLoop + } else { + currentLoop - 1 + } + } + + /** + * Returns whether all animation loops has ended. If drawable is recycled false is returned. + * + * @return true if all animation loops has ended + */ + val isAnimationCompleted: Boolean + get() = mNativeInfoHandle.isAnimationCompleted + + /** + * Returns duration of the given frame (in milliseconds). If there is no data (no Graphics + * Control Extension blocks or drawable is recycled) 0 is returned. + * + * @param index index of the frame + * @return duration of the given frame in milliseconds + * @throws IndexOutOfBoundsException if index < 0 or index >= number of frames + */ + @Throws(IndexOutOfBoundsException::class) + fun getFrameDuration(@IntRange(from = 0) index: Int): Int { + return mNativeInfoHandle.getFrameDuration(index) + } + + /** + * @return The corner radius applied when drawing this drawable. 0 when drawable is not rounded. + + * Sets the corner radius to be applied when drawing the bitmap. + * Note that changing corner radius will cause replacing current [Paint] shader by [BitmapShader]. + * Transform set by [.setTransform] will also be replaced. + * + * @param cornerRadius corner radius or 0 to remove rounding + */ + @get:FloatRange(from = 0.0) + var cornerRadius: Float + get() { + return if (mTransform is CornerRadiusTransform) { + (mTransform as CornerRadiusTransform).cornerRadius + } else 0.0F + } + set(cornerRadius) { + mTransform = CornerRadiusTransform(cornerRadius) + (mTransform as CornerRadiusTransform).onBoundsChange(mDstRect) + } + + /** + * @return The current [Transform] implementation that customizes + * how the GIF's current Bitmap is drawn or null if nothing has been set. + * + * Specify a [Transform] implementation to customize how the GIF's current Bitmap is drawn. + * + * @param transform new [Transform] or null to remove current one + */ + var transform: Transform? + get() = mTransform + set(transform) { + mTransform = transform + if (mTransform != null) { + mTransform!!.onBoundsChange(mDstRect) + } + } + + companion object { + /** + * An [GifDrawable.GifDrawable] wrapper but returns null + * instead of throwing exception if creation fails. + * + * @param res resources to read from + * @param resourceId resource id + * @return correct drawable or null if creation failed + */ + fun createFromResource(res: Resources, @RawRes @DrawableRes resourceId: Int): GifDrawable? { + return try { + GifDrawable(res, resourceId) + } catch (ignored: IOException) { + null + } + } + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawableBuilder.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawableBuilder.java deleted file mode 100644 index fa494b09..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawableBuilder.java +++ /dev/null @@ -1,13 +0,0 @@ -package pl.droidsonroids.gif; - -/** - * Builder for {@link pl.droidsonroids.gif.GifDrawable} which can be used to construct new drawables - * by reusing old ones. - */ -public class GifDrawableBuilder extends GifDrawableInit{ - - @Override - protected GifDrawableBuilder self() { - return this; - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawableBuilder.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawableBuilder.kt new file mode 100644 index 00000000..f322a96d --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawableBuilder.kt @@ -0,0 +1,10 @@ +package pl.droidsonroids.gif + +/** + * Builder for [pl.droidsonroids.gif.GifDrawable] which can be used to construct new drawables + * by reusing old ones. + */ +class GifDrawableBuilder : GifDrawableInit() { + + override fun self(): GifDrawableBuilder = this +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawableInit.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawableInit.java deleted file mode 100644 index b92cc9b6..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawableInit.java +++ /dev/null @@ -1,305 +0,0 @@ -package pl.droidsonroids.gif; - -import android.content.ContentResolver; -import android.content.res.AssetFileDescriptor; -import android.content.res.AssetManager; -import android.content.res.Resources; -import android.net.Uri; -import androidx.annotation.IntRange; -import androidx.annotation.Nullable; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.util.concurrent.ScheduledThreadPoolExecutor; - -import pl.droidsonroids.gif.annotations.Beta; - -/** - * The base class for using the builder pattern with subclasses. - * - * @param The type of the builder that is a subclass of this class. - * @see link - */ -public abstract class GifDrawableInit> { - - private InputSource mInputSource; - private GifDrawable mOldDrawable; - private ScheduledThreadPoolExecutor mExecutor; - private boolean mIsRenderingTriggeredOnDraw = true; - private final GifOptions mOptions = new GifOptions(); - - /** - * Used in accordance with `getThis()` pattern. - * - * @return The builder type. - * @see link - */ - protected abstract T self(); - - /** - * Sample size controlling subsampling, see {@link GifOptions#setInSampleSize(int)} for more details. - * Note that this call will overwrite sample size set previously by {@link #options(GifOptions)} - * - * @param sampleSize the sample size - * @return this builder instance, to chain calls - */ - public T sampleSize(@IntRange(from = 1, to = Character.MAX_VALUE) final int sampleSize) { - mOptions.setInSampleSize(sampleSize); - return self(); - } - - /** - * Appropriate constructor wrapper. Must be preceded by on of {@code from()} calls. - * - * @return new drawable instance - * @throws IOException when creation fails - */ - public GifDrawable build() throws IOException { - if (mInputSource == null) { - throw new NullPointerException("Source is not set"); - } - return mInputSource.createGifDrawable(mOldDrawable, mExecutor, mIsRenderingTriggeredOnDraw, mOptions); - } - - /** - * Sets drawable to be reused when creating new one. - * - * @param drawable drawable to be reused - * @return this builder instance, to chain calls - */ - public T with(GifDrawable drawable) { - mOldDrawable = drawable; - return self(); - } - - /** - * Sets thread pool size for rendering tasks. - * Warning: custom executor set by {@link #taskExecutor(java.util.concurrent.ScheduledThreadPoolExecutor)} - * will be overwritten after setting pool size - * - * @param threadPoolSize size of the pool - * @return this builder instance, to chain calls - */ - public T threadPoolSize(int threadPoolSize) { - mExecutor = new ScheduledThreadPoolExecutor(threadPoolSize); - return self(); - } - - /** - * Sets or resets executor for rendering tasks. - * Warning: value set by {@link #threadPoolSize(int)} will not be taken into account after setting executor - * - * @param executor executor to be used or null for default (each drawable instance has its own executor) - * @return this builder instance, to chain calls - */ - public T taskExecutor(ScheduledThreadPoolExecutor executor) { - mExecutor = executor; - return self(); - } - - /** - * Sets whether rendering of the next frame is scheduled after drawing current one (so animation - * will be paused if drawing does not happen) or just after rendering frame (no matter if it is - * drawn or not). However animation will never run if drawable is set to not visible. See - * {@link GifDrawable#isVisible()} for more information about drawable visibility. - * By default this option is enabled. Note that drawing does not happen if view containing - * drawable is obscured. Disabling this option will prevent that however battery draining will be - * higher. - * - * @param isRenderingTriggeredOnDraw whether rendering of the next frame is scheduled after drawing (default) - * current one or just after it is rendered - * @return this builder instance, to chain calls - */ - public T renderingTriggeredOnDraw(boolean isRenderingTriggeredOnDraw) { - mIsRenderingTriggeredOnDraw = isRenderingTriggeredOnDraw; - return self(); - } - - /** - * Equivalent to {@link #renderingTriggeredOnDraw(boolean)}. This method does not follow naming convention - * and is preserved for backwards compatibility only. - * - * @param isRenderingTriggeredOnDraw whether rendering of the next frame is scheduled after drawing (default) - * current one or just after it is rendered - * @return this builder instance, to chain calls - */ - public T setRenderingTriggeredOnDraw(boolean isRenderingTriggeredOnDraw) { - return renderingTriggeredOnDraw(isRenderingTriggeredOnDraw); - } - - /** - * Indicates whether the content of this source is opaque. GIF that is known to be opaque can - * take a faster drawing case than non-opaque one. See {@link GifTextureView#setOpaque(boolean)} - * for more information.
- * Currently it is used only by {@link GifTextureView}, not by {@link GifDrawable}. - *

- * Note that this call will overwrite sample size set previously by {@link #sampleSize(int)} - * - * @param options null-ok; options controlling parameters like subsampling and opacity - * @return this builder instance, to chain calls - */ - @Beta - public T options(@Nullable GifOptions options) { - mOptions.setFrom(options); - return self(); - } - - /** - * Wrapper of {@link pl.droidsonroids.gif.GifDrawable#GifDrawable(java.io.InputStream)} - * - * @param inputStream data source - * @return this builder instance, to chain calls - */ - public T from(InputStream inputStream) { - mInputSource = new InputSource.InputStreamSource(inputStream); - return self(); - } - - /** - * Wrapper of {@link pl.droidsonroids.gif.GifDrawable#GifDrawable(android.content.res.AssetFileDescriptor)} - * - * @param assetFileDescriptor data source - * @return this builder instance, to chain calls - */ - public T from(AssetFileDescriptor assetFileDescriptor) { - mInputSource = new InputSource.AssetFileDescriptorSource(assetFileDescriptor); - return self(); - } - - /** - * Wrapper of {@link pl.droidsonroids.gif.GifDrawable#GifDrawable(java.io.FileDescriptor)} - * - * @param fileDescriptor data source - * @return this builder instance, to chain calls - */ - public T from(FileDescriptor fileDescriptor) { - mInputSource = new InputSource.FileDescriptorSource(fileDescriptor); - return self(); - } - - /** - * Wrapper of {@link pl.droidsonroids.gif.GifDrawable#GifDrawable(android.content.res.AssetManager, java.lang.String)} - * - * @param assetManager assets source - * @param assetName asset file name - * @return this builder instance, to chain calls - */ - public T from(AssetManager assetManager, String assetName) { - mInputSource = new InputSource.AssetSource(assetManager, assetName); - return self(); - } - - /** - * Wrapper of {@link pl.droidsonroids.gif.GifDrawable#GifDrawable(android.content.ContentResolver, android.net.Uri)} - * - * @param uri data source - * @param contentResolver resolver used to query {@code uri} - * @return this builder instance, to chain calls - */ - public T from(ContentResolver contentResolver, Uri uri) { - mInputSource = new InputSource.UriSource(contentResolver, uri); - return self(); - } - - /** - * Wrapper of {@link pl.droidsonroids.gif.GifDrawable#GifDrawable(java.io.File)} - * - * @param file data source - * @return this builder instance, to chain calls - */ - public T from(File file) { - mInputSource = new InputSource.FileSource(file); - return self(); - } - - /** - * Wrapper of {@link pl.droidsonroids.gif.GifDrawable#GifDrawable(java.lang.String)} - * - * @param filePath data source - * @return this builder instance, to chain calls - */ - public T from(String filePath) { - mInputSource = new InputSource.FileSource(filePath); - return self(); - } - - /** - * Wrapper of {@link pl.droidsonroids.gif.GifDrawable#GifDrawable(byte[])} - * - * @param bytes data source - * @return this builder instance, to chain calls - */ - public T from(byte[] bytes) { - mInputSource = new InputSource.ByteArraySource(bytes); - return self(); - } - - /** - * Wrapper of {@link pl.droidsonroids.gif.GifDrawable#GifDrawable(java.nio.ByteBuffer)} - * - * @param byteBuffer data source - * @return this builder instance, to chain calls - */ - public T from(ByteBuffer byteBuffer) { - mInputSource = new InputSource.DirectByteBufferSource(byteBuffer); - return self(); - } - - /** - * Wrapper of {@link pl.droidsonroids.gif.GifDrawable#GifDrawable(android.content.res.Resources, int)} - * - * @param resources Resources to read from - * @param resourceId resource id (data source) - * @return this builder instance, to chain calls - */ - public T from(Resources resources, int resourceId) { - mInputSource = new InputSource.ResourcesSource(resources, resourceId); - return self(); - } - - /** - * Getter for the input source. - * - * @return Current {@link InputSource} or null if it wasn't set. - */ - public InputSource getInputSource() { - return mInputSource; - } - - /** - * Getter for the old drawable. - * - * @return Instance of the old {@link GifDrawable} or null if it wasn't set. - */ - public GifDrawable getOldDrawable() { - return mOldDrawable; - } - - /** - * Getter for the executor. - * - * @return {@link ScheduledThreadPoolExecutor} or null if it wasn't set. - */ - public ScheduledThreadPoolExecutor getExecutor() { - return mExecutor; - } - - /** - * @return True if rendering of the next frame is scheduled after drawing, false otherwise. - */ - public boolean isRenderingTriggeredOnDraw() { - return mIsRenderingTriggeredOnDraw; - } - - /** - * Getter for the GIF options. - * - * @return {@link GifOptions}. - */ - public GifOptions getOptions() { - return mOptions; - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawableInit.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawableInit.kt new file mode 100644 index 00000000..49f1eeb6 --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifDrawableInit.kt @@ -0,0 +1,297 @@ +package pl.droidsonroids.gif + +import android.content.ContentResolver +import android.content.res.AssetFileDescriptor +import android.content.res.AssetManager +import android.content.res.Resources +import android.net.Uri +import androidx.annotation.IntRange +import pl.droidsonroids.gif.InputSource.AssetFileDescriptorSource +import pl.droidsonroids.gif.InputSource.AssetSource +import pl.droidsonroids.gif.InputSource.ByteArraySource +import pl.droidsonroids.gif.InputSource.DirectByteBufferSource +import pl.droidsonroids.gif.InputSource.FileDescriptorSource +import pl.droidsonroids.gif.InputSource.ResourcesSource +import pl.droidsonroids.gif.InputSource.UriSource +import pl.droidsonroids.gif.annotations.Beta +import java.io.File +import java.io.FileDescriptor +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer +import java.util.concurrent.ScheduledThreadPoolExecutor + +/** + * The base class for using the builder pattern with subclasses. + * @see [link](https://community.oracle.com/blogs/emcmanus/2010/10/24/using-builder-pattern-subclasses) + */ +abstract class GifDrawableInit?> { + /** + * Getter for the input source. + * + * @return Current [InputSource] or null if it wasn't set. + */ + private var inputSource: InputSource? = null + + /** + * Getter for the old drawable. + * + * @return Instance of the old [GifDrawable] or null if it wasn't set. + */ + private var oldDrawable: GifDrawable? = null + + /** + * Getter for the executor. + * + * @return [ScheduledThreadPoolExecutor] or null if it wasn't set. + */ + private var executor: ScheduledThreadPoolExecutor? = null + + /** + * @return True if rendering of the next frame is scheduled after drawing, false otherwise. + */ + private var isRenderingTriggeredOnDraw = true + + /** + * Getter for the GIF options. + * + * @return [GifOptions]. + */ + val options = GifOptions() + + /** + * Used in accordance with `getThis()` pattern. + * + * @return The builder type. + * @see [link](http://www.angelikalanger.com/GenericsFAQ/FAQSections/ProgrammingIdioms.html.FAQ205) + */ + protected abstract fun self(): T + + /** + * Sample size controlling subsampling, see [GifOptions.setInSampleSize] for more details. + * Note that this call will overwrite sample size set previously by [.options] + * + * @param sampleSize the sample size + * @return this builder instance, to chain calls + */ + fun sampleSize(@IntRange(from = 1, to = Character.MAX_VALUE.code.toLong()) sampleSize: Int): T { + options.setInSampleSize(sampleSize) + return self() + } + + /** + * Appropriate constructor wrapper. Must be preceded by on of `from()` calls. + * + * @return new drawable instance + * @throws IOException when creation fails + */ + @Throws(IOException::class) + fun build(): GifDrawable { + if (inputSource == null) { + throw NullPointerException("Source is not set") + } + return inputSource!!.createGifDrawable( + oldDrawable, + executor, + isRenderingTriggeredOnDraw, + options + ) + } + + /** + * Sets drawable to be reused when creating new one. + * + * @param drawable drawable to be reused + * @return this builder instance, to chain calls + */ + fun with(drawable: GifDrawable?): T { + oldDrawable = drawable + return self() + } + + /** + * Sets thread pool size for rendering tasks. + * Warning: custom executor set by [.taskExecutor] + * will be overwritten after setting pool size + * + * @param threadPoolSize size of the pool + * @return this builder instance, to chain calls + */ + fun threadPoolSize(threadPoolSize: Int): T { + executor = ScheduledThreadPoolExecutor(threadPoolSize) + return self() + } + + /** + * Sets or resets executor for rendering tasks. + * Warning: value set by [.threadPoolSize] will not be taken into account after setting executor + * + * @param executor executor to be used or null for default (each drawable instance has its own executor) + * @return this builder instance, to chain calls + */ + fun taskExecutor(executor: ScheduledThreadPoolExecutor?): T { + this.executor = executor + return self() + } + + /** + * Sets whether rendering of the next frame is scheduled after drawing current one (so animation + * will be paused if drawing does not happen) or just after rendering frame (no matter if it is + * drawn or not). However animation will never run if drawable is set to not visible. See + * [GifDrawable.isVisible] for more information about drawable visibility. + * By default this option is enabled. Note that drawing does not happen if view containing + * drawable is obscured. Disabling this option will prevent that however battery draining will be + * higher. + * + * @param isRenderingTriggeredOnDraw whether rendering of the next frame is scheduled after drawing (default) + * current one or just after it is rendered + * @return this builder instance, to chain calls + */ + private fun renderingTriggeredOnDraw(isRenderingTriggeredOnDraw: Boolean): T { + this.isRenderingTriggeredOnDraw = isRenderingTriggeredOnDraw + return self() + } + + /** + * Equivalent to [.renderingTriggeredOnDraw]. This method does not follow naming convention + * and is preserved for backwards compatibility only. + * + * @param isRenderingTriggeredOnDraw whether rendering of the next frame is scheduled after drawing (default) + * current one or just after it is rendered + * @return this builder instance, to chain calls + */ + fun setRenderingTriggeredOnDraw(isRenderingTriggeredOnDraw: Boolean): T { + return renderingTriggeredOnDraw(isRenderingTriggeredOnDraw) + } + + /** + * Indicates whether the content of this source is opaque. GIF that is known to be opaque can + * take a faster drawing case than non-opaque one. See [GifTextureView.setOpaque] + * for more information.

+ * Currently it is used only by [GifTextureView], not by [GifDrawable]. + * + * + * Note that this call will overwrite sample size set previously by [.sampleSize] + * + * @param options null-ok; options controlling parameters like subsampling and opacity + * @return this builder instance, to chain calls + */ + @Beta + fun options(options: GifOptions): T { + options.setFrom(options) + return self() + } + + /** + * Wrapper of [pl.droidsonroids.gif.GifDrawable.GifDrawable] + * + * @param inputStream data source + * @return this builder instance, to chain calls + */ + fun from(inputStream: InputStream): T { + inputSource = InputSource.InputStreamSource(inputStream) + return self() + } + + /** + * Wrapper of [pl.droidsonroids.gif.GifDrawable.GifDrawable] + * + * @param assetFileDescriptor data source + * @return this builder instance, to chain calls + */ + fun from(assetFileDescriptor: AssetFileDescriptor): T { + inputSource = AssetFileDescriptorSource(assetFileDescriptor) + return self() + } + + /** + * Wrapper of [pl.droidsonroids.gif.GifDrawable.GifDrawable] + * + * @param fileDescriptor data source + * @return this builder instance, to chain calls + */ + fun from(fileDescriptor: FileDescriptor): T { + inputSource = FileDescriptorSource(fileDescriptor) + return self() + } + + /** + * Wrapper of [pl.droidsonroids.gif.GifDrawable.GifDrawable] + * + * @param assetManager assets source + * @param assetName asset file name + * @return this builder instance, to chain calls + */ + fun from(assetManager: AssetManager, assetName: String): T { + inputSource = AssetSource(assetManager, assetName) + return self() + } + + /** + * Wrapper of [pl.droidsonroids.gif.GifDrawable.GifDrawable] + * + * @param uri data source + * @param contentResolver resolver used to query `uri` + * @return this builder instance, to chain calls + */ + fun from(contentResolver: ContentResolver, uri: Uri): T { + inputSource = UriSource(contentResolver, uri) + return self() + } + + /** + * Wrapper of [pl.droidsonroids.gif.GifDrawable.GifDrawable] + * + * @param file data source + * @return this builder instance, to chain calls + */ + fun from(file: File): T { + inputSource = InputSource.FileSource(file) + return self() + } + + /** + * Wrapper of [pl.droidsonroids.gif.GifDrawable.GifDrawable] + * + * @param filePath data source + * @return this builder instance, to chain calls + */ + fun from(filePath: String): T { + inputSource = InputSource.FileSource(filePath) + return self() + } + + /** + * Wrapper of [pl.droidsonroids.gif.GifDrawable.GifDrawable] + * + * @param bytes data source + * @return this builder instance, to chain calls + */ + fun from(bytes: ByteArray): T { + inputSource = ByteArraySource(bytes) + return self() + } + + /** + * Wrapper of [pl.droidsonroids.gif.GifDrawable.GifDrawable] + * + * @param byteBuffer data source + * @return this builder instance, to chain calls + */ + fun from(byteBuffer: ByteBuffer): T { + inputSource = DirectByteBufferSource(byteBuffer) + return self() + } + + /** + * Wrapper of [pl.droidsonroids.gif.GifDrawable.GifDrawable] + * + * @param resources Resources to read from + * @param resourceId resource id (data source) + * @return this builder instance, to chain calls + */ + fun from(resources: Resources, resourceId: Int): T { + inputSource = ResourcesSource(resources, resourceId) + return self() + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifError.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifError.java deleted file mode 100644 index 8e705488..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifError.java +++ /dev/null @@ -1,136 +0,0 @@ -package pl.droidsonroids.gif; - -import androidx.annotation.NonNull; - -import java.util.Locale; - -/** - * Encapsulation of decoding errors occurring in native code. - * Three digit codes are equal to GIFLib error codes. - * - * @author koral-- - */ -@SuppressWarnings("MagicNumber") //error code constants matching native ones -public enum GifError { - /** - * Special value indicating lack of errors. - */ - NO_ERROR(0, "No error"), - /** - * Failed to open given input. - */ - OPEN_FAILED(101, "Failed to open given input"), - /** - * Failed to read from given input. - */ - READ_FAILED(102, "Failed to read from given input"), - /** - * Data is not in GIF format. - */ - NOT_GIF_FILE(103, "Data is not in GIF format"), - /** - * No screen descriptor detected. - */ - NO_SCRN_DSCR(104, "No screen descriptor detected"), - /** - * No image descriptor detected. - */ - NO_IMAG_DSCR(105, "No image descriptor detected"), - /** - * Neither global nor local color map found. - */ - NO_COLOR_MAP(106, "Neither global nor local color map found"), - /** - * Wrong record type detected. - */ - WRONG_RECORD(107, "Wrong record type detected"), - /** - * Number of pixels bigger than width * height. - */ - DATA_TOO_BIG(108, "Number of pixels bigger than width * height"), - /** - * Failed to allocate required memory. - */ - NOT_ENOUGH_MEM(109, "Failed to allocate required memory"), - /** - * Failed to close given input. - */ - CLOSE_FAILED(110, "Failed to close given input"), - /** - * Given file was not opened for read. - */ - NOT_READABLE(111, "Given file was not opened for read"), - /** - * Image is defective, decoding aborted. - */ - IMAGE_DEFECT(112, "Image is defective, decoding aborted"), - /** - * Image EOF detected before image complete. - * EOF means GIF terminator, not the end of input source. - */ - EOF_TOO_SOON(113, "Image EOF detected before image complete"), - /** - * No frames found, at least one frame required. - */ - NO_FRAMES(1000, "No frames found, at least one frame required"), - /** - * Invalid screen size, dimensions must be positive. - */ - INVALID_SCR_DIMS(1001, "Invalid screen size, dimensions must be positive"), - /** - * Invalid image size, dimensions must be positive. - * - * @deprecated This error is no longer thrown. - */ - @Deprecated - INVALID_IMG_DIMS(1002, "Invalid image size, dimensions must be positive"), - /** - * Image size exceeds screen size. Occurs only if input source changes after opening. - * Otherwise canvas is extended. - */ - IMG_NOT_CONFINED(1003, "Image size exceeds screen size"), - /** - * Input source rewind has failed, animation is stopped. - */ - REWIND_FAILED(1004, "Input source rewind failed, animation stopped"), - /** - * Invalid and/or indirect byte buffer specified. - */ - INVALID_BYTE_BUFFER(1005, "Invalid and/or indirect byte buffer specified"), - /** - * Unknown error, should never appear - */ - UNKNOWN(-1, "Unknown error"); - /** - * Human readable description of the error. - */ - @NonNull - public final String description; - int errorCode; - - GifError(int code, @NonNull String description) { - errorCode = code; - this.description = description; - } - - static GifError fromCode(int code) { - for (GifError err : GifError.values()) - if (err.errorCode == code) { - return err; - } - GifError unk = UNKNOWN; - unk.errorCode = code; - return unk; - } - - /** - * @return error code - */ - public int getErrorCode() { - return errorCode; - } - - String getFormattedDescription() { - return String.format(Locale.ENGLISH, "GifError %d: %s", errorCode, description); - } -} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifError.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifError.kt new file mode 100644 index 00000000..97e2d87c --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifError.kt @@ -0,0 +1,144 @@ +package pl.droidsonroids.gif + +import java.util.Locale + +/** + * Encapsulation of decoding errors occurring in native code. + * Three digit codes are equal to GIFLib error codes. + * + * @author koral-- + */ +//error code constants matching native ones +enum class GifError( + /** + * @return error code + */ + var errorCode: Int, + /** + * Human readable description of the error. + */ + val description: String +) { + /** + * Special value indicating lack of errors. + */ + NO_ERROR(0, "No error"), + + /** + * Failed to open given input. + */ + OPEN_FAILED(101, "Failed to open given input"), + + /** + * Failed to read from given input. + */ + READ_FAILED(102, "Failed to read from given input"), + + /** + * Data is not in GIF format. + */ + NOT_GIF_FILE(103, "Data is not in GIF format"), + + /** + * No screen descriptor detected. + */ + NO_SCRN_DSCR(104, "No screen descriptor detected"), + + /** + * No image descriptor detected. + */ + NO_IMAG_DSCR(105, "No image descriptor detected"), + + /** + * Neither global nor local color map found. + */ + NO_COLOR_MAP(106, "Neither global nor local color map found"), + + /** + * Wrong record type detected. + */ + WRONG_RECORD(107, "Wrong record type detected"), + + /** + * Number of pixels bigger than width * height. + */ + DATA_TOO_BIG(108, "Number of pixels bigger than width * height"), + + /** + * Failed to allocate required memory. + */ + NOT_ENOUGH_MEM(109, "Failed to allocate required memory"), + + /** + * Failed to close given input. + */ + CLOSE_FAILED(110, "Failed to close given input"), + + /** + * Given file was not opened for read. + */ + NOT_READABLE(111, "Given file was not opened for read"), + + /** + * Image is defective, decoding aborted. + */ + IMAGE_DEFECT(112, "Image is defective, decoding aborted"), + + /** + * Image EOF detected before image complete. + * EOF means GIF terminator, not the end of input source. + */ + EOF_TOO_SOON(113, "Image EOF detected before image complete"), + + /** + * No frames found, at least one frame required. + */ + NO_FRAMES(1000, "No frames found, at least one frame required"), + + /** + * Invalid screen size, dimensions must be positive. + */ + INVALID_SCR_DIMS(1001, "Invalid screen size, dimensions must be positive"), + + /** + * Invalid image size, dimensions must be positive. + * + */ + @Deprecated("This error is no longer thrown.") + INVALID_IMG_DIMS(1002, "Invalid image size, dimensions must be positive"), + + /** + * Image size exceeds screen size. Occurs only if input source changes after opening. + * Otherwise canvas is extended. + */ + IMG_NOT_CONFINED(1003, "Image size exceeds screen size"), + + /** + * Input source rewind has failed, animation is stopped. + */ + REWIND_FAILED(1004, "Input source rewind failed, animation stopped"), + + /** + * Invalid and/or indirect byte buffer specified. + */ + INVALID_BYTE_BUFFER(1005, "Invalid and/or indirect byte buffer specified"), + + /** + * Unknown error, should never appear + */ + UNKNOWN(-1, "Unknown error"); + + val formattedDescription: String + get() = String.format(Locale.ENGLISH, "GifError %d: %s", errorCode, description) + + companion object { + fun fromCode(code: Int): GifError { + for (err in values()) if (err.errorCode == code) { + return err + } + val unk = UNKNOWN + unk.errorCode = code + return unk + } + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifIOException.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifIOException.java deleted file mode 100644 index 7597d9d9..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifIOException.java +++ /dev/null @@ -1,41 +0,0 @@ -package pl.droidsonroids.gif; - -import androidx.annotation.NonNull; - -import java.io.IOException; - -/** - * Exception encapsulating {@link GifError}s. - * - * @author koral-- - */ -public class GifIOException extends IOException { - private static final long serialVersionUID = 13038402904505L; - /** - * Reason which caused an exception - */ - @NonNull - public final GifError reason; - - private final String mErrnoMessage; - - @Override - public String getMessage() { - if (mErrnoMessage == null) { - return reason.getFormattedDescription(); - } - return reason.getFormattedDescription() + ": " + mErrnoMessage; - } - - GifIOException(int errorCode, String errnoMessage) { - reason = GifError.fromCode(errorCode); - mErrnoMessage = errnoMessage; - } - - static GifIOException fromCode(final int nativeErrorCode) { - if (nativeErrorCode == GifError.NO_ERROR.errorCode) { - return null; - } - return new GifIOException(nativeErrorCode, null); - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifIOException.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifIOException.kt new file mode 100644 index 00000000..85848735 --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifIOException.kt @@ -0,0 +1,33 @@ +package pl.droidsonroids.gif + +import java.io.IOException + +/** + * Exception encapsulating [GifError]s. + * + * @author koral-- + */ +class GifIOException internal constructor(errorCode: Int, private val mErrnoMessage: String?) : + IOException() { + /** + * Reason which caused an exception + */ + val reason: GifError + override val message: String + get() = if (mErrnoMessage == null) { + reason.formattedDescription + } else "${reason.formattedDescription}: $mErrnoMessage" + + init { + reason = GifError.fromCode(errorCode) + } + + companion object { + private const val serialVersionUID = 13038402904505L + fun fromCode(nativeErrorCode: Int?): GifIOException? { + return if (nativeErrorCode != null && nativeErrorCode == GifError.NO_ERROR.errorCode) { + null + } else GifIOException(nativeErrorCode!!, null) + } + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifImageButton.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifImageButton.java deleted file mode 100644 index f2ff9998..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifImageButton.java +++ /dev/null @@ -1,141 +0,0 @@ -package pl.droidsonroids.gif; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Build; -import android.os.Parcelable; -import androidx.annotation.RequiresApi; -import android.util.AttributeSet; -import android.widget.ImageButton; - -/** - * An {@link ImageButton} which tries treating background and src as {@link GifDrawable} - * - * @author koral-- - */ -public class GifImageButton extends ImageButton { - - private boolean mFreezesAnimation; - - /** - * A corresponding superclass constructor wrapper. - * - * @param context - * @see ImageButton#ImageButton(Context) - */ - public GifImageButton(Context context) { - super(context); - } - - /** - * Like equivalent from superclass but also try to interpret src and background - * attributes as {@link GifDrawable}. - * - * @param context - * @param attrs - * @see ImageButton#ImageButton(Context, AttributeSet) - */ - public GifImageButton(Context context, AttributeSet attrs) { - super(context, attrs); - postInit(GifViewUtils.initImageView(this, attrs, 0, 0)); - } - - /** - * Like equivalent from superclass but also try to interpret src and background - * attributes as GIFs. - * - * @param context - * @param attrs - * @param defStyle - * @see ImageButton#ImageButton(Context, AttributeSet, int) - */ - public GifImageButton(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - postInit(GifViewUtils.initImageView(this, attrs, defStyle, 0)); - } - - /** - * Like equivalent from superclass but also try to interpret src and background - * attributes as GIFs. - * - * @param context - * @param attrs - * @param defStyle - * @param defStyleRes - * @see ImageButton#ImageButton(Context, AttributeSet, int, int) - */ - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - public GifImageButton(Context context, AttributeSet attrs, int defStyle, int defStyleRes) { - super(context, attrs, defStyle, defStyleRes); - postInit(GifViewUtils.initImageView(this, attrs, defStyle, defStyleRes)); - } - - private void postInit(GifViewUtils.GifImageViewAttributes result) { - mFreezesAnimation = result.freezesAnimation; - if (result.mSourceResId > 0) { - super.setImageResource(result.mSourceResId); - } - if (result.mBackgroundResId > 0) { - super.setBackgroundResource(result.mBackgroundResId); - } - } - - /** - * Sets the content of this GifImageView to the specified Uri. - * If uri destination is not a GIF then {@link android.widget.ImageView#setImageURI(android.net.Uri)} - * is called as fallback. - * For supported URI schemes see: {@link android.content.ContentResolver#openAssetFileDescriptor(android.net.Uri, String)}. - * - * @param uri The Uri of an image - */ - @Override - public void setImageURI(Uri uri) { - if (!GifViewUtils.setGifImageUri(this, uri)) { - super.setImageURI(uri); - } - } - - @Override - public void setImageResource(int resId) { - if (!GifViewUtils.setResource(this, true, resId)) { - super.setImageResource(resId); - } - } - - @Override - public void setBackgroundResource(int resId) { - if (!GifViewUtils.setResource(this, false, resId)) { - super.setBackgroundResource(resId); - } - } - - @Override - public Parcelable onSaveInstanceState() { - Drawable source = mFreezesAnimation ? getDrawable() : null; - Drawable background = mFreezesAnimation ? getBackground() : null; - return new GifViewSavedState(super.onSaveInstanceState(), source, background); - } - - @Override - public void onRestoreInstanceState(Parcelable state) { - if (!(state instanceof GifViewSavedState)) { - super.onRestoreInstanceState(state); - return; - } - GifViewSavedState ss = (GifViewSavedState) state; - super.onRestoreInstanceState(ss.getSuperState()); - ss.restoreState(getDrawable(), 0); - ss.restoreState(getBackground(), 1); - } - - /** - * Sets whether animation position is saved in {@link #onSaveInstanceState()} and restored - * in {@link #onRestoreInstanceState(Parcelable)} - * - * @param freezesAnimation whether animation position is saved - */ - public void setFreezesAnimation(boolean freezesAnimation) { - mFreezesAnimation = freezesAnimation; - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifImageButton.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifImageButton.kt new file mode 100644 index 00000000..86837d4b --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifImageButton.kt @@ -0,0 +1,138 @@ +package pl.droidsonroids.gif + +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Parcelable +import android.util.AttributeSet +import android.widget.ImageButton +import androidx.annotation.RequiresApi +import pl.droidsonroids.gif.GifViewUtils.GifImageViewAttributes + +/** + * An [ImageButton] which tries treating background and src as [GifDrawable] + * + * @author koral-- + */ +class GifImageButton : ImageButton { + private var mFreezesAnimation = false + + /** + * A corresponding superclass constructor wrapper. + * + * @param context + * @see ImageButton.ImageButton + */ + constructor(context: Context?) : super(context) + + /** + * Like equivalent from superclass but also try to interpret src and background + * attributes as [GifDrawable]. + * + * @param context + * @param attrs + * @see ImageButton.ImageButton + */ + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + postInit(GifViewUtils.initImageView(this, attrs, 0, 0)) + } + + /** + * Like equivalent from superclass but also try to interpret src and background + * attributes as GIFs. + * + * @param context + * @param attrs + * @param defStyle + * @see ImageButton.ImageButton + */ + constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super( + context, + attrs, + defStyle + ) { + postInit(GifViewUtils.initImageView(this, attrs, defStyle, 0)) + } + + /** + * Like equivalent from superclass but also try to interpret src and background + * attributes as GIFs. + * + * @param context + * @param attrs + * @param defStyle + * @param defStyleRes + * @see ImageButton.ImageButton + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + constructor(context: Context?, attrs: AttributeSet?, defStyle: Int, defStyleRes: Int) : super( + context, + attrs, + defStyle, + defStyleRes + ) { + postInit(GifViewUtils.initImageView(this, attrs, defStyle, defStyleRes)) + } + + private fun postInit(result: GifImageViewAttributes) { + mFreezesAnimation = result.freezesAnimation + if (result.mSourceResId > 0) { + super.setImageResource(result.mSourceResId) + } + if (result.mBackgroundResId > 0) { + super.setBackgroundResource(result.mBackgroundResId) + } + } + + /** + * Sets the content of this GifImageView to the specified Uri. + * If uri destination is not a GIF then [android.widget.ImageView.setImageURI] + * is called as fallback. + * For supported URI schemes see: [android.content.ContentResolver.openAssetFileDescriptor]. + * + * @param uri The Uri of an image + */ + override fun setImageURI(uri: Uri?) { + if (!GifViewUtils.setGifImageUri(this, uri)) { + super.setImageURI(uri) + } + } + + override fun setImageResource(resId: Int) { + if (!GifViewUtils.setResource(this, true, resId)) { + super.setImageResource(resId) + } + } + + override fun setBackgroundResource(resId: Int) { + if (!GifViewUtils.setResource(this, false, resId)) { + super.setBackgroundResource(resId) + } + } + + public override fun onSaveInstanceState(): Parcelable { + val source = if (mFreezesAnimation) drawable else null + val background = if (mFreezesAnimation) background else null + return GifViewSavedState(super.onSaveInstanceState(), source, background) + } + + public override fun onRestoreInstanceState(state: Parcelable) { + if (state !is GifViewSavedState) { + super.onRestoreInstanceState(state) + return + } + super.onRestoreInstanceState(state.superState) + state.restoreState(drawable, 0) + state.restoreState(background, 1) + } + + /** + * Sets whether animation position is saved in [.onSaveInstanceState] and restored + * in [.onRestoreInstanceState] + * + * @param freezesAnimation whether animation position is saved + */ + fun setFreezesAnimation(freezesAnimation: Boolean) { + mFreezesAnimation = freezesAnimation + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifImageView.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifImageView.java deleted file mode 100644 index 8bf75599..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifImageView.java +++ /dev/null @@ -1,141 +0,0 @@ -package pl.droidsonroids.gif; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Build; -import android.os.Parcelable; -import androidx.annotation.RequiresApi; -import android.util.AttributeSet; -import android.widget.ImageView; - -/** - * An {@link ImageView} which tries treating background and src as {@link GifDrawable} - * - * @author koral-- - */ -public class GifImageView extends ImageView { - - private boolean mFreezesAnimation; - - /** - * A corresponding superclass constructor wrapper. - * - * @param context - * @see ImageView#ImageView(Context) - */ - public GifImageView(Context context) { - super(context); - } - - /** - * Like equivalent from superclass but also try to interpret src and background - * attributes as {@link GifDrawable}. - * - * @param context - * @param attrs - * @see ImageView#ImageView(Context, AttributeSet) - */ - public GifImageView(Context context, AttributeSet attrs) { - super(context, attrs); - postInit(GifViewUtils.initImageView(this, attrs, 0, 0)); - } - - /** - * Like equivalent from superclass but also try to interpret src and background - * attributes as GIFs. - * - * @param context - * @param attrs - * @param defStyle - * @see ImageView#ImageView(Context, AttributeSet, int) - */ - public GifImageView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - postInit(GifViewUtils.initImageView(this, attrs, defStyle, 0)); - } - - /** - * Like equivalent from superclass but also try to interpret src and background - * attributes as GIFs. - * - * @param context - * @param attrs - * @param defStyle - * @param defStyleRes - * @see ImageView#ImageView(Context, AttributeSet, int, int) - */ - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - public GifImageView(Context context, AttributeSet attrs, int defStyle, int defStyleRes) { - super(context, attrs, defStyle, defStyleRes); - postInit(GifViewUtils.initImageView(this, attrs, defStyle, defStyleRes)); - } - - private void postInit(GifViewUtils.GifImageViewAttributes result) { - mFreezesAnimation = result.freezesAnimation; - if (result.mSourceResId > 0) { - super.setImageResource(result.mSourceResId); - } - if (result.mBackgroundResId > 0) { - super.setBackgroundResource(result.mBackgroundResId); - } - } - - /** - * Sets the content of this GifImageView to the specified Uri. - * If uri destination is not a GIF then {@link android.widget.ImageView#setImageURI(android.net.Uri)} - * is called as fallback. - * For supported URI schemes see: {@link android.content.ContentResolver#openAssetFileDescriptor(android.net.Uri, String)}. - * - * @param uri The Uri of an image - */ - @Override - public void setImageURI(Uri uri) { - if (!GifViewUtils.setGifImageUri(this, uri)) { - super.setImageURI(uri); - } - } - - @Override - public void setImageResource(int resId) { - if (!GifViewUtils.setResource(this, true, resId)) { - super.setImageResource(resId); - } - } - - @Override - public void setBackgroundResource(int resId) { - if (!GifViewUtils.setResource(this, false, resId)) { - super.setBackgroundResource(resId); - } - } - - @Override - public Parcelable onSaveInstanceState() { - Drawable source = mFreezesAnimation ? getDrawable() : null; - Drawable background = mFreezesAnimation ? getBackground() : null; - return new GifViewSavedState(super.onSaveInstanceState(), source, background); - } - - @Override - public void onRestoreInstanceState(Parcelable state) { - if (!(state instanceof GifViewSavedState)) { - super.onRestoreInstanceState(state); - return; - } - GifViewSavedState ss = (GifViewSavedState) state; - super.onRestoreInstanceState(ss.getSuperState()); - ss.restoreState(getDrawable(), 0); - ss.restoreState(getBackground(), 1); - } - - /** - * Sets whether animation position is saved in {@link #onSaveInstanceState()} and restored - * in {@link #onRestoreInstanceState(Parcelable)} - * - * @param freezesAnimation whether animation position is saved - */ - public void setFreezesAnimation(boolean freezesAnimation) { - mFreezesAnimation = freezesAnimation; - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifImageView.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifImageView.kt new file mode 100644 index 00000000..5bfa0950 --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifImageView.kt @@ -0,0 +1,138 @@ +package pl.droidsonroids.gif + +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Parcelable +import android.util.AttributeSet +import android.widget.ImageView +import androidx.annotation.RequiresApi +import pl.droidsonroids.gif.GifViewUtils.GifImageViewAttributes + +/** + * An [ImageView] which tries treating background and src as [GifDrawable] + * + * @author koral-- + */ +class GifImageView : ImageView { + private var mFreezesAnimation = false + + /** + * A corresponding superclass constructor wrapper. + * + * @param context + * @see ImageView.ImageView + */ + constructor(context: Context?) : super(context) + + /** + * Like equivalent from superclass but also try to interpret src and background + * attributes as [GifDrawable]. + * + * @param context + * @param attrs + * @see ImageView.ImageView + */ + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + postInit(GifViewUtils.initImageView(this, attrs, 0, 0)) + } + + /** + * Like equivalent from superclass but also try to interpret src and background + * attributes as GIFs. + * + * @param context + * @param attrs + * @param defStyle + * @see ImageView.ImageView + */ + constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super( + context, + attrs, + defStyle + ) { + postInit(GifViewUtils.initImageView(this, attrs, defStyle, 0)) + } + + /** + * Like equivalent from superclass but also try to interpret src and background + * attributes as GIFs. + * + * @param context + * @param attrs + * @param defStyle + * @param defStyleRes + * @see ImageView.ImageView + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + constructor(context: Context?, attrs: AttributeSet?, defStyle: Int, defStyleRes: Int) : super( + context, + attrs, + defStyle, + defStyleRes + ) { + postInit(GifViewUtils.initImageView(this, attrs, defStyle, defStyleRes)) + } + + private fun postInit(result: GifImageViewAttributes) { + mFreezesAnimation = result.freezesAnimation + if (result.mSourceResId > 0) { + super.setImageResource(result.mSourceResId) + } + if (result.mBackgroundResId > 0) { + super.setBackgroundResource(result.mBackgroundResId) + } + } + + /** + * Sets the content of this GifImageView to the specified Uri. + * If uri destination is not a GIF then [android.widget.ImageView.setImageURI] + * is called as fallback. + * For supported URI schemes see: [android.content.ContentResolver.openAssetFileDescriptor]. + * + * @param uri The Uri of an image + */ + override fun setImageURI(uri: Uri?) { + if (!GifViewUtils.setGifImageUri(this, uri)) { + super.setImageURI(uri) + } + } + + override fun setImageResource(resId: Int) { + if (!GifViewUtils.setResource(this, true, resId)) { + super.setImageResource(resId) + } + } + + override fun setBackgroundResource(resId: Int) { + if (!GifViewUtils.setResource(this, false, resId)) { + super.setBackgroundResource(resId) + } + } + + public override fun onSaveInstanceState(): Parcelable? { + val source = if (mFreezesAnimation) drawable else null + val background = if (mFreezesAnimation) background else null + return GifViewSavedState(super.onSaveInstanceState(), source, background) + } + + public override fun onRestoreInstanceState(state: Parcelable) { + if (state !is GifViewSavedState) { + super.onRestoreInstanceState(state) + return + } + super.onRestoreInstanceState(state.superState) + state.restoreState(drawable, 0) + state.restoreState(background, 1) + } + + /** + * Sets whether animation position is saved in [.onSaveInstanceState] and restored + * in [.onRestoreInstanceState] + * + * @param freezesAnimation whether animation position is saved + */ + fun setFreezesAnimation(freezesAnimation: Boolean) { + mFreezesAnimation = freezesAnimation + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifInfoHandle.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifInfoHandle.java deleted file mode 100755 index 83008675..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifInfoHandle.java +++ /dev/null @@ -1,378 +0,0 @@ -package pl.droidsonroids.gif; - -import android.content.ContentResolver; -import android.content.res.AssetFileDescriptor; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Build; - -import androidx.annotation.FloatRange; -import androidx.annotation.IntRange; -import androidx.annotation.RequiresApi; - -import android.system.ErrnoException; -import android.system.Os; -import android.view.Surface; - -import java.io.FileDescriptor; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; - -/** - * Native library wrapper - */ -final class GifInfoHandle { - static { - LibraryLoader.loadLibrary(); - } - - /** - * Pointer to native structure. Access must be synchronized, heap corruption may occur otherwise - * when {@link #recycle()} is called during another operation. - */ - private volatile long gifInfoPtr; - - GifInfoHandle() { - } - - GifInfoHandle(FileDescriptor fileDescriptor) throws GifIOException { - gifInfoPtr = openFileDescriptor(fileDescriptor, 0, true); - } - - GifInfoHandle(byte[] bytes) throws GifIOException { - gifInfoPtr = openByteArray(bytes); - } - - GifInfoHandle(ByteBuffer buffer) throws GifIOException { - gifInfoPtr = openDirectByteBuffer(buffer); - } - - GifInfoHandle(String filePath) throws GifIOException { - gifInfoPtr = openFile(filePath); - } - - GifInfoHandle(InputStream stream) throws GifIOException { - if (!stream.markSupported()) { - throw new IllegalArgumentException("InputStream does not support marking"); - } - gifInfoPtr = openStream(stream); - } - - GifInfoHandle(AssetFileDescriptor afd) throws IOException { - try { - gifInfoPtr = openFileDescriptor(afd.getFileDescriptor(), afd.getStartOffset(), false); - } finally { - try { - afd.close(); - } catch (IOException ignored) { - //no-op - } - } - } - - private static long openFileDescriptor(FileDescriptor fileDescriptor, long offset, boolean closeOriginalDescriptor) throws GifIOException { - final int nativeFileDescriptor; - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O_MR1) { - try { - nativeFileDescriptor = getNativeFileDescriptor(fileDescriptor, closeOriginalDescriptor); - } catch (Exception e) { //cannot catch ErrnoException due to VerifyError on API <= 19 - throw new GifIOException(GifError.OPEN_FAILED.errorCode, e.getMessage()); - } - } else { - nativeFileDescriptor = extractNativeFileDescriptor(fileDescriptor, closeOriginalDescriptor); - } - return openNativeFileDescriptor(nativeFileDescriptor, offset); - } - - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - private static int getNativeFileDescriptor(FileDescriptor fileDescriptor, boolean closeOriginalDescriptor) throws GifIOException, ErrnoException { - try { - final int nativeFileDescriptor = createTempNativeFileDescriptor(); - Os.dup2(fileDescriptor, nativeFileDescriptor); - return nativeFileDescriptor; - } finally { - if (closeOriginalDescriptor) { - Os.close(fileDescriptor); - } - } - } - - static GifInfoHandle openUri(ContentResolver resolver, Uri uri) throws IOException { - if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { //workaround for #128 - return new GifInfoHandle(uri.getPath()); - } - final AssetFileDescriptor assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r"); - if (assetFileDescriptor == null) { - throw new IOException("Could not open AssetFileDescriptor for " + uri); - } - return new GifInfoHandle(assetFileDescriptor); - } - - static native long openNativeFileDescriptor(int fd, long offset) throws GifIOException; - - static native int extractNativeFileDescriptor(FileDescriptor fileDescriptor, boolean closeOriginalDescriptor) throws GifIOException; - - static native int createTempNativeFileDescriptor() throws GifIOException; - - static native long openByteArray(byte[] bytes) throws GifIOException; - - static native long openDirectByteBuffer(ByteBuffer buffer) throws GifIOException; - - static native long openStream(InputStream stream) throws GifIOException; - - static native long openFile(String filePath) throws GifIOException; - - private static native long renderFrame(long gifFileInPtr, Bitmap frameBuffer); - - private static native void bindSurface(long gifInfoPtr, Surface surface, long[] savedState); - - private static native void free(long gifFileInPtr); - - private static native boolean reset(long gifFileInPtr); - - private static native void setSpeedFactor(long gifFileInPtr, float factor); - - private static native String getComment(long gifFileInPtr); - - private static native int getLoopCount(long gifFileInPtr); - - private static native void setLoopCount(long gifFileInPtr, char loopCount); - - private static native long getSourceLength(long gifFileInPtr); - - private static native int getDuration(long gifFileInPtr); - - private static native int getCurrentPosition(long gifFileInPtr); - - private static native void seekToTime(long gifFileInPtr, int position, Bitmap buffer); - - private static native void seekToFrame(long gifFileInPtr, int frameNr, Bitmap buffer); - - private static native void saveRemainder(long gifFileInPtr); - - private static native long restoreRemainder(long gifFileInPtr); - - private static native long getAllocationByteCount(long gifFileInPtr); - - private static native long getMetadataByteCount(long gifFileInPtr); - - private static native int getNativeErrorCode(long gifFileInPtr); - - private static native int getCurrentFrameIndex(long gifFileInPtr); - - private static native int getCurrentLoop(long gifFileInPtr); - - private static native void postUnbindSurface(long gifFileInPtr); - - private static native boolean isAnimationCompleted(long gifInfoPtr); - - private static native long[] getSavedState(long gifInfoPtr); - - private static native int restoreSavedState(long gifInfoPtr, long[] savedState, Bitmap mBuffer); - - private static native int getFrameDuration(long gifInfoPtr, int index); - - private static native void setOptions(long gifInfoPtr, char sampleSize, boolean isOpaque); - - private static native int getWidth(long gifFileInPtr); - - private static native int getHeight(long gifFileInPtr); - - private static native int getNumberOfFrames(long gifInfoPtr); - - private static native boolean isOpaque(long gifInfoPtr); - - private static native void startDecoderThread(long gifInfoPtr); - - private static native void stopDecoderThread(long gifInfoPtr); - - private static native void glTexImage2D(long gifInfoPtr, int target, int level); - - private static native void glTexSubImage2D(long gifInfoPtr, int target, int level); - - private static native void seekToFrameGL(long gifInfoPtr, int index); - - private static native void initTexImageDescriptor(long gifInfoPtr); - - synchronized long renderFrame(Bitmap frameBuffer) { - return renderFrame(gifInfoPtr, frameBuffer); - } - - void bindSurface(Surface surface, long[] savedState) { - bindSurface(gifInfoPtr, surface, savedState); - } - - synchronized void recycle() { - free(gifInfoPtr); - gifInfoPtr = 0L; - } - - synchronized long restoreRemainder() { - return restoreRemainder(gifInfoPtr); - } - - synchronized boolean reset() { - return reset(gifInfoPtr); - } - - synchronized void saveRemainder() { - saveRemainder(gifInfoPtr); - } - - synchronized String getComment() { - return getComment(gifInfoPtr); - } - - synchronized int getLoopCount() { - return getLoopCount(gifInfoPtr); - } - - void setLoopCount(@IntRange(from = 0, to = Character.MAX_VALUE) final int loopCount) { - if (loopCount < 0 || loopCount > Character.MAX_VALUE) { - throw new IllegalArgumentException("Loop count of range <0, 65535>"); - } - synchronized (this) { - setLoopCount(gifInfoPtr, (char) loopCount); - } - } - - synchronized long getSourceLength() { - return getSourceLength(gifInfoPtr); - } - - synchronized int getNativeErrorCode() { - return getNativeErrorCode(gifInfoPtr); - } - - void setSpeedFactor(@FloatRange(from = 0, fromInclusive = false) float factor) { - if (factor <= 0f || Float.isNaN(factor)) { - throw new IllegalArgumentException("Speed factor is not positive"); - } - if (factor < 1f / Integer.MAX_VALUE) { - factor = 1f / Integer.MAX_VALUE; - } - synchronized (this) { - setSpeedFactor(gifInfoPtr, factor); - } - } - - synchronized int getDuration() { - return getDuration(gifInfoPtr); - } - - synchronized int getCurrentPosition() { - return getCurrentPosition(gifInfoPtr); - } - - synchronized int getCurrentFrameIndex() { - return getCurrentFrameIndex(gifInfoPtr); - } - - synchronized int getCurrentLoop() { - return getCurrentLoop(gifInfoPtr); - } - - synchronized void seekToTime(@IntRange(from = 0, to = Integer.MAX_VALUE) final int position, final Bitmap buffer) { - seekToTime(gifInfoPtr, position, buffer); - } - - synchronized void seekToFrame(@IntRange(from = 0, to = Integer.MAX_VALUE) final int frameIndex, - final Bitmap buffer) { - seekToFrame(gifInfoPtr, frameIndex, buffer); - } - - synchronized long getAllocationByteCount() { - return getAllocationByteCount(gifInfoPtr); - } - - synchronized long getMetadataByteCount() { - return getMetadataByteCount(gifInfoPtr); - } - - synchronized boolean isRecycled() { - return gifInfoPtr == 0L; - } - - @Override - protected void finalize() throws Throwable { - try { - recycle(); - } finally { - super.finalize(); - } - } - - synchronized void postUnbindSurface() { - postUnbindSurface(gifInfoPtr); - } - - synchronized boolean isAnimationCompleted() { - return isAnimationCompleted(gifInfoPtr); - } - - synchronized long[] getSavedState() { - return getSavedState(gifInfoPtr); - } - - synchronized int restoreSavedState(long[] savedState, Bitmap mBuffer) { - return restoreSavedState(gifInfoPtr, savedState, mBuffer); - } - - synchronized int getFrameDuration(@IntRange(from = 0) final int index) { - throwIfFrameIndexOutOfBounds(index); - return getFrameDuration(gifInfoPtr, index); - } - - void setOptions(char sampleSize, boolean isOpaque) { - setOptions(gifInfoPtr, sampleSize, isOpaque); - } - - synchronized int getWidth() { - return getWidth(gifInfoPtr); - } - - synchronized int getHeight() { - return getHeight(gifInfoPtr); - } - - synchronized int getNumberOfFrames() { - return getNumberOfFrames(gifInfoPtr); - } - - synchronized boolean isOpaque() { - return isOpaque(gifInfoPtr); - } - - void glTexImage2D(int target, int level) { - glTexImage2D(gifInfoPtr, target, level); - } - - void glTexSubImage2D(int target, int level) { - glTexSubImage2D(gifInfoPtr, target, level); - } - - void startDecoderThread() { - startDecoderThread(gifInfoPtr); - } - - void stopDecoderThread() { - stopDecoderThread(gifInfoPtr); - } - - void initTexImageDescriptor() { - initTexImageDescriptor(gifInfoPtr); - } - - void seekToFrameGL(@IntRange(from = 0) final int index) { - throwIfFrameIndexOutOfBounds(index); - seekToFrameGL(gifInfoPtr, index); - } - - private void throwIfFrameIndexOutOfBounds(@IntRange(from = 0) final int index) { - final int numberOfFrames = getNumberOfFrames(gifInfoPtr); - if (index < 0 || index >= numberOfFrames) { - throw new IndexOutOfBoundsException("Frame index is not in range <0;" + numberOfFrames + '>'); - } - } -} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifInfoHandle.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifInfoHandle.kt new file mode 100644 index 00000000..9111996e --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifInfoHandle.kt @@ -0,0 +1,372 @@ +package pl.droidsonroids.gif + +import android.content.ContentResolver +import android.content.res.AssetFileDescriptor +import android.graphics.Bitmap +import android.net.Uri +import android.os.Build +import android.system.ErrnoException +import android.system.Os +import android.view.Surface +import androidx.annotation.FloatRange +import androidx.annotation.IntRange +import androidx.annotation.RequiresApi +import pl.droidsonroids.gif.LibraryLoader.loadLibrary +import java.io.FileDescriptor +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer + +/** + * Native library wrapper + */ +class GifInfoHandle { + /** + * Pointer to native structure. Access must be synchronized, heap corruption may occur otherwise + * when [.recycle] is called during another operation. + */ + @Volatile + private var gifInfoPtr: Long = 0 + + constructor() + constructor(fileDescriptor: FileDescriptor) { + gifInfoPtr = openFileDescriptor(fileDescriptor, 0, true) + } + + constructor(bytes: ByteArray?) { + gifInfoPtr = openByteArray(bytes) + } + + constructor(buffer: ByteBuffer?) { + gifInfoPtr = openDirectByteBuffer(buffer) + } + + constructor(filePath: String?) { + gifInfoPtr = openFile(filePath) + } + + constructor(stream: InputStream) { + require(stream.markSupported()) { "InputStream does not support marking" } + gifInfoPtr = openStream(stream) + } + + constructor(afd: AssetFileDescriptor) { + gifInfoPtr = try { + openFileDescriptor(afd.fileDescriptor, afd.startOffset, false) + } finally { + try { + afd.close() + } catch (ignored: IOException) { + //no-op + } + } + } + + @Synchronized + fun renderFrame(frameBuffer: Bitmap?): Long { + return renderFrame(gifInfoPtr, frameBuffer) + } + + fun bindSurface(surface: Surface, savedState: LongArray?) { + bindSurface(gifInfoPtr, surface, savedState) + } + + @Synchronized + fun recycle() { + free(gifInfoPtr) + gifInfoPtr = 0L + } + + @Synchronized + fun restoreRemainder(): Long { + return restoreRemainder(gifInfoPtr) + } + + @Synchronized + fun reset(): Boolean { + return reset(gifInfoPtr) + } + + @Synchronized + fun saveRemainder() { + saveRemainder(gifInfoPtr) + } + + @get:Synchronized + val comment: String + get() = getComment(gifInfoPtr) + + @get:Synchronized + var loopCount: Int + get() = getLoopCount(gifInfoPtr) + set(loopCount) { + require(!(loopCount < 0 || loopCount > Character.MAX_VALUE.code)) { "Loop count of range <0, 65535>" } + synchronized(this) { setLoopCount(gifInfoPtr, loopCount.toChar()) } + } + + @get:Synchronized + val inputSourceByteCount: Long + get() = getSourceLength(gifInfoPtr) + + /** + * Returns length of the input source obtained at the opening time or -1 if + * length cannot be determined. Returned value does not change during runtime. + * If GifDrawable is constructed from [InputStream] -1 is always returned. + * In case of byte array and [ByteBuffer] length is always known. + * In other cases length -1 can be returned if length cannot be determined. + * + * @return number of bytes backed by input source or -1 if it is unknown + */ + + @get:Synchronized + val nativeErrorCode: Int + get() = getNativeErrorCode(gifInfoPtr) + + fun setSpeedFactor(@FloatRange(from = 0.0, fromInclusive = false) factor: Float) { + var floatFactor = factor + require(!(floatFactor <= 0f || floatFactor.isNaN())) { "Speed factor is not positive" } + if (floatFactor < 1f / Int.MAX_VALUE) { + floatFactor = 1f / Int.MAX_VALUE + } + synchronized(this) { setSpeedFactor(gifInfoPtr, floatFactor) } + } + + @get:Synchronized + val duration: Int + get() = getDuration(gifInfoPtr) + + @get:Synchronized + val currentPosition: Int + get() = getCurrentPosition(gifInfoPtr) + + @get:Synchronized + val currentFrameIndex: Int + get() = getCurrentFrameIndex(gifInfoPtr) + + @get:Synchronized + val currentLoop: Int + get() = getCurrentLoop(gifInfoPtr) + + @Synchronized + fun seekToTime(@IntRange(from = 0, to = Int.MAX_VALUE.toLong()) position: Int, buffer: Bitmap) { + seekToTime(gifInfoPtr, position, buffer) + } + + @Synchronized + fun seekToFrame( + @IntRange(from = 0, to = Int.MAX_VALUE.toLong()) frameIndex: Int, + buffer: Bitmap + ) { + seekToFrame(gifInfoPtr, frameIndex, buffer) + } + + @get:Synchronized + val allocationByteCount: Long + get() = getAllocationByteCount(gifInfoPtr) + + @get:Synchronized + val metadataAllocationByteCount: Long + get() = getMetadataByteCount(gifInfoPtr) + + @get:Synchronized + val isRecycled: Boolean + get() = gifInfoPtr == 0L + + protected fun finalize() { + recycle() + } + + @Synchronized + fun postUnbindSurface() { + postUnbindSurface(gifInfoPtr) + } + + @get:Synchronized + val isAnimationCompleted: Boolean + get() = isAnimationCompleted(gifInfoPtr) + + @get:Synchronized + val savedState: LongArray + get() = getSavedState(gifInfoPtr) + + @Synchronized + fun restoreSavedState(savedState: LongArray, mBuffer: Bitmap?): Int { + return restoreSavedState(gifInfoPtr, savedState, mBuffer) + } + + @Synchronized + fun getFrameDuration(@IntRange(from = 0) index: Int): Int { + throwIfFrameIndexOutOfBounds(index) + return getFrameDuration(gifInfoPtr, index) + } + + fun setOptions(sampleSize: Char, isOpaque: Boolean) { + setOptions(gifInfoPtr, sampleSize, isOpaque) + } + + @get:Synchronized + val width: Int + get() = getWidth(gifInfoPtr) + + @get:Synchronized + val height: Int + get() = getHeight(gifInfoPtr) + + @get:Synchronized + val numberOfFrames: Int + get() = getNumberOfFrames(gifInfoPtr) + + @get:Synchronized + val isOpaque: Boolean + get() = isOpaque(gifInfoPtr) + + fun glTexImage2D(target: Int, level: Int) { + glTexImage2D(gifInfoPtr, target, level) + } + + fun glTexSubImage2D(target: Int, level: Int) { + glTexSubImage2D(gifInfoPtr, target, level) + } + + fun startDecoderThread() { + startDecoderThread(gifInfoPtr) + } + + fun stopDecoderThread() { + stopDecoderThread(gifInfoPtr) + } + + fun initTexImageDescriptor() { + initTexImageDescriptor(gifInfoPtr) + } + + fun seekToFrameGL(@IntRange(from = 0) index: Int) { + throwIfFrameIndexOutOfBounds(index) + seekToFrameGL(gifInfoPtr, index) + } + + private fun throwIfFrameIndexOutOfBounds(@IntRange(from = 0) index: Int) { + val numberOfFrames = getNumberOfFrames(gifInfoPtr) + if (index < 0 || index >= numberOfFrames) { + throw IndexOutOfBoundsException("Frame index is not in range <0;$numberOfFrames>") + } + } + + companion object { + init { + loadLibrary() + } + + @Throws(GifIOException::class) + private fun openFileDescriptor( + fileDescriptor: FileDescriptor, + offset: Long, + closeOriginalDescriptor: Boolean + ): Long { + val nativeFileDescriptor = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O_MR1) { + try { + getNativeFileDescriptor(fileDescriptor, closeOriginalDescriptor) + } catch (e: Exception) { //cannot catch ErrnoException due to VerifyError on API <= 19 + throw GifIOException(GifError.OPEN_FAILED.errorCode, e.message) + } + } else { + extractNativeFileDescriptor(fileDescriptor, closeOriginalDescriptor) + } + return openNativeFileDescriptor(nativeFileDescriptor, offset) + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + @Throws( + GifIOException::class, ErrnoException::class + ) + private fun getNativeFileDescriptor( + fileDescriptor: FileDescriptor, + closeOriginalDescriptor: Boolean + ): Int { + return try { + val nativeFileDescriptor = createTempNativeFileDescriptor() + Os.dup2(fileDescriptor, nativeFileDescriptor) + nativeFileDescriptor + } finally { + if (closeOriginalDescriptor) { + Os.close(fileDescriptor) + } + } + } + + @Throws(IOException::class) + fun openUri(resolver: ContentResolver?, uri: Uri): GifInfoHandle { + if (ContentResolver.SCHEME_FILE == uri.scheme) { //workaround for #128 + return GifInfoHandle(uri.path) + } + val assetFileDescriptor = resolver?.openAssetFileDescriptor(uri, "r") + ?: throw IOException("Could not open AssetFileDescriptor for $uri") + return GifInfoHandle(assetFileDescriptor) + } + + @Throws(GifIOException::class) + external fun openNativeFileDescriptor(fd: Int, offset: Long): Long + + @Throws(GifIOException::class) + external fun extractNativeFileDescriptor( + fileDescriptor: FileDescriptor?, + closeOriginalDescriptor: Boolean + ): Int + + @Throws(GifIOException::class) + external fun createTempNativeFileDescriptor(): Int + + @Throws(GifIOException::class) + external fun openByteArray(bytes: ByteArray?): Long + + @Throws(GifIOException::class) + external fun openDirectByteBuffer(buffer: ByteBuffer?): Long + + @Throws(GifIOException::class) + external fun openStream(stream: InputStream?): Long + + @Throws(GifIOException::class) + external fun openFile(filePath: String?): Long + private external fun renderFrame(gifFileInPtr: Long, frameBuffer: Bitmap?): Long + private external fun bindSurface(gifInfoPtr: Long, surface: Surface, savedState: LongArray?) + private external fun free(gifFileInPtr: Long) + private external fun reset(gifFileInPtr: Long): Boolean + private external fun setSpeedFactor(gifFileInPtr: Long, factor: Float) + private external fun getComment(gifFileInPtr: Long): String + private external fun getLoopCount(gifFileInPtr: Long): Int + private external fun setLoopCount(gifFileInPtr: Long, loopCount: Char) + private external fun getSourceLength(gifFileInPtr: Long): Long + private external fun getDuration(gifFileInPtr: Long): Int + private external fun getCurrentPosition(gifFileInPtr: Long): Int + private external fun seekToTime(gifFileInPtr: Long, position: Int, buffer: Bitmap) + private external fun seekToFrame(gifFileInPtr: Long, frameNr: Int, buffer: Bitmap) + private external fun saveRemainder(gifFileInPtr: Long) + private external fun restoreRemainder(gifFileInPtr: Long): Long + private external fun getAllocationByteCount(gifFileInPtr: Long): Long + private external fun getMetadataByteCount(gifFileInPtr: Long): Long + private external fun getNativeErrorCode(gifFileInPtr: Long): Int + private external fun getCurrentFrameIndex(gifFileInPtr: Long): Int + private external fun getCurrentLoop(gifFileInPtr: Long): Int + private external fun postUnbindSurface(gifFileInPtr: Long) + private external fun isAnimationCompleted(gifInfoPtr: Long): Boolean + private external fun getSavedState(gifInfoPtr: Long): LongArray + private external fun restoreSavedState( + gifInfoPtr: Long, + savedState: LongArray, + mBuffer: Bitmap? + ): Int + + private external fun getFrameDuration(gifInfoPtr: Long, index: Int): Int + private external fun setOptions(gifInfoPtr: Long, sampleSize: Char, isOpaque: Boolean) + private external fun getWidth(gifFileInPtr: Long): Int + private external fun getHeight(gifFileInPtr: Long): Int + private external fun getNumberOfFrames(gifInfoPtr: Long): Int + private external fun isOpaque(gifInfoPtr: Long): Boolean + private external fun startDecoderThread(gifInfoPtr: Long) + private external fun stopDecoderThread(gifInfoPtr: Long) + private external fun glTexImage2D(gifInfoPtr: Long, target: Int, level: Int) + private external fun glTexSubImage2D(gifInfoPtr: Long, target: Int, level: Int) + private external fun seekToFrameGL(gifInfoPtr: Long, index: Int) + private external fun initTexImageDescriptor(gifInfoPtr: Long) + } +} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifOptions.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifOptions.java deleted file mode 100644 index dedc3228..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifOptions.java +++ /dev/null @@ -1,70 +0,0 @@ -package pl.droidsonroids.gif; - -import androidx.annotation.IntRange; -import androidx.annotation.Nullable; - -/** - * Options controlling various GIF parameters similar to - * {@link android.graphics.BitmapFactory.Options} - */ -public class GifOptions { - - char inSampleSize; - boolean inIsOpaque; - - public GifOptions() { - reset(); - } - - private void reset() { - inSampleSize = 1; - inIsOpaque = false; - } - - /** - * If set to a value {@code > 1}, requests the decoder to subsample the original - * frames, returning a smaller frame buffer to save memory. The sample size is - * the number of pixels in either dimension that correspond to a single - * pixel in the decoded bitmap. For example, inSampleSize == 4 returns - * an image that is 1/4 the width/height of the original, and 1/16 the - * number of pixels. Values outside range {@code <1, 65635>} are treated as 1. - * Unlike {@link android.graphics.BitmapFactory.Options#inSampleSize} - * values which are not powers of 2 are also supported. - * Default value is 1. - * - * @param inSampleSize the sample size - */ - public void setInSampleSize(@IntRange(from = 1, to = Character.MAX_VALUE) int inSampleSize) { - if (inSampleSize < 1 || inSampleSize > Character.MAX_VALUE) { - this.inSampleSize = 1; - } else { - this.inSampleSize = (char) inSampleSize; - } - } - - /** - * Indicates whether the content is opaque. GIF that is known to be opaque can - * take a faster drawing case than non-opaque one, since alpha channel processing may be skipped. - *

- * Common usage is setting this to true when view where GIF is displayed is known to be non-transparent - * and its background is irrelevant.In such case even if GIF contains transparent areas, - * they will appear black. - *

- * See also {@link GifTextureView#setOpaque(boolean)}. - * Default value is {@code false}, which means that content can be transparent. - * - * @param inIsOpaque whether the content is opaque - */ - public void setInIsOpaque(boolean inIsOpaque) { - this.inIsOpaque = inIsOpaque; - } - - void setFrom(@Nullable GifOptions source) { - if (source == null) { - reset(); - } else { - inIsOpaque = source.inIsOpaque; - inSampleSize = source.inSampleSize; - } - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifOptions.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifOptions.kt new file mode 100644 index 00000000..f64032cb --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifOptions.kt @@ -0,0 +1,72 @@ +package pl.droidsonroids.gif + +import androidx.annotation.IntRange + +/** + * Options controlling various GIF parameters similar to + * [android.graphics.BitmapFactory.Options] + */ +class GifOptions { + var inSampleSize = 0.toChar() + + /** + * Indicates whether the content is opaque. GIF that is known to be opaque can + * take a faster drawing case than non-opaque one, since alpha channel processing may be skipped. + * + * + * Common usage is setting this to true when view where GIF is displayed is known to be non-transparent + * and its background is irrelevant.In such case even if GIF contains transparent areas, + * they will appear black. + * + * + * See also [GifTextureView.setOpaque]. + * Default value is `false`, which means that content can be transparent. + * + * @param inIsOpaque whether the content is opaque + */ + var inIsOpaque = false + + init { + reset() + } + + private fun reset() { + inSampleSize = 1.toChar() + inIsOpaque = false + } + + /** + * If set to a value `> 1`, requests the decoder to subsample the original + * frames, returning a smaller frame buffer to save memory. The sample size is + * the number of pixels in either dimension that correspond to a single + * pixel in the decoded bitmap. For example, inSampleSize == 4 returns + * an image that is 1/4 the width/height of the original, and 1/16 the + * number of pixels. Values outside range `<1, 65635>` are treated as 1. + * Unlike [android.graphics.BitmapFactory.Options.inSampleSize] + * values which are not powers of 2 are also supported. + * Default value is 1. + * + * @param inSampleSize the sample size + */ + fun setInSampleSize( + @IntRange( + from = 1, + to = Character.MAX_VALUE.code.toLong() + ) inSampleSize: Int + ) { + if (inSampleSize < 1 || inSampleSize > Character.MAX_VALUE.code) { + this.inSampleSize = 1.toChar() + } else { + this.inSampleSize = inSampleSize.toChar() + } + } + + fun setFrom(source: GifOptions?) { + if (source == null) { + reset() + } else { + inIsOpaque = source.inIsOpaque + inSampleSize = source.inSampleSize + } + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifRenderingExecutor.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifRenderingExecutor.java deleted file mode 100644 index bdb5da53..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifRenderingExecutor.java +++ /dev/null @@ -1,23 +0,0 @@ -package pl.droidsonroids.gif; - -import java.util.concurrent.ScheduledThreadPoolExecutor; - -/** - * Default executor for rendering tasks - {@link java.util.concurrent.ScheduledThreadPoolExecutor} - * with 1 worker thread and {@link java.util.concurrent.ThreadPoolExecutor.DiscardPolicy}. - */ -final class GifRenderingExecutor extends ScheduledThreadPoolExecutor { - - // Lazy initialization via inner-class holder - private static final class InstanceHolder { - private static final GifRenderingExecutor INSTANCE = new GifRenderingExecutor(); - } - - static GifRenderingExecutor getInstance() { - return InstanceHolder.INSTANCE; - } - - private GifRenderingExecutor() { - super(1, new DiscardPolicy()); - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifRenderingExecutor.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifRenderingExecutor.kt new file mode 100644 index 00000000..1cfcfeee --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifRenderingExecutor.kt @@ -0,0 +1,9 @@ +package pl.droidsonroids.gif + +import java.util.concurrent.ScheduledThreadPoolExecutor + +/** + * Default executor for rendering tasks - [java.util.concurrent.ScheduledThreadPoolExecutor] + * with 1 worker thread and [java.util.concurrent.ThreadPoolExecutor.DiscardPolicy]. + */ +object GifRenderingExecutor : ScheduledThreadPoolExecutor(1, DiscardPolicy()) \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTexImage2D.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTexImage2D.java deleted file mode 100755 index 4c096874..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTexImage2D.java +++ /dev/null @@ -1,163 +0,0 @@ -package pl.droidsonroids.gif; - -import androidx.annotation.FloatRange; -import androidx.annotation.IntRange; -import androidx.annotation.Nullable; - -import java.io.IOException; -import java.nio.Buffer; - -/** - * Provides support for animated GIFs in OpenGL. - * There are 2 possible usages: - *

    - *
  1. Automatic animation according to timing defined in GIF file - {@link #startDecoderThread()} and {@link #stopDecoderThread()}.
  2. - *
  3. Manual frame advancing - {@link #seekToFrame(int)}.
  4. - *
- * Note that call {@link #seekToFrame(int)} while decoder thread is running will cause frame change - * but it can be immediately changed again by decoder thread. - *
- * Current frame can be copied to 2D texture when needed. See {@link #glTexImage2D(int, int)} and {@link #glTexSubImage2D(int, int)}. - */ -public class GifTexImage2D { - private final GifInfoHandle mGifInfoHandle; - - /** - * Constructs new GifTexImage2D. - * Decoder thread is initially stopped, use {@link #startDecoderThread()} to start it. - * - * @param inputSource source - * @param options null-ok; options controlling parameters like subsampling and opacity - * @throws IOException when creation fails - */ - public GifTexImage2D(final InputSource inputSource, @Nullable GifOptions options) throws IOException { - if (options == null) { - options = new GifOptions(); - } - mGifInfoHandle = inputSource.open(); - mGifInfoHandle.setOptions(options.inSampleSize, options.inIsOpaque); - mGifInfoHandle.initTexImageDescriptor(); - } - - /** - * See {@link GifDrawable#getFrameDuration(int)} - * - * @param index index of the frame - * @return duration of the given frame in milliseconds - * @throws IndexOutOfBoundsException if {@code index < 0 || index >= } - */ - public int getFrameDuration(@IntRange(from = 0) int index) { - return mGifInfoHandle.getFrameDuration(index); - } - - /** - * Seeks to given frame - * - * @param index index of the frame - * @throws IndexOutOfBoundsException if {@code index < 0 || index >= } - */ - public void seekToFrame(@IntRange(from = 0) int index) { - mGifInfoHandle.seekToFrameGL(index); - } - - /** - * @return number of frames in GIF, at least one - */ - public int getNumberOfFrames() { - return mGifInfoHandle.getNumberOfFrames(); - } - - /** - * @return index of recently rendered frame or -1 if this object is recycled - */ - public int getCurrentFrameIndex() { - return mGifInfoHandle.getCurrentFrameIndex(); - } - - /** - * Sets new animation speed factor. See {@link GifDrawable#setSpeed(float)}. - * - * @param factor new speed factor, eg. 0.5f means half speed, 1.0f - normal, 2.0f - double speed - * @throws IllegalArgumentException if factor<=0 - */ - public void setSpeed(@FloatRange(from = 0, fromInclusive = false) final float factor) { - mGifInfoHandle.setSpeedFactor(factor); - } - - /** - * Equivalent of {@link android.opengl.GLES20#glTexImage2D(int, int, int, int, int, int, int, int, Buffer)}. - * Where Buffer contains pixels of the current frame. - * - * @param level level-of-detail number - * @param target target texture - */ - public void glTexImage2D(int target, int level) { - mGifInfoHandle.glTexImage2D(target, level); - } - - /** - * Equivalent of {@link android.opengl.GLES20#glTexSubImage2D(int, int, int, int, int, int, int, int, Buffer)}. - * Where Buffer contains pixels of the current frame. - * - * @param level level-of-detail number - * @param target target texture - */ - public void glTexSubImage2D(int target, int level) { - mGifInfoHandle.glTexSubImage2D(target, level); - } - - /** - * Creates frame buffer and starts decoding thread. Does nothing if already started. - */ - public void startDecoderThread() { - mGifInfoHandle.startDecoderThread(); - } - - /** - * Stops decoder thread and releases frame buffer. Does nothing if already stopped. - */ - public void stopDecoderThread() { - mGifInfoHandle.stopDecoderThread(); - } - - /** - * See {@link GifDrawable#recycle()}. Decoder thread is stopped automatically. - */ - public void recycle() { - if (mGifInfoHandle != null) { - mGifInfoHandle.recycle(); - } - } - - /** - * @return width of the GIF canvas, 0 if recycled - */ - public int getWidth() { - return mGifInfoHandle.getWidth(); - } - - /** - * @return height of the GIF canvas, 0 if recycled - */ - public int getHeight() { - return mGifInfoHandle.getHeight(); - } - - /** - * See {@link GifDrawable#getDuration()} - * - * @return duration of of one loop the animation in milliseconds. Result is always multiple of 10. - */ - public int getDuration() { - return mGifInfoHandle.getDuration(); - } - - @Override - protected final void finalize() throws Throwable { - try { - recycle(); - } finally { - super.finalize(); - } - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTexImage2D.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTexImage2D.kt new file mode 100644 index 00000000..cbfa098e --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTexImage2D.kt @@ -0,0 +1,152 @@ +package pl.droidsonroids.gif + +import androidx.annotation.FloatRange +import androidx.annotation.IntRange +import kotlin.jvm.Throws + +/** + * Provides support for animated GIFs in OpenGL. + * There are 2 possible usages: + * + * 1. Automatic animation according to timing defined in GIF file - [.startDecoderThread] and [.stopDecoderThread]. + * 1. Manual frame advancing - [.seekToFrame]. + * + * Note that call [.seekToFrame] while decoder thread is running will cause frame change + * but it can be immediately changed again by decoder thread. + *

+ * Current frame can be copied to 2D texture when needed. See [.glTexImage2D] and [.glTexSubImage2D]. + */ +class GifTexImage2D(inputSource: InputSource, options: GifOptions?) { + private val mGifInfoHandle: GifInfoHandle + + /** + * Constructs new GifTexImage2D. + * Decoder thread is initially stopped, use [.startDecoderThread] to start it. + * + * @param inputSource source + * @param options null-ok; options controlling parameters like subsampling and opacity + * @throws IOException when creation fails + */ + init { + var gifOptions = options + if (gifOptions == null) { + gifOptions = GifOptions() + } + mGifInfoHandle = inputSource.open() + mGifInfoHandle.setOptions(gifOptions.inSampleSize, gifOptions.inIsOpaque) + mGifInfoHandle.initTexImageDescriptor() + } + + /** + * See [GifDrawable.getFrameDuration] + * + * @param index index of the frame + * @return duration of the given frame in milliseconds + * @throws IndexOutOfBoundsException if `index < 0 || index >= ` + */ + @Throws(IndexOutOfBoundsException::class) + fun getFrameDuration(@IntRange(from = 0) index: Int): Int { + return mGifInfoHandle.getFrameDuration(index) + } + + /** + * Seeks to given frame + * + * @param index index of the frame + * @throws IndexOutOfBoundsException if `index < 0 || index >= ` + */ + @Throws(IndexOutOfBoundsException::class) + fun seekToFrame(@IntRange(from = 0) index: Int) { + mGifInfoHandle.seekToFrameGL(index) + } + + /** + * @return number of frames in GIF, at least one + */ + val numberOfFrames: Int + get() = mGifInfoHandle.numberOfFrames + + /** + * @return index of recently rendered frame or -1 if this object is recycled + */ + val currentFrameIndex: Int + get() = mGifInfoHandle.currentFrameIndex + + /** + * Sets new animation speed factor. See [GifDrawable.setSpeed]. + * + * @param factor new speed factor, eg. 0.5f means half speed, 1.0f - normal, 2.0f - double speed + * @throws IllegalArgumentException if factor<=0 + */ + @Throws(IllegalArgumentException::class) + fun setSpeed(@FloatRange(from = 0.0, fromInclusive = false) factor: Float) { + mGifInfoHandle.setSpeedFactor(factor) + } + + /** + * Equivalent of [android.opengl.GLES20.glTexImage2D]. + * Where `Buffer` contains pixels of the current frame. + * + * @param level level-of-detail number + * @param target target texture + */ + fun glTexImage2D(target: Int, level: Int) { + mGifInfoHandle.glTexImage2D(target, level) + } + + /** + * Equivalent of [android.opengl.GLES20.glTexSubImage2D]. + * Where `Buffer` contains pixels of the current frame. + * + * @param level level-of-detail number + * @param target target texture + */ + fun glTexSubImage2D(target: Int, level: Int) { + mGifInfoHandle.glTexSubImage2D(target, level) + } + + /** + * Creates frame buffer and starts decoding thread. Does nothing if already started. + */ + fun startDecoderThread() { + mGifInfoHandle.startDecoderThread() + } + + /** + * Stops decoder thread and releases frame buffer. Does nothing if already stopped. + */ + fun stopDecoderThread() { + mGifInfoHandle.stopDecoderThread() + } + + /** + * See [GifDrawable.recycle]. Decoder thread is stopped automatically. + */ + fun recycle() { + mGifInfoHandle.recycle() + } + + /** + * @return width of the GIF canvas, 0 if recycled + */ + val width: Int + get() = mGifInfoHandle.width + + /** + * @return height of the GIF canvas, 0 if recycled + */ + val height: Int + get() = mGifInfoHandle.height + + /** + * See [GifDrawable.getDuration] + * + * @return duration of of one loop the animation in milliseconds. Result is always multiple of 10. + */ + val duration: Int + get() = mGifInfoHandle.duration + + protected fun finalize() { + recycle() + } +} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTextView.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTextView.java deleted file mode 100644 index dc8a213c..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTextView.java +++ /dev/null @@ -1,229 +0,0 @@ -package pl.droidsonroids.gif; - -import android.content.Context; -import android.content.res.Resources; -import android.content.res.Resources.NotFoundException; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.os.Parcelable; -import androidx.annotation.RequiresApi; -import android.util.AttributeSet; -import android.widget.TextView; - -import java.io.IOException; - -/** - * A {@link TextView} which handles GIFs as compound drawables. NOTE: - * {@code android:drawableStart} and {@code android:drawableEnd} from XML are - * not supported but can be set using - * {@link #setCompoundDrawablesRelativeWithIntrinsicBounds(int, int, int, int)} - * - * @author koral-- - */ -public class GifTextView extends TextView { - - private GifViewUtils.GifViewAttributes viewAttributes; - - /** - * A corresponding superclass constructor wrapper. - * - * @param context - */ - public GifTextView(Context context) { - super(context); - } - - /** - * Like equivalent from superclass but also try to interpret compound drawables defined in XML - * attributes as GIFs. - * - * @param context - * @param attrs - * @see TextView#TextView(Context, AttributeSet) - */ - public GifTextView(Context context, AttributeSet attrs) { - super(context, attrs); - init(attrs, 0, 0); - } - - /** - * Like equivalent from superclass but also try to interpret compound drawables defined in XML - * attributes as GIFs. - * - * @param context - * @param attrs - * @param defStyle - * @see TextView#TextView(Context, AttributeSet, int) - */ - public GifTextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(attrs, defStyle, 0); - } - - /** - * Like equivalent from superclass but also try to interpret compound drawables defined in XML - * attributes as GIFs. - * - * @param context - * @param attrs - * @param defStyle - * @param defStyleRes - * @see TextView#TextView(Context, AttributeSet, int, int) - */ - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - public GifTextView(Context context, AttributeSet attrs, int defStyle, int defStyleRes) { - super(context, attrs, defStyle, defStyleRes); - init(attrs, defStyle, defStyleRes); - } - - private static void setDrawablesVisible(final Drawable[] drawables, final boolean visible) { - for (final Drawable drawable : drawables) { - if (drawable != null) { - drawable.setVisible(visible, false); - } - } - } - - private void init(AttributeSet attrs, int defStyle, int defStyleRes) { - if (attrs != null) { - Drawable left = getGifOrDefaultDrawable(attrs.getAttributeResourceValue(GifViewUtils.ANDROID_NS, "drawableLeft", 0)); - Drawable top = getGifOrDefaultDrawable(attrs.getAttributeResourceValue(GifViewUtils.ANDROID_NS, "drawableTop", 0)); - Drawable right = getGifOrDefaultDrawable(attrs.getAttributeResourceValue(GifViewUtils.ANDROID_NS, "drawableRight", 0)); - Drawable bottom = getGifOrDefaultDrawable(attrs.getAttributeResourceValue(GifViewUtils.ANDROID_NS, "drawableBottom", 0)); - Drawable start = getGifOrDefaultDrawable(attrs.getAttributeResourceValue(GifViewUtils.ANDROID_NS, "drawableStart", 0)); - Drawable end = getGifOrDefaultDrawable(attrs.getAttributeResourceValue(GifViewUtils.ANDROID_NS, "drawableEnd", 0)); - - if (getLayoutDirection() == LAYOUT_DIRECTION_LTR) { - if (start == null) { - start = left; - } - if (end == null) { - end = right; - } - } else { - if (start == null) { - start = right; - } - if (end == null) { - end = left; - } - } - setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom); - setBackground(getGifOrDefaultDrawable(attrs.getAttributeResourceValue(GifViewUtils.ANDROID_NS, "background", 0))); - viewAttributes = new GifViewUtils.GifViewAttributes(this, attrs, defStyle, defStyleRes); - applyGifViewAttributes(); - } - viewAttributes = new GifViewUtils.GifViewAttributes(); - } - - private void applyGifViewAttributes() { - if (viewAttributes.mLoopCount < 0) { - return; - } - for (final Drawable drawable : getCompoundDrawables()) { - GifViewUtils.applyLoopCount(viewAttributes.mLoopCount, drawable); - } - for (final Drawable drawable : getCompoundDrawablesRelative()) { - GifViewUtils.applyLoopCount(viewAttributes.mLoopCount, drawable); - } - GifViewUtils.applyLoopCount(viewAttributes.mLoopCount, getBackground()); - } - - @SuppressWarnings("deprecation") //Resources#getDrawable(int) - private Drawable getGifOrDefaultDrawable(int resId) { - if (resId == 0) { - return null; - } - final Resources resources = getResources(); - final String resourceTypeName = resources.getResourceTypeName(resId); - if (!isInEditMode() && GifViewUtils.SUPPORTED_RESOURCE_TYPE_NAMES.contains(resourceTypeName)) { - try { - return new GifDrawable(resources, resId); - } catch (IOException | NotFoundException ignored) { - // ignored - } - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - return resources.getDrawable(resId, getContext().getTheme()); - } else { - return resources.getDrawable(resId); - } - } - - @Override - public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) { - setCompoundDrawablesWithIntrinsicBounds(getGifOrDefaultDrawable(left), getGifOrDefaultDrawable(top), getGifOrDefaultDrawable(right), getGifOrDefaultDrawable(bottom)); - } - - @Override - public void setCompoundDrawablesRelativeWithIntrinsicBounds(int start, int top, int end, int bottom) { - setCompoundDrawablesRelativeWithIntrinsicBounds(getGifOrDefaultDrawable(start), getGifOrDefaultDrawable(top), getGifOrDefaultDrawable(end), getGifOrDefaultDrawable(bottom)); - } - - @Override - public Parcelable onSaveInstanceState() { - Drawable[] savedDrawables = new Drawable[7]; - if (viewAttributes.freezesAnimation) { - Drawable[] compoundDrawables = getCompoundDrawables(); - System.arraycopy(compoundDrawables, 0, savedDrawables, 0, compoundDrawables.length); - - Drawable[] compoundDrawablesRelative = getCompoundDrawablesRelative(); - savedDrawables[4] = compoundDrawablesRelative[0]; //start - savedDrawables[5] = compoundDrawablesRelative[2]; //end - savedDrawables[6] = getBackground(); - } - return new GifViewSavedState(super.onSaveInstanceState(), savedDrawables); - } - - @Override - public void onRestoreInstanceState(Parcelable state) { - if (!(state instanceof GifViewSavedState)) { - super.onRestoreInstanceState(state); - return; - } - GifViewSavedState ss = (GifViewSavedState) state; - super.onRestoreInstanceState(ss.getSuperState()); - - Drawable[] compoundDrawables = getCompoundDrawables(); - ss.restoreState(compoundDrawables[0], 0); - ss.restoreState(compoundDrawables[1], 1); - ss.restoreState(compoundDrawables[2], 2); - ss.restoreState(compoundDrawables[3], 3); - Drawable[] compoundDrawablesRelative = getCompoundDrawablesRelative(); - ss.restoreState(compoundDrawablesRelative[0], 4); - ss.restoreState(compoundDrawablesRelative[2], 5); - ss.restoreState(getBackground(), 6); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - setCompoundDrawablesVisible(true); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - setCompoundDrawablesVisible(false); - } - - @Override - public void setBackgroundResource(int resId) { - setBackground(getGifOrDefaultDrawable(resId)); - } - - private void setCompoundDrawablesVisible(final boolean visible) { - setDrawablesVisible(getCompoundDrawables(), visible); - setDrawablesVisible(getCompoundDrawablesRelative(), visible); - } - - /** - * Sets whether animation position is saved in {@link #onSaveInstanceState()} and restored - * in {@link #onRestoreInstanceState(Parcelable)}. This is applicable to all compound drawables. - * - * @param freezesAnimation whether animation position is saved - */ - public void setFreezesAnimation(boolean freezesAnimation) { - viewAttributes.freezesAnimation = freezesAnimation; - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTextView.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTextView.kt new file mode 100644 index 00000000..6d984eef --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTextView.kt @@ -0,0 +1,284 @@ +package pl.droidsonroids.gif + +import android.content.Context +import android.content.res.Resources.NotFoundException +import android.graphics.drawable.Drawable +import android.os.Build +import android.os.Parcelable +import android.util.AttributeSet +import android.widget.TextView +import androidx.annotation.RequiresApi +import pl.droidsonroids.gif.GifViewUtils.GifViewAttributes +import pl.droidsonroids.gif.GifViewUtils.applyLoopCount +import java.io.IOException + +/** + * A [TextView] which handles GIFs as compound drawables. NOTE: + * `android:drawableStart` and `android:drawableEnd` from XML are + * not supported but can be set using + * [.setCompoundDrawablesRelativeWithIntrinsicBounds] + * + * @author koral-- + */ +class GifTextView : TextView { + private lateinit var viewAttributes: GifViewAttributes + + /** + * A corresponding superclass constructor wrapper. + * + * @param context + */ + constructor(context: Context?) : super(context) + + /** + * Like equivalent from superclass but also try to interpret compound drawables defined in XML + * attributes as GIFs. + * + * @param context + * @param attrs + * @see TextView#TextView + */ + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + init(attrs, 0, 0) + } + + /** + * Like equivalent from superclass but also try to interpret compound drawables defined in XML + * attributes as GIFs. + * + * @param context + * @param attrs + * @param defStyle + * @see TextView#TextView + */ + constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super( + context, + attrs, + defStyle + ) { + init(attrs, defStyle, 0) + } + + /** + * Like equivalent from superclass but also try to interpret compound drawables defined in XML + * attributes as GIFs. + * + * @param context + * @param attrs + * @param defStyle + * @param defStyleRes + * @see TextView#TextView + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + constructor(context: Context?, attrs: AttributeSet?, defStyle: Int, defStyleRes: Int) : super( + context, + attrs, + defStyle, + defStyleRes + ) { + init(attrs, defStyle, defStyleRes) + } + + private fun init(attrs: AttributeSet?, defStyle: Int, defStyleRes: Int) { + if (attrs != null) { + val left = getGifOrDefaultDrawable( + attrs.getAttributeResourceValue( + GifViewUtils.ANDROID_NS, + "drawableLeft", + 0 + ) + ) + val top = getGifOrDefaultDrawable( + attrs.getAttributeResourceValue( + GifViewUtils.ANDROID_NS, + "drawableTop", + 0 + ) + ) + val right = getGifOrDefaultDrawable( + attrs.getAttributeResourceValue( + GifViewUtils.ANDROID_NS, + "drawableRight", + 0 + ) + ) + val bottom = getGifOrDefaultDrawable( + attrs.getAttributeResourceValue( + GifViewUtils.ANDROID_NS, + "drawableBottom", + 0 + ) + ) + var start = getGifOrDefaultDrawable( + attrs.getAttributeResourceValue( + GifViewUtils.ANDROID_NS, + "drawableStart", + 0 + ) + ) + var end = getGifOrDefaultDrawable( + attrs.getAttributeResourceValue( + GifViewUtils.ANDROID_NS, + "drawableEnd", + 0 + ) + ) + if (layoutDirection == LAYOUT_DIRECTION_LTR) { + if (start == null) { + start = left + } + if (end == null) { + end = right + } + } else { + if (start == null) { + start = right + } + if (end == null) { + end = left + } + } + setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom) + background = getGifOrDefaultDrawable( + attrs.getAttributeResourceValue( + GifViewUtils.ANDROID_NS, + "background", + 0 + ) + ) + viewAttributes = GifViewAttributes(this, attrs, defStyle, defStyleRes) + applyGifViewAttributes() + } else { + viewAttributes = GifViewAttributes() + } + } + + private fun applyGifViewAttributes() { + if (viewAttributes.mLoopCount < 0) { + return + } + for (drawable in compoundDrawables) { + applyLoopCount(viewAttributes.mLoopCount, drawable) + } + for (drawable in compoundDrawablesRelative) { + applyLoopCount(viewAttributes.mLoopCount, drawable) + } + applyLoopCount(viewAttributes.mLoopCount, background) + } + + @Suppress("deprecation") //Resources#getDrawable(int) + private fun getGifOrDefaultDrawable(resId: Int): Drawable? { + if (resId == 0) { + return null + } + val resources = resources + val resourceTypeName = resources.getResourceTypeName(resId) + if (!isInEditMode && GifViewUtils.SUPPORTED_RESOURCE_TYPE_NAMES.contains(resourceTypeName)) { + try { + return GifDrawable(resources, resId) + } catch (ignored: IOException) { + } catch (ignored: NotFoundException) { + } + } + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + resources.getDrawable(resId, context.theme) + } else { + resources.getDrawable(resId) + } + } + + override fun setCompoundDrawablesWithIntrinsicBounds( + left: Int, + top: Int, + right: Int, + bottom: Int + ) { + setCompoundDrawablesWithIntrinsicBounds( + getGifOrDefaultDrawable(left), + getGifOrDefaultDrawable(top), + getGifOrDefaultDrawable(right), + getGifOrDefaultDrawable(bottom) + ) + } + + override fun setCompoundDrawablesRelativeWithIntrinsicBounds( + start: Int, + top: Int, + end: Int, + bottom: Int + ) { + setCompoundDrawablesRelativeWithIntrinsicBounds( + getGifOrDefaultDrawable(start), + getGifOrDefaultDrawable(top), + getGifOrDefaultDrawable(end), + getGifOrDefaultDrawable(bottom) + ) + } + + override fun onSaveInstanceState(): Parcelable { + val savedDrawables = arrayOfNulls(7) + if (viewAttributes.freezesAnimation) { + val compoundDrawables = compoundDrawables + System.arraycopy(compoundDrawables, 0, savedDrawables, 0, compoundDrawables.size) + val compoundDrawablesRelative = compoundDrawablesRelative + savedDrawables[4] = compoundDrawablesRelative[0] //start + savedDrawables[5] = compoundDrawablesRelative[2] //end + savedDrawables[6] = background + } + return GifViewSavedState(super.onSaveInstanceState(), *savedDrawables) + } + + override fun onRestoreInstanceState(state: Parcelable) { + if (state !is GifViewSavedState) { + super.onRestoreInstanceState(state) + return + } + super.onRestoreInstanceState(state.superState) + val compoundDrawables = compoundDrawables + state.restoreState(compoundDrawables[0], 0) + state.restoreState(compoundDrawables[1], 1) + state.restoreState(compoundDrawables[2], 2) + state.restoreState(compoundDrawables[3], 3) + val compoundDrawablesRelative = compoundDrawablesRelative + state.restoreState(compoundDrawablesRelative[0], 4) + state.restoreState(compoundDrawablesRelative[2], 5) + state.restoreState(background, 6) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + setCompoundDrawablesVisible(true) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + setCompoundDrawablesVisible(false) + } + + override fun setBackgroundResource(resId: Int) { + background = getGifOrDefaultDrawable(resId) + } + + private fun setCompoundDrawablesVisible(visible: Boolean) { + setDrawablesVisible(compoundDrawables, visible) + setDrawablesVisible(compoundDrawablesRelative, visible) + } + + /** + * Sets whether animation position is saved in [.onSaveInstanceState] and restored + * in [.onRestoreInstanceState]. This is applicable to all compound drawables. + * + * @param freezesAnimation whether animation position is saved + */ + fun setFreezesAnimation(freezesAnimation: Boolean) { + viewAttributes.freezesAnimation = freezesAnimation + } + + companion object { + private fun setDrawablesVisible(drawables: Array, visible: Boolean) { + for (drawable in drawables) { + drawable?.setVisible(visible, false) + } + } + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTextureView.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTextureView.java deleted file mode 100644 index 47f54985..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTextureView.java +++ /dev/null @@ -1,534 +0,0 @@ -package pl.droidsonroids.gif; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.RectF; -import android.graphics.SurfaceTexture; -import android.opengl.EGL14; -import android.opengl.GLES20; -import android.os.Build; -import android.os.Parcelable; -import android.util.AttributeSet; -import android.util.TypedValue; -import android.view.Surface; -import android.view.TextureView; -import android.widget.ImageView.ScaleType; - -import java.io.IOException; -import java.lang.ref.WeakReference; - -import androidx.annotation.FloatRange; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; - -import javax.microedition.khronos.egl.EGL10; -import javax.microedition.khronos.egl.EGLConfig; -import javax.microedition.khronos.egl.EGLContext; -import javax.microedition.khronos.egl.EGLDisplay; -import javax.microedition.khronos.egl.EGLSurface; - -/** - *

{@link TextureView} which can display animated GIFs. GifTextureView can only be used in a - * hardware accelerated window. When rendered in software, GifTextureView will draw nothing.

- *

GIF source can be specified in XML or by calling {@link #setInputSource(InputSource)}

- *
 {@code
- *      }
- * 
- * Note that src attribute comes from app namespace (you can call it whatever you want) not from - * android one. Drawable, raw, mipmap resources and assets can be specified through XML. If value is a string - * (referenced from resources or entered directly) it will be treated as an asset. - *

Unlike {@link TextureView} GifTextureView is transparent by default, but it can be changed by - * {@link #setOpaque(boolean)}. - * You can use scale types the same way as in {@link android.widget.ImageView}.

- */ -public class GifTextureView extends TextureView { - - private static final ScaleType[] sScaleTypeArray = { - ScaleType.MATRIX, - ScaleType.FIT_XY, - ScaleType.FIT_START, - ScaleType.FIT_CENTER, - ScaleType.FIT_END, - ScaleType.CENTER, - ScaleType.CENTER_CROP, - ScaleType.CENTER_INSIDE - }; - private ScaleType mScaleType = ScaleType.FIT_CENTER; - private final Matrix mTransform = new Matrix(); - private InputSource mInputSource; - private RenderThread mRenderThread; - private float mSpeedFactor = 1f; - private GifViewUtils.GifViewAttributes viewAttributes; - - public GifTextureView(Context context) { - super(context); - init(null, 0, 0); - } - - public GifTextureView(Context context, AttributeSet attrs) { - super(context, attrs); - init(attrs, 0, 0); - } - - public GifTextureView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(attrs, defStyleAttr, 0); - } - - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - public GifTextureView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - init(attrs, defStyleAttr, defStyleRes); - } - - private void init(AttributeSet attrs, int defStyleAttr, int defStyleRes) { - if (attrs != null) { - final int scaleTypeIndex = attrs.getAttributeIntValue(GifViewUtils.ANDROID_NS, "scaleType", -1); - if (scaleTypeIndex >= 0 && scaleTypeIndex < sScaleTypeArray.length) { - mScaleType = sScaleTypeArray[scaleTypeIndex]; - } - final TypedArray textureViewAttributes = getContext().obtainStyledAttributes(attrs, R.styleable - .GifTextureView, defStyleAttr, defStyleRes); - mInputSource = findSource(textureViewAttributes); - super.setOpaque(textureViewAttributes.getBoolean(R.styleable.GifTextureView_isOpaque, false)); - textureViewAttributes.recycle(); - viewAttributes = new GifViewUtils.GifViewAttributes(this, attrs, defStyleAttr, defStyleRes); - } else { - super.setOpaque(false); - viewAttributes = new GifViewUtils.GifViewAttributes(); - } - if (!isInEditMode()) { - mRenderThread = new RenderThread(this); - if (mInputSource != null) { - mRenderThread.start(); - } - } - } - - /** - * Always throws {@link UnsupportedOperationException}. Changing {@link SurfaceTextureListener} - * is not supported. - * - * @param listener ignored - */ - @Override - public void setSurfaceTextureListener(SurfaceTextureListener listener) { - throw new UnsupportedOperationException("Changing SurfaceTextureListener is not supported"); - } - - /** - * Always returns null since changing {@link SurfaceTextureListener} is not supported. - * - * @return always null - */ - @Override - public SurfaceTextureListener getSurfaceTextureListener() { - return null; - } - - /** - * Always throws {@link UnsupportedOperationException}. Changing {@link SurfaceTexture} is not - * supported. - * - * @param surfaceTexture ignored - */ - @Override - public void setSurfaceTexture(SurfaceTexture surfaceTexture) { - throw new UnsupportedOperationException("Changing SurfaceTexture is not supported"); - } - - private static InputSource findSource(final TypedArray textureViewAttributes) { - final TypedValue value = new TypedValue(); - if (!textureViewAttributes.getValue(R.styleable.GifTextureView_gifSource, value)) { - return null; - } - - if (value.resourceId != 0) { - final String resourceTypeName = textureViewAttributes.getResources().getResourceTypeName(value.resourceId); - if (GifViewUtils.SUPPORTED_RESOURCE_TYPE_NAMES.contains(resourceTypeName)) { - return new InputSource.ResourcesSource(textureViewAttributes.getResources(), value.resourceId); - } else if (!"string".equals(resourceTypeName)) { - throw new IllegalArgumentException( - "Expected string, drawable, mipmap or raw resource type. '" + resourceTypeName - + "' is not supported"); - } - } - return new InputSource.AssetSource(textureViewAttributes.getResources().getAssets(), value.string.toString()); - } - - private static class RenderThread extends Thread implements SurfaceTextureListener { - - final ConditionVariable isSurfaceValid = new ConditionVariable(); - private GifInfoHandle mGifInfoHandle = new GifInfoHandle(); - private IOException mIOException; - long[] mSavedState; - private final WeakReference mGifTextureViewReference; - - RenderThread(final GifTextureView gifTextureView) { - super("GifRenderThread"); - mGifTextureViewReference = new WeakReference<>(gifTextureView); - } - - @Override - public void run() { - try { - final GifTextureView gifTextureView = mGifTextureViewReference.get(); - if (gifTextureView == null) { - return; - } - mGifInfoHandle = gifTextureView.mInputSource.open(); - mGifInfoHandle.setOptions((char) 1, gifTextureView.isOpaque()); - if (gifTextureView.viewAttributes.mLoopCount >= 0) { - mGifInfoHandle.setLoopCount(gifTextureView.viewAttributes.mLoopCount); - } - } catch (IOException ex) { - mIOException = ex; - return; - } - - final GifTextureView gifTextureView = mGifTextureViewReference.get(); - if (gifTextureView == null) { - mGifInfoHandle.recycle(); - return; - } - - gifTextureView.setSuperSurfaceTextureListener(this); - final boolean isSurfaceAvailable = gifTextureView.isAvailable(); - isSurfaceValid.set(isSurfaceAvailable); - if (isSurfaceAvailable) { - gifTextureView.post(new Runnable() { - @Override - public void run() { - gifTextureView.updateTextureViewSize(mGifInfoHandle); - } - }); - } - mGifInfoHandle.setSpeedFactor(gifTextureView.mSpeedFactor); - - while (!isInterrupted()) { - try { - isSurfaceValid.block(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; - } - final GifTextureView currentGifTextureView = mGifTextureViewReference.get(); - if (currentGifTextureView == null) { - break; - } - final SurfaceTexture surfaceTexture = currentGifTextureView.getSurfaceTexture(); - if (surfaceTexture == null) { - continue; - } - final Surface surface = new Surface(surfaceTexture); - try { - mGifInfoHandle.bindSurface(surface, mSavedState); - } finally { - surface.release(); - } - } - mGifInfoHandle.recycle(); - mGifInfoHandle = new GifInfoHandle(); - } - - @Override - public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { - final GifTextureView gifTextureView = mGifTextureViewReference.get(); - if (gifTextureView != null) { - gifTextureView.updateTextureViewSize(mGifInfoHandle); - } - isSurfaceValid.open(); - } - - @Override - public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { - //no-op - } - - @Override - public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { - isSurfaceValid.close(); - mGifInfoHandle.postUnbindSurface(); - interrupt(); - return true; - } - - @Override - public void onSurfaceTextureUpdated(SurfaceTexture surface) { - //no-op - } - - void dispose(@NonNull final GifTextureView gifTextureView, @Nullable final PlaceholderDrawListener drawer) { - isSurfaceValid.close(); - final SurfaceTextureListener listener = - drawer != null ? new PlaceholderDrawingSurfaceTextureListener(drawer) : null; - gifTextureView.setSuperSurfaceTextureListener(listener); - mGifInfoHandle.postUnbindSurface(); - interrupt(); - } - } - - private void setSuperSurfaceTextureListener(SurfaceTextureListener listener) { - super.setSurfaceTextureListener(listener); - } - - /** - * Indicates whether the content of this GifTextureView is opaque. The - * content is assumed to be non-opaque by default (unlike {@link TextureView}. - * View that is known to be opaque can take a faster drawing case than non-opaque one.
- * Opacity change will cause animation to restart. - * - * @param opaque True if the content of this GifTextureView is opaque, - * false otherwise - */ - @Override - public void setOpaque(boolean opaque) { - if (opaque != isOpaque()) { - super.setOpaque(opaque); - setInputSource(mInputSource); - } - } - - @Override - protected void onDetachedFromWindow() { - mRenderThread.dispose(this, null); - super.onDetachedFromWindow(); - final SurfaceTexture surfaceTexture = getSurfaceTexture(); - if (surfaceTexture != null) { - surfaceTexture.release(); - } - } - - /** - * Sets the source of the animation. Pass {@code null} to remove current source. - * Equivalent of {@code setInputSource(inputSource, null)}. - * - * @param inputSource new animation source, may be null - */ - public synchronized void setInputSource(@Nullable InputSource inputSource) { - setInputSource(inputSource, null); - } - - /** - * Sets the source of the animation and optionally placeholder drawer. Pass {@code null inputSource} to remove current source. - * {@code placeholderDrawListener} is overwritten on {@code setInputSource(inputSource)} call. - * - * @param inputSource new animation source, may be null - * @param placeholderDrawListener placeholder draw listener, may be null - */ - public synchronized void setInputSource(@Nullable InputSource inputSource, - @Nullable PlaceholderDrawListener placeholderDrawListener) { - mRenderThread.dispose(this, placeholderDrawListener); - try { - mRenderThread.join(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - mInputSource = inputSource; - mRenderThread = new RenderThread(this); - if (inputSource != null) { - mRenderThread.start(); - } else { - clearSurface(); - } - } - - private void clearSurface() { - final SurfaceTexture surfaceTexture = getSurfaceTexture(); - if (surfaceTexture != null) { - final Surface surface = new Surface(surfaceTexture); - try { - surface.unlockCanvasAndPost(surface.lockCanvas(null)); - } finally { - surface.release(); - } - } - } - - /** - * Equivalent of {@link GifDrawable#setSpeed(float)}. - * - * @param factor new speed factor, eg. 0.5f means half speed, 1.0f - normal, 2.0f - double speed - * @throws IllegalArgumentException if {@code factor <= 0} - * @see GifDrawable#setSpeed(float) - */ - public void setSpeed(@FloatRange(from = 0, fromInclusive = false) float factor) { - mSpeedFactor = factor; - mRenderThread.mGifInfoHandle.setSpeedFactor(factor); - } - - /** - * Returns last {@link IOException} occurred during loading or playing GIF (in such case only {@link GifIOException} - * can be returned. Null is returned when source is not set, surface was not yet created or no error - * occurred. - * - * @return exception occurred during loading or playing GIF or null - */ - @Nullable - public IOException getIOException() { - if (mRenderThread.mIOException != null) { - return mRenderThread.mIOException; - } else { - return GifIOException.fromCode(mRenderThread.mGifInfoHandle.getNativeErrorCode()); - } - } - - /** - * Controls how the image should be resized or moved to match the size - * of this GifTextureView. - * - * @param scaleType The desired scaling mode. - */ - public void setScaleType(@NonNull ScaleType scaleType) { - mScaleType = scaleType; - updateTextureViewSize(mRenderThread.mGifInfoHandle); - } - - /** - * @return the current scale type in use by this View. - * @see ScaleType - */ - public ScaleType getScaleType() { - return mScaleType; - } - - private void updateTextureViewSize(final GifInfoHandle gifInfoHandle) { - final Matrix transform = new Matrix(); - final float viewWidth = getWidth(); - final float viewHeight = getHeight(); - final float scaleRef; - final float scaleX = gifInfoHandle.getWidth() / viewWidth; - final float scaleY = gifInfoHandle.getHeight() / viewHeight; - RectF src = new RectF(0, 0, gifInfoHandle.getWidth(), gifInfoHandle.getHeight()); - RectF dst = new RectF(0, 0, viewWidth, viewHeight); - switch (mScaleType) { - case CENTER: - transform.setScale(scaleX, scaleY, viewWidth / 2, viewHeight / 2); - break; - case CENTER_CROP: - scaleRef = 1 / Math.min(scaleX, scaleY); - transform.setScale(scaleRef * scaleX, scaleRef * scaleY, viewWidth / 2, viewHeight / 2); - break; - case CENTER_INSIDE: - if (gifInfoHandle.getWidth() <= viewWidth && gifInfoHandle.getHeight() <= viewHeight) { - scaleRef = 1.0f; - } else { - scaleRef = Math.min(1 / scaleX, 1 / scaleY); - } - transform.setScale(scaleRef * scaleX, scaleRef * scaleY, viewWidth / 2, viewHeight / 2); - break; - case FIT_CENTER: - transform.setRectToRect(src, dst, Matrix.ScaleToFit.CENTER); - transform.preScale(scaleX, scaleY); - break; - case FIT_END: - transform.setRectToRect(src, dst, Matrix.ScaleToFit.END); - transform.preScale(scaleX, scaleY); - break; - case FIT_START: - transform.setRectToRect(src, dst, Matrix.ScaleToFit.START); - transform.preScale(scaleX, scaleY); - break; - case FIT_XY: - return; - case MATRIX: - transform.set(mTransform); - transform.preScale(scaleX, scaleY); - break; - } - super.setTransform(transform); - } - - /** - * Wrapper of {@link #setTransform(Matrix)}. Introduced to preserve the same API as in - * {@link GifImageView}. - * - * @param matrix The transform to apply to the content of this view. - */ - public void setImageMatrix(Matrix matrix) { - setTransform(matrix); - } - - /** - * Works like {@link TextureView#setTransform(Matrix)} but transform will take effect only if - * scale type is set to {@link ScaleType#MATRIX} through XML attribute or via {@link #setScaleType(ScaleType)} - * - * @param transform The transform to apply to the content of this view. - */ - @Override - public void setTransform(Matrix transform) { - mTransform.set(transform); - updateTextureViewSize(mRenderThread.mGifInfoHandle); - } - - /** - * Returns the transform associated with this texture view, either set explicitly by {@link #setTransform(Matrix)} - * or computed according to the current scale type. - * - * @param transform The {@link Matrix} in which to copy the current transform. Can be null. - * @return The specified matrix if not null or a new {@link Matrix} instance otherwise. - * @see #setTransform(android.graphics.Matrix) - * @see #setScaleType(ScaleType) - */ - @Override - public Matrix getTransform(Matrix transform) { - if (transform == null) { - transform = new Matrix(); - } - transform.set(mTransform); - return transform; - } - - @Override - public Parcelable onSaveInstanceState() { - mRenderThread.mSavedState = mRenderThread.mGifInfoHandle.getSavedState(); - return new GifViewSavedState(super.onSaveInstanceState(), - viewAttributes.freezesAnimation ? mRenderThread.mSavedState : null); - } - - @Override - public void onRestoreInstanceState(Parcelable state) { - if (!(state instanceof GifViewSavedState)) { - super.onRestoreInstanceState(state); - return; - } - GifViewSavedState ss = (GifViewSavedState) state; - super.onRestoreInstanceState(ss.getSuperState()); - mRenderThread.mSavedState = ss.mStates[0]; - } - - /** - * Sets whether animation position is saved in {@link #onSaveInstanceState()} and restored - * in {@link #onRestoreInstanceState(Parcelable)} - * - * @param freezesAnimation whether animation position is saved - */ - public void setFreezesAnimation(boolean freezesAnimation) { - viewAttributes.freezesAnimation = freezesAnimation; - } - - /** - * This listener can be used to be notified when the {@link GifTextureView} content placeholder can be drawn. - * Placeholder is displayed before proper input source is loaded and remains visible when input source loading fails. - */ - public interface PlaceholderDrawListener { - /** - * Called when surface is ready and placeholder has to be drawn. - * It may occur more than once (eg. if {@code View} visibility is toggled before input source is loaded) - * or never (eg. when {@code View} is never visible).
- * Note that it is an error to use {@code canvas} after this method return. - * - * @param canvas canvas to draw into - */ - void onDrawPlaceholder(Canvas canvas); - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTextureView.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTextureView.kt new file mode 100644 index 00000000..43ca7ab1 --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifTextureView.kt @@ -0,0 +1,530 @@ +package pl.droidsonroids.gif + +import android.content.Context +import android.content.res.TypedArray +import android.graphics.Canvas +import android.graphics.Matrix +import android.graphics.RectF +import android.graphics.SurfaceTexture +import android.os.Build +import android.os.Parcelable +import android.util.AttributeSet +import android.util.TypedValue +import android.view.Surface +import android.view.TextureView +import android.widget.ImageView.ScaleType +import androidx.annotation.FloatRange +import androidx.annotation.RequiresApi +import pl.droidsonroids.gif.GifIOException.Companion.fromCode +import pl.droidsonroids.gif.GifViewUtils.GifViewAttributes +import pl.droidsonroids.gif.InputSource.AssetSource +import pl.droidsonroids.gif.InputSource.ResourcesSource +import java.io.IOException +import java.lang.ref.WeakReference +import kotlin.jvm.Throws +import kotlin.math.min + +/** + * + * [TextureView] which can display animated GIFs. GifTextureView can only be used in a + * hardware accelerated window. When rendered in software, GifTextureView will draw nothing. + * + * GIF source can be specified in XML or by calling [.setInputSource] + *
 ` `
+
* + * Note that **src** attribute comes from app namespace (you can call it whatever you want) not from + * android one. Drawable, raw, mipmap resources and assets can be specified through XML. If value is a string + * (referenced from resources or entered directly) it will be treated as an asset. + * + * Unlike [TextureView] GifTextureView is transparent by default, but it can be changed by + * [.setOpaque]. + * You can use scale types the same way as in [android.widget.ImageView]. + */ +class GifTextureView : TextureView { + private var mScaleType = ScaleType.FIT_CENTER + private val mTransform = Matrix() + private var mInputSource: InputSource? = null + private var mRenderThread: RenderThread? = null + private var mSpeedFactor = 1f + private var viewAttributes: GifViewAttributes? = null + + constructor(context: Context?) : super(context!!) { + init(null, 0, 0) + } + + constructor(context: Context?, attrs: AttributeSet?) : super( + context!!, attrs + ) { + init(attrs, 0, 0) + } + + constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( + context!!, attrs, defStyleAttr + ) { + init(attrs, defStyleAttr, 0) + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + constructor( + context: Context?, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super( + context!!, attrs, defStyleAttr, defStyleRes + ) { + init(attrs, defStyleAttr, defStyleRes) + } + + private fun init(attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) { + if (attrs != null) { + val scaleTypeIndex = + attrs.getAttributeIntValue(GifViewUtils.ANDROID_NS, "scaleType", -1) + if (scaleTypeIndex >= 0 && scaleTypeIndex < sScaleTypeArray.size) { + mScaleType = sScaleTypeArray[scaleTypeIndex] + } + val textureViewAttributes = context.obtainStyledAttributes( + attrs, + R.styleable.GifTextureView, + defStyleAttr, + defStyleRes + ) + mInputSource = findSource(textureViewAttributes) + super.setOpaque( + textureViewAttributes.getBoolean( + R.styleable.GifTextureView_isOpaque, + false + ) + ) + textureViewAttributes.recycle() + viewAttributes = GifViewAttributes(this, attrs, defStyleAttr, defStyleRes) + } else { + super.setOpaque(false) + viewAttributes = GifViewAttributes() + } + if (!isInEditMode) { + mRenderThread = RenderThread(this) + if (mInputSource != null) { + mRenderThread!!.start() + } + } + } + + /** + * Always throws [UnsupportedOperationException]. Changing [SurfaceTextureListener] + * is not supported. + * + * @param listener ignored + */ + override fun setSurfaceTextureListener(listener: SurfaceTextureListener?) { + throw UnsupportedOperationException("Changing SurfaceTextureListener is not supported") + } + + /** + * Always returns null since changing [SurfaceTextureListener] is not supported. + * + * @return always null + */ + override fun getSurfaceTextureListener(): SurfaceTextureListener? { + return null + } + + /** + * Always throws [UnsupportedOperationException]. Changing [SurfaceTexture] is not + * supported. + * + * @param surfaceTexture ignored + */ + override fun setSurfaceTexture(surfaceTexture: SurfaceTexture) { + throw UnsupportedOperationException("Changing SurfaceTexture is not supported") + } + + private class RenderThread internal constructor(gifTextureView: GifTextureView) : + Thread("GifRenderThread"), SurfaceTextureListener { + val isSurfaceValid = ConditionVariable() + var mGifInfoHandle = GifInfoHandle() + var mIOException: IOException? = null + var mSavedState: LongArray? = null + private val mGifTextureViewReference: WeakReference + + init { + mGifTextureViewReference = WeakReference(gifTextureView) + } + + override fun run() { + try { + val gifTextureView = mGifTextureViewReference.get() ?: return + mGifInfoHandle = gifTextureView.mInputSource?.open()!! + mGifInfoHandle.setOptions(1.toChar(), gifTextureView.isOpaque) + if (gifTextureView.viewAttributes != null && gifTextureView.viewAttributes!!.mLoopCount >= 0) { + mGifInfoHandle.loopCount = gifTextureView.viewAttributes!!.mLoopCount + } + } catch (ex: IOException) { + mIOException = ex + return + } + val gifTextureView = mGifTextureViewReference.get() + if (gifTextureView == null) { + mGifInfoHandle.recycle() + return + } + gifTextureView.setSuperSurfaceTextureListener(this) + val isSurfaceAvailable = gifTextureView.isAvailable + isSurfaceValid.set(isSurfaceAvailable) + if (isSurfaceAvailable) { + gifTextureView.post(Runnable { gifTextureView.updateTextureViewSize(mGifInfoHandle) }) + } + mGifInfoHandle.setSpeedFactor(gifTextureView.mSpeedFactor) + while (!isInterrupted) { + try { + isSurfaceValid.block() + } catch (e: InterruptedException) { + currentThread().interrupt() + break + } + val currentGifTextureView = mGifTextureViewReference.get() ?: break + val surfaceTexture = currentGifTextureView.surfaceTexture ?: continue + val surface = Surface(surfaceTexture) + try { + mGifInfoHandle.bindSurface(surface, mSavedState) + } finally { + surface.release() + } + } + mGifInfoHandle.recycle() + mGifInfoHandle = GifInfoHandle() + } + + override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { + val gifTextureView = mGifTextureViewReference.get() + gifTextureView?.updateTextureViewSize(mGifInfoHandle) + isSurfaceValid.open() + } + + override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) { + //no-op + } + + override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { + isSurfaceValid.close() + mGifInfoHandle.postUnbindSurface() + interrupt() + return true + } + + override fun onSurfaceTextureUpdated(surface: SurfaceTexture) { + //no-op + } + + fun dispose(gifTextureView: GifTextureView, drawer: PlaceholderDrawListener?) { + isSurfaceValid.close() + val listener: SurfaceTextureListener? = + drawer?.let { PlaceholderDrawingSurfaceTextureListener(it) } + gifTextureView.setSuperSurfaceTextureListener(listener) + mGifInfoHandle.postUnbindSurface() + interrupt() + } + } + + private fun setSuperSurfaceTextureListener(listener: SurfaceTextureListener?) { + super.setSurfaceTextureListener(listener) + } + + /** + * Indicates whether the content of this GifTextureView is opaque. The + * content is assumed to be **non-opaque** by default (unlike [TextureView]. + * View that is known to be opaque can take a faster drawing case than non-opaque one.

+ * Opacity change will cause animation to restart. + * + * @param opaque True if the content of this GifTextureView is opaque, + * false otherwise + */ + override fun setOpaque(opaque: Boolean) { + if (opaque != isOpaque) { + super.setOpaque(opaque) + setInputSource(mInputSource) + } + } + + override fun onDetachedFromWindow() { + mRenderThread?.dispose(this, null) + super.onDetachedFromWindow() + val surfaceTexture = surfaceTexture + surfaceTexture?.release() + } + + /** + * Sets the source of the animation. Pass `null` to remove current source. + * Equivalent of `setInputSource(inputSource, null)`. + * + * @param inputSource new animation source, may be null + */ + @Synchronized + fun setInputSource(inputSource: InputSource?) { + setInputSource(inputSource, null) + } + + /** + * Sets the source of the animation and optionally placeholder drawer. Pass `null inputSource` to remove current source. + * `placeholderDrawListener` is overwritten on `setInputSource(inputSource)` call. + * + * @param inputSource new animation source, may be null + * @param placeholderDrawListener placeholder draw listener, may be null + */ + @Synchronized + fun setInputSource( + inputSource: InputSource?, + placeholderDrawListener: PlaceholderDrawListener? + ) { + mRenderThread?.dispose(this, placeholderDrawListener) + try { + mRenderThread?.join() + } catch (e: InterruptedException) { + e.printStackTrace() + } + mInputSource = inputSource + mRenderThread = RenderThread(this) + if (inputSource != null) { + mRenderThread?.start() + } else { + clearSurface() + } + } + + private fun clearSurface() { + val surfaceTexture = surfaceTexture + if (surfaceTexture != null) { + val surface = Surface(surfaceTexture) + try { + surface.unlockCanvasAndPost(surface.lockCanvas(null)) + } finally { + surface.release() + } + } + } + + /** + * Equivalent of [GifDrawable.setSpeed]. + * + * @param factor new speed factor, eg. 0.5f means half speed, 1.0f - normal, 2.0f - double speed + * @throws IllegalArgumentException if `factor <= 0` + * @see GifDrawable.setSpeed + */ + @Throws(IllegalArgumentException::class) + fun setSpeed(@FloatRange(from = 0.0, fromInclusive = false) factor: Float) { + mSpeedFactor = factor + mRenderThread?.mGifInfoHandle?.setSpeedFactor(factor) + } + + val iOException: IOException? + /** + * Returns last [IOException] occurred during loading or playing GIF (in such case only [GifIOException] + * can be returned. Null is returned when source is not set, surface was not yet created or no error + * occurred. + * + * @return exception occurred during loading or playing GIF or null + */ + get() = if (mRenderThread?.mIOException != null) { + mRenderThread?.mIOException + } else { + fromCode(mRenderThread?.mGifInfoHandle?.nativeErrorCode) + } + var scaleType: ScaleType + /** + * @return the current scale type in use by this View. + * @see ScaleType + */ + get() = mScaleType + /** + * Controls how the image should be resized or moved to match the size + * of this GifTextureView. + * + * @param scaleType The desired scaling mode. + */ + set(scaleType) { + mScaleType = scaleType + updateTextureViewSize(mRenderThread!!.mGifInfoHandle) + } + + private fun updateTextureViewSize(gifInfoHandle: GifInfoHandle?) { + + if (gifInfoHandle == null) return + + val transform = Matrix() + val viewWidth = width.toFloat() + val viewHeight = height.toFloat() + val scaleRef: Float + val scaleX = gifInfoHandle.width / viewWidth + val scaleY = gifInfoHandle.height / viewHeight + val src = RectF(0f, 0f, gifInfoHandle.width.toFloat(), gifInfoHandle.height.toFloat()) + val dst = RectF(0f, 0f, viewWidth, viewHeight) + when (mScaleType) { + ScaleType.CENTER -> transform.setScale(scaleX, scaleY, viewWidth / 2, viewHeight / 2) + ScaleType.CENTER_CROP -> { + scaleRef = 1 / min(scaleX, scaleY) + transform.setScale( + scaleRef * scaleX, + scaleRef * scaleY, + viewWidth / 2, + viewHeight / 2 + ) + } + + ScaleType.CENTER_INSIDE -> { + scaleRef = + if (gifInfoHandle.width <= viewWidth && gifInfoHandle.height <= viewHeight) { + 1.0f + } else { + min(1 / scaleX, 1 / scaleY) + } + transform.setScale( + scaleRef * scaleX, + scaleRef * scaleY, + viewWidth / 2, + viewHeight / 2 + ) + } + + ScaleType.FIT_CENTER -> { + transform.setRectToRect(src, dst, Matrix.ScaleToFit.CENTER) + transform.preScale(scaleX, scaleY) + } + + ScaleType.FIT_END -> { + transform.setRectToRect(src, dst, Matrix.ScaleToFit.END) + transform.preScale(scaleX, scaleY) + } + + ScaleType.FIT_START -> { + transform.setRectToRect(src, dst, Matrix.ScaleToFit.START) + transform.preScale(scaleX, scaleY) + } + + ScaleType.FIT_XY -> return + ScaleType.MATRIX -> { + transform.set(mTransform) + transform.preScale(scaleX, scaleY) + } + } + super.setTransform(transform) + } + + /** + * Wrapper of [.setTransform]. Introduced to preserve the same API as in + * [GifImageView]. + * + * @param matrix The transform to apply to the content of this view. + */ + fun setImageMatrix(matrix: Matrix?) { + setTransform(matrix) + } + + /** + * Works like [TextureView.setTransform] but transform will take effect only if + * scale type is set to [ScaleType.MATRIX] through XML attribute or via [.setScaleType] + * + * @param transform The transform to apply to the content of this view. + */ + override fun setTransform(transform: Matrix?) { + mTransform.set(transform) + updateTextureViewSize(mRenderThread!!.mGifInfoHandle) + } + + /** + * Returns the transform associated with this texture view, either set explicitly by [.setTransform] + * or computed according to the current scale type. + * + * @param transform The [Matrix] in which to copy the current transform. Can be null. + * @return The specified matrix if not null or a new [Matrix] instance otherwise. + * @see .setTransform + * @see .setScaleType + */ + override fun getTransform(transform: Matrix?): Matrix { + var matrix = transform + if (matrix == null) { + matrix = Matrix() + } + matrix.set(mTransform) + return matrix + } + + public override fun onSaveInstanceState(): Parcelable { + mRenderThread?.mSavedState = mRenderThread?.mGifInfoHandle?.savedState + return GifViewSavedState( + super.onSaveInstanceState(), + if (viewAttributes?.freezesAnimation == true) mRenderThread?.mSavedState else null + ) + } + + public override fun onRestoreInstanceState(state: Parcelable) { + if (state !is GifViewSavedState) { + super.onRestoreInstanceState(state) + return + } + super.onRestoreInstanceState(state.superState) + mRenderThread?.mSavedState = state.mStates[0] + } + + /** + * Sets whether animation position is saved in [.onSaveInstanceState] and restored + * in [.onRestoreInstanceState] + * + * @param freezesAnimation whether animation position is saved + */ + fun setFreezesAnimation(freezesAnimation: Boolean) { + viewAttributes?.freezesAnimation = freezesAnimation + } + + /** + * This listener can be used to be notified when the [GifTextureView] content placeholder can be drawn. + * Placeholder is displayed before proper input source is loaded and remains visible when input source loading fails. + */ + interface PlaceholderDrawListener { + /** + * Called when surface is ready and placeholder has to be drawn. + * It may occur more than once (eg. if `View` visibility is toggled before input source is loaded) + * or never (eg. when `View` is never visible).

+ * Note that it is an error to use `canvas` after this method return. + * + * @param canvas canvas to draw into + */ + fun onDrawPlaceholder(canvas: Canvas) + } + + companion object { + private val sScaleTypeArray = arrayOf( + ScaleType.MATRIX, + ScaleType.FIT_XY, + ScaleType.FIT_START, + ScaleType.FIT_CENTER, + ScaleType.FIT_END, + ScaleType.CENTER, + ScaleType.CENTER_CROP, + ScaleType.CENTER_INSIDE + ) + + private fun findSource(textureViewAttributes: TypedArray): InputSource? { + val value = TypedValue() + if (!textureViewAttributes.getValue(R.styleable.GifTextureView_gifSource, value)) { + return null + } + if (value.resourceId != 0) { + val resourceTypeName = + textureViewAttributes.resources.getResourceTypeName(value.resourceId) + if (GifViewUtils.SUPPORTED_RESOURCE_TYPE_NAMES.contains(resourceTypeName)) { + return ResourcesSource(textureViewAttributes.resources, value.resourceId) + } else require("string" == resourceTypeName) { + ("Expected string, drawable, mipmap or raw resource type. '" + resourceTypeName + + "' is not supported") + } + } + return AssetSource(textureViewAttributes.resources.assets, value.string.toString()) + } + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifViewSavedState.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifViewSavedState.java deleted file mode 100644 index 5cc27df7..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifViewSavedState.java +++ /dev/null @@ -1,64 +0,0 @@ -package pl.droidsonroids.gif; - -import android.graphics.drawable.Drawable; -import android.os.Parcel; -import android.os.Parcelable; -import androidx.annotation.NonNull; -import android.view.View; - -class GifViewSavedState extends View.BaseSavedState { - - final long[][] mStates; - - GifViewSavedState(Parcelable superState, Drawable... drawables) { - super(superState); - mStates = new long[drawables.length][]; - for (int i = 0; i < drawables.length; i++) { - Drawable drawable = drawables[i]; - if (drawable instanceof GifDrawable) { - mStates[i] = ((GifDrawable) drawable).mNativeInfoHandle.getSavedState(); - } else { - mStates[i] = null; - } - } - } - - private GifViewSavedState(Parcel in) { - super(in); - mStates = new long[in.readInt()][]; - for (int i = 0; i < mStates.length; i++) - mStates[i] = in.createLongArray(); - } - - GifViewSavedState(Parcelable superState, long[] savedState) { - super(superState); - mStates = new long[1][]; - mStates[0] = savedState; - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeInt(mStates.length); - for (long[] mState : mStates) - dest.writeLongArray(mState); - } - - public static final Creator CREATOR = new Creator() { - - public GifViewSavedState createFromParcel(Parcel in) { - return new GifViewSavedState(in); - } - - public GifViewSavedState[] newArray(int size) { - return new GifViewSavedState[size]; - } - }; - - void restoreState(Drawable drawable, int i) { - if (mStates[i] != null && drawable instanceof GifDrawable) { - final GifDrawable gifDrawable = (GifDrawable) drawable; - gifDrawable.startAnimation(gifDrawable.mNativeInfoHandle.restoreSavedState(mStates[i], gifDrawable.mBuffer)); - } - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifViewSavedState.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifViewSavedState.kt new file mode 100644 index 00000000..1ae7e3d0 --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifViewSavedState.kt @@ -0,0 +1,64 @@ +package pl.droidsonroids.gif + +import android.graphics.drawable.Drawable +import android.os.Parcel +import android.os.Parcelable +import android.os.Parcelable.Creator +import android.view.View + +internal class GifViewSavedState : View.BaseSavedState { + val mStates: Array + + constructor(superState: Parcelable?, vararg drawables: Drawable?) : super(superState) { + mStates = arrayOfNulls(drawables.size) + for (i in drawables.indices) { + val drawable = drawables[i] + if (drawable is GifDrawable) { + mStates[i] = drawable.mNativeInfoHandle.savedState + } else { + mStates[i] = null + } + } + } + + private constructor(`in`: Parcel) : super(`in`) { + mStates = arrayOfNulls(`in`.readInt()) + for (i in mStates.indices) mStates[i] = `in`.createLongArray() + } + + constructor(superState: Parcelable?, savedState: LongArray?) : super(superState) { + mStates = arrayOfNulls(1) + mStates[0] = savedState + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + super.writeToParcel(dest, flags) + dest.writeInt(mStates.size) + for (mState in mStates) dest.writeLongArray(mState) + } + + fun restoreState(drawable: Drawable?, i: Int) { + if (mStates[i] != null && drawable is GifDrawable) { + drawable.startAnimation( + drawable.mNativeInfoHandle.restoreSavedState( + mStates[i]!!, + drawable.mBuffer + ).toLong() + ) + } + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Creator { + override fun createFromParcel(parcel: Parcel): GifViewSavedState { + return GifViewSavedState(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifViewUtils.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifViewUtils.java deleted file mode 100644 index 3f14e5e5..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifViewUtils.java +++ /dev/null @@ -1,146 +0,0 @@ -package pl.droidsonroids.gif; - -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.RawRes; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.util.TypedValue; -import android.view.View; -import android.widget.ImageView; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; - -final class GifViewUtils { - static final String ANDROID_NS = "http://schemas.android.com/apk/res/android"; - static final List SUPPORTED_RESOURCE_TYPE_NAMES = Arrays.asList("raw", "drawable", "mipmap"); - - private GifViewUtils() { - } - - static GifImageViewAttributes initImageView(ImageView view, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - if (attrs != null && !view.isInEditMode()) { - final GifImageViewAttributes viewAttributes = new GifImageViewAttributes(view, attrs, defStyleAttr, defStyleRes); - final int loopCount = viewAttributes.mLoopCount; - if (loopCount >= 0) { - applyLoopCount(loopCount, view.getDrawable()); - applyLoopCount(loopCount, view.getBackground()); - } - return viewAttributes; - } - return new GifImageViewAttributes(); - } - - static void applyLoopCount(final int loopCount, final Drawable drawable) { - if (drawable instanceof GifDrawable) { - ((GifDrawable) drawable).setLoopCount(loopCount); - } - } - - @SuppressWarnings("deprecation") - static boolean setResource(ImageView view, boolean isSrc, int resId) { - Resources res = view.getResources(); - if (res != null) { - try { - final String resourceTypeName = res.getResourceTypeName(resId); - if (!SUPPORTED_RESOURCE_TYPE_NAMES.contains(resourceTypeName)) { - return false; - } - GifDrawable d = new GifDrawable(res, resId); - if (isSrc) { - view.setImageDrawable(d); - } else { - view.setBackground(d); - } - return true; - } catch (IOException | Resources.NotFoundException ignored) { - //ignored - } - } - return false; - } - - static boolean setGifImageUri(ImageView imageView, Uri uri) { - if (uri != null) { - try { - imageView.setImageDrawable(new GifDrawable(imageView.getContext().getContentResolver(), uri)); - return true; - } catch (IOException ignored) { - //ignored - } - } - return false; - } - - static float getDensityScale(@NonNull Resources res, @DrawableRes @RawRes int id) { - final TypedValue value = new TypedValue(); - res.getValue(id, value, true); - final int resourceDensity = value.density; - final int density; - if (resourceDensity == TypedValue.DENSITY_DEFAULT) { - density = DisplayMetrics.DENSITY_DEFAULT; - } else if (resourceDensity != TypedValue.DENSITY_NONE) { - density = resourceDensity; - } else { - density = 0; - } - final int targetDensity = res.getDisplayMetrics().densityDpi; - - if (density > 0 && targetDensity > 0) { - return (float) targetDensity / density; - } - return 1f; - } - - static class GifViewAttributes { - boolean freezesAnimation; - final int mLoopCount; - - GifViewAttributes(final View view, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) { - final TypedArray gifViewAttributes = view.getContext().obtainStyledAttributes(attrs, R.styleable.GifView, defStyleAttr, defStyleRes); - freezesAnimation = gifViewAttributes.getBoolean(R.styleable.GifView_freezesAnimation, false); - mLoopCount = gifViewAttributes.getInt(R.styleable.GifView_loopCount, -1); - gifViewAttributes.recycle(); - } - - GifViewAttributes() { - freezesAnimation = false; - mLoopCount = -1; - } - } - - static class GifImageViewAttributes extends GifViewAttributes { - final int mSourceResId; - final int mBackgroundResId; - - GifImageViewAttributes(final ImageView view, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) { - super(view, attrs, defStyleAttr, defStyleRes); - mSourceResId = getResourceId(view, attrs, true); - mBackgroundResId = getResourceId(view, attrs, false); - } - - GifImageViewAttributes() { - super(); - mSourceResId = 0; - mBackgroundResId = 0; - } - - private static int getResourceId(ImageView view, AttributeSet attrs, final boolean isSrc) { - final int resId = attrs.getAttributeResourceValue(ANDROID_NS, isSrc ? "src" : "background", 0); - if (resId > 0) { - final String resourceTypeName = view.getResources().getResourceTypeName(resId); - if (SUPPORTED_RESOURCE_TYPE_NAMES.contains(resourceTypeName) && !setResource(view, isSrc, resId)) { - return resId; - } - } - return 0; - } - - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifViewUtils.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifViewUtils.kt new file mode 100644 index 00000000..738de535 --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/GifViewUtils.kt @@ -0,0 +1,161 @@ +package pl.droidsonroids.gif + +import android.content.res.Resources +import android.content.res.Resources.NotFoundException +import android.graphics.drawable.Drawable +import android.net.Uri +import android.util.AttributeSet +import android.util.DisplayMetrics +import android.util.TypedValue +import android.view.View +import android.widget.ImageView +import androidx.annotation.DrawableRes +import androidx.annotation.RawRes +import java.io.IOException + +internal object GifViewUtils { + const val ANDROID_NS = "http://schemas.android.com/apk/res/android" + + @JvmField + val SUPPORTED_RESOURCE_TYPE_NAMES: List = mutableListOf("raw", "drawable", "mipmap") + fun initImageView( + view: ImageView, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ): GifImageViewAttributes { + if (attrs != null && !view.isInEditMode) { + val viewAttributes = GifImageViewAttributes(view, attrs, defStyleAttr, defStyleRes) + val loopCount = viewAttributes.mLoopCount + if (loopCount >= 0) { + applyLoopCount(loopCount, view.drawable) + applyLoopCount(loopCount, view.background) + } + return viewAttributes + } + return GifImageViewAttributes() + } + + fun applyLoopCount(loopCount: Int, drawable: Drawable?) { + if (drawable is GifDrawable) { + drawable.loopCount = loopCount + } + } + + fun setResource(view: ImageView, isSrc: Boolean, resId: Int): Boolean { + val res = view.resources + if (res != null) { + try { + val resourceTypeName = res.getResourceTypeName(resId) + if (!SUPPORTED_RESOURCE_TYPE_NAMES.contains(resourceTypeName)) { + return false + } + val d = GifDrawable(res, resId) + if (isSrc) { + view.setImageDrawable(d) + } else { + view.background = d + } + return true + } catch (ignored: IOException) { + //ignored + } catch (ignored: NotFoundException) { + } + } + return false + } + + fun setGifImageUri(imageView: ImageView, uri: Uri?): Boolean { + if (uri != null) { + try { + imageView.setImageDrawable(GifDrawable(imageView.context.contentResolver, uri)) + return true + } catch (ignored: IOException) { + //ignored + } + } + return false + } + + fun getDensityScale(res: Resources, @DrawableRes @RawRes id: Int): Float { + val value = TypedValue() + res.getValue(id, value, true) + val resourceDensity = value.density + val density = if (resourceDensity == TypedValue.DENSITY_DEFAULT) { + DisplayMetrics.DENSITY_DEFAULT + } else if (resourceDensity != TypedValue.DENSITY_NONE) { + resourceDensity + } else { + 0 + } + val targetDensity = res.displayMetrics.densityDpi + return if (density > 0 && targetDensity > 0) { + targetDensity.toFloat() / density + } else 1f + } + + internal open class GifViewAttributes { + var freezesAnimation: Boolean + val mLoopCount: Int + + constructor(view: View, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) { + val gifViewAttributes = view.context.obtainStyledAttributes( + attrs, + R.styleable.GifView, + defStyleAttr, + defStyleRes + ) + freezesAnimation = + gifViewAttributes.getBoolean(R.styleable.GifView_freezesAnimation, false) + mLoopCount = gifViewAttributes.getInt(R.styleable.GifView_loopCount, -1) + gifViewAttributes.recycle() + } + + constructor() { + freezesAnimation = false + mLoopCount = -1 + } + } + + internal class GifImageViewAttributes : GifViewAttributes { + val mSourceResId: Int + val mBackgroundResId: Int + + constructor( + view: ImageView, + attrs: AttributeSet, + defStyleAttr: Int, + defStyleRes: Int + ) : super(view, attrs, defStyleAttr, defStyleRes) { + mSourceResId = getResourceId(view, attrs, true) + mBackgroundResId = getResourceId(view, attrs, false) + } + + constructor() : super() { + mSourceResId = 0 + mBackgroundResId = 0 + } + + companion object { + private fun getResourceId(view: ImageView, attrs: AttributeSet, isSrc: Boolean): Int { + val resId = attrs.getAttributeResourceValue( + ANDROID_NS, + if (isSrc) "src" else "background", + 0 + ) + if (resId > 0) { + val resourceTypeName = view.resources.getResourceTypeName(resId) + if (SUPPORTED_RESOURCE_TYPE_NAMES.contains(resourceTypeName) && !setResource( + view, + isSrc, + resId + ) + ) { + return resId + } + } + return 0 + } + } + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/InputSource.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/InputSource.java deleted file mode 100644 index 641a16c5..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/InputSource.java +++ /dev/null @@ -1,251 +0,0 @@ -package pl.droidsonroids.gif; - -import android.content.ContentResolver; -import android.content.res.AssetFileDescriptor; -import android.content.res.AssetManager; -import android.content.res.Resources; -import android.net.Uri; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.util.concurrent.ScheduledThreadPoolExecutor; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RawRes; - -/** - * Abstract class for all input sources, to be used with {@link GifTextureView} - */ -public abstract class InputSource { - private InputSource() { - } - - abstract GifInfoHandle open() throws IOException; - - final GifDrawable createGifDrawable(final GifDrawable oldDrawable, final ScheduledThreadPoolExecutor executor, - final boolean isRenderingAlwaysEnabled, final GifOptions options) throws IOException { - - return new GifDrawable(createHandleWith(options), oldDrawable, executor, isRenderingAlwaysEnabled); - } - - final GifInfoHandle createHandleWith(@NonNull GifOptions options) throws IOException { - final GifInfoHandle handle = open(); - handle.setOptions(options.inSampleSize, options.inIsOpaque); - return handle; - } - - /** - * Input using {@link ByteBuffer} as a source. It must be direct. - */ - public static final class DirectByteBufferSource extends InputSource { - private final ByteBuffer byteBuffer; - - /** - * Constructs new source. - * Buffer can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed. - * - * @param byteBuffer source buffer, must be direct - */ - public DirectByteBufferSource(@NonNull ByteBuffer byteBuffer) { - this.byteBuffer = byteBuffer; - } - - @Override - GifInfoHandle open() throws GifIOException { - return new GifInfoHandle(byteBuffer); - } - } - - /** - * Input using byte array as a source. - */ - public static final class ByteArraySource extends InputSource { - private final byte[] bytes; - - /** - * Constructs new source. - * Array can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed. - * - * @param bytes source array - */ - public ByteArraySource(@NonNull byte[] bytes) { - this.bytes = bytes; - } - - @Override - GifInfoHandle open() throws GifIOException { - return new GifInfoHandle(bytes); - } - } - - /** - * Input using {@link File} or path as source. - */ - public static final class FileSource extends InputSource { - private final String mPath; - - /** - * Constructs new source. - * - * @param file source file - */ - public FileSource(@NonNull File file) { - mPath = file.getPath(); - } - - /** - * Constructs new source. - * - * @param filePath source file path - */ - public FileSource(@NonNull String filePath) { - mPath = filePath; - } - - @Override - GifInfoHandle open() throws GifIOException { - return new GifInfoHandle(mPath); - } - } - - /** - * Input using {@link Uri} as source. - */ - public static final class UriSource extends InputSource { - private final ContentResolver mContentResolver; - private final Uri mUri; - - /** - * Constructs new source. - * - * @param uri GIF Uri, cannot be null. - * @param contentResolver resolver, null is allowed for file:// scheme Uris only - */ - public UriSource(@Nullable ContentResolver contentResolver, @NonNull Uri uri) { - mContentResolver = contentResolver; - mUri = uri; - } - - @Override - GifInfoHandle open() throws IOException { - return GifInfoHandle.openUri(mContentResolver, mUri); - } - } - - /** - * Input using android asset as source. - */ - public static final class AssetSource extends InputSource { - private final AssetManager mAssetManager; - private final String mAssetName; - - /** - * Constructs new source. - * - * @param assetManager AssetManager to read from - * @param assetName name of the asset - */ - public AssetSource(@NonNull AssetManager assetManager, @NonNull String assetName) { - mAssetManager = assetManager; - mAssetName = assetName; - } - - @Override - GifInfoHandle open() throws IOException { - return new GifInfoHandle(mAssetManager.openFd(mAssetName)); - } - } - - /** - * Input using {@link FileDescriptor} as a source. - */ - public static final class FileDescriptorSource extends InputSource { - private final FileDescriptor mFd; - - /** - * Constructs new source. - * - * @param fileDescriptor source file descriptor - */ - public FileDescriptorSource(@NonNull FileDescriptor fileDescriptor) { - mFd = fileDescriptor; - } - - @Override - GifInfoHandle open() throws IOException { - return new GifInfoHandle(mFd); - } - } - - /** - * Input using {@link InputStream} as a source. - */ - public static final class InputStreamSource extends InputSource { - private final InputStream inputStream; - - /** - * Constructs new source. - * - * @param inputStream source input stream, it must support marking - */ - public InputStreamSource(@NonNull InputStream inputStream) { - this.inputStream = inputStream; - } - - @Override - GifInfoHandle open() throws IOException { - return new GifInfoHandle(inputStream); - } - } - - /** - * Input using android resource (raw or drawable) as a source. - */ - public static class ResourcesSource extends InputSource { - private final Resources mResources; - private final int mResourceId; - - /** - * Constructs new source. - * - * @param resources Resources to read from - * @param resourceId resource id - */ - public ResourcesSource(@NonNull Resources resources, @RawRes @DrawableRes int resourceId) { - mResources = resources; - mResourceId = resourceId; - } - - @Override - GifInfoHandle open() throws IOException { - return new GifInfoHandle(mResources.openRawResourceFd(mResourceId)); - } - } - - /** - * Input using {@link AssetFileDescriptor} as a source. - */ - public static class AssetFileDescriptorSource extends InputSource { - private final AssetFileDescriptor mAssetFileDescriptor; - - /** - * Constructs new source. - * - * @param assetFileDescriptor source asset file descriptor. - */ - public AssetFileDescriptorSource(@NonNull AssetFileDescriptor assetFileDescriptor) { - mAssetFileDescriptor = assetFileDescriptor; - } - - @Override - GifInfoHandle open() throws IOException { - return new GifInfoHandle(mAssetFileDescriptor); - } - } - -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/InputSource.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/InputSource.kt new file mode 100644 index 00000000..fc67105c --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/InputSource.kt @@ -0,0 +1,203 @@ +package pl.droidsonroids.gif + +import android.content.ContentResolver +import android.content.res.AssetFileDescriptor +import android.content.res.AssetManager +import android.content.res.Resources +import android.net.Uri +import androidx.annotation.DrawableRes +import androidx.annotation.RawRes +import pl.droidsonroids.gif.GifIOException +import pl.droidsonroids.gif.GifInfoHandle.Companion.openUri +import java.io.File +import java.io.FileDescriptor +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer +import java.util.concurrent.ScheduledThreadPoolExecutor + +/** + * Abstract class for all input sources, to be used with [GifTextureView] + */ +abstract class InputSource private constructor() { + @Throws(IOException::class) + abstract fun open(): GifInfoHandle + + @Throws(IOException::class) + fun createGifDrawable( + oldDrawable: GifDrawable?, executor: ScheduledThreadPoolExecutor?, + isRenderingAlwaysEnabled: Boolean, options: GifOptions + ): GifDrawable { + return GifDrawable( + createHandleWith(options), + oldDrawable, + executor, + isRenderingAlwaysEnabled + ) + } + + @Throws(IOException::class) + fun createHandleWith(options: GifOptions): GifInfoHandle { + val handle = open() + handle.setOptions(options.inSampleSize, options.inIsOpaque) + return handle + } + + /** + * Input using [ByteBuffer] as a source. It must be direct. + */ + class DirectByteBufferSource + /** + * Constructs new source. + * Buffer can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed. + * + * @param byteBuffer source buffer, must be direct + */(private val byteBuffer: ByteBuffer) : InputSource() { + @Throws(GifIOException::class) + override fun open(): GifInfoHandle { + return GifInfoHandle(byteBuffer) + } + } + + /** + * Input using byte array as a source. + */ + class ByteArraySource + /** + * Constructs new source. + * Array can be larger than size of the GIF data. Bytes beyond GIF terminator are not accessed. + * + * @param bytes source array + */(private val bytes: ByteArray) : InputSource() { + @Throws(GifIOException::class) + override fun open(): GifInfoHandle { + return GifInfoHandle(bytes) + } + } + + /** + * Input using [File] or path as source. + */ + class FileSource : InputSource { + private val mPath: String + + /** + * Constructs new source. + * + * @param file source file + */ + constructor(file: File) { + mPath = file.path + } + + /** + * Constructs new source. + * + * @param filePath source file path + */ + constructor(filePath: String) { + mPath = filePath + } + + @Throws(GifIOException::class) + override fun open(): GifInfoHandle { + return GifInfoHandle(mPath) + } + } + + /** + * Input using [Uri] as source. + */ + class UriSource + /** + * Constructs new source. + * + * @param uri GIF Uri, cannot be null. + * @param contentResolver resolver, null is allowed for file:// scheme Uris only + */(private val mContentResolver: ContentResolver?, private val mUri: Uri) : InputSource() { + @Throws(IOException::class) + override fun open(): GifInfoHandle { + return openUri(mContentResolver, mUri) + } + } + + /** + * Input using android asset as source. + */ + class AssetSource + /** + * Constructs new source. + * + * @param assetManager AssetManager to read from + * @param assetName name of the asset + */(private val mAssetManager: AssetManager, private val mAssetName: String) : InputSource() { + @Throws(IOException::class) + override fun open(): GifInfoHandle { + return GifInfoHandle(mAssetManager.openFd(mAssetName)) + } + } + + /** + * Input using [FileDescriptor] as a source. + */ + class FileDescriptorSource + /** + * Constructs new source. + * + * @param fileDescriptor source file descriptor + */(private val mFd: FileDescriptor) : InputSource() { + @Throws(IOException::class) + override fun open(): GifInfoHandle { + return GifInfoHandle(mFd) + } + } + + /** + * Input using [InputStream] as a source. + */ + class InputStreamSource + /** + * Constructs new source. + * + * @param inputStream source input stream, it must support marking + */(private val inputStream: InputStream) : InputSource() { + @Throws(IOException::class) + override fun open(): GifInfoHandle { + return GifInfoHandle(inputStream) + } + } + + /** + * Input using android resource (raw or drawable) as a source. + */ + class ResourcesSource + /** + * Constructs new source. + * + * @param resources Resources to read from + * @param resourceId resource id + */( + private val mResources: Resources, + @param:RawRes @param:DrawableRes private val mResourceId: Int + ) : InputSource() { + @Throws(IOException::class) + override fun open(): GifInfoHandle { + return GifInfoHandle(mResources.openRawResourceFd(mResourceId)) + } + } + + /** + * Input using [AssetFileDescriptor] as a source. + */ + class AssetFileDescriptorSource + /** + * Constructs new source. + * + * @param assetFileDescriptor source asset file descriptor. + */(private val mAssetFileDescriptor: AssetFileDescriptor) : InputSource() { + @Throws(IOException::class) + override fun open(): GifInfoHandle { + return GifInfoHandle(mAssetFileDescriptor) + } + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/InvalidationHandler.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/InvalidationHandler.java deleted file mode 100644 index 4bdb2bab..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/InvalidationHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -package pl.droidsonroids.gif; - -import android.os.Handler; -import android.os.Looper; -import android.os.Message; - -import androidx.annotation.NonNull; - -import java.lang.ref.WeakReference; - -class InvalidationHandler extends Handler { - - static final int MSG_TYPE_INVALIDATION = -1; - - private final WeakReference mDrawableRef; - - InvalidationHandler(final GifDrawable gifDrawable) { - super(Looper.getMainLooper()); - mDrawableRef = new WeakReference<>(gifDrawable); - } - - @Override - public void handleMessage(@NonNull final Message msg) { - final GifDrawable gifDrawable = mDrawableRef.get(); - if (gifDrawable == null) { - return; - } - if (msg.what == MSG_TYPE_INVALIDATION) { - gifDrawable.invalidateSelf(); - } else { - for (AnimationListener listener : gifDrawable.mListeners) { - listener.onAnimationCompleted(msg.what); - } - } - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/InvalidationHandler.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/InvalidationHandler.kt new file mode 100644 index 00000000..7d38ce44 --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/InvalidationHandler.kt @@ -0,0 +1,29 @@ +package pl.droidsonroids.gif + +import android.os.Handler +import android.os.Looper +import android.os.Message +import java.lang.ref.WeakReference + +class InvalidationHandler(gifDrawable: GifDrawable) : Handler(Looper.getMainLooper()) { + private val mDrawableRef: WeakReference + + init { + mDrawableRef = WeakReference(gifDrawable) + } + + override fun handleMessage(msg: Message) { + val gifDrawable = mDrawableRef.get() ?: return + if (msg.what == MSG_TYPE_INVALIDATION) { + gifDrawable.invalidateSelf() + } else { + for (listener in gifDrawable.mListeners) { + listener.onAnimationCompleted(msg.what) + } + } + } + + companion object { + const val MSG_TYPE_INVALIDATION = -1 + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/LibraryLoader.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/LibraryLoader.java deleted file mode 100644 index 05bc0b20..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/LibraryLoader.java +++ /dev/null @@ -1,54 +0,0 @@ -package pl.droidsonroids.gif; - -import android.annotation.SuppressLint; -import android.content.Context; - -import androidx.annotation.NonNull; - -import java.lang.reflect.Method; -import com.getkeepsafe.relinker.ReLinker; -/** - * Helper used to work around native libraries loading on some systems. - * See ReLinker for more details. - */ -public class LibraryLoader { - private static final String BASE_LIBRARY_NAME = "pl_droidsonroids_gif"; - @SuppressLint("StaticFieldLeak") //workaround for Android bug - private static Context sAppContext; - - private LibraryLoader() { - } - - /** - * Initializes loader with given `Context`. Subsequent calls should have no effect since application Context is retrieved. - * Libraries will not be loaded immediately but only when needed. - * - * @param context any Context except null - */ - public static void initialize(@NonNull final Context context) { - sAppContext = context.getApplicationContext(); - } - - private static Context getContext() { - if (sAppContext == null) { - try { - @SuppressLint("PrivateApi") - final Class activityThread = Class.forName("android.app.ActivityThread"); - @SuppressLint("DiscouragedPrivateApi") - final Method currentApplicationMethod = activityThread.getDeclaredMethod("currentApplication"); - sAppContext = (Context) currentApplicationMethod.invoke(null); - } catch (Exception e) { - throw new IllegalStateException("LibraryLoader not initialized. Call LibraryLoader.initialize() before using library classes.", e); - } - } - return sAppContext; - } - - static void loadLibrary() { - try { - System.loadLibrary(BASE_LIBRARY_NAME); - } catch (final UnsatisfiedLinkError e) { - ReLinker.loadLibrary(getContext(), BASE_LIBRARY_NAME); - } - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/LibraryLoader.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/LibraryLoader.kt new file mode 100644 index 00000000..a9463ddb --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/LibraryLoader.kt @@ -0,0 +1,54 @@ +package pl.droidsonroids.gif + +import android.annotation.SuppressLint +import android.content.Context +import com.getkeepsafe.relinker.ReLinker + +/** + * Helper used to work around native libraries loading on some systems. + * See [ReLinker](https://medium.com/keepsafe-engineering/the-perils-of-loading-native-libraries-on-android-befa49dce2db) for more details. + */ +object LibraryLoader { + private const val BASE_LIBRARY_NAME = "pl_droidsonroids_gif" + + @SuppressLint("StaticFieldLeak") //workaround for Android bug + private var sAppContext: Context? = null + + /** + * Initializes loader with given `Context`. Subsequent calls should have no effect since application Context is retrieved. + * Libraries will not be loaded immediately but only when needed. + * + * @param context any Context except null + */ + fun initialize(context: Context) { + sAppContext = context.applicationContext + } + + private val context: Context? + get() { + if (sAppContext == null) { + try { + @SuppressLint("PrivateApi") val activityThread = + Class.forName("android.app.ActivityThread") + @SuppressLint("DiscouragedPrivateApi") val currentApplicationMethod = + activityThread.getDeclaredMethod("currentApplication") + sAppContext = currentApplicationMethod.invoke(null) as Context + } catch (e: Exception) { + throw IllegalStateException( + "LibraryLoader not initialized. Call LibraryLoader.initialize() before using library classes.", + e + ) + } + } + return sAppContext + } + + @JvmStatic + fun loadLibrary() { + try { + System.loadLibrary(BASE_LIBRARY_NAME) + } catch (e: UnsatisfiedLinkError) { + ReLinker.loadLibrary(context, BASE_LIBRARY_NAME) + } + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/MultiCallback.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/MultiCallback.java deleted file mode 100644 index a285cb8d..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/MultiCallback.java +++ /dev/null @@ -1,147 +0,0 @@ -package pl.droidsonroids.gif; - -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Drawable.Callback; -import android.view.View; -import android.widget.TextView; - -import java.lang.ref.WeakReference; -import java.util.concurrent.CopyOnWriteArrayList; - -import androidx.annotation.NonNull; - -/** - * {@link Callback} which allows single {@link Drawable} to be associated with multiple callbacks, - * eg. with multiple {@link View}s. - * If drawable needs to be redrawn all currently associated callbacks are invoked. - * If Callback is a View it is then invalidated. - * - * @author koral-- - * @author Doctoror - */ -public class MultiCallback implements Callback { - - private final CopyOnWriteArrayList mCallbacks = new CopyOnWriteArrayList<>(); - private final boolean mUseViewInvalidate; - - /** - * Equivalent to {@link #MultiCallback(boolean)} with false value. - */ - public MultiCallback() { - this(false); - } - - /** - * Set useViewInvalidate to true if displayed {@link Drawable} is not supported by - * {@link Drawable.Callback#invalidateDrawable(Drawable)} of the target. For example if it is located inside {@link android.text.style.ImageSpan} - * displayed in {@link TextView}. - * - * @param useViewInvalidate whether {@link View#invalidate()} should be used instead of {@link Drawable.Callback#invalidateDrawable(Drawable)} - */ - public MultiCallback(final boolean useViewInvalidate) { - mUseViewInvalidate = useViewInvalidate; - } - - @Override - public void invalidateDrawable(@NonNull final Drawable who) { - for (int i = 0; i < mCallbacks.size(); i++) { - final CallbackWeakReference reference = mCallbacks.get(i); - final Callback callback = reference.get(); - if (callback != null) { - if (mUseViewInvalidate && callback instanceof View) { - ((View) callback).invalidate(); - } else { - callback.invalidateDrawable(who); - } - } else { - // Always remove null references to reduce list size - mCallbacks.remove(reference); - } - } - } - - @Override - public void scheduleDrawable(@NonNull final Drawable who, @NonNull final Runnable what, final long when) { - for (int i = 0; i < mCallbacks.size(); i++) { - final CallbackWeakReference reference = mCallbacks.get(i); - final Callback callback = reference.get(); - if (callback != null) { - callback.scheduleDrawable(who, what, when); - } else { - // Always remove null references to reduce Set size - mCallbacks.remove(reference); - } - } - } - - @Override - public void unscheduleDrawable(@NonNull final Drawable who, @NonNull final Runnable what) { - for (int i = 0; i < mCallbacks.size(); i++) { - final CallbackWeakReference reference = mCallbacks.get(i); - final Callback callback = reference.get(); - if (callback != null) { - callback.unscheduleDrawable(who, what); - } else { - // Always remove null references to reduce list size - mCallbacks.remove(reference); - } - } - } - - /** - * Associates given {@link Callback}. If callback has been already added, nothing happens. - * - * @param callback Callback to be associated - */ - public void addView(final Callback callback) { - for (int i = 0; i < mCallbacks.size(); i++) { - final CallbackWeakReference reference = mCallbacks.get(i); - final Callback item = reference.get(); - if (item == null) { - // Always remove null references to reduce list size - mCallbacks.remove(reference); - } - } - mCallbacks.addIfAbsent(new CallbackWeakReference(callback)); - } - - /** - * Disassociates given {@link Callback}. If callback is not associated, nothing happens. - * - * @param callback Callback to be disassociated - */ - public void removeView(final Callback callback) { - for (int i = 0; i < mCallbacks.size(); i++) { - final CallbackWeakReference reference = mCallbacks.get(i); - final Callback item = reference.get(); - if (item == null || item == callback) { - // Always remove null references to reduce list size - mCallbacks.remove(reference); - } - } - } - - static final class CallbackWeakReference extends WeakReference { - CallbackWeakReference(final Callback r) { - super(r); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - return get() == ((CallbackWeakReference) o).get(); - } - - @Override - public int hashCode() { - final Callback callback = get(); - return callback != null ? callback.hashCode() : 0; - } - } -} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/MultiCallback.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/MultiCallback.kt new file mode 100644 index 00000000..75da873b --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/MultiCallback.kt @@ -0,0 +1,116 @@ +package pl.droidsonroids.gif + +import android.graphics.drawable.Drawable +import android.view.View +import java.lang.ref.WeakReference +import java.util.concurrent.CopyOnWriteArrayList + +/** + * [Callback] which allows single [Drawable] to be associated with multiple callbacks, + * eg. with multiple [View]s. + * If drawable needs to be redrawn all currently associated callbacks are invoked. + * If Callback is a View it is then invalidated. + * + * @author koral-- + * @author Doctoror + */ +class MultiCallback +/** + * Equivalent to [.MultiCallback] with `false` value. + */ @JvmOverloads constructor(private val mUseViewInvalidate: Boolean = false) : Drawable.Callback { + private val mCallbacks = CopyOnWriteArrayList() + + /** + * Set `useViewInvalidate` to `true` if displayed [Drawable] is not supported by + * [Drawable.Callback.invalidateDrawable] of the target. For example if it is located inside [android.text.style.ImageSpan] + * displayed in [TextView]. + * + * @param mUseViewInvalidate whether [View.invalidate] should be used instead of [Drawable.Callback.invalidateDrawable] + */ + override fun invalidateDrawable(who: Drawable) { + for (i in mCallbacks.indices) { + val reference = mCallbacks[i] + val callback = reference.get() + if (callback != null) { + if (mUseViewInvalidate && callback is View) { + callback.invalidate() + } else { + callback.invalidateDrawable(who) + } + } else { + // Always remove null references to reduce list size + mCallbacks.remove(reference) + } + } + } + + override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) { + for (i in mCallbacks.indices) { + val reference = mCallbacks[i] + val callback = reference.get() + callback?.scheduleDrawable(who, what, `when`) + ?: // Always remove null references to reduce Set size + mCallbacks.remove(reference) + } + } + + override fun unscheduleDrawable(who: Drawable, what: Runnable) { + for (i in mCallbacks.indices) { + val reference = mCallbacks[i] + val callback = reference.get() + callback?.unscheduleDrawable(who, what) + ?: // Always remove null references to reduce list size + mCallbacks.remove(reference) + } + } + + /** + * Associates given [Callback]. If callback has been already added, nothing happens. + * + * @param callback Callback to be associated + */ + fun addView(callback: Drawable.Callback?) { + for (i in mCallbacks.indices) { + val reference = mCallbacks[i] + val item = reference.get() + if (item == null) { + // Always remove null references to reduce list size + mCallbacks.remove(reference) + } + } + mCallbacks.addIfAbsent(CallbackWeakReference(callback)) + } + + /** + * Disassociates given [Callback]. If callback is not associated, nothing happens. + * + * @param callback Callback to be disassociated + */ + fun removeView(callback: Drawable.Callback) { + for (i in mCallbacks.indices) { + val reference = mCallbacks[i] + val item = reference.get() + if (item == null || item === callback) { + // Always remove null references to reduce list size + mCallbacks.remove(reference) + } + } + } + + internal class CallbackWeakReference(r: Drawable.Callback?) : + WeakReference(r) { + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + return if (other == null || javaClass != other.javaClass) { + false + } else get() === (other as CallbackWeakReference).get() + } + + override fun hashCode(): Int { + val callback = get() + return callback?.hashCode() ?: 0 + } + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/PlaceholderDrawingSurfaceTextureListener.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/PlaceholderDrawingSurfaceTextureListener.java deleted file mode 100644 index bccd731e..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/PlaceholderDrawingSurfaceTextureListener.java +++ /dev/null @@ -1,38 +0,0 @@ -package pl.droidsonroids.gif; - -import android.graphics.Canvas; -import android.graphics.SurfaceTexture; -import android.view.Surface; -import android.view.TextureView; - -class PlaceholderDrawingSurfaceTextureListener implements TextureView.SurfaceTextureListener { - private final GifTextureView.PlaceholderDrawListener mDrawer; - - PlaceholderDrawingSurfaceTextureListener(GifTextureView.PlaceholderDrawListener drawer) { - mDrawer = drawer; - } - - @Override - public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { - final Surface surface = new Surface(surfaceTexture); - final Canvas canvas = surface.lockCanvas(null); - mDrawer.onDrawPlaceholder(canvas); - surface.unlockCanvasAndPost(canvas); - surface.release(); - } - - @Override - public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) { - //no-op - } - - @Override - public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { - return false; - } - - @Override - public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { - //no-op - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/PlaceholderDrawingSurfaceTextureListener.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/PlaceholderDrawingSurfaceTextureListener.kt new file mode 100644 index 00000000..640c2fd3 --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/PlaceholderDrawingSurfaceTextureListener.kt @@ -0,0 +1,36 @@ +package pl.droidsonroids.gif + +import android.graphics.SurfaceTexture +import android.view.Surface +import android.view.TextureView.SurfaceTextureListener + +internal class PlaceholderDrawingSurfaceTextureListener(private val mDrawer: GifTextureView.PlaceholderDrawListener) : + SurfaceTextureListener { + override fun onSurfaceTextureAvailable( + surfaceTexture: SurfaceTexture, + width: Int, + height: Int + ) { + val surface = Surface(surfaceTexture) + val canvas = surface.lockCanvas(null) + mDrawer.onDrawPlaceholder(canvas) + surface.unlockCanvasAndPost(canvas) + surface.release() + } + + override fun onSurfaceTextureSizeChanged( + surfaceTexture: SurfaceTexture, + width: Int, + height: Int + ) { + //no-op + } + + override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean { + return false + } + + override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) { + //no-op + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/RenderTask.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/RenderTask.java deleted file mode 100644 index 49d1e16d..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/RenderTask.java +++ /dev/null @@ -1,35 +0,0 @@ -package pl.droidsonroids.gif; - -import android.os.SystemClock; - -import java.util.concurrent.TimeUnit; - -import static pl.droidsonroids.gif.InvalidationHandler.MSG_TYPE_INVALIDATION; - -class RenderTask extends SafeRunnable { - - RenderTask(GifDrawable gifDrawable) { - super(gifDrawable); - } - - @Override - public void doWork() { - final long invalidationDelay = mGifDrawable.mNativeInfoHandle.renderFrame(mGifDrawable.mBuffer); - if (invalidationDelay >= 0) { - mGifDrawable.mNextFrameRenderTime = SystemClock.uptimeMillis() + invalidationDelay; - if (mGifDrawable.isVisible() && mGifDrawable.mIsRunning && !mGifDrawable.mIsRenderingTriggeredOnDraw) { - mGifDrawable.mExecutor.remove(this); - mGifDrawable.mRenderTaskSchedule = mGifDrawable.mExecutor.schedule(this, invalidationDelay, TimeUnit.MILLISECONDS); - } - if (!mGifDrawable.mListeners.isEmpty() && mGifDrawable.getCurrentFrameIndex() == mGifDrawable.mNativeInfoHandle.getNumberOfFrames() - 1) { - mGifDrawable.mInvalidationHandler.sendEmptyMessageAtTime(mGifDrawable.getCurrentLoop(), mGifDrawable.mNextFrameRenderTime); - } - } else { - mGifDrawable.mNextFrameRenderTime = Long.MIN_VALUE; - mGifDrawable.mIsRunning = false; - } - if (mGifDrawable.isVisible() && !mGifDrawable.mInvalidationHandler.hasMessages(MSG_TYPE_INVALIDATION)) { - mGifDrawable.mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0); - } - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/RenderTask.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/RenderTask.kt new file mode 100644 index 00000000..fa2fdb04 --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/RenderTask.kt @@ -0,0 +1,37 @@ +package pl.droidsonroids.gif + +import android.os.SystemClock +import java.util.concurrent.TimeUnit + +internal class RenderTask(gifDrawable: GifDrawable) : SafeRunnable(gifDrawable) { + + override fun doWork() { + val invalidationDelay = mGifDrawable.mNativeInfoHandle.renderFrame(mGifDrawable.mBuffer) + if (invalidationDelay >= 0) { + mGifDrawable.mNextFrameRenderTime = SystemClock.uptimeMillis() + invalidationDelay + if (mGifDrawable.isVisible && mGifDrawable.mIsRunning && !mGifDrawable.mIsRenderingTriggeredOnDraw) { + mGifDrawable.mExecutor.remove(this) + mGifDrawable.mRenderTaskSchedule = + mGifDrawable.mExecutor.schedule(this, invalidationDelay, TimeUnit.MILLISECONDS) + } + if (!mGifDrawable.mListeners.isEmpty() && mGifDrawable.currentFrameIndex == mGifDrawable.mNativeInfoHandle.numberOfFrames - 1) { + mGifDrawable.mInvalidationHandler.sendEmptyMessageAtTime( + mGifDrawable.currentLoop, + mGifDrawable.mNextFrameRenderTime + ) + } + } else { + mGifDrawable.mNextFrameRenderTime = Long.MIN_VALUE + mGifDrawable.mIsRunning = false + } + if (mGifDrawable.isVisible && !mGifDrawable.mInvalidationHandler.hasMessages( + InvalidationHandler.MSG_TYPE_INVALIDATION + ) + ) { + mGifDrawable.mInvalidationHandler.sendEmptyMessageAtTime( + InvalidationHandler.MSG_TYPE_INVALIDATION, + 0 + ) + } + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/SafeRunnable.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/SafeRunnable.java deleted file mode 100644 index 2a409647..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/SafeRunnable.java +++ /dev/null @@ -1,30 +0,0 @@ -package pl.droidsonroids.gif; - -/** - * Runnable for {@link java.util.concurrent.Executor} which propagates exceptions to default uncaught - * exception handler. - */ -abstract class SafeRunnable implements Runnable { - final GifDrawable mGifDrawable; - - SafeRunnable(GifDrawable gifDrawable) { - mGifDrawable = gifDrawable; - } - - @Override - public final void run() { - try { - if (!mGifDrawable.isRecycled()) { - doWork(); - } - } catch (Throwable throwable) { - final Thread.UncaughtExceptionHandler uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); - if (uncaughtExceptionHandler != null) { - uncaughtExceptionHandler.uncaughtException(Thread.currentThread(), throwable); - } - throw throwable; - } - } - - abstract void doWork(); -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/SafeRunnable.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/SafeRunnable.kt new file mode 100644 index 00000000..84021e0b --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/SafeRunnable.kt @@ -0,0 +1,21 @@ +package pl.droidsonroids.gif + +/** + * Runnable for [java.util.concurrent.Executor] which propagates exceptions to default uncaught + * exception handler. + */ +internal abstract class SafeRunnable(@JvmField val mGifDrawable: GifDrawable) : Runnable { + override fun run() { + try { + if (!mGifDrawable.isRecycled) { + doWork() + } + } catch (throwable: Throwable) { + val uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() + uncaughtExceptionHandler?.uncaughtException(Thread.currentThread(), throwable) + throw throwable + } + } + + abstract fun doWork() +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/annotations/Beta.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/annotations/Beta.kt similarity index 66% rename from android-gif-drawable/src/main/java/pl/droidsonroids/gif/annotations/Beta.java rename to android-gif-drawable/src/main/java/pl/droidsonroids/gif/annotations/Beta.kt index cccace09..71c1b706 100644 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/annotations/Beta.java +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/annotations/Beta.kt @@ -15,13 +15,7 @@ * * Originally from https://code.google.com/p/guava-libraries/source/browse/guava/src/com/google/common/annotations/Beta.java */ -package pl.droidsonroids.gif.annotations; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +package pl.droidsonroids.gif.annotations /** * Signifies that a public API (public class, method or field) is subject to @@ -31,20 +25,23 @@ * about the quality or performance of the API in question, only the fact that * it is not "API-frozen." * - *

It is generally safe for applications to depend on beta APIs, at + * + * It is generally safe for *applications* to depend on beta APIs, at * the cost of some extra work during upgrades. However it is generally - * inadvisable for libraries (which get included on users' CLASSPATHs, + * inadvisable for *libraries* (which get included on users' CLASSPATHs, * outside the library developers' control) to do so. * - **/ -@Retention(RetentionPolicy.CLASS) -@Target({ - ElementType.ANNOTATION_TYPE, - ElementType.CONSTRUCTOR, - ElementType.FIELD, - ElementType.METHOD, - ElementType.TYPE }) -@Documented + */ +@Retention(AnnotationRetention.BINARY) +@Target( + AnnotationTarget.ANNOTATION_CLASS, + AnnotationTarget.CONSTRUCTOR, + AnnotationTarget.FIELD, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER, + AnnotationTarget.CLASS +) +@MustBeDocumented @Beta -public @interface Beta { -} \ No newline at end of file +annotation class Beta \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/package-info.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/package-info.java deleted file mode 100644 index 22ca14da..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Animated GIF library for Android. See {@link pl.droidsonroids.gif.GifDrawable}, {@link pl.droidsonroids.gif.GifTextureView} - * and {@link pl.droidsonroids.gif.GifTexImage2D} classes for more details. - */ -package pl.droidsonroids.gif; \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/package-info.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/package-info.kt new file mode 100644 index 00000000..530f3395 --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/package-info.kt @@ -0,0 +1,5 @@ +/** + * Animated GIF library for Android. See [pl.droidsonroids.gif.GifDrawable], [pl.droidsonroids.gif.GifTextureView] + * and [pl.droidsonroids.gif.GifTexImage2D] classes for more details. + */ +package pl.droidsonroids.gif diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/CornerRadiusTransform.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/CornerRadiusTransform.java deleted file mode 100644 index 137708a1..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/CornerRadiusTransform.java +++ /dev/null @@ -1,88 +0,0 @@ -package pl.droidsonroids.gif.transforms; - -import android.graphics.Bitmap; -import android.graphics.BitmapShader; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Shader; - -import androidx.annotation.FloatRange; -import androidx.annotation.NonNull; - -/** - * {@link Transform} which adds rounded corners. - */ -public class CornerRadiusTransform implements Transform { - - private float mCornerRadius; - private Shader mShader; - private final RectF mDstRectF = new RectF(); - - /** - * @param cornerRadius corner radius, may be 0. - */ - public CornerRadiusTransform(@FloatRange(from = 0) float cornerRadius) { - setCornerRadiusSafely(cornerRadius); - } - - /** - * Sets the corner radius to be applied when drawing the bitmap. - * - * @param cornerRadius corner radius or 0 to remove rounding - */ - public void setCornerRadius(@FloatRange(from = 0) float cornerRadius) { - setCornerRadiusSafely(cornerRadius); - } - - private void setCornerRadiusSafely(@FloatRange(from = 0) float cornerRadius) { - cornerRadius = Math.max(0, cornerRadius); - if (cornerRadius != mCornerRadius) { - mCornerRadius = cornerRadius; - mShader = null; - } - } - - /** - * @return The corner radius applied when drawing this drawable. 0 when drawable is not rounded. - */ - @FloatRange(from = 0) - public float getCornerRadius() { - return mCornerRadius; - } - - /** - * Returns current transform bounds - latest received by {@link #onBoundsChange(Rect)}. - * - * @return current transform bounds - */ - @NonNull - public RectF getBounds() { - return mDstRectF; - } - - @Override - public void onBoundsChange(Rect bounds) { - mDstRectF.set(bounds); - mShader = null; - } - - @Override - public void onDraw(Canvas canvas, Paint paint, Bitmap buffer) { - if (mCornerRadius == 0) { - canvas.drawBitmap(buffer, null, mDstRectF, paint); - return; - } - if (mShader == null) { - mShader = new BitmapShader(buffer, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); - final Matrix shaderMatrix = new Matrix(); - shaderMatrix.setTranslate(mDstRectF.left, mDstRectF.top); - shaderMatrix.preScale(mDstRectF.width() / buffer.getWidth(), mDstRectF.height() / buffer.getHeight()); - mShader.setLocalMatrix(shaderMatrix); - } - paint.setShader(mShader); - canvas.drawRoundRect(mDstRectF, mCornerRadius, mCornerRadius, paint); - } -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/CornerRadiusTransform.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/CornerRadiusTransform.kt new file mode 100644 index 00000000..76ac0bed --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/CornerRadiusTransform.kt @@ -0,0 +1,77 @@ +package pl.droidsonroids.gif.transforms + +import android.graphics.Bitmap +import android.graphics.BitmapShader +import android.graphics.Canvas +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import androidx.annotation.FloatRange +import kotlin.math.max + +/** + * [Transform] which adds rounded corners. + * + * @param cornerRadius corner radius, may be 0. + */ +class CornerRadiusTransform(@FloatRange(from = 0.0) cornerRadius: Float) : Transform { + private var mCornerRadius = 0f + private var mShader: Shader? = null + + /** + * Returns current transform bounds - latest received by [.onBoundsChange]. + * + * @return current transform bounds + */ + private val mDstRectF = RectF() + + init { + setCornerRadiusSafely(cornerRadius) + } + + private fun setCornerRadiusSafely(@FloatRange(from = 0.0) cornerRadius: Float) { + val radius = max(0f, cornerRadius) + if (radius != mCornerRadius) { + mCornerRadius = radius + mShader = null + } + } + + /** + * Sets the corner radius to be applied when drawing the bitmap. + * + * @param cornerRadius corner radius or 0 to remove rounding + * @return The corner radius applied when drawing this drawable. 0 when drawable is not rounded. + */ + @get:FloatRange(from = 0.0) + var cornerRadius: Float + get() = mCornerRadius + set(cornerRadius) { + setCornerRadiusSafely(cornerRadius) + } + + override fun onBoundsChange(bounds: Rect) { + mDstRectF.set(bounds) + mShader = null + } + + override fun onDraw(canvas: Canvas, paint: Paint, buffer: Bitmap) { + if (mCornerRadius == 0f) { + canvas.drawBitmap(buffer, null, mDstRectF, paint) + return + } + if (mShader == null) { + buffer.let { + mShader = BitmapShader(it, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) + val shaderMatrix = Matrix() + shaderMatrix.setTranslate(mDstRectF.left, mDstRectF.top) + shaderMatrix.preScale(mDstRectF.width() / it.width, mDstRectF.height() / it.height) + (mShader as BitmapShader).setLocalMatrix(shaderMatrix) + } + } + paint.shader = mShader + canvas.drawRoundRect(mDstRectF, mCornerRadius, mCornerRadius, paint) + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/Transform.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/Transform.java deleted file mode 100644 index b2f88251..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/Transform.java +++ /dev/null @@ -1,31 +0,0 @@ -package pl.droidsonroids.gif.transforms; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; - -import pl.droidsonroids.gif.GifDrawable; - -/** - * Interface to support clients performing custom transformations before the current GIF Bitmap is drawn. - */ -public interface Transform { - - /** - * Called by {@link GifDrawable} when its bounds changes by {@link GifDrawable#onBoundsChange(Rect)} - * and when transform is associated using {@link GifDrawable#setTransform(Transform)}. - * In this latter case the latest {@link GifDrawable} bounds (empty {@link Rect} if they were not set yet). - * @param bounds new bounds - */ - void onBoundsChange(Rect bounds); - - /** - * Called by {@link GifDrawable} when its {@link GifDrawable#draw(Canvas)} is called. - * - * @param canvas The canvas supplied by the system to draw on. - * @param paint The paint to use for custom drawing. - * @param buffer The current Bitmap for the GIF. - */ - void onDraw(Canvas canvas, Paint paint, Bitmap buffer); -} diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/Transform.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/Transform.kt new file mode 100644 index 00000000..c97a3d9b --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/Transform.kt @@ -0,0 +1,28 @@ +package pl.droidsonroids.gif.transforms + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect + +/** + * Interface to support clients performing custom transformations before the current GIF Bitmap is drawn. + */ +interface Transform { + /** + * Called by [GifDrawable] when its bounds changes by [GifDrawable.onBoundsChange] + * and when transform is associated using [GifDrawable.setTransform]. + * In this latter case the latest [GifDrawable] bounds (empty [Rect] if they were not set yet). + * @param bounds new bounds + */ + fun onBoundsChange(bounds: Rect) + + /** + * Called by [GifDrawable] when its [GifDrawable.draw] is called. + * + * @param canvas The canvas supplied by the system to draw on. + * @param paint The paint to use for custom drawing. + * @param buffer The current Bitmap for the GIF. + */ + fun onDraw(canvas: Canvas, paint: Paint, buffer: Bitmap) +} \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/package-info.java b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/package-info.java deleted file mode 100644 index 247c4ad3..00000000 --- a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Transformations performed before drawing GIF frames. See {@link pl.droidsonroids.gif.transforms.Transform} - */ -package pl.droidsonroids.gif.transforms; \ No newline at end of file diff --git a/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/package-info.kt b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/package-info.kt new file mode 100644 index 00000000..33cc338b --- /dev/null +++ b/android-gif-drawable/src/main/java/pl/droidsonroids/gif/transforms/package-info.kt @@ -0,0 +1,4 @@ +/** + * Transformations performed before drawing GIF frames. See [pl.droidsonroids.gif.transforms.Transform] + */ +package pl.droidsonroids.gif.transforms diff --git a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/CallbackWeakReferenceTest.java b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/CallbackWeakReferenceTest.java deleted file mode 100644 index f5c3e5c2..00000000 --- a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/CallbackWeakReferenceTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package pl.droidsonroids.gif; - -import android.graphics.drawable.Drawable; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import pl.droidsonroids.gif.MultiCallback.CallbackWeakReference; - -import static org.assertj.core.api.Java6Assertions.assertThat; - -@RunWith(MockitoJUnitRunner.class) -public class CallbackWeakReferenceTest { - - @Mock Drawable.Callback callback; - @Mock Drawable.Callback anotherCallback; - - @Test - public void testEquals() throws Exception { - final CallbackWeakReference reference = new CallbackWeakReference(callback); - final CallbackWeakReference anotherReference = new CallbackWeakReference(callback); - assertThat(reference).isEqualTo(anotherReference); - } - - @Test - public void testNotEqualReferences() throws Exception { - final CallbackWeakReference reference = new CallbackWeakReference(callback); - final CallbackWeakReference anotherReference = new CallbackWeakReference(anotherCallback); - assertThat(reference).isNotEqualTo(anotherReference); - } - - @Test - public void testNotEqualDifferentObjects() throws Exception { - final CallbackWeakReference reference = new CallbackWeakReference(callback); - assertThat(reference).isNotEqualTo(null); - assertThat(reference).isNotEqualTo(callback); - } - - @Test - public void testHashCode() throws Exception { - final CallbackWeakReference reference = new CallbackWeakReference(callback); - final CallbackWeakReference anotherReference = new CallbackWeakReference(callback); - assertThat(reference.hashCode()).isEqualTo(anotherReference.hashCode()); - } - - @Test - public void testHashCodeNull() throws Exception { - final CallbackWeakReference reference = new CallbackWeakReference(callback); - final CallbackWeakReference anotherReference = new CallbackWeakReference(null); - assertThat(reference.hashCode()).isNotEqualTo(anotherReference.hashCode()); - } -} \ No newline at end of file diff --git a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/CallbackWeakReferenceTest.kt b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/CallbackWeakReferenceTest.kt new file mode 100644 index 00000000..2a0b46f2 --- /dev/null +++ b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/CallbackWeakReferenceTest.kt @@ -0,0 +1,63 @@ +package pl.droidsonroids.gif + +import android.graphics.drawable.Drawable +import org.assertj.core.api.Java6Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner +import pl.droidsonroids.gif.MultiCallback.CallbackWeakReference + +@RunWith(MockitoJUnitRunner::class) +class CallbackWeakReferenceTest { + + @Mock + var callback: Drawable.Callback? = null + + @Mock + var anotherCallback: Drawable.Callback? = null + + @Test + fun testEquals() { + val reference = CallbackWeakReference(callback) + + val anotherReference = CallbackWeakReference(callback) + + assertThat(reference).isEqualTo(anotherReference) + } + + @Test + fun testNotEqualReferences() { + val reference = CallbackWeakReference(callback) + + val anotherReference = CallbackWeakReference(anotherCallback) + + assertThat(reference).isNotEqualTo(anotherReference) + } + + @Test + fun testNotEqualDifferentObjects() { + val reference = CallbackWeakReference(callback) + + assertThat(reference).isNotEqualTo(null) + assertThat(reference).isNotEqualTo(callback) + } + + @Test + fun testHashCode() { + val reference = CallbackWeakReference(callback) + + val anotherReference = CallbackWeakReference(callback) + + assertThat(reference.hashCode()).isEqualTo(anotherReference.hashCode()) + } + + @Test + fun testHashCodeNull() { + val reference = CallbackWeakReference(callback) + + val anotherReference = CallbackWeakReference(null) + + assertThat(reference.hashCode()).isNotEqualTo(anotherReference.hashCode()) + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/ConditionVariableTest.java b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/ConditionVariableTest.java deleted file mode 100644 index 089aedc7..00000000 --- a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/ConditionVariableTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package pl.droidsonroids.gif; - -import net.jodah.concurrentunit.Waiter; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.Timeout; - -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -public class ConditionVariableTest { - - private static final int TEST_TIMEOUT = 500; - private static final int BLOCK_DURATION = 200; - - @Rule - public Timeout timeout = new Timeout(TEST_TIMEOUT, TimeUnit.MILLISECONDS); - - private ConditionVariable conditionVariable; - private Waiter waiter; - - @Before - public void setUp() { - conditionVariable = new ConditionVariable(); - waiter = new Waiter(); - } - - @Test - public void testBlock() throws Exception { - blockAndWait(); - } - - @Test - public void testOpen() throws Exception { - new Thread() { - @Override - public void run() { - conditionVariable.open(); - waiter.resume(); - } - }.start(); - conditionVariable.block(); - waiter.await(); - } - - @Test - public void testInitiallyOpened() throws Exception { - conditionVariable.set(true); - conditionVariable.block(); - } - - @Test - public void testInitiallyClosed() throws Exception { - conditionVariable.set(false); - blockAndWait(); - } - - @Test - public void testClose() throws Exception { - conditionVariable.close(); - blockAndWait(); - } - - private void blockAndWait() throws InterruptedException, TimeoutException { - final Thread thread = new Thread() { - @Override - public void run() { - try { - waiter.resume(); - conditionVariable.block(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - waiter.rethrow(e); - } - waiter.fail("ConditionVariable not blocked"); - } - }; - thread.start(); - thread.join(BLOCK_DURATION); - waiter.await(); - } -} \ No newline at end of file diff --git a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/ConditionVariableTest.kt b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/ConditionVariableTest.kt new file mode 100644 index 00000000..306c0584 --- /dev/null +++ b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/ConditionVariableTest.kt @@ -0,0 +1,81 @@ +package pl.droidsonroids.gif + +import net.jodah.concurrentunit.Waiter +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.Timeout +import java.util.concurrent.TimeUnit + +class ConditionVariableTest { + + @get:Rule + val timeout = Timeout(TEST_TIMEOUT.toLong(), TimeUnit.MILLISECONDS) + + private lateinit var conditionVariable: ConditionVariable + private lateinit var waiter: Waiter + + @Before + fun setUp() { + conditionVariable = ConditionVariable() + waiter = Waiter() + } + + @Test + fun testBlock() { + blockAndWait() + } + + @Test + fun testOpen() { + object : Thread() { + override fun run() { + conditionVariable.open() + waiter.resume() + } + }.start() + conditionVariable.block() + waiter.await() + } + + @Test + fun testInitiallyOpened() { + conditionVariable.set(true) + conditionVariable.block() + } + + @Test + fun testInitiallyClosed() { + conditionVariable.set(false) + blockAndWait() + } + + @Test + fun testClose() { + conditionVariable.close() + blockAndWait() + } + + private fun blockAndWait() { + val thread: Thread = object : Thread() { + override fun run() { + try { + waiter.resume() + conditionVariable.block() + } catch (e: InterruptedException) { + currentThread().interrupt() + waiter.rethrow(e) + } + waiter.fail("ConditionVariable not blocked") + } + } + thread.start() + thread.join(BLOCK_DURATION.toLong()) + waiter.await() + } + + companion object { + private const val TEST_TIMEOUT = 500 + private const val BLOCK_DURATION = 200 + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifDrawableBuilderTest.java b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifDrawableBuilderTest.java deleted file mode 100644 index fd8938c0..00000000 --- a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifDrawableBuilderTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package pl.droidsonroids.gif; - -import org.junit.Test; - -import static org.assertj.core.api.Java6Assertions.assertThat; - -public class GifDrawableBuilderTest { - - @Test - public void testOptionsAndSampleSizeConflict() throws Exception { - GifDrawableBuilder builder = new GifDrawableBuilder(); - GifOptions options = new GifOptions(); - builder.options(options); - builder.sampleSize(3); - assertThat(options.inSampleSize).isEqualTo((char) 1); - } - -} \ No newline at end of file diff --git a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifDrawableBuilderTest.kt b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifDrawableBuilderTest.kt new file mode 100644 index 00000000..cde23007 --- /dev/null +++ b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifDrawableBuilderTest.kt @@ -0,0 +1,18 @@ +package pl.droidsonroids.gif + +import org.assertj.core.api.Java6Assertions.assertThat +import org.junit.Test + +class GifDrawableBuilderTest { + + @Test + fun testOptionsAndSampleSizeConflict() { + val builder = GifDrawableBuilder() + + val options = GifOptions() + builder.options(options) + builder.sampleSize(3) + + assertThat(options.inSampleSize).isEqualTo(1.toChar()) + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifOptionsTest.java b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifOptionsTest.java deleted file mode 100644 index abaa1a36..00000000 --- a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifOptionsTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package pl.droidsonroids.gif; - -import org.junit.Before; -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class GifOptionsTest { - - private GifOptions gifOptions; - - @Before - public void setUp() { - gifOptions = new GifOptions(); - } - - @Test - public void testInitialValues() { - assertThat(gifOptions.inSampleSize).isEqualTo((char) 1); - assertThat(gifOptions.inIsOpaque).isFalse(); - } - - @Test - public void setInSampleSize() { - gifOptions.setInSampleSize(2); - assertThat(gifOptions.inSampleSize).isEqualTo((char) 2); - } - - @Test - public void setInIsOpaque() { - gifOptions.setInIsOpaque(true); - assertThat(gifOptions.inIsOpaque).isTrue(); - } - - @Test - public void copyFromNonNull() { - GifOptions source = new GifOptions(); - source.setInIsOpaque(false); - source.setInSampleSize(8); - gifOptions.setFrom(source); - assertThat(gifOptions).isEqualToComparingFieldByField(source); - } - - @Test - public void copyFromNull() { - GifOptions defaultOptions = new GifOptions(); - gifOptions.setInIsOpaque(false); - gifOptions.setInSampleSize(8); - gifOptions.setFrom(null); - assertThat(gifOptions).isEqualToComparingFieldByField(defaultOptions); - } -} \ No newline at end of file diff --git a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifOptionsTest.kt b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifOptionsTest.kt new file mode 100644 index 00000000..d41955e9 --- /dev/null +++ b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifOptionsTest.kt @@ -0,0 +1,57 @@ +package pl.droidsonroids.gif + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test + +class GifOptionsTest { + + private lateinit var gifOptions: GifOptions + + @Before + fun setUp() { + gifOptions = GifOptions() + } + + @Test + fun testInitialValues() { + assertThat(gifOptions.inSampleSize).isEqualTo(1.toChar()) + assertThat(gifOptions.inIsOpaque).isFalse + } + + @Test + fun setInSampleSize() { + gifOptions.setInSampleSize(2) + + assertThat(gifOptions.inSampleSize).isEqualTo(2.toChar()) + } + + @Test + fun setInIsOpaque() { + gifOptions.inIsOpaque = true + + assertThat(gifOptions.inIsOpaque).isTrue + } + + @Test + fun copyFromNonNull() { + val source = GifOptions() + + source.inIsOpaque = false + source.setInSampleSize(8) + gifOptions.setFrom(source) + + assertThat(gifOptions).isEqualToComparingFieldByField(source) + } + + @Test + fun copyFromNull() { + val defaultOptions = GifOptions() + + gifOptions.inIsOpaque = false + gifOptions.setInSampleSize(8) + gifOptions.setFrom(null) + + assertThat(gifOptions).isEqualToComparingFieldByField(defaultOptions) + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifViewUtilsDensityTest.java b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifViewUtilsDensityTest.java deleted file mode 100644 index 81046d1b..00000000 --- a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifViewUtilsDensityTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package pl.droidsonroids.gif; - -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.os.Build; -import android.util.DisplayMetrics; -import android.util.TypedValue; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; - -import androidx.annotation.RequiresApi; - -import static android.util.DisplayMetrics.DENSITY_DEFAULT; -import static android.util.DisplayMetrics.DENSITY_HIGH; -import static android.util.DisplayMetrics.DENSITY_LOW; -import static android.util.DisplayMetrics.DENSITY_MEDIUM; -import static android.util.DisplayMetrics.DENSITY_TV; -import static android.util.DisplayMetrics.DENSITY_XHIGH; -import static android.util.DisplayMetrics.DENSITY_XXHIGH; -import static android.util.DisplayMetrics.DENSITY_XXXHIGH; -import static org.assertj.core.api.Java6Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.when; -import static pl.droidsonroids.gif.GifViewUtils.getDensityScale; - -@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2) -@RunWith(MockitoJUnitRunner.class) -public class GifViewUtilsDensityTest { - - @Mock - Resources resources; - - private Resources getMockedResources(final int resourceDensity, final int displayDensity) { - doAnswer(new Answer() { - @Override - public TypedValue answer(InvocationOnMock invocation) throws Throwable { - final TypedValue outValue = invocation.getArgument(1); - outValue.density = resourceDensity; - return outValue; - } - }).when(resources).getValue(anyInt(), any(TypedValue.class), anyBoolean()); - - final DisplayMetrics displayMetrics = new DisplayMetrics(); - displayMetrics.densityDpi = displayDensity; - when(resources.getDisplayMetrics()).thenReturn(displayMetrics); - return resources; - } - - @Test - public void testHighResourceDensities() throws Exception { - assertThat(getDensityScale(getMockedResources(DENSITY_HIGH, DENSITY_HIGH), 0)).isEqualTo(1); - assertThat(getDensityScale(getMockedResources(DENSITY_HIGH, DENSITY_LOW), 0)).isEqualTo(0.5f); - assertThat(getDensityScale(getMockedResources(DENSITY_HIGH, DENSITY_MEDIUM), 0)).isEqualTo(2f / 3); - assertThat(getDensityScale(getMockedResources(DENSITY_HIGH, DENSITY_XHIGH), 0)).isEqualTo(4f / 3); - assertThat(getDensityScale(getMockedResources(DENSITY_HIGH, DENSITY_XXHIGH), 0)).isEqualTo(2); - assertThat(getDensityScale(getMockedResources(DENSITY_HIGH, DENSITY_XXXHIGH), 0)).isEqualTo(8f / 3); - assertThat(getDensityScale(getMockedResources(DENSITY_HIGH, DENSITY_TV), 0)).isEqualTo(213f / 240); - } - - @Test - public void testLowHighDensity() throws Exception { - assertThat(getDensityScale(getMockedResources(DENSITY_LOW, DENSITY_HIGH), 0)).isEqualTo(2); - } - - @Test - public void testNoneResourceDensities() throws Exception { - assertThat(getDensityScale(getMockedResources(TypedValue.DENSITY_NONE, DENSITY_LOW), 0)).isEqualTo(1); - assertThat(getDensityScale(getMockedResources(TypedValue.DENSITY_NONE, DENSITY_MEDIUM), 0)).isEqualTo(1); - assertThat(getDensityScale(getMockedResources(TypedValue.DENSITY_NONE, DENSITY_DEFAULT), 0)).isEqualTo(1); - assertThat(getDensityScale(getMockedResources(TypedValue.DENSITY_NONE, DENSITY_TV), 0)).isEqualTo(1); - assertThat(getDensityScale(getMockedResources(TypedValue.DENSITY_NONE, DENSITY_HIGH), 0)).isEqualTo(1); - assertThat(getDensityScale(getMockedResources(TypedValue.DENSITY_NONE, DENSITY_XXHIGH), 0)).isEqualTo(1); - assertThat(getDensityScale(getMockedResources(TypedValue.DENSITY_NONE, DENSITY_XXXHIGH), 0)).isEqualTo(1); - } - - @Test - public void testNoneDisplayDensities() throws Exception { - assertThat(getDensityScale(getMockedResources(DENSITY_LOW, Bitmap.DENSITY_NONE), 0)).isEqualTo(1); - assertThat(getDensityScale(getMockedResources(DENSITY_MEDIUM, Bitmap.DENSITY_NONE), 0)).isEqualTo(1); - assertThat(getDensityScale(getMockedResources(TypedValue.DENSITY_DEFAULT, Bitmap.DENSITY_NONE), 0)).isEqualTo(1); - assertThat(getDensityScale(getMockedResources(DENSITY_HIGH, Bitmap.DENSITY_NONE), 0)).isEqualTo(1); - assertThat(getDensityScale(getMockedResources(DENSITY_XXHIGH, Bitmap.DENSITY_NONE), 0)).isEqualTo(1); - } - - @Test - public void testInvalidDensities() throws Exception { - assertThat(getDensityScale(getMockedResources(DENSITY_HIGH, -1), 0)).isEqualTo(1); - assertThat(getDensityScale(getMockedResources(-1, DENSITY_HIGH), 0)).isEqualTo(1); - assertThat(getDensityScale(getMockedResources(-1, -1), 0)).isEqualTo(1); - assertThat(getDensityScale(getMockedResources(DENSITY_HIGH, 0), 0)).isEqualTo(1); - } -} \ No newline at end of file diff --git a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifViewUtilsDensityTest.kt b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifViewUtilsDensityTest.kt new file mode 100644 index 00000000..da1db488 --- /dev/null +++ b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/GifViewUtilsDensityTest.kt @@ -0,0 +1,263 @@ +package pl.droidsonroids.gif + +import android.content.res.Resources +import android.graphics.Bitmap +import android.os.Build +import android.util.DisplayMetrics +import android.util.TypedValue +import androidx.annotation.RequiresApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.Mockito.doAnswer +import org.mockito.junit.MockitoJUnitRunner +import pl.droidsonroids.gif.GifViewUtils.getDensityScale + +@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2) +@RunWith(MockitoJUnitRunner::class) +class GifViewUtilsDensityTest { + + @Mock + lateinit var resources: Resources + + private fun getMockedResources(resourceDensity: Int, displayDensity: Int): Resources { + doAnswer { invocation -> + val outValue = invocation.getArgument(1) + outValue.density = resourceDensity + outValue + }.`when`(resources)?.getValue( + ArgumentMatchers.anyInt(), ArgumentMatchers.any( + TypedValue::class.java + ), ArgumentMatchers.anyBoolean() + ) + + val displayMetrics = DisplayMetrics() + displayMetrics.densityDpi = displayDensity + + `when`(resources.displayMetrics).thenReturn(displayMetrics) + + return resources + } + + @Test + fun testHighResourceDensities() { + assertThat( + getDensityScale( + getMockedResources( + DisplayMetrics.DENSITY_HIGH, + DisplayMetrics.DENSITY_HIGH + ), 0 + ) + ).isEqualTo(1.0f) + + assertThat( + getDensityScale( + getMockedResources( + DisplayMetrics.DENSITY_HIGH, + DisplayMetrics.DENSITY_LOW + ), 0 + ) + ).isEqualTo(0.5f) + + assertThat( + getDensityScale( + getMockedResources( + DisplayMetrics.DENSITY_HIGH, + DisplayMetrics.DENSITY_MEDIUM + ), 0 + ) + ).isEqualTo(2f / 3) + + assertThat( + getDensityScale( + getMockedResources( + DisplayMetrics.DENSITY_HIGH, + DisplayMetrics.DENSITY_XHIGH + ), 0 + ) + ).isEqualTo(4f / 3) + + assertThat( + getDensityScale( + getMockedResources( + DisplayMetrics.DENSITY_HIGH, + DisplayMetrics.DENSITY_XXHIGH + ), 0 + ) + ).isEqualTo(2.0f) + + assertThat( + getDensityScale( + getMockedResources( + DisplayMetrics.DENSITY_HIGH, + DisplayMetrics.DENSITY_XXXHIGH + ), 0 + ) + ).isEqualTo(8f / 3) + + assertThat( + getDensityScale( + getMockedResources( + DisplayMetrics.DENSITY_HIGH, + DisplayMetrics.DENSITY_TV + ), 0 + ) + ).isEqualTo(213f / 240) + } + + @Test + fun testLowHighDensity() { + assertThat( + getDensityScale( + getMockedResources( + DisplayMetrics.DENSITY_LOW, + DisplayMetrics.DENSITY_HIGH + ), 0 + ) + ).isEqualTo(2.0f) + } + + @Test + fun testNoneResourceDensities() { + assertThat( + getDensityScale( + getMockedResources( + TypedValue.DENSITY_NONE, + DisplayMetrics.DENSITY_LOW + ), 0 + ) + ).isEqualTo(1.0f) + + assertThat( + getDensityScale( + getMockedResources( + TypedValue.DENSITY_NONE, + DisplayMetrics.DENSITY_MEDIUM + ), 0 + ) + ).isEqualTo(1.0f) + + assertThat( + getDensityScale( + getMockedResources( + TypedValue.DENSITY_NONE, + DisplayMetrics.DENSITY_DEFAULT + ), 0 + ) + ).isEqualTo(1.0f) + + assertThat( + getDensityScale( + getMockedResources( + TypedValue.DENSITY_NONE, + DisplayMetrics.DENSITY_TV + ), 0 + ) + ).isEqualTo(1.0f) + + assertThat( + getDensityScale( + getMockedResources( + TypedValue.DENSITY_NONE, + DisplayMetrics.DENSITY_HIGH + ), 0 + ) + ).isEqualTo(1.0f) + + assertThat( + getDensityScale( + getMockedResources( + TypedValue.DENSITY_NONE, + DisplayMetrics.DENSITY_XXHIGH + ), 0 + ) + ).isEqualTo(1.0f) + + assertThat( + getDensityScale( + getMockedResources( + TypedValue.DENSITY_NONE, + DisplayMetrics.DENSITY_XXXHIGH + ), 0 + ) + ).isEqualTo(1.0f) + } + + @Test + fun testNoneDisplayDensities() { + assertThat( + getDensityScale( + getMockedResources( + DisplayMetrics.DENSITY_LOW, + Bitmap.DENSITY_NONE + ), 0 + ) + ).isEqualTo(1.0f) + + assertThat( + getDensityScale( + getMockedResources( + DisplayMetrics.DENSITY_MEDIUM, + Bitmap.DENSITY_NONE + ), 0 + ) + ).isEqualTo(1.0f) + + assertThat( + getDensityScale( + getMockedResources( + TypedValue.DENSITY_DEFAULT, + Bitmap.DENSITY_NONE + ), 0 + ) + ).isEqualTo(1.0f) + + assertThat( + getDensityScale( + getMockedResources( + DisplayMetrics.DENSITY_HIGH, + Bitmap.DENSITY_NONE + ), 0 + ) + ).isEqualTo(1.0f) + + assertThat( + getDensityScale( + getMockedResources( + DisplayMetrics.DENSITY_XXHIGH, + Bitmap.DENSITY_NONE + ), 0 + ) + ).isEqualTo(1.0f) + } + + @Test + fun testInvalidDensities() { + assertThat( + getDensityScale( + getMockedResources(DisplayMetrics.DENSITY_HIGH, -1), + 0 + ) + ).isEqualTo(1.0f) + + assertThat( + getDensityScale( + getMockedResources(-1, DisplayMetrics.DENSITY_HIGH), + 0 + ) + ).isEqualTo(1.0f) + + assertThat(getDensityScale(getMockedResources(-1, -1), 0)).isEqualTo(1.0f) + + assertThat( + getDensityScale( + getMockedResources(DisplayMetrics.DENSITY_HIGH, 0), + 0 + ) + ).isEqualTo(1.0f) + } +} \ No newline at end of file diff --git a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/MultiCallbackTest.java b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/MultiCallbackTest.java deleted file mode 100644 index 0d017507..00000000 --- a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/MultiCallbackTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package pl.droidsonroids.gif; - -import android.graphics.drawable.Drawable; -import android.view.View; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.Spy; -import org.robolectric.RobolectricTestRunner; - -import static org.mockito.Mockito.verify; - -@RunWith(RobolectricTestRunner.class) -public class MultiCallbackTest { - - @Mock View view; - @Spy Drawable drawable; - private Runnable action; - private MultiCallback simpleMultiCallback; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - simpleMultiCallback = new MultiCallback(); - action = new Runnable() { - @Override - public void run() { - //no-op - } - }; - } - - @Test - public void testInvalidateDrawable() { - simpleMultiCallback.addView(view); - drawable.setCallback(simpleMultiCallback); - drawable.invalidateSelf(); - verify(view).invalidateDrawable(drawable); - } - - @Test - public void testScheduleDrawable() { - simpleMultiCallback.addView(view); - drawable.setCallback(simpleMultiCallback); - drawable.scheduleSelf(action, 0); - verify(view).scheduleDrawable(drawable, action, 0); - } - - @Test - public void testUnscheduleDrawable() { - simpleMultiCallback.addView(view); - drawable.setCallback(simpleMultiCallback); - drawable.unscheduleSelf(action); - verify(view).unscheduleDrawable(drawable, action); - } - - @Test - public void testViewRemoval() { - simpleMultiCallback.addView(view); - drawable.setCallback(simpleMultiCallback); - drawable.invalidateSelf(); - simpleMultiCallback.removeView(view); - drawable.invalidateSelf(); - verify(view).invalidateDrawable(drawable); - } - - @Test - public void testViewInvalidate() { - final MultiCallback viewInvalidateMultiCallback = new MultiCallback(true); - viewInvalidateMultiCallback.addView(view); - drawable.setCallback(viewInvalidateMultiCallback); - drawable.invalidateSelf(); - verify(view).invalidate(); - } -} \ No newline at end of file diff --git a/android-gif-drawable/src/test/java/pl/droidsonroids/gif/MultiCallbackTest.kt b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/MultiCallbackTest.kt new file mode 100644 index 00000000..9d2dcd5a --- /dev/null +++ b/android-gif-drawable/src/test/java/pl/droidsonroids/gif/MultiCallbackTest.kt @@ -0,0 +1,87 @@ +package pl.droidsonroids.gif + +import android.graphics.drawable.Drawable +import android.view.View +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import org.mockito.Spy +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class MultiCallbackTest { + + @Mock + lateinit var view: View + + @Spy + lateinit var drawable: Drawable + + private lateinit var action: Runnable + private lateinit var simpleMultiCallback: MultiCallback + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + simpleMultiCallback = MultiCallback() + action = Runnable { + //no-op + } + } + + @Test + fun testInvalidateDrawable() { + simpleMultiCallback.addView(view) + drawable.callback = simpleMultiCallback + + drawable.invalidateSelf() + + verify(view).invalidateDrawable(drawable) + } + + @Test + fun testScheduleDrawable() { + simpleMultiCallback.addView(view) + drawable.callback = simpleMultiCallback + + drawable.scheduleSelf(action, 0) + + verify(view).scheduleDrawable(drawable, action, 0) + } + + @Test + fun testUnscheduleDrawable() { + simpleMultiCallback.addView(view) + drawable.callback = simpleMultiCallback + + drawable.unscheduleSelf(action) + + verify(view).unscheduleDrawable(drawable, action) + } + + @Test + fun testViewRemoval() { + simpleMultiCallback.addView(view) + drawable.callback = simpleMultiCallback + + drawable.invalidateSelf() + simpleMultiCallback.removeView(view) + drawable.invalidateSelf() + + verify(view).invalidateDrawable(drawable) + } + + @Test + fun testViewInvalidate() { + val viewInvalidateMultiCallback = MultiCallback(true) + viewInvalidateMultiCallback.addView(view) + + drawable.callback = viewInvalidateMultiCallback + drawable.invalidateSelf() + + verify(view).invalidate() + } +} \ No newline at end of file diff --git a/sample/src/main/java/pl/droidsonroids/gif/sample/GifTexImage2DFragment.kt b/sample/src/main/java/pl/droidsonroids/gif/sample/GifTexImage2DFragment.kt index 39437620..d31f75bd 100644 --- a/sample/src/main/java/pl/droidsonroids/gif/sample/GifTexImage2DFragment.kt +++ b/sample/src/main/java/pl/droidsonroids/gif/sample/GifTexImage2DFragment.kt @@ -21,7 +21,7 @@ class GifTexImage2DFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val options = GifOptions() - options.setInIsOpaque(true) + options.inIsOpaque = true val gifTexImage2D = GifTexImage2D(InputSource.ResourcesSource(resources, R.drawable.anim_flag_chile), options) gifTexImage2D.startDecoderThread() gifTexImage2DProgram = GifTexImage2DProgram(gifTexImage2D) diff --git a/sample/src/main/java/pl/droidsonroids/gif/sample/wallpaper/GifWallpaperService.kt b/sample/src/main/java/pl/droidsonroids/gif/sample/wallpaper/GifWallpaperService.kt index c9261155..4945f2d5 100644 --- a/sample/src/main/java/pl/droidsonroids/gif/sample/wallpaper/GifWallpaperService.kt +++ b/sample/src/main/java/pl/droidsonroids/gif/sample/wallpaper/GifWallpaperService.kt @@ -14,7 +14,7 @@ import kotlin.coroutines.CoroutineContext class GifWallpaperService : WallpaperService() { override fun onCreateEngine(): GifWallpaperEngine { val options = GifOptions() - options.setInIsOpaque(true) + options.inIsOpaque = true val gifTexImage2D = GifTexImage2D(InputSource.ResourcesSource(resources, R.drawable.led7), options) return GifWallpaperEngine(gifTexImage2D) }