Repository: facebook/fresco Branch: main Commit: 322f67e65293 Files: 2570 Total size: 38.2 MB Directory structure: gitextract_dq5d7atw/ ├── .github/ │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── stale.yml │ └── workflows/ │ ├── build.yml │ ├── gradle-wrapper-validation.yml │ └── release.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── animated-base/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ ├── fresco/ │ │ │ └── animation/ │ │ │ ├── bitmap/ │ │ │ │ ├── cache/ │ │ │ │ │ ├── AnimationFrameCacheKey.kt │ │ │ │ │ └── FrescoFrameCache.kt │ │ │ │ └── wrapper/ │ │ │ │ ├── AnimatedDrawableBackendAnimationInformation.kt │ │ │ │ └── AnimatedDrawableBackendFrameRenderer.kt │ │ │ ├── drawable/ │ │ │ │ └── animator/ │ │ │ │ └── AnimatedDrawableValueAnimatorHelper.kt │ │ │ └── factory/ │ │ │ ├── AnimatedFactoryV2Impl.kt │ │ │ └── DefaultBitmapAnimationDrawableFactory.kt │ │ └── imagepipeline/ │ │ ├── animated/ │ │ │ ├── base/ │ │ │ │ ├── AnimatedDrawableBackend.java │ │ │ │ ├── AnimatedDrawableFrameInfo.java │ │ │ │ ├── AnimatedDrawableOptions.java │ │ │ │ ├── AnimatedDrawableOptionsBuilder.java │ │ │ │ ├── AnimatedImage.java │ │ │ │ ├── AnimatedImageFrame.java │ │ │ │ ├── AnimatedImageResult.java │ │ │ │ ├── AnimatedImageResultBuilder.java │ │ │ │ ├── AnimatedImageValidator.kt │ │ │ │ └── package-info.java │ │ │ ├── factory/ │ │ │ │ ├── AnimatedImageDecoder.kt │ │ │ │ ├── AnimatedImageDecoderBase.kt │ │ │ │ └── AnimatedImageFactory.kt │ │ │ ├── impl/ │ │ │ │ ├── AnimatedDrawableBackendImpl.java │ │ │ │ ├── AnimatedDrawableBackendProvider.java │ │ │ │ ├── AnimatedFrameCache.java │ │ │ │ ├── AnimatedImageCompositor.java │ │ │ │ └── package-info.java │ │ │ └── util/ │ │ │ ├── AnimatedDrawableUtil.kt │ │ │ └── package-info.kt │ │ └── image/ │ │ ├── CloseableAnimatedImage.java │ │ └── package-info.java │ └── test/ │ └── java/ │ ├── android/ │ │ └── net/ │ │ └── http/ │ │ └── AndroidHttpClient.java │ └── com/ │ └── facebook/ │ ├── fresco/ │ │ └── animation/ │ │ └── bitmap/ │ │ ├── cache/ │ │ │ └── FrescoFrameCacheTest.kt │ │ └── wrapper/ │ │ ├── AnimatedDrawableBackendAnimationInformationTest.kt │ │ └── AnimatedDrawableBackendFrameRendererTest.kt │ └── imagepipeline/ │ ├── animated/ │ │ ├── impl/ │ │ │ ├── AnimatedDrawableBackendImplTest.kt │ │ │ └── AnimatedFrameCacheTest.kt │ │ ├── testing/ │ │ │ └── TestAnimatedDrawableBackend.java │ │ └── util/ │ │ └── AnimatedDrawableUtilTest.kt │ └── producers/ │ ├── AnimatedRepeatedPostprocessorProducerTest.kt │ └── AnimatedSingleUsePostprocessorProducerTest.kt ├── animated-drawable/ │ ├── .gitignore │ ├── README.md │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── fresco/ │ │ └── animation/ │ │ ├── backend/ │ │ │ ├── AnimationBackend.java │ │ │ ├── AnimationBackendDelegate.kt │ │ │ ├── AnimationBackendDelegateWithInactivityCheck.java │ │ │ └── AnimationInformation.java │ │ ├── bitmap/ │ │ │ ├── BitmapAnimationBackend.kt │ │ │ ├── BitmapFrameCache.kt │ │ │ ├── BitmapFrameRenderer.kt │ │ │ ├── cache/ │ │ │ │ ├── KeepLastFrameCache.kt │ │ │ │ └── NoOpCache.kt │ │ │ └── preparation/ │ │ │ ├── BitmapFramePreparationStrategy.kt │ │ │ ├── BitmapFramePreparer.kt │ │ │ ├── DefaultBitmapFramePreparer.kt │ │ │ ├── FixedNumberBitmapFramePreparationStrategy.kt │ │ │ ├── FrameLoaderStrategy.kt │ │ │ ├── loadframe/ │ │ │ │ ├── AnimationLoaderExecutor.kt │ │ │ │ └── FpsCompressorInfo.kt │ │ │ └── ondemandanimation/ │ │ │ ├── AnimationBitmapFrame.kt │ │ │ ├── AnimationCoordinator.kt │ │ │ ├── AnimationLoaderFactory.kt │ │ │ ├── BufferFrameLoader.kt │ │ │ ├── CircularList.kt │ │ │ └── FrameLoader.kt │ │ ├── drawable/ │ │ │ ├── AnimatedDrawable2.kt │ │ │ ├── AnimatedDrawable2DebugDrawListener.kt │ │ │ ├── AnimationFrameScheduler.kt │ │ │ ├── AnimationListener.kt │ │ │ ├── BaseAnimationListener.kt │ │ │ ├── KAnimatedDrawable2.kt │ │ │ └── animator/ │ │ │ └── AnimatedDrawable2ValueAnimatorHelper.kt │ │ └── frame/ │ │ ├── DropFramesFrameScheduler.kt │ │ └── FrameScheduler.java │ └── test/ │ └── java/ │ ├── com/ │ │ └── facebook/ │ │ └── fresco/ │ │ └── animation/ │ │ ├── backend/ │ │ │ ├── AnimationBackendDelegateTest.kt │ │ │ └── AnimationBackendDelegateWithInactivityCheckTest.kt │ │ ├── bitmap/ │ │ │ ├── BitmapAnimationBackendTest.kt │ │ │ └── preparation/ │ │ │ ├── DefaultBitmapFramePreparerTest.kt │ │ │ └── FixedNumberBitmapFramePreparationStrategyTest.kt │ │ └── frame/ │ │ └── DropFramesFrameSchedulerTest.kt │ └── javax/ │ └── microedition/ │ └── khronos/ │ └── opengles/ │ └── GL.java ├── animated-gif/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── facebook/ │ │ │ └── animated/ │ │ │ └── gif/ │ │ │ ├── AnimatedImageGifValidator.kt │ │ │ ├── GifFrame.java │ │ │ ├── GifImage.java │ │ │ └── GifImageDecoder.kt │ │ └── jni/ │ │ ├── Application.mk │ │ ├── gifimage/ │ │ │ ├── Android.mk │ │ │ ├── OnLoad.cpp │ │ │ ├── gif.cpp │ │ │ ├── jni_helpers.cpp │ │ │ ├── jni_helpers.h │ │ │ ├── locks.h │ │ │ └── secure_memcpy.h │ │ └── third-party/ │ │ └── giflib/ │ │ └── Android.mk │ └── test/ │ └── java/ │ └── com/ │ └── facebook/ │ └── animated/ │ └── gif/ │ └── GifImageDecoderTest.kt ├── animated-gif-lite/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── animated/ │ └── giflite/ │ ├── GifDecoder.java │ ├── decoder/ │ │ └── GifMetadataDecoder.java │ ├── draw/ │ │ ├── MovieAnimatedImage.kt │ │ ├── MovieDrawer.kt │ │ ├── MovieFrame.kt │ │ └── MovieScaleHolder.kt │ └── drawable/ │ └── GifAnimationBackend.kt ├── animated-webp/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── animated/ │ │ ├── webp/ │ │ │ ├── WebPFrame.java │ │ │ ├── WebPImage.java │ │ │ └── WebPImageDecoder.kt │ │ └── webpdrawable/ │ │ └── WebpAnimationBackend.kt │ └── test/ │ └── java/ │ └── com/ │ └── facebook/ │ └── animated/ │ └── webp/ │ └── WebPImageDecoderTest.kt ├── bots/ │ └── IssueCommands.txt ├── build.gradle ├── buildSrc/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── buildsrc/ │ ├── FrescoConfig.kt │ ├── GradleDeps.kt │ ├── TestDeps.kt │ ├── dependencies-samples.kt │ └── dependencies.kt ├── ci/ │ ├── build-and-test.sh │ └── print-debug-info.sh ├── docs/ │ ├── .gitignore │ ├── CNAME │ ├── Gemfile │ ├── NOGREP │ ├── README.md │ ├── _config.yml │ ├── _data/ │ │ ├── authors.yml │ │ ├── nav.yml │ │ ├── nav_docs.yml │ │ ├── powered_by.yml │ │ ├── powered_by_highlight.yml │ │ └── promo.yml │ ├── _docs/ │ │ ├── 03-customizing-image-formats.md │ │ ├── animations.md │ │ ├── building-from-source.md │ │ ├── caching.md │ │ ├── closeable-references.md │ │ ├── concepts.md │ │ ├── configure-image-pipeline.md │ │ ├── datasources-datasubscribers.md │ │ ├── drawee-branches.md │ │ ├── faq.md │ │ ├── gotchas.md │ │ ├── image-requests.md │ │ ├── images-in-notifications.md │ │ ├── index.md │ │ ├── intro-image-pipeline.md │ │ ├── listening-to-events.md │ │ ├── media-variations.md │ │ ├── placeholder-failure-retry.md │ │ ├── post-processor.md │ │ ├── prefetching.md │ │ ├── progress-bars.md │ │ ├── progressive-jpegs.md │ │ ├── proguard.md │ │ ├── requesting-multiple-images.md │ │ ├── resizing.md │ │ ├── rotation.md │ │ ├── rounded-corners-and-circles.md │ │ ├── sample-apps.md │ │ ├── scaletypes.md │ │ ├── shared-transitions.md │ │ ├── supported-uris.md │ │ ├── troubleshooting.md │ │ ├── using-controllerbuilder.md │ │ ├── using-image-pipeline.md │ │ ├── using-other-network-layers.md │ │ ├── using-simpledraweeview.md │ │ ├── webp-support.md │ │ └── writing-custom-views.md │ ├── _includes/ │ │ ├── blog_pagination.html │ │ ├── content/ │ │ │ ├── gridblocks.html │ │ │ └── items/ │ │ │ └── gridblock.html │ │ ├── doc.html │ │ ├── doc_paging.html │ │ ├── footer.html │ │ ├── head.html │ │ ├── hero.html │ │ ├── home_header.html │ │ ├── nav.html │ │ ├── nav_search.html │ │ ├── plugins/ │ │ │ ├── all_share.html │ │ │ ├── button.html │ │ │ ├── fb_pagelike.html │ │ │ ├── github_star.html │ │ │ ├── github_watch.html │ │ │ ├── google_share.html │ │ │ ├── group_join.html │ │ │ ├── like_button.html │ │ │ ├── plugin_row.html │ │ │ ├── post_social_plugins.html │ │ │ ├── slideshow.html │ │ │ └── twitter_share.html │ │ ├── post.html │ │ ├── powered_by.html │ │ ├── react/ │ │ │ ├── collection_nav.html │ │ │ ├── header_nav.html │ │ │ ├── nav_blog.html │ │ │ └── nav_docs.html │ │ ├── social_plugins.html │ │ └── ui/ │ │ └── button.html │ ├── _layouts/ │ │ ├── basic.html │ │ ├── blog.html │ │ ├── blog_default.html │ │ ├── default.html │ │ ├── doc_default.html │ │ ├── doc_page.html │ │ ├── docs.html │ │ ├── home.html │ │ ├── page.html │ │ ├── plain.html │ │ ├── post.html │ │ └── redirect.html │ ├── _sass/ │ │ ├── _base.scss │ │ ├── _blog.scss │ │ ├── _buttons.scss │ │ ├── _footer.scss │ │ ├── _gridBlock.scss │ │ ├── _header.scss │ │ ├── _poweredby.scss │ │ ├── _promo.scss │ │ ├── _react_docs_nav.scss │ │ ├── _react_header_nav.scss │ │ ├── _reset.scss │ │ ├── _search.scss │ │ ├── _slideshow.scss │ │ ├── _syntax-highlighting.scss │ │ └── _tables.scss │ ├── css/ │ │ └── main.scss │ ├── docs/ │ │ └── index.html │ ├── index.md │ ├── javadoc/ │ │ ├── assets/ │ │ │ ├── customizations.css │ │ │ ├── customizations.js │ │ │ ├── doclava-developer-core.css │ │ │ ├── doclava-developer-docs.css │ │ │ ├── doclava-developer-docs.js │ │ │ ├── doclava-developer-reference.js │ │ │ ├── jquery-history.js │ │ │ ├── navtree_data.js │ │ │ ├── prettify.js │ │ │ ├── search_autocomplete.js │ │ │ └── style.css │ │ ├── index.html │ │ └── reference/ │ │ ├── classes.html │ │ ├── com/ │ │ │ └── facebook/ │ │ │ ├── animated/ │ │ │ │ ├── gif/ │ │ │ │ │ ├── GifFrame.html │ │ │ │ │ ├── GifImage.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── giflite/ │ │ │ │ │ ├── GifDecoder.html │ │ │ │ │ ├── decoder/ │ │ │ │ │ │ ├── GifMetadataDecoder.html │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ ├── draw/ │ │ │ │ │ │ ├── MovieAnimatedImage.html │ │ │ │ │ │ ├── MovieDrawer.html │ │ │ │ │ │ ├── MovieFrame.html │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ ├── drawable/ │ │ │ │ │ │ ├── GifAnimationBackend.html │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── webp/ │ │ │ │ │ ├── WebPFrame.html │ │ │ │ │ ├── WebPImage.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ └── webpdrawable/ │ │ │ │ ├── WebpAnimationBackend.html │ │ │ │ ├── package-descr.html │ │ │ │ └── package-summary.html │ │ │ ├── binaryresource/ │ │ │ │ ├── BinaryResource.html │ │ │ │ ├── ByteArrayBinaryResource.html │ │ │ │ ├── FileBinaryResource.html │ │ │ │ ├── package-descr.html │ │ │ │ └── package-summary.html │ │ │ ├── cache/ │ │ │ │ ├── common/ │ │ │ │ │ ├── BaseCacheEventListener.html │ │ │ │ │ ├── CacheErrorLogger.CacheErrorCategory.html │ │ │ │ │ ├── CacheErrorLogger.html │ │ │ │ │ ├── CacheEvent.html │ │ │ │ │ ├── CacheEventListener.EvictionReason.html │ │ │ │ │ ├── CacheEventListener.html │ │ │ │ │ ├── CacheKey.html │ │ │ │ │ ├── CacheKeyUtil.html │ │ │ │ │ ├── DebuggingCacheKey.html │ │ │ │ │ ├── HasDebugData.html │ │ │ │ │ ├── MultiCacheKey.html │ │ │ │ │ ├── NoOpCacheErrorLogger.html │ │ │ │ │ ├── NoOpCacheEventListener.html │ │ │ │ │ ├── SimpleCacheKey.html │ │ │ │ │ ├── WriterCallback.html │ │ │ │ │ ├── WriterCallbacks.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ └── disk/ │ │ │ │ ├── DefaultDiskStorage.FileType.html │ │ │ │ ├── DefaultDiskStorage.html │ │ │ │ ├── DefaultEntryEvictionComparatorSupplier.html │ │ │ │ ├── DiskCacheConfig.Builder.html │ │ │ │ ├── DiskCacheConfig.html │ │ │ │ ├── DiskStorage.DiskDumpInfo.html │ │ │ │ ├── DiskStorage.DiskDumpInfoEntry.html │ │ │ │ ├── DiskStorage.Entry.html │ │ │ │ ├── DiskStorage.Inserter.html │ │ │ │ ├── DiskStorage.html │ │ │ │ ├── DiskStorageCache.Params.html │ │ │ │ ├── DiskStorageCache.html │ │ │ │ ├── DynamicDefaultDiskStorage.html │ │ │ │ ├── EntryEvictionComparator.html │ │ │ │ ├── EntryEvictionComparatorSupplier.html │ │ │ │ ├── FileCache.html │ │ │ │ ├── ScoreBasedEvictionComparatorSupplier.html │ │ │ │ ├── SettableCacheEvent.html │ │ │ │ ├── package-descr.html │ │ │ │ └── package-summary.html │ │ │ ├── callercontext/ │ │ │ │ ├── CallerContextVerifier.html │ │ │ │ ├── package-descr.html │ │ │ │ └── package-summary.html │ │ │ ├── common/ │ │ │ │ ├── activitylistener/ │ │ │ │ │ ├── ActivityListener.html │ │ │ │ │ ├── ActivityListenerManager.html │ │ │ │ │ ├── BaseActivityListener.html │ │ │ │ │ ├── ListenableActivity.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── disk/ │ │ │ │ │ ├── DiskTrimmable.html │ │ │ │ │ ├── DiskTrimmableRegistry.html │ │ │ │ │ ├── NoOpDiskTrimmableRegistry.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── executors/ │ │ │ │ │ ├── CallerThreadExecutor.html │ │ │ │ │ ├── ConstrainedExecutorService.html │ │ │ │ │ ├── DefaultSerialExecutorService.html │ │ │ │ │ ├── HandlerExecutorService.html │ │ │ │ │ ├── HandlerExecutorServiceImpl.html │ │ │ │ │ ├── ScheduledFutureImpl.html │ │ │ │ │ ├── SerialExecutorService.html │ │ │ │ │ ├── StatefulRunnable.html │ │ │ │ │ ├── UiThreadImmediateExecutorService.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── file/ │ │ │ │ │ ├── FileTree.html │ │ │ │ │ ├── FileTreeVisitor.html │ │ │ │ │ ├── FileUtils.CreateDirectoryException.html │ │ │ │ │ ├── FileUtils.FileDeleteException.html │ │ │ │ │ ├── FileUtils.ParentDirNotFoundException.html │ │ │ │ │ ├── FileUtils.RenameException.html │ │ │ │ │ ├── FileUtils.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── internal/ │ │ │ │ │ ├── AndroidPredicates.html │ │ │ │ │ ├── ByteStreams.html │ │ │ │ │ ├── Closeables.html │ │ │ │ │ ├── CountingOutputStream.html │ │ │ │ │ ├── DoNotStrip.html │ │ │ │ │ ├── Files.html │ │ │ │ │ ├── Fn.html │ │ │ │ │ ├── ImmutableList.html │ │ │ │ │ ├── ImmutableMap.html │ │ │ │ │ ├── ImmutableSet.html │ │ │ │ │ ├── Ints.html │ │ │ │ │ ├── Objects.ToStringHelper.html │ │ │ │ │ ├── Objects.html │ │ │ │ │ ├── Preconditions.html │ │ │ │ │ ├── Predicate.html │ │ │ │ │ ├── Sets.html │ │ │ │ │ ├── Supplier.html │ │ │ │ │ ├── Suppliers.html │ │ │ │ │ ├── Throwables.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── lifecycle/ │ │ │ │ │ ├── AttachDetachListener.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── logging/ │ │ │ │ │ ├── FLog.html │ │ │ │ │ ├── FLogDefaultLoggingDelegate.html │ │ │ │ │ ├── LoggingDelegate.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── media/ │ │ │ │ │ ├── MediaUtils.html │ │ │ │ │ ├── MimeTypeMapWrapper.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── memory/ │ │ │ │ │ ├── ByteArrayPool.html │ │ │ │ │ ├── MemoryTrimType.html │ │ │ │ │ ├── MemoryTrimmable.html │ │ │ │ │ ├── MemoryTrimmableRegistry.html │ │ │ │ │ ├── NoOpMemoryTrimmableRegistry.html │ │ │ │ │ ├── Pool.html │ │ │ │ │ ├── PooledByteArrayBufferedInputStream.html │ │ │ │ │ ├── PooledByteBuffer.ClosedException.html │ │ │ │ │ ├── PooledByteBuffer.html │ │ │ │ │ ├── PooledByteBufferFactory.html │ │ │ │ │ ├── PooledByteBufferInputStream.html │ │ │ │ │ ├── PooledByteBufferOutputStream.html │ │ │ │ │ ├── PooledByteStreams.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── references/ │ │ │ │ │ ├── CloseableReference.CloseableRefType.html │ │ │ │ │ ├── CloseableReference.LeakHandler.html │ │ │ │ │ ├── CloseableReference.html │ │ │ │ │ ├── DefaultCloseableReference.html │ │ │ │ │ ├── FinalizerCloseableReference.html │ │ │ │ │ ├── HasBitmap.html │ │ │ │ │ ├── NoOpCloseableReference.html │ │ │ │ │ ├── OOMSoftReference.html │ │ │ │ │ ├── RefCountCloseableReference.html │ │ │ │ │ ├── ResourceReleaser.html │ │ │ │ │ ├── SharedReference.NullReferenceException.html │ │ │ │ │ ├── SharedReference.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── statfs/ │ │ │ │ │ ├── StatFsHelper.StorageType.html │ │ │ │ │ ├── StatFsHelper.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── streams/ │ │ │ │ │ ├── LimitedInputStream.html │ │ │ │ │ ├── TailAppendingInputStream.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── time/ │ │ │ │ │ ├── AwakeTimeSinceBootClock.html │ │ │ │ │ ├── Clock.html │ │ │ │ │ ├── CurrentThreadTimeClock.html │ │ │ │ │ ├── MonotonicClock.html │ │ │ │ │ ├── MonotonicNanoClock.html │ │ │ │ │ ├── RealtimeSinceBootClock.html │ │ │ │ │ ├── SystemClock.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── util/ │ │ │ │ │ ├── ByteConstants.html │ │ │ │ │ ├── ExceptionWithNoStacktrace.html │ │ │ │ │ ├── HashCodeUtil.html │ │ │ │ │ ├── Hex.html │ │ │ │ │ ├── SecureHashUtil.html │ │ │ │ │ ├── StreamUtil.html │ │ │ │ │ ├── TriState.html │ │ │ │ │ ├── UriUtil.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ └── webp/ │ │ │ │ ├── BitmapCreator.html │ │ │ │ ├── WebpBitmapFactory.WebpErrorLogger.html │ │ │ │ ├── WebpBitmapFactory.html │ │ │ │ ├── WebpSupportStatus.html │ │ │ │ ├── package-descr.html │ │ │ │ └── package-summary.html │ │ │ ├── datasource/ │ │ │ │ ├── AbstractDataSource.DataSourceInstrumenter.html │ │ │ │ ├── AbstractDataSource.html │ │ │ │ ├── BaseBooleanSubscriber.html │ │ │ │ ├── BaseDataSubscriber.html │ │ │ │ ├── DataSource.html │ │ │ │ ├── DataSources.html │ │ │ │ ├── DataSubscriber.html │ │ │ │ ├── FirstAvailableDataSourceSupplier.html │ │ │ │ ├── IncreasingQualityDataSourceSupplier.html │ │ │ │ ├── RetainingDataSourceSupplier.html │ │ │ │ ├── SimpleDataSource.html │ │ │ │ ├── package-descr.html │ │ │ │ └── package-summary.html │ │ │ ├── drawable/ │ │ │ │ └── base/ │ │ │ │ ├── DrawableWithCaches.html │ │ │ │ ├── package-descr.html │ │ │ │ └── package-summary.html │ │ │ ├── drawee/ │ │ │ │ ├── backends/ │ │ │ │ │ └── pipeline/ │ │ │ │ │ ├── DefaultDrawableFactory.html │ │ │ │ │ ├── DraweeConfig.Builder.html │ │ │ │ │ ├── DraweeConfig.html │ │ │ │ │ ├── Fresco.html │ │ │ │ │ ├── PipelineDraweeController.html │ │ │ │ │ ├── PipelineDraweeControllerBuilder.html │ │ │ │ │ ├── PipelineDraweeControllerBuilderSupplier.html │ │ │ │ │ ├── PipelineDraweeControllerFactory.html │ │ │ │ │ ├── debug/ │ │ │ │ │ │ ├── DebugOverlayImageOriginColor.html │ │ │ │ │ │ ├── DebugOverlayImageOriginListener.html │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ ├── info/ │ │ │ │ │ │ ├── ForwardingImageOriginListener.html │ │ │ │ │ │ ├── ForwardingImagePerfDataListener.html │ │ │ │ │ │ ├── ImageLoadStatus.html │ │ │ │ │ │ ├── ImageOrigin.html │ │ │ │ │ │ ├── ImageOriginListener.html │ │ │ │ │ │ ├── ImageOriginRequestListener.html │ │ │ │ │ │ ├── ImageOriginUtils.html │ │ │ │ │ │ ├── ImagePerfData.html │ │ │ │ │ │ ├── ImagePerfDataListener.html │ │ │ │ │ │ ├── ImagePerfMonitor.html │ │ │ │ │ │ ├── ImagePerfNotifier.html │ │ │ │ │ │ ├── ImagePerfState.html │ │ │ │ │ │ ├── ImagePerfUtils.html │ │ │ │ │ │ ├── VisibilityState.html │ │ │ │ │ │ ├── internal/ │ │ │ │ │ │ │ ├── ImagePerfControllerListener.html │ │ │ │ │ │ │ ├── ImagePerfControllerListener2.html │ │ │ │ │ │ │ ├── ImagePerfImageOriginListener.html │ │ │ │ │ │ │ ├── ImagePerfRequestListener.html │ │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── components/ │ │ │ │ │ ├── DeferredReleaser.Releasable.html │ │ │ │ │ ├── DeferredReleaser.html │ │ │ │ │ ├── DraweeEventTracker.Event.html │ │ │ │ │ ├── DraweeEventTracker.html │ │ │ │ │ ├── RetryManager.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── controller/ │ │ │ │ │ ├── AbstractDraweeController.html │ │ │ │ │ ├── AbstractDraweeControllerBuilder.CacheLevel.html │ │ │ │ │ ├── AbstractDraweeControllerBuilder.html │ │ │ │ │ ├── BaseControllerListener.html │ │ │ │ │ ├── ControllerListener.html │ │ │ │ │ ├── ControllerViewportVisibilityListener.html │ │ │ │ │ ├── ForwardingControllerListener.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── debug/ │ │ │ │ │ ├── DebugControllerOverlayDrawable.html │ │ │ │ │ ├── listener/ │ │ │ │ │ │ ├── ImageLoadingTimeControllerListener.html │ │ │ │ │ │ ├── ImageLoadingTimeListener.html │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── drawable/ │ │ │ │ │ ├── ArrayDrawable.html │ │ │ │ │ ├── AutoRotateDrawable.html │ │ │ │ │ ├── CloneableDrawable.html │ │ │ │ │ ├── DrawableParent.html │ │ │ │ │ ├── DrawableProperties.html │ │ │ │ │ ├── DrawableUtils.html │ │ │ │ │ ├── FadeDrawable.OnFadeListener.html │ │ │ │ │ ├── FadeDrawable.html │ │ │ │ │ ├── ForwardingDrawable.html │ │ │ │ │ ├── InstrumentedDrawable.Listener.html │ │ │ │ │ ├── InstrumentedDrawable.html │ │ │ │ │ ├── MatrixDrawable.html │ │ │ │ │ ├── OrientedDrawable.html │ │ │ │ │ ├── ProgressBarDrawable.html │ │ │ │ │ ├── Rounded.html │ │ │ │ │ ├── RoundedBitmapDrawable.html │ │ │ │ │ ├── RoundedColorDrawable.html │ │ │ │ │ ├── RoundedCornersDrawable.Type.html │ │ │ │ │ ├── RoundedCornersDrawable.html │ │ │ │ │ ├── RoundedDrawable.html │ │ │ │ │ ├── RoundedNinePatchDrawable.html │ │ │ │ │ ├── ScaleTypeDrawable.html │ │ │ │ │ ├── ScalingUtils.AbstractScaleType.html │ │ │ │ │ ├── ScalingUtils.InterpolatingScaleType.html │ │ │ │ │ ├── ScalingUtils.ScaleType.html │ │ │ │ │ ├── ScalingUtils.StatefulScaleType.html │ │ │ │ │ ├── ScalingUtils.html │ │ │ │ │ ├── TransformAwareDrawable.html │ │ │ │ │ ├── TransformCallback.html │ │ │ │ │ ├── VisibilityAwareDrawable.html │ │ │ │ │ ├── VisibilityCallback.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── generic/ │ │ │ │ │ ├── GenericDraweeHierarchy.html │ │ │ │ │ ├── GenericDraweeHierarchyBuilder.html │ │ │ │ │ ├── GenericDraweeHierarchyInflater.html │ │ │ │ │ ├── RootDrawable.html │ │ │ │ │ ├── RoundingParams.RoundingMethod.html │ │ │ │ │ ├── RoundingParams.html │ │ │ │ │ ├── WrappingUtils.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── gestures/ │ │ │ │ │ ├── GestureDetector.ClickListener.html │ │ │ │ │ ├── GestureDetector.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── interfaces/ │ │ │ │ │ ├── DraweeController.html │ │ │ │ │ ├── DraweeHierarchy.html │ │ │ │ │ ├── SettableDraweeHierarchy.html │ │ │ │ │ ├── SimpleDraweeControllerBuilder.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── span/ │ │ │ │ │ ├── DraweeSpan.html │ │ │ │ │ ├── DraweeSpanStringBuilder.DraweeSpanChangedListener.html │ │ │ │ │ ├── DraweeSpanStringBuilder.html │ │ │ │ │ ├── SimpleDraweeSpanTextView.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ └── view/ │ │ │ │ ├── AspectRatioMeasure.Spec.html │ │ │ │ ├── AspectRatioMeasure.html │ │ │ │ ├── DraweeHolder.html │ │ │ │ ├── DraweeTransition.html │ │ │ │ ├── DraweeView.html │ │ │ │ ├── GenericDraweeView.html │ │ │ │ ├── MultiDraweeHolder.html │ │ │ │ ├── SimpleDraweeView.html │ │ │ │ ├── package-descr.html │ │ │ │ └── package-summary.html │ │ │ ├── fresco/ │ │ │ │ ├── animation/ │ │ │ │ │ ├── backend/ │ │ │ │ │ │ ├── AnimationBackend.html │ │ │ │ │ │ ├── AnimationBackendDelegate.html │ │ │ │ │ │ ├── AnimationBackendDelegateWithInactivityCheck.InactivityListener.html │ │ │ │ │ │ ├── AnimationBackendDelegateWithInactivityCheck.html │ │ │ │ │ │ ├── AnimationInformation.html │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ ├── bitmap/ │ │ │ │ │ │ ├── BitmapAnimationBackend.FrameListener.html │ │ │ │ │ │ ├── BitmapAnimationBackend.FrameType.html │ │ │ │ │ │ ├── BitmapAnimationBackend.html │ │ │ │ │ │ ├── BitmapFrameCache.FrameCacheListener.html │ │ │ │ │ │ ├── BitmapFrameCache.html │ │ │ │ │ │ ├── BitmapFrameRenderer.html │ │ │ │ │ │ ├── cache/ │ │ │ │ │ │ │ ├── AnimationFrameCacheKey.html │ │ │ │ │ │ │ ├── FrescoFrameCache.html │ │ │ │ │ │ │ ├── KeepLastFrameCache.html │ │ │ │ │ │ │ ├── NoOpCache.html │ │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ ├── package-summary.html │ │ │ │ │ │ ├── preparation/ │ │ │ │ │ │ │ ├── BitmapFramePreparationStrategy.html │ │ │ │ │ │ │ ├── BitmapFramePreparer.html │ │ │ │ │ │ │ ├── DefaultBitmapFramePreparer.html │ │ │ │ │ │ │ ├── FixedNumberBitmapFramePreparationStrategy.html │ │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ │ └── wrapper/ │ │ │ │ │ │ ├── AnimatedDrawableBackendAnimationInformation.html │ │ │ │ │ │ ├── AnimatedDrawableBackendFrameRenderer.html │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ ├── drawable/ │ │ │ │ │ │ ├── AnimatedDrawable2.DrawListener.html │ │ │ │ │ │ ├── AnimatedDrawable2.html │ │ │ │ │ │ ├── AnimatedDrawable2DebugDrawListener.html │ │ │ │ │ │ ├── AnimationListener.html │ │ │ │ │ │ ├── BaseAnimationListener.html │ │ │ │ │ │ ├── animator/ │ │ │ │ │ │ │ ├── AnimatedDrawable2ValueAnimatorHelper.html │ │ │ │ │ │ │ ├── AnimatedDrawableValueAnimatorHelper.html │ │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ ├── factory/ │ │ │ │ │ │ ├── AnimatedFactoryV2Impl.html │ │ │ │ │ │ ├── ExperimentalBitmapAnimationDrawableFactory.html │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ └── frame/ │ │ │ │ │ ├── DropFramesFrameScheduler.html │ │ │ │ │ ├── FrameScheduler.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── middleware/ │ │ │ │ │ ├── MiddlewareUtils.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ └── ui/ │ │ │ │ └── common/ │ │ │ │ ├── BaseControllerListener2.html │ │ │ │ ├── ControllerListener2.Extras.html │ │ │ │ ├── ControllerListener2.html │ │ │ │ ├── DimensionsInfo.html │ │ │ │ ├── ForwardingControllerListener2.html │ │ │ │ ├── LoggingListener.html │ │ │ │ ├── MultiUriHelper.html │ │ │ │ ├── OnDrawControllerListener.html │ │ │ │ ├── package-descr.html │ │ │ │ └── package-summary.html │ │ │ ├── imageformat/ │ │ │ │ ├── DefaultImageFormatChecker.html │ │ │ │ ├── DefaultImageFormats.html │ │ │ │ ├── ImageFormat.FormatChecker.html │ │ │ │ ├── ImageFormat.html │ │ │ │ ├── ImageFormatChecker.html │ │ │ │ ├── ImageFormatCheckerUtils.html │ │ │ │ ├── package-descr.html │ │ │ │ └── package-summary.html │ │ │ ├── imagepipeline/ │ │ │ │ ├── animated/ │ │ │ │ │ ├── base/ │ │ │ │ │ │ ├── AnimatedDrawableBackend.html │ │ │ │ │ │ ├── AnimatedDrawableFrameInfo.BlendOperation.html │ │ │ │ │ │ ├── AnimatedDrawableFrameInfo.DisposalMethod.html │ │ │ │ │ │ ├── AnimatedDrawableFrameInfo.html │ │ │ │ │ │ ├── AnimatedDrawableOptions.html │ │ │ │ │ │ ├── AnimatedDrawableOptionsBuilder.html │ │ │ │ │ │ ├── AnimatedImage.html │ │ │ │ │ │ ├── AnimatedImageFrame.html │ │ │ │ │ │ ├── AnimatedImageResult.html │ │ │ │ │ │ ├── AnimatedImageResultBuilder.html │ │ │ │ │ │ ├── DelegatingAnimatedDrawableBackend.html │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ ├── factory/ │ │ │ │ │ │ ├── AnimatedFactory.html │ │ │ │ │ │ ├── AnimatedFactoryProvider.html │ │ │ │ │ │ ├── AnimatedImageDecoder.html │ │ │ │ │ │ ├── AnimatedImageFactory.html │ │ │ │ │ │ ├── AnimatedImageFactoryImpl.html │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ ├── impl/ │ │ │ │ │ │ ├── AnimatedDrawableBackendImpl.html │ │ │ │ │ │ ├── AnimatedDrawableBackendProvider.html │ │ │ │ │ │ ├── AnimatedFrameCache.html │ │ │ │ │ │ ├── AnimatedImageCompositor.Callback.html │ │ │ │ │ │ ├── AnimatedImageCompositor.html │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ └── util/ │ │ │ │ │ ├── AnimatedDrawableUtil.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── backends/ │ │ │ │ │ ├── okhttp3/ │ │ │ │ │ │ ├── OkHttpImagePipelineConfigFactory.html │ │ │ │ │ │ ├── OkHttpNetworkFetcher.OkHttpNetworkFetchState.html │ │ │ │ │ │ ├── OkHttpNetworkFetcher.html │ │ │ │ │ │ ├── package-descr.html │ │ │ │ │ │ └── package-summary.html │ │ │ │ │ └── volley/ │ │ │ │ │ ├── RawRequest.html │ │ │ │ │ ├── VolleyImagePipelineConfigFactory.html │ │ │ │ │ ├── VolleyNetworkFetcher.VolleyNetworkFetchState.html │ │ │ │ │ ├── VolleyNetworkFetcher.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── bitmaps/ │ │ │ │ │ ├── ArtBitmapFactory.html │ │ │ │ │ ├── EmptyJpegGenerator.html │ │ │ │ │ ├── GingerbreadBitmapFactory.html │ │ │ │ │ ├── HoneycombBitmapCreator.html │ │ │ │ │ ├── HoneycombBitmapFactory.html │ │ │ │ │ ├── PlatformBitmapFactory.html │ │ │ │ │ ├── PlatformBitmapFactoryProvider.html │ │ │ │ │ ├── SimpleBitmapReleaser.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── cache/ │ │ │ │ │ ├── AbstractAdaptiveCountingMemoryCache.html │ │ │ │ │ ├── BitmapMemoryCacheFactory.html │ │ │ │ │ ├── BitmapMemoryCacheKey.html │ │ │ │ │ ├── BitmapMemoryCacheTrimStrategy.html │ │ │ │ │ ├── BoundedLinkedHashSet.html │ │ │ │ │ ├── BufferedDiskCache.html │ │ │ │ │ ├── CacheKeyFactory.html │ │ │ │ │ ├── CountingLruBitmapMemoryCacheFactory.html │ │ │ │ │ ├── CountingLruMap.html │ │ │ │ │ ├── CountingMemoryCache.Entry.html │ │ │ │ │ ├── CountingMemoryCache.EntryStateObserver.html │ │ │ │ │ ├── CountingMemoryCache.html │ │ │ │ │ ├── CountingMemoryCacheInspector.DumpInfo.html │ │ │ │ │ ├── CountingMemoryCacheInspector.DumpInfoEntry.html │ │ │ │ │ ├── CountingMemoryCacheInspector.html │ │ │ │ │ ├── DefaultBitmapMemoryCacheParamsSupplier.html │ │ │ │ │ ├── DefaultCacheKeyFactory.html │ │ │ │ │ ├── DefaultEncodedMemoryCacheParamsSupplier.html │ │ │ │ │ ├── EncodedCountingMemoryCacheFactory.html │ │ │ │ │ ├── EncodedMemoryCacheFactory.html │ │ │ │ │ ├── ImageCacheStatsTracker.html │ │ │ │ │ ├── InstrumentedMemoryCache.html │ │ │ │ │ ├── InstrumentedMemoryCacheBitmapMemoryCacheFactory.html │ │ │ │ │ ├── LruCountingMemoryCache.html │ │ │ │ │ ├── MemoryCache.CacheTrimStrategy.html │ │ │ │ │ ├── MemoryCache.html │ │ │ │ │ ├── MemoryCacheParams.html │ │ │ │ │ ├── MemoryCacheTracker.html │ │ │ │ │ ├── NativeMemoryCacheTrimStrategy.html │ │ │ │ │ ├── NoOpImageCacheStatsTracker.html │ │ │ │ │ ├── StagingArea.html │ │ │ │ │ ├── ValueDescriptor.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── common/ │ │ │ │ │ ├── BytesRange.html │ │ │ │ │ ├── ImageDecodeOptions.html │ │ │ │ │ ├── ImageDecodeOptionsBuilder.html │ │ │ │ │ ├── Priority.html │ │ │ │ │ ├── ResizeOptions.html │ │ │ │ │ ├── RotationOptions.RotationAngle.html │ │ │ │ │ ├── RotationOptions.html │ │ │ │ │ ├── SourceUriType.html │ │ │ │ │ ├── TooManyBitmapsException.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── core/ │ │ │ │ │ ├── CloseableReferenceFactory.html │ │ │ │ │ ├── DefaultExecutorSupplier.html │ │ │ │ │ ├── DiskStorageCacheFactory.html │ │ │ │ │ ├── DiskStorageFactory.html │ │ │ │ │ ├── DynamicDefaultDiskStorageFactory.html │ │ │ │ │ ├── ExecutorSupplier.html │ │ │ │ │ ├── FileCacheFactory.html │ │ │ │ │ ├── ImagePipeline.html │ │ │ │ │ ├── ImagePipelineConfig.Builder.html │ │ │ │ │ ├── ImagePipelineConfig.DefaultImageRequestConfig.html │ │ │ │ │ ├── ImagePipelineConfig.html │ │ │ │ │ ├── ImagePipelineExperiments.Builder.html │ │ │ │ │ ├── ImagePipelineExperiments.DefaultProducerFactoryMethod.html │ │ │ │ │ ├── ImagePipelineExperiments.ProducerFactoryMethod.html │ │ │ │ │ ├── ImagePipelineExperiments.html │ │ │ │ │ ├── ImagePipelineFactory.html │ │ │ │ │ ├── ImageTranscoderType.html │ │ │ │ │ ├── MemoryChunkType.html │ │ │ │ │ ├── NativeCodeSetup.html │ │ │ │ │ ├── PriorityThreadFactory.html │ │ │ │ │ ├── ProducerFactory.html │ │ │ │ │ ├── ProducerSequenceFactory.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── datasource/ │ │ │ │ │ ├── AbstractProducerToDataSourceAdapter.html │ │ │ │ │ ├── BaseBitmapDataSubscriber.html │ │ │ │ │ ├── BaseBitmapReferenceDataSubscriber.html │ │ │ │ │ ├── BaseListBitmapDataSubscriber.html │ │ │ │ │ ├── CloseableProducerToDataSourceAdapter.html │ │ │ │ │ ├── ListDataSource.html │ │ │ │ │ ├── ProducerToDataSourceAdapter.html │ │ │ │ │ ├── SettableDataSource.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── debug/ │ │ │ │ │ ├── CloseableReferenceLeakTracker.Listener.html │ │ │ │ │ ├── CloseableReferenceLeakTracker.html │ │ │ │ │ ├── DebugImageTracker.html │ │ │ │ │ ├── FlipperCacheKeyFactory.html │ │ │ │ │ ├── FlipperCloseableReferenceLeakTracker.html │ │ │ │ │ ├── FlipperImageTracker.ImageDebugData.html │ │ │ │ │ ├── FlipperImageTracker.html │ │ │ │ │ ├── LruMap.html │ │ │ │ │ ├── NoOpCloseableReferenceLeakTracker.html │ │ │ │ │ ├── NoOpDebugImageTracker.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── decoder/ │ │ │ │ │ ├── DecodeException.html │ │ │ │ │ ├── DefaultImageDecoder.html │ │ │ │ │ ├── ImageDecoder.html │ │ │ │ │ ├── ImageDecoderConfig.Builder.html │ │ │ │ │ ├── ImageDecoderConfig.html │ │ │ │ │ ├── ProgressiveJpegConfig.html │ │ │ │ │ ├── ProgressiveJpegParser.html │ │ │ │ │ ├── SimpleProgressiveJpegConfig.DynamicValueConfig.html │ │ │ │ │ ├── SimpleProgressiveJpegConfig.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── drawable/ │ │ │ │ │ ├── DrawableFactory.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── filter/ │ │ │ │ │ ├── InPlaceRoundFilter.html │ │ │ │ │ ├── IterativeBoxBlurFilter.html │ │ │ │ │ ├── RenderScriptBlurFilter.html │ │ │ │ │ ├── XferRoundFilter.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── image/ │ │ │ │ │ ├── CloseableAnimatedImage.html │ │ │ │ │ ├── CloseableBitmap.html │ │ │ │ │ ├── CloseableImage.html │ │ │ │ │ ├── CloseableStaticBitmap.html │ │ │ │ │ ├── EncodedImage.html │ │ │ │ │ ├── EncodedImageOrigin.html │ │ │ │ │ ├── HasImageMetadata.html │ │ │ │ │ ├── ImageInfo.html │ │ │ │ │ ├── ImmutableQualityInfo.html │ │ │ │ │ ├── OriginalEncodedImageInfo.html │ │ │ │ │ ├── QualityInfo.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── instrumentation/ │ │ │ │ │ ├── FrescoInstrumenter.Instrumenter.html │ │ │ │ │ ├── FrescoInstrumenter.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── listener/ │ │ │ │ │ ├── BaseRequestListener.html │ │ │ │ │ ├── BaseRequestListener2.html │ │ │ │ │ ├── ForwardingRequestListener.html │ │ │ │ │ ├── ForwardingRequestListener2.html │ │ │ │ │ ├── RequestListener.html │ │ │ │ │ ├── RequestListener2.html │ │ │ │ │ ├── RequestLoggingListener.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── memory/ │ │ │ │ │ ├── AshmemMemoryChunk.html │ │ │ │ │ ├── AshmemMemoryChunkPool.html │ │ │ │ │ ├── BasePool.InvalidSizeException.html │ │ │ │ │ ├── BasePool.InvalidValueException.html │ │ │ │ │ ├── BasePool.PoolSizeViolationException.html │ │ │ │ │ ├── BasePool.SizeTooLargeException.html │ │ │ │ │ ├── BasePool.html │ │ │ │ │ ├── BitmapCounter.html │ │ │ │ │ ├── BitmapCounterConfig.Builder.html │ │ │ │ │ ├── BitmapCounterConfig.html │ │ │ │ │ ├── BitmapCounterProvider.html │ │ │ │ │ ├── BitmapPool.html │ │ │ │ │ ├── BitmapPoolBackend.html │ │ │ │ │ ├── BitmapPoolType.html │ │ │ │ │ ├── BucketMap.html │ │ │ │ │ ├── BucketsBitmapPool.html │ │ │ │ │ ├── BufferMemoryChunk.html │ │ │ │ │ ├── BufferMemoryChunkPool.html │ │ │ │ │ ├── DefaultBitmapPoolParams.html │ │ │ │ │ ├── DefaultByteArrayPoolParams.html │ │ │ │ │ ├── DefaultFlexByteArrayPoolParams.html │ │ │ │ │ ├── DefaultNativeMemoryChunkPoolParams.html │ │ │ │ │ ├── DummyBitmapPool.html │ │ │ │ │ ├── DummyTrackingInUseBitmapPool.html │ │ │ │ │ ├── FlexByteArrayPool.html │ │ │ │ │ ├── GenericByteArrayPool.html │ │ │ │ │ ├── LruBitmapPool.html │ │ │ │ │ ├── LruBucketsPoolBackend.html │ │ │ │ │ ├── MemoryChunk.html │ │ │ │ │ ├── MemoryChunkPool.html │ │ │ │ │ ├── MemoryChunkUtil.html │ │ │ │ │ ├── MemoryPooledByteBuffer.html │ │ │ │ │ ├── MemoryPooledByteBufferFactory.html │ │ │ │ │ ├── MemoryPooledByteBufferOutputStream.InvalidStreamException.html │ │ │ │ │ ├── MemoryPooledByteBufferOutputStream.html │ │ │ │ │ ├── NativeMemoryChunk.html │ │ │ │ │ ├── NativeMemoryChunkPool.html │ │ │ │ │ ├── NoOpPoolStatsTracker.html │ │ │ │ │ ├── PoolConfig.Builder.html │ │ │ │ │ ├── PoolConfig.html │ │ │ │ │ ├── PoolFactory.html │ │ │ │ │ ├── PoolParams.html │ │ │ │ │ ├── PoolStatsTracker.html │ │ │ │ │ ├── SharedByteArray.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── multiuri/ │ │ │ │ │ ├── MultiUri.Builder.html │ │ │ │ │ ├── MultiUri.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── nativecode/ │ │ │ │ │ ├── Bitmaps.html │ │ │ │ │ ├── DalvikPurgeableDecoder.html │ │ │ │ │ ├── ImagePipelineNativeLoader.html │ │ │ │ │ ├── NativeBlurFilter.html │ │ │ │ │ ├── NativeCodeInitializer.html │ │ │ │ │ ├── NativeFiltersLoader.html │ │ │ │ │ ├── NativeImageTranscoderFactory.html │ │ │ │ │ ├── NativeJpegTranscoder.html │ │ │ │ │ ├── NativeJpegTranscoderFactory.html │ │ │ │ │ ├── NativeJpegTranscoderSoLoader.html │ │ │ │ │ ├── NativeRoundingFilter.html │ │ │ │ │ ├── StaticWebpNativeLoader.html │ │ │ │ │ ├── WebpTranscoder.html │ │ │ │ │ ├── WebpTranscoderFactory.html │ │ │ │ │ ├── WebpTranscoderImpl.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── platform/ │ │ │ │ │ ├── ArtDecoder.html │ │ │ │ │ ├── DefaultDecoder.html │ │ │ │ │ ├── GingerbreadPurgeableDecoder.html │ │ │ │ │ ├── KitKatPurgeableDecoder.html │ │ │ │ │ ├── OreoDecoder.html │ │ │ │ │ ├── PlatformDecoder.html │ │ │ │ │ ├── PlatformDecoderFactory.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── postprocessors/ │ │ │ │ │ ├── BlurPostProcessor.html │ │ │ │ │ ├── IterativeBoxBlurPostProcessor.html │ │ │ │ │ ├── RoundAsCirclePostprocessor.html │ │ │ │ │ ├── RoundPostprocessor.html │ │ │ │ │ ├── RoundedCornersPostprocessor.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── producers/ │ │ │ │ │ ├── AddImageTransformMetaDataProducer.html │ │ │ │ │ ├── BaseConsumer.html │ │ │ │ │ ├── BaseNetworkFetcher.html │ │ │ │ │ ├── BaseProducerContext.html │ │ │ │ │ ├── BaseProducerContextCallbacks.html │ │ │ │ │ ├── BitmapMemoryCacheGetProducer.html │ │ │ │ │ ├── BitmapMemoryCacheKeyMultiplexProducer.html │ │ │ │ │ ├── BitmapMemoryCacheProducer.html │ │ │ │ │ ├── BitmapPrepareProducer.html │ │ │ │ │ ├── BitmapProbeProducer.html │ │ │ │ │ ├── BranchOnSeparateImagesProducer.html │ │ │ │ │ ├── Consumer.Status.html │ │ │ │ │ ├── Consumer.html │ │ │ │ │ ├── DataFetchProducer.html │ │ │ │ │ ├── DecodeProducer.html │ │ │ │ │ ├── DelayProducer.html │ │ │ │ │ ├── DelegatingConsumer.html │ │ │ │ │ ├── DiskCacheReadProducer.html │ │ │ │ │ ├── DiskCacheWriteProducer.html │ │ │ │ │ ├── EncodedCacheKeyMultiplexProducer.html │ │ │ │ │ ├── EncodedMemoryCacheProducer.html │ │ │ │ │ ├── EncodedProbeProducer.html │ │ │ │ │ ├── ExperimentalThreadHandoffProducerQueueImpl.html │ │ │ │ │ ├── FetchState.html │ │ │ │ │ ├── HttpUrlConnectionNetworkFetcher.HttpUrlConnectionNetworkFetchState.html │ │ │ │ │ ├── HttpUrlConnectionNetworkFetcher.html │ │ │ │ │ ├── InternalProducerListener.html │ │ │ │ │ ├── InternalRequestListener.html │ │ │ │ │ ├── JobScheduler.JobRunnable.html │ │ │ │ │ ├── JobScheduler.html │ │ │ │ │ ├── LocalAssetFetchProducer.html │ │ │ │ │ ├── LocalContentUriFetchProducer.html │ │ │ │ │ ├── LocalContentUriThumbnailFetchProducer.html │ │ │ │ │ ├── LocalExifThumbnailProducer.html │ │ │ │ │ ├── LocalFetchProducer.html │ │ │ │ │ ├── LocalFileFetchProducer.html │ │ │ │ │ ├── LocalResourceFetchProducer.html │ │ │ │ │ ├── LocalVideoThumbnailProducer.html │ │ │ │ │ ├── MultiplexProducer.html │ │ │ │ │ ├── NetworkFetchProducer.html │ │ │ │ │ ├── NetworkFetcher.Callback.html │ │ │ │ │ ├── NetworkFetcher.html │ │ │ │ │ ├── NullProducer.html │ │ │ │ │ ├── PartialDiskCacheProducer.html │ │ │ │ │ ├── PostprocessedBitmapMemoryCacheProducer.CachedPostprocessorConsumer.html │ │ │ │ │ ├── PostprocessedBitmapMemoryCacheProducer.html │ │ │ │ │ ├── PostprocessorProducer.html │ │ │ │ │ ├── PriorityNetworkFetcher.NonrecoverableException.html │ │ │ │ │ ├── PriorityNetworkFetcher.PriorityFetchState.html │ │ │ │ │ ├── PriorityNetworkFetcher.html │ │ │ │ │ ├── PriorityStarvingThrottlingProducer.html │ │ │ │ │ ├── Producer.html │ │ │ │ │ ├── ProducerContext.ExtraKeys.html │ │ │ │ │ ├── ProducerContext.html │ │ │ │ │ ├── ProducerContextCallbacks.html │ │ │ │ │ ├── ProducerListener.html │ │ │ │ │ ├── ProducerListener2.html │ │ │ │ │ ├── QualifiedResourceFetchProducer.html │ │ │ │ │ ├── RemoveImageTransformMetaDataProducer.html │ │ │ │ │ ├── ResizeAndRotateProducer.html │ │ │ │ │ ├── SettableProducerContext.html │ │ │ │ │ ├── StatefulProducerRunnable.html │ │ │ │ │ ├── SwallowResultProducer.html │ │ │ │ │ ├── ThreadHandoffProducer.html │ │ │ │ │ ├── ThreadHandoffProducerQueue.html │ │ │ │ │ ├── ThreadHandoffProducerQueueImpl.html │ │ │ │ │ ├── ThrottlingProducer.html │ │ │ │ │ ├── ThumbnailBranchProducer.html │ │ │ │ │ ├── ThumbnailProducer.html │ │ │ │ │ ├── ThumbnailSizeChecker.html │ │ │ │ │ ├── WebpTranscodeProducer.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── request/ │ │ │ │ │ ├── BasePostprocessor.html │ │ │ │ │ ├── BaseRepeatedPostProcessor.html │ │ │ │ │ ├── HasImageRequest.html │ │ │ │ │ ├── ImageRequest.CacheChoice.html │ │ │ │ │ ├── ImageRequest.RequestLevel.html │ │ │ │ │ ├── ImageRequest.html │ │ │ │ │ ├── ImageRequestBuilder.BuilderException.html │ │ │ │ │ ├── ImageRequestBuilder.html │ │ │ │ │ ├── Postprocessor.html │ │ │ │ │ ├── RepeatedPostprocessor.html │ │ │ │ │ ├── RepeatedPostprocessorRunner.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── systrace/ │ │ │ │ │ ├── DefaultFrescoSystrace.html │ │ │ │ │ ├── FrescoSystrace.ArgsBuilder.html │ │ │ │ │ ├── FrescoSystrace.Systrace.html │ │ │ │ │ ├── FrescoSystrace.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ ├── transcoder/ │ │ │ │ │ ├── DownsampleUtil.html │ │ │ │ │ ├── ImageTranscodeResult.html │ │ │ │ │ ├── ImageTranscoder.html │ │ │ │ │ ├── ImageTranscoderFactory.html │ │ │ │ │ ├── JpegTranscoderUtils.html │ │ │ │ │ ├── MultiImageTranscoderFactory.html │ │ │ │ │ ├── SimpleImageTranscoder.html │ │ │ │ │ ├── SimpleImageTranscoderFactory.html │ │ │ │ │ ├── TranscodeStatus.html │ │ │ │ │ ├── package-descr.html │ │ │ │ │ └── package-summary.html │ │ │ │ └── transformation/ │ │ │ │ ├── BitmapTransformation.html │ │ │ │ ├── package-descr.html │ │ │ │ └── package-summary.html │ │ │ ├── imageutils/ │ │ │ │ ├── BitmapUtil.html │ │ │ │ ├── HeifExifUtil.html │ │ │ │ ├── ImageMetaData.html │ │ │ │ ├── JfifUtil.html │ │ │ │ ├── WebpUtil.html │ │ │ │ ├── package-descr.html │ │ │ │ └── package-summary.html │ │ │ ├── webpsupport/ │ │ │ │ ├── WebpBitmapFactoryImpl.html │ │ │ │ ├── package-descr.html │ │ │ │ └── package-summary.html │ │ │ └── widget/ │ │ │ └── text/ │ │ │ └── span/ │ │ │ ├── BetterImageSpan.BetterImageSpanAlignment.html │ │ │ ├── BetterImageSpan.html │ │ │ ├── package-descr.html │ │ │ └── package-summary.html │ │ ├── current.xml │ │ ├── hierarchy.html │ │ ├── index.html │ │ ├── lists.js │ │ ├── package-list │ │ └── packages.html │ ├── sample-license.md │ ├── static/ │ │ ├── JSXTransformer.js │ │ ├── js/ │ │ │ └── docsearch.js │ │ ├── linkify.js │ │ ├── pygments.css │ │ ├── sample-images/ │ │ │ └── animation.keyframes │ │ ├── site.css │ │ └── transformer.js │ └── support.md ├── drawee/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── facebook/ │ │ │ └── drawee/ │ │ │ ├── components/ │ │ │ │ ├── DeferredReleaser.java │ │ │ │ ├── DeferredReleaserConcurrentImpl.java │ │ │ │ ├── DraweeEventTracker.java │ │ │ │ └── RetryManager.java │ │ │ ├── controller/ │ │ │ │ ├── AbstractDraweeController.java │ │ │ │ ├── AbstractDraweeControllerBuilder.java │ │ │ │ ├── BaseControllerListener.java │ │ │ │ ├── ControllerListener.java │ │ │ │ ├── ControllerViewportVisibilityListener.java │ │ │ │ ├── ForwardingControllerListener.java │ │ │ │ └── package-info.java │ │ │ ├── debug/ │ │ │ │ ├── DebugControllerOverlayDrawable.java │ │ │ │ └── listener/ │ │ │ │ ├── ImageLoadingTimeControllerListener.kt │ │ │ │ └── ImageLoadingTimeListener.kt │ │ │ ├── drawable/ │ │ │ │ ├── ArrayDrawable.java │ │ │ │ ├── AutoRotateDrawable.java │ │ │ │ ├── CloneableDrawable.kt │ │ │ │ ├── DrawableParent.kt │ │ │ │ ├── DrawableProperties.kt │ │ │ │ ├── DrawableUtils.kt │ │ │ │ ├── FadeDrawable.java │ │ │ │ ├── ForwardingDrawable.java │ │ │ │ ├── InstrumentedDrawable.kt │ │ │ │ ├── MatrixDrawable.java │ │ │ │ ├── OrientedDrawable.kt │ │ │ │ ├── ProgressBarDrawable.kt │ │ │ │ ├── Rounded.kt │ │ │ │ ├── RoundedBitmapDrawable.java │ │ │ │ ├── RoundedColorDrawable.java │ │ │ │ ├── RoundedCornersDrawable.java │ │ │ │ ├── RoundedDrawable.java │ │ │ │ ├── RoundedNinePatchDrawable.kt │ │ │ │ ├── ScaleTypeDrawable.kt │ │ │ │ ├── TransformAwareDrawable.kt │ │ │ │ ├── TransformCallback.kt │ │ │ │ ├── VisibilityAwareDrawable.kt │ │ │ │ ├── VisibilityCallback.kt │ │ │ │ └── package-info.kt │ │ │ ├── generic/ │ │ │ │ ├── GenericDraweeHierarchy.java │ │ │ │ ├── GenericDraweeHierarchyBuilder.java │ │ │ │ ├── GenericDraweeHierarchyInflater.java │ │ │ │ ├── RootDrawable.java │ │ │ │ ├── RoundingParams.java │ │ │ │ ├── WrappingUtils.java │ │ │ │ └── package-info.java │ │ │ ├── gestures/ │ │ │ │ └── GestureDetector.java │ │ │ ├── interfaces/ │ │ │ │ ├── DraweeController.java │ │ │ │ ├── DraweeHierarchy.java │ │ │ │ ├── SettableDraweeHierarchy.java │ │ │ │ ├── SimpleDraweeControllerBuilder.java │ │ │ │ └── package-info.java │ │ │ └── view/ │ │ │ ├── AspectRatioMeasure.java │ │ │ ├── DraweeHolder.java │ │ │ ├── DraweeTransition.java │ │ │ ├── DraweeView.java │ │ │ ├── GenericDraweeView.java │ │ │ ├── MultiDraweeHolder.java │ │ │ ├── SimpleDraweeView.java │ │ │ └── package-info.java │ │ └── res/ │ │ └── values/ │ │ └── attrs.xml │ └── test/ │ ├── java/ │ │ ├── com/ │ │ │ └── facebook/ │ │ │ └── drawee/ │ │ │ ├── components/ │ │ │ │ └── DeferredReleaserStressTest.java │ │ │ ├── controller/ │ │ │ │ └── AbstractDraweeControllerTest.java │ │ │ ├── debug/ │ │ │ │ ├── DebugControllerOverlayDrawableInternalTest.java │ │ │ │ ├── DebugControllerOverlayDrawableTest.java │ │ │ │ └── DebugControllerOverlayDrawableTestHelper.java │ │ │ ├── drawable/ │ │ │ │ ├── AndroidGraphicsTestUtils.java │ │ │ │ ├── ArrayDrawableTest.java │ │ │ │ ├── DrawableTestUtils.java │ │ │ │ ├── DrawableUtilsTest.java │ │ │ │ ├── FadeDrawableAllOnTest.java │ │ │ │ ├── FadeDrawableOnFadeListenerTest.java │ │ │ │ ├── FadeDrawableTest.java │ │ │ │ ├── ForwardingDrawableTest.java │ │ │ │ ├── MatrixDrawableTest.java │ │ │ │ ├── OrientedDrawableTest.java │ │ │ │ ├── RoundedBitmapDrawableTest.java │ │ │ │ ├── RoundedColorDrawableTest.java │ │ │ │ ├── RoundedCornersDrawableTest.java │ │ │ │ ├── ScaleTypeDrawableTest.java │ │ │ │ ├── ScalingUtilsTest.java │ │ │ │ └── SettableDrawableTest.java │ │ │ ├── generic/ │ │ │ │ ├── GenericDraweeHierarchyBuilderTest.java │ │ │ │ └── RoundingParamsTest.java │ │ │ ├── testing/ │ │ │ │ ├── DraweeMocks.java │ │ │ │ └── DraweeMocksTest.java │ │ │ └── view/ │ │ │ ├── AspectRatioMeasureTest.java │ │ │ ├── DraweeHolderTest.java │ │ │ ├── DraweeViewTest.java │ │ │ └── MultiDraweeHolderTest.java │ │ └── javax/ │ │ └── microedition/ │ │ └── khronos/ │ │ └── opengles/ │ │ └── GL.java │ └── resources/ │ └── org.robolectric.Config.properties ├── drawee-backends/ │ └── drawee-pipeline/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── drawee/ │ │ └── backends/ │ │ └── pipeline/ │ │ ├── DefaultDrawableFactory.java │ │ ├── DraweeConfig.java │ │ ├── Fresco.java │ │ ├── PipelineDraweeController.java │ │ ├── PipelineDraweeControllerBuilder.java │ │ ├── PipelineDraweeControllerBuilderSupplier.java │ │ ├── PipelineDraweeControllerFactory.java │ │ ├── info/ │ │ │ ├── ForwardingImageOriginListener.java │ │ │ ├── ForwardingImagePerfDataListener.java │ │ │ ├── ImageOrigin.kt │ │ │ ├── ImageOriginListener.java │ │ │ ├── ImageOriginRequestListener.java │ │ │ ├── ImageOriginUtils.java │ │ │ ├── ImagePerfExtra.kt │ │ │ ├── ImagePerfMonitor.java │ │ │ └── internal/ │ │ │ ├── ImagePerfRequestListener.java │ │ │ └── ImagePerfStateManager.java │ │ └── package-info.java │ └── test/ │ └── java/ │ └── com/ │ └── facebook/ │ └── drawee/ │ └── backends/ │ └── pipeline/ │ └── info/ │ └── ImagePerfMonitorTest.java ├── drawee-span/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── drawee/ │ │ └── span/ │ │ ├── DraweeSpan.java │ │ ├── DraweeSpanStringBuilder.java │ │ └── SimpleDraweeSpanTextView.java │ └── test/ │ └── java/ │ └── com/ │ └── facebook/ │ └── drawee/ │ └── span/ │ ├── DraweeSpanStringBuilderTest.java │ └── DraweeSpanTest.java ├── fbcore/ │ ├── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── proguard-fresco.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ ├── com/ │ │ │ └── facebook/ │ │ │ ├── common/ │ │ │ │ ├── activitylistener/ │ │ │ │ │ ├── ActivityListener.java │ │ │ │ │ ├── ActivityListenerManager.java │ │ │ │ │ ├── BaseActivityListener.java │ │ │ │ │ └── ListenableActivity.java │ │ │ │ ├── callercontext/ │ │ │ │ │ ├── ContextChain.java │ │ │ │ │ └── ImageAttribution.java │ │ │ │ ├── closeables/ │ │ │ │ │ └── AutoCleanupDelegate.kt │ │ │ │ ├── disk/ │ │ │ │ │ ├── DiskTrimmable.kt │ │ │ │ │ ├── DiskTrimmableRegistry.kt │ │ │ │ │ ├── NoOpDiskTrimmableRegistry.kt │ │ │ │ │ └── package-info.kt │ │ │ │ ├── executors/ │ │ │ │ │ ├── CallerThreadExecutor.java │ │ │ │ │ ├── ConstrainedExecutorService.java │ │ │ │ │ ├── DefaultSerialExecutorService.java │ │ │ │ │ ├── HandlerExecutorService.java │ │ │ │ │ ├── HandlerExecutorServiceImpl.java │ │ │ │ │ ├── ScheduledFutureImpl.java │ │ │ │ │ ├── SerialExecutorService.java │ │ │ │ │ ├── StatefulRunnable.java │ │ │ │ │ └── UiThreadImmediateExecutorService.java │ │ │ │ ├── file/ │ │ │ │ │ ├── FileTree.java │ │ │ │ │ ├── FileTreeVisitor.java │ │ │ │ │ └── FileUtils.java │ │ │ │ ├── internal/ │ │ │ │ │ ├── AndroidPredicates.java │ │ │ │ │ ├── ByteStreams.java │ │ │ │ │ ├── Closeables.java │ │ │ │ │ ├── CountingOutputStream.java │ │ │ │ │ ├── DoNotStrip.java │ │ │ │ │ ├── Files.java │ │ │ │ │ ├── Fn.java │ │ │ │ │ ├── ImmutableList.java │ │ │ │ │ ├── ImmutableMap.java │ │ │ │ │ ├── ImmutableSet.java │ │ │ │ │ ├── Objects.java │ │ │ │ │ ├── Preconditions.java │ │ │ │ │ ├── Predicate.java │ │ │ │ │ ├── Sets.java │ │ │ │ │ ├── Supplier.java │ │ │ │ │ ├── Suppliers.java │ │ │ │ │ ├── Throwables.java │ │ │ │ │ ├── package-info.java │ │ │ │ │ └── proguard_annotations.pro │ │ │ │ ├── lifecycle/ │ │ │ │ │ └── AttachDetachListener.kt │ │ │ │ ├── logging/ │ │ │ │ │ ├── FLog.kt │ │ │ │ │ ├── FLogDefaultLoggingDelegate.java │ │ │ │ │ ├── LoggingDelegate.java │ │ │ │ │ ├── logging.pro │ │ │ │ │ └── package-info.java │ │ │ │ ├── media/ │ │ │ │ │ ├── MediaUtils.kt │ │ │ │ │ └── MimeTypeMapWrapper.kt │ │ │ │ ├── memory/ │ │ │ │ │ ├── ByteArrayPool.java │ │ │ │ │ ├── DecodeBufferHelper.java │ │ │ │ │ ├── MemoryTrimType.java │ │ │ │ │ ├── MemoryTrimmable.java │ │ │ │ │ ├── MemoryTrimmableRegistry.java │ │ │ │ │ ├── NoOpMemoryTrimmableRegistry.java │ │ │ │ │ ├── Pool.java │ │ │ │ │ ├── PooledByteArrayBufferedInputStream.java │ │ │ │ │ ├── PooledByteBuffer.kt │ │ │ │ │ ├── PooledByteBufferFactory.java │ │ │ │ │ ├── PooledByteBufferInputStream.java │ │ │ │ │ ├── PooledByteBufferOutputStream.java │ │ │ │ │ ├── PooledByteStreams.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── references/ │ │ │ │ │ ├── CloseableReference.java │ │ │ │ │ ├── DefaultCloseableReference.java │ │ │ │ │ ├── FinalizerCloseableReference.java │ │ │ │ │ ├── HasBitmap.java │ │ │ │ │ ├── NoOpCloseableReference.java │ │ │ │ │ ├── OOMSoftReference.java │ │ │ │ │ ├── RefCountCloseableReference.java │ │ │ │ │ ├── ResourceReleaser.java │ │ │ │ │ ├── SharedReference.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── statfs/ │ │ │ │ │ └── StatFsHelper.java │ │ │ │ ├── streams/ │ │ │ │ │ ├── LimitedInputStream.kt │ │ │ │ │ └── TailAppendingInputStream.kt │ │ │ │ ├── time/ │ │ │ │ │ ├── AwakeTimeSinceBootClock.java │ │ │ │ │ ├── Clock.java │ │ │ │ │ ├── CurrentThreadTimeClock.java │ │ │ │ │ ├── MonotonicClock.java │ │ │ │ │ ├── MonotonicNanoClock.java │ │ │ │ │ ├── RealtimeSinceBootClock.java │ │ │ │ │ └── SystemClock.java │ │ │ │ ├── util/ │ │ │ │ │ ├── ByteConstants.java │ │ │ │ │ ├── ExceptionWithNoStacktrace.java │ │ │ │ │ ├── HashCodeUtil.kt │ │ │ │ │ ├── Hex.kt │ │ │ │ │ ├── SecureHashUtil.kt │ │ │ │ │ ├── StreamUtil.java │ │ │ │ │ ├── TriState.java │ │ │ │ │ └── UriUtil.java │ │ │ │ └── webp/ │ │ │ │ ├── BitmapCreator.java │ │ │ │ ├── WebpBitmapFactory.java │ │ │ │ └── WebpSupportStatus.java │ │ │ ├── datasource/ │ │ │ │ ├── AbstractDataSource.java │ │ │ │ ├── BaseBooleanSubscriber.java │ │ │ │ ├── BaseDataSubscriber.java │ │ │ │ ├── DataSource.java │ │ │ │ ├── DataSources.java │ │ │ │ ├── DataSubscriber.java │ │ │ │ ├── FirstAvailableDataSourceSupplier.java │ │ │ │ ├── IncreasingQualityDataSourceSupplier.java │ │ │ │ ├── RetainingDataSourceSupplier.java │ │ │ │ ├── SimpleDataSource.kt │ │ │ │ ├── SuccessfulVoidDataSource.kt │ │ │ │ └── package-info.java │ │ │ ├── memory/ │ │ │ │ └── helper/ │ │ │ │ └── HashCode.kt │ │ │ └── widget/ │ │ │ └── text/ │ │ │ └── span/ │ │ │ └── BetterImageSpan.java │ │ └── pom.xml │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── facebook/ │ │ ├── common/ │ │ │ ├── callercontext/ │ │ │ │ └── ContextChainTest.kt │ │ │ ├── executors/ │ │ │ │ ├── HandlerExecutorServiceImplTest.java │ │ │ │ └── StatefulRunnableTest.java │ │ │ ├── file/ │ │ │ │ └── FileUtilsTest.java │ │ │ ├── media/ │ │ │ │ └── MediaUtilsTest.java │ │ │ ├── references/ │ │ │ │ ├── CloseableReferenceTest.java │ │ │ │ └── SharedReferenceTest.java │ │ │ ├── streams/ │ │ │ │ ├── LimitedInputStreamTest.java │ │ │ │ └── TailAppendingInputStreamTest.java │ │ │ └── util/ │ │ │ ├── HashCodeUtilTest.java │ │ │ ├── StreamUtilTest.java │ │ │ ├── TriStateTest.java │ │ │ └── UriUtilTest.java │ │ ├── datasource/ │ │ │ ├── AbstractDataSourceTest.java │ │ │ ├── DataSourceTestUtils.java │ │ │ ├── DataSourcesTest.java │ │ │ ├── FirstAvailableDataSourceSupplierTest.java │ │ │ └── IncreasingQualityDataSourceSupplierTest.java │ │ └── widget/ │ │ └── text/ │ │ └── span/ │ │ ├── BetterImageSpanMarginTest.java │ │ └── BetterImageSpanTest.java │ └── resources/ │ └── robolectric.properties ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── imagepipeline/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── imagepipeline/ │ │ ├── bitmaps/ │ │ │ ├── Api31BitmapFactory.kt │ │ │ ├── ArtBitmapFactory.kt │ │ │ ├── EmptyJpegGenerator.kt │ │ │ ├── HoneycombBitmapCreator.kt │ │ │ └── PlatformBitmapFactoryProvider.kt │ │ ├── cache/ │ │ │ ├── BitmapMemoryCacheKey.kt │ │ │ ├── BufferedDiskCache.kt │ │ │ ├── CacheKeyFactory.kt │ │ │ ├── CacheMissException.kt │ │ │ ├── DefaultBitmapMemoryCacheParamsSupplier.java │ │ │ ├── DefaultCacheKeyFactory.java │ │ │ ├── DefaultEncodedMemoryCacheParamsSupplier.java │ │ │ ├── EncodedCountingMemoryCacheFactory.java │ │ │ ├── EncodedMemoryCacheFactory.java │ │ │ ├── ImageCacheStatsTracker.kt │ │ │ ├── InstrumentedMemoryCache.java │ │ │ ├── InstrumentedMemoryCacheBitmapMemoryCacheFactory.java │ │ │ ├── MemoryCacheTracker.kt │ │ │ ├── NativeMemoryCacheTrimStrategy.java │ │ │ ├── NoOpImageCacheStatsTracker.java │ │ │ ├── StagingArea.java │ │ │ └── package-info.kt │ │ ├── core/ │ │ │ ├── CloseableReferenceFactory.java │ │ │ ├── DiskCachesStore.kt │ │ │ ├── DiskCachesStoreFactory.kt │ │ │ ├── DiskStorageCacheFactory.java │ │ │ ├── DiskStorageFactory.kt │ │ │ ├── DownsampleMode.kt │ │ │ ├── DynamicDefaultDiskStorageFactory.java │ │ │ ├── FileCacheFactory.kt │ │ │ ├── ImagePipeline.kt │ │ │ ├── ImagePipelineConfig.kt │ │ │ ├── ImagePipelineConfigInterface.kt │ │ │ ├── ImagePipelineExperiments.kt │ │ │ ├── ImagePipelineFactory.java │ │ │ ├── ImageTranscoderType.java │ │ │ ├── MemoryChunkType.java │ │ │ ├── NativeCodeSetup.java │ │ │ ├── ProducerFactory.kt │ │ │ ├── ProducerSequenceFactory.kt │ │ │ └── package-info.kt │ │ ├── datasource/ │ │ │ ├── AbstractProducerToDataSourceAdapter.kt │ │ │ ├── BaseBitmapDataSubscriber.kt │ │ │ ├── BaseBitmapReferenceDataSubscriber.kt │ │ │ ├── BaseListBitmapDataSubscriber.kt │ │ │ ├── CloseableProducerToDataSourceAdapter.java │ │ │ ├── ListDataSource.java │ │ │ ├── ProducerToDataSourceAdapter.kt │ │ │ ├── SettableDataSource.kt │ │ │ └── package-info.kt │ │ ├── debug/ │ │ │ ├── CloseableReferenceLeakTracker.kt │ │ │ └── NoOpCloseableReferenceLeakTracker.kt │ │ ├── decoder/ │ │ │ ├── DecodeException.kt │ │ │ ├── DefaultImageDecoder.java │ │ │ ├── ImageDecoderConfig.java │ │ │ ├── ProgressiveJpegConfig.java │ │ │ ├── ProgressiveJpegParser.java │ │ │ ├── SimpleProgressiveJpegConfig.java │ │ │ └── package-info.kt │ │ ├── filter/ │ │ │ ├── InPlaceRoundFilter.kt │ │ │ ├── IterativeBoxBlurFilter.kt │ │ │ ├── NTSCDampeningFilterUtil.kt │ │ │ ├── RenderScriptBlurFilter.kt │ │ │ └── XferRoundFilter.kt │ │ ├── listener/ │ │ │ ├── BaseRequestListener.kt │ │ │ ├── BaseRequestListener2.kt │ │ │ ├── ForwardingRequestListener.java │ │ │ ├── ForwardingRequestListener2.kt │ │ │ ├── RequestListener.java │ │ │ ├── RequestListener2.java │ │ │ ├── RequestLoggingListener.kt │ │ │ └── package-info.kt │ │ ├── memory/ │ │ │ ├── BasePool.kt │ │ │ ├── BitmapCounter.java │ │ │ ├── BitmapCounterConfig.kt │ │ │ ├── BitmapCounterProvider.kt │ │ │ ├── BitmapPool.kt │ │ │ ├── BitmapPoolBackend.kt │ │ │ ├── BitmapPoolType.kt │ │ │ ├── Bucket.java │ │ │ ├── BucketMap.java │ │ │ ├── BucketsBitmapPool.kt │ │ │ ├── DefaultBitmapPoolParams.kt │ │ │ ├── DefaultByteArrayPoolParams.kt │ │ │ ├── DefaultFlexByteArrayPoolParams.kt │ │ │ ├── DefaultNativeMemoryChunkPoolParams.kt │ │ │ ├── DummyBitmapPool.kt │ │ │ ├── DummyTrackingInUseBitmapPool.kt │ │ │ ├── FlexByteArrayPool.kt │ │ │ ├── GenericByteArrayPool.kt │ │ │ ├── LruBitmapPool.java │ │ │ ├── LruBucketsPoolBackend.java │ │ │ ├── MemoryChunk.kt │ │ │ ├── MemoryChunkPool.java │ │ │ ├── MemoryChunkUtil.kt │ │ │ ├── MemoryPooledByteBuffer.kt │ │ │ ├── MemoryPooledByteBufferFactory.kt │ │ │ ├── MemoryPooledByteBufferOutputStream.kt │ │ │ ├── NoOpPoolStatsTracker.java │ │ │ ├── OOMSoftReferenceBucket.java │ │ │ ├── PoolBackend.kt │ │ │ ├── PoolConfig.kt │ │ │ ├── PoolFactory.kt │ │ │ ├── PoolParams.java │ │ │ ├── PoolStatsTracker.kt │ │ │ ├── SharedByteArray.java │ │ │ └── package-info.kt │ │ ├── multiuri/ │ │ │ └── MultiUri.java │ │ ├── platform/ │ │ │ ├── ArtDecoder.kt │ │ │ ├── DefaultDecoder.java │ │ │ ├── OreoDecoder.kt │ │ │ ├── PlatformDecoder.kt │ │ │ ├── PlatformDecoderFactory.kt │ │ │ ├── PlatformDecoderOptions.kt │ │ │ └── PreverificationHelper.kt │ │ ├── postprocessors/ │ │ │ ├── BlurPostProcessor.kt │ │ │ ├── DownScaleBlurPostProcessor.kt │ │ │ ├── RoundPostprocessor.kt │ │ │ └── TintPostProcessor.kt │ │ ├── producers/ │ │ │ ├── AddImageTransformMetaDataProducer.java │ │ │ ├── BaseConsumer.kt │ │ │ ├── BaseNetworkFetcher.kt │ │ │ ├── BaseProducerContext.java │ │ │ ├── BaseProducerContextCallbacks.kt │ │ │ ├── BitmapMemoryCacheGetProducer.kt │ │ │ ├── BitmapMemoryCacheKeyMultiplexProducer.java │ │ │ ├── BitmapMemoryCacheProducer.java │ │ │ ├── BitmapPrepareProducer.java │ │ │ ├── BitmapProbeProducer.java │ │ │ ├── BranchOnSeparateImagesProducer.java │ │ │ ├── Consumer.kt │ │ │ ├── CustomProducerSequenceFactory.java │ │ │ ├── DataFetchProducer.java │ │ │ ├── DecodeProducer.kt │ │ │ ├── DelayProducer.kt │ │ │ ├── DelegatingConsumer.kt │ │ │ ├── DiskCacheDecision.kt │ │ │ ├── DiskCacheReadProducer.java │ │ │ ├── DiskCacheWriteProducer.kt │ │ │ ├── EncodedCacheKeyMultiplexProducer.java │ │ │ ├── EncodedMemoryCacheProducer.java │ │ │ ├── EncodedProbeProducer.java │ │ │ ├── ExperimentalThreadHandoffProducerQueueImpl.kt │ │ │ ├── FetchState.kt │ │ │ ├── HttpUrlConnectionNetworkFetcher.java │ │ │ ├── InternalProducerListener.kt │ │ │ ├── InternalRequestListener.kt │ │ │ ├── JobScheduler.java │ │ │ ├── LocalAssetFetchProducer.kt │ │ │ ├── LocalContentUriFetchProducer.kt │ │ │ ├── LocalContentUriThumbnailFetchProducer.java │ │ │ ├── LocalExifThumbnailProducer.java │ │ │ ├── LocalFetchProducer.java │ │ │ ├── LocalFileFetchProducer.kt │ │ │ ├── LocalResourceFetchProducer.kt │ │ │ ├── LocalThumbnailBitmapSdk29Producer.java │ │ │ ├── LocalVideoThumbnailProducer.java │ │ │ ├── MultiplexProducer.java │ │ │ ├── NetworkFetchProducer.java │ │ │ ├── NetworkFetcher.java │ │ │ ├── PartialDiskCacheProducer.java │ │ │ ├── PostprocessedBitmapMemoryCacheProducer.java │ │ │ ├── PostprocessorProducer.kt │ │ │ ├── PriorityStarvingThrottlingProducer.java │ │ │ ├── Producer.kt │ │ │ ├── ProducerConstants.kt │ │ │ ├── ProducerContext.kt │ │ │ ├── ProducerContextCallbacks.kt │ │ │ ├── ProducerListener.java │ │ │ ├── ProducerListener2.java │ │ │ ├── QualifiedResourceFetchProducer.kt │ │ │ ├── RemoveImageTransformMetaDataProducer.kt │ │ │ ├── ResizeAndRotateProducer.java │ │ │ ├── SettableProducerContext.java │ │ │ ├── StatefulProducerRunnable.kt │ │ │ ├── SwallowResultProducer.java │ │ │ ├── ThreadHandoffProducer.kt │ │ │ ├── ThreadHandoffProducerQueue.java │ │ │ ├── ThreadHandoffProducerQueueImpl.kt │ │ │ ├── ThrottlingProducer.java │ │ │ ├── ThumbnailBranchProducer.java │ │ │ ├── ThumbnailProducer.kt │ │ │ ├── ThumbnailSizeChecker.kt │ │ │ └── package-info.kt │ │ ├── request/ │ │ │ ├── BasePostprocessor.java │ │ │ ├── BaseRepeatedPostProcessor.kt │ │ │ ├── HasImageRequest.kt │ │ │ ├── ImageRequest.java │ │ │ ├── ImageRequestBuilder.java │ │ │ ├── Postprocessor.java │ │ │ ├── RepeatedPostprocessor.kt │ │ │ ├── RepeatedPostprocessorRunner.kt │ │ │ └── package-info.kt │ │ ├── transcoder/ │ │ │ ├── MultiImageTranscoderFactory.kt │ │ │ ├── SimpleImageTranscoder.kt │ │ │ └── SimpleImageTranscoderFactory.kt │ │ └── xml/ │ │ ├── XmlDrawableFactory.kt │ │ └── XmlFormatDecoder.kt │ └── test/ │ ├── java/ │ │ ├── com/ │ │ │ └── facebook/ │ │ │ ├── common/ │ │ │ │ └── memory/ │ │ │ │ ├── PooledByteArrayBufferedInputStreamTest.kt │ │ │ │ ├── PooledByteBufferInputStreamTest.kt │ │ │ │ └── PooledByteStreamsTest.kt │ │ │ └── imagepipeline/ │ │ │ ├── cache/ │ │ │ │ ├── BufferedDiskCacheTest.kt │ │ │ │ └── StagingAreaTest.kt │ │ │ ├── core/ │ │ │ │ ├── ImagePipelineConfigTest.kt │ │ │ │ ├── ImagePipelineTest.java │ │ │ │ └── ProducerSequenceFactoryTest.java │ │ │ ├── datasource/ │ │ │ │ ├── CloseableProducerToDataSourceAdapterTest.kt │ │ │ │ ├── ListDataSourceTest.kt │ │ │ │ └── ProducerToDataSourceAdapterTest.kt │ │ │ ├── decoder/ │ │ │ │ └── ProgressiveJpegParserTest.kt │ │ │ ├── filter/ │ │ │ │ ├── InPlaceRoundFilterTest.kt │ │ │ │ ├── IterativeBoxBlurFilterTest.kt │ │ │ │ ├── RenderScriptBlurFilterTest.kt │ │ │ │ └── XferRoundFilterTest.kt │ │ │ ├── memory/ │ │ │ │ ├── BasePoolTest.kt │ │ │ │ ├── BitmapCounterTest.kt │ │ │ │ ├── BitmapPoolTest.java │ │ │ │ ├── BucketMapTest.kt │ │ │ │ ├── CloseableReferences.java │ │ │ │ ├── FlexByteArrayPoolTest.java │ │ │ │ ├── GenericByteArrayPoolTest.java │ │ │ │ ├── LruBitmapPoolTest.kt │ │ │ │ └── SharedByteArrayTest.java │ │ │ ├── platform/ │ │ │ │ └── ArtDecoderTest.kt │ │ │ ├── prioritization/ │ │ │ │ └── PriorityTest.kt │ │ │ ├── producers/ │ │ │ │ ├── AddImageTransformMetaDataProducerTest.java │ │ │ │ ├── BaseConsumerTest.kt │ │ │ │ ├── BitmapMemoryCacheGetProducerTest.java │ │ │ │ ├── BitmapMemoryCacheProducerTest.java │ │ │ │ ├── BitmapPrepareProducerTest.java │ │ │ │ ├── BranchOnSeparateImagesProducerTest.java │ │ │ │ ├── DataFetchProducerTest.kt │ │ │ │ ├── DecodeProducerTest.java │ │ │ │ ├── DiskCacheReadProducerTest.java │ │ │ │ ├── DiskCacheWriteProducerTest.java │ │ │ │ ├── DownsampleUtilTest.kt │ │ │ │ ├── EncodedMemoryCacheProducerTest.java │ │ │ │ ├── HttpUrlConnectionNetworkFetcherTest.kt │ │ │ │ ├── JobSchedulerTest.kt │ │ │ │ ├── LocalAssetFetchProducerTest.kt │ │ │ │ ├── LocalContentUriFetchProducerTest.kt │ │ │ │ ├── LocalContentUriThumbnailFetchProducerTest.kt │ │ │ │ ├── LocalExifThumbnailProducerTest.kt │ │ │ │ ├── LocalFileFetchProducerTest.kt │ │ │ │ ├── LocalResourceFetchProducerTest.kt │ │ │ │ ├── LocalVideoThumbnailProducerTest.kt │ │ │ │ ├── MultiplexProducerTest.java │ │ │ │ ├── NetworkFetchProducerTest.java │ │ │ │ ├── NetworkFetcherCallbackTest.java │ │ │ │ ├── PartialDiskCacheProducerTest.java │ │ │ │ ├── PostprocessedBitmapMemoryCacheProducerTest.java │ │ │ │ ├── PriorityStarvingThrottlingProducerTest.java │ │ │ │ ├── QualifiedResourceFetchProducerTest.kt │ │ │ │ ├── RemoveImageTransformMetaDataProducerTest.java │ │ │ │ ├── RepeatedPostprocessorProducerTest.java │ │ │ │ ├── ResizeAndRotateProducerTest.kt │ │ │ │ ├── SettableProducerContextTest.kt │ │ │ │ ├── SingleUsePostprocessorProducerTest.java │ │ │ │ ├── StatefulProducerRunnableTest.kt │ │ │ │ ├── SwallowResultProducerTest.java │ │ │ │ ├── ThreadHandoffProducerTest.kt │ │ │ │ ├── ThrottlingProducerTest.java │ │ │ │ ├── ThumbnailBranchProducerTest.java │ │ │ │ └── ThumbnailSizeCheckerTest.kt │ │ │ └── request/ │ │ │ ├── ForwardingRequestListenerTest.kt │ │ │ ├── ImageRequestBuilderCacheEnabledTest.kt │ │ │ └── ImageRequestTest.kt │ │ └── org/ │ │ └── mockito/ │ │ └── configuration/ │ │ └── MockitoConfiguration.java │ └── resources/ │ ├── org/ │ │ └── powermock/ │ │ └── extensions/ │ │ └── configuration.properties │ └── org.robolectric.Config.properties ├── imagepipeline-backends/ │ ├── imagepipeline-okhttp3/ │ │ ├── build.gradle │ │ ├── gradle.properties │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── imagepipeline/ │ │ └── backends/ │ │ └── okhttp3/ │ │ ├── OkHttpImagePipelineConfigFactory.kt │ │ ├── OkHttpNetworkFetcher.kt │ │ ├── OkHttpNetworkFetcherException.kt │ │ └── package-info.kt │ └── imagepipeline-volley/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── imagepipeline/ │ └── backends/ │ └── volley/ │ ├── RawRequest.java │ ├── VolleyImagePipelineConfigFactory.java │ └── VolleyNetworkFetcher.java ├── imagepipeline-base/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ ├── binaryresource/ │ │ │ ├── BinaryResource.kt │ │ │ ├── ByteArrayBinaryResource.kt │ │ │ └── FileBinaryResource.kt │ │ ├── cache/ │ │ │ ├── common/ │ │ │ │ ├── BaseCacheEventListener.java │ │ │ │ ├── CacheErrorLogger.java │ │ │ │ ├── CacheEvent.java │ │ │ │ ├── CacheEventListener.java │ │ │ │ ├── CacheKey.java │ │ │ │ ├── CacheKeyUtil.kt │ │ │ │ ├── DebuggingCacheKey.java │ │ │ │ ├── HasDebugData.kt │ │ │ │ ├── MultiCacheKey.java │ │ │ │ ├── NoOpCacheErrorLogger.java │ │ │ │ ├── NoOpCacheEventListener.java │ │ │ │ ├── SimpleCacheKey.java │ │ │ │ ├── WriterCallback.java │ │ │ │ ├── WriterCallbacks.java │ │ │ │ └── package-info.java │ │ │ └── disk/ │ │ │ ├── DefaultDiskStorage.kt │ │ │ ├── DefaultEntryEvictionComparatorSupplier.java │ │ │ ├── DiskCacheConfig.java │ │ │ ├── DiskStorage.java │ │ │ ├── DiskStorageCache.kt │ │ │ ├── DynamicDefaultDiskStorage.java │ │ │ ├── EntryEvictionComparator.java │ │ │ ├── EntryEvictionComparatorSupplier.java │ │ │ ├── FileCache.kt │ │ │ ├── ScoreBasedEvictionComparatorSupplier.java │ │ │ ├── SettableCacheEvent.java │ │ │ └── package-info.java │ │ ├── callercontext/ │ │ │ └── CallerContextVerifier.kt │ │ ├── drawable/ │ │ │ └── base/ │ │ │ └── DrawableWithCaches.kt │ │ ├── imageformat/ │ │ │ ├── DefaultImageFormatChecker.kt │ │ │ ├── DefaultImageFormats.kt │ │ │ ├── ImageFormat.kt │ │ │ ├── ImageFormatChecker.kt │ │ │ └── ImageFormatCheckerUtils.kt │ │ ├── imagepipeline/ │ │ │ ├── animated/ │ │ │ │ └── factory/ │ │ │ │ ├── AnimatedFactory.kt │ │ │ │ ├── AnimatedFactoryProvider.kt │ │ │ │ └── package-info.kt │ │ │ ├── bitmaps/ │ │ │ │ ├── PlatformBitmapFactory.java │ │ │ │ ├── SimpleBitmapReleaser.java │ │ │ │ └── package-info.kt │ │ │ ├── cache/ │ │ │ │ ├── AbstractAdaptiveCountingMemoryCache.java │ │ │ │ ├── BitmapMemoryCacheFactory.kt │ │ │ │ ├── BitmapMemoryCacheTrimStrategy.kt │ │ │ │ ├── BoundedLinkedHashSet.kt │ │ │ │ ├── CountingLruBitmapMemoryCacheFactory.java │ │ │ │ ├── CountingLruMap.java │ │ │ │ ├── CountingMemoryCache.java │ │ │ │ ├── CountingMemoryCacheInspector.kt │ │ │ │ ├── LruCountingMemoryCache.java │ │ │ │ ├── MemoryCache.kt │ │ │ │ ├── MemoryCacheParams.kt │ │ │ │ ├── ValueDescriptor.kt │ │ │ │ └── package-info.kt │ │ │ ├── common/ │ │ │ │ ├── BytesRange.kt │ │ │ │ ├── ImageDecodeOptions.java │ │ │ │ ├── ImageDecodeOptionsBuilder.java │ │ │ │ ├── Priority.kt │ │ │ │ ├── ResizeOptions.kt │ │ │ │ ├── RotationOptions.kt │ │ │ │ ├── SourceUriType.kt │ │ │ │ ├── TooManyBitmapsException.kt │ │ │ │ └── package-info.kt │ │ │ ├── core/ │ │ │ │ ├── DefaultExecutorSupplier.kt │ │ │ │ ├── ExecutorSupplier.kt │ │ │ │ ├── PriorityThreadFactory.kt │ │ │ │ └── package-info.kt │ │ │ ├── decoder/ │ │ │ │ ├── ImageDecoder.kt │ │ │ │ └── package-info.kt │ │ │ ├── drawable/ │ │ │ │ ├── DrawableFactory.kt │ │ │ │ └── package-info.kt │ │ │ ├── image/ │ │ │ │ ├── BaseCloseableImage.java │ │ │ │ ├── BaseCloseableStaticBitmap.java │ │ │ │ ├── CloseableBitmap.java │ │ │ │ ├── CloseableImage.java │ │ │ │ ├── CloseableStaticBitmap.java │ │ │ │ ├── CloseableXml.kt │ │ │ │ ├── DefaultCloseableImage.java │ │ │ │ ├── DefaultCloseableStaticBitmap.java │ │ │ │ ├── DefaultCloseableXml.kt │ │ │ │ ├── EncodedImage.java │ │ │ │ ├── HasImageMetadata.java │ │ │ │ ├── ImageInfo.java │ │ │ │ ├── ImageInfoImpl.java │ │ │ │ ├── ImmutableQualityInfo.java │ │ │ │ ├── QualityInfo.java │ │ │ │ └── package-info.kt │ │ │ ├── instrumentation/ │ │ │ │ └── FrescoInstrumenter.kt │ │ │ ├── nativecode/ │ │ │ │ └── NativeImageTranscoderFactory.kt │ │ │ ├── network/ │ │ │ │ └── NetworkResponseData.kt │ │ │ ├── systrace/ │ │ │ │ ├── DefaultFrescoSystrace.kt │ │ │ │ └── FrescoSystrace.kt │ │ │ ├── transcoder/ │ │ │ │ ├── DownsampleUtil.kt │ │ │ │ ├── ImageTranscodeResult.kt │ │ │ │ ├── ImageTranscoder.kt │ │ │ │ ├── ImageTranscoderFactory.kt │ │ │ │ ├── JpegTranscoderUtils.kt │ │ │ │ └── TranscodeStatus.kt │ │ │ └── transformation/ │ │ │ ├── BitmapTransformation.kt │ │ │ ├── CircularTransformation.kt │ │ │ └── TransformationUtils.kt │ │ └── imageutils/ │ │ ├── BitmapUtil.kt │ │ ├── HeifExifUtil.kt │ │ ├── ImageMetaData.kt │ │ ├── JfifUtil.kt │ │ ├── StreamProcessor.kt │ │ ├── TiffUtil.kt │ │ └── WebpUtil.kt │ └── test/ │ ├── java/ │ │ ├── com/ │ │ │ └── facebook/ │ │ │ ├── cache/ │ │ │ │ ├── common/ │ │ │ │ │ └── CacheEventAssert.kt │ │ │ │ └── disk/ │ │ │ │ ├── DefaultDiskStorageTest.kt │ │ │ │ ├── DefaultEntryEvictionComparatorSupplierTest.kt │ │ │ │ ├── DiskStorageCacheTest.kt │ │ │ │ ├── DynamicDefaultDiskStorageTest.kt │ │ │ │ ├── ScoreBasedEvictionComparatorSupplierTest.kt │ │ │ │ └── SettableCacheEventTest.kt │ │ │ ├── imageformat/ │ │ │ │ └── ImageFormatCheckerTest.kt │ │ │ ├── imagepipeline/ │ │ │ │ ├── cache/ │ │ │ │ │ ├── AbstractAdaptiveCountingMemoryCacheTest.kt │ │ │ │ │ ├── BoundedLinkedHashSetTest.kt │ │ │ │ │ ├── CountingLruMapTest.kt │ │ │ │ │ └── LruCountingMemoryCacheTest.kt │ │ │ │ ├── common/ │ │ │ │ │ ├── BytesRangeTest.kt │ │ │ │ │ ├── ImageDecodeOptionsTest.kt │ │ │ │ │ └── ResizeOptionsTest.kt │ │ │ │ └── image/ │ │ │ │ ├── CloseableBitmapTest.kt │ │ │ │ ├── CloseableStaticBitmapTest.kt │ │ │ │ └── EncodedImageTest.kt │ │ │ └── imageutils/ │ │ │ ├── BitmapUtilTest.kt │ │ │ ├── JfifTestUtils.kt │ │ │ ├── JfifTestUtilsTest.kt │ │ │ ├── JfifUtilTest.kt │ │ │ └── WebPUtilTest.kt │ │ └── org/ │ │ └── mockito/ │ │ └── configuration/ │ │ └── MockitoConfiguration.java │ └── resources/ │ ├── com/ │ │ └── facebook/ │ │ └── imageformat/ │ │ ├── heifs/ │ │ │ └── 1.heif │ │ └── xmls/ │ │ ├── AndroidManifest.xml │ │ ├── README.md │ │ ├── compiled/ │ │ │ ├── layer_list.xml │ │ │ ├── level_list.xml │ │ │ ├── state_list.xml │ │ │ └── vector_drawable.xml │ │ ├── convert.sh │ │ └── raw/ │ │ └── drawable/ │ │ ├── layer_list.xml │ │ ├── level_list.xml │ │ ├── state_list.xml │ │ └── vector_drawable.xml │ └── org.robolectric.Config.properties ├── imagepipeline-base-test/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── imagepipeline/ │ └── testing/ │ ├── DeltaQueue.java │ ├── FakeClock.java │ ├── ScheduledQueue.java │ ├── TestExecutorService.java │ ├── TestNativeLoader.kt │ ├── TestScheduledExecutorService.java │ ├── TestScheduledFuture.java │ ├── TrivialBufferPooledByteBuffer.java │ └── TrivialPooledByteBuffer.java ├── imagepipeline-native/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── imagepipeline/ │ │ └── nativecode/ │ │ ├── Bitmaps.kt │ │ ├── DalvikPurgeableDecoder.kt │ │ └── ImagePipelineNativeLoader.kt │ └── jni/ │ ├── Application.mk │ ├── bitmaps/ │ │ ├── Android.mk │ │ ├── Bitmaps.c │ │ └── Bitmaps.h │ ├── imagepipeline/ │ │ ├── Android.mk │ │ ├── exceptions.cpp │ │ ├── exceptions.h │ │ ├── init.cpp │ │ └── logging.h │ └── memchunk/ │ ├── Android.mk │ ├── NativeMemoryChunk.c │ └── NativeMemoryChunk.h ├── imagepipeline-test/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── imagepipeline/ │ ├── memory/ │ │ ├── IntPair.java │ │ └── PoolStats.java │ └── testing/ │ └── MockBitmapFactory.kt ├── memory-types/ │ ├── ashmem/ │ │ ├── build.gradle │ │ ├── gradle.properties │ │ └── src/ │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── facebook/ │ │ │ └── imagepipeline/ │ │ │ └── memory/ │ │ │ ├── AshmemMemoryChunk.java │ │ │ └── AshmemMemoryChunkPool.java │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── imagepipeline/ │ │ ├── memory/ │ │ │ ├── AshmemMemoryChunkPoolTest.kt │ │ │ ├── MemoryPooledByteBufferFactoryTest.java │ │ │ ├── MemoryPooledByteBufferOutputStreamTest.java │ │ │ └── MemoryPooledByteBufferTest.java │ │ └── testing/ │ │ ├── FakeAshmemMemoryChunk.java │ │ └── FakeAshmemMemoryChunkPool.java │ ├── nativememory/ │ │ ├── build.gradle │ │ ├── gradle.properties │ │ └── src/ │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── facebook/ │ │ │ └── imagepipeline/ │ │ │ └── memory/ │ │ │ ├── NativeMemoryChunk.java │ │ │ └── NativeMemoryChunkPool.java │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── imagepipeline/ │ │ ├── memory/ │ │ │ ├── MemoryPooledByteBufferFactoryTest.java │ │ │ ├── MemoryPooledByteBufferOutputStreamTest.java │ │ │ ├── MemoryPooledByteBufferTest.java │ │ │ ├── NativeMemoryChunkPoolTest.java │ │ │ └── TestUsingNativeMemoryChunk.java │ │ └── testing/ │ │ ├── FakeNativeMemoryChunk.java │ │ └── FakeNativeMemoryChunkPool.java │ └── simple/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── imagepipeline/ │ │ └── memory/ │ │ ├── BufferMemoryChunk.kt │ │ └── BufferMemoryChunkPool.kt │ └── test/ │ └── java/ │ └── com/ │ └── facebook/ │ └── imagepipeline/ │ ├── memory/ │ │ ├── BufferMemoryChunkPoolTest.java │ │ ├── MemoryPooledByteBufferFactoryTest.java │ │ ├── MemoryPooledByteBufferOutputStreamTest.java │ │ └── MemoryPooledByteBufferTest.java │ └── testing/ │ └── FakeBufferMemoryChunkPool.kt ├── middleware/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── middleware/ │ ├── HasExtraData.kt │ └── MiddlewareUtils.kt ├── mockito-config/ │ ├── build.gradle │ └── src/ │ └── main/ │ └── java/ │ └── org/ │ └── mockito/ │ └── configuration/ │ └── MockitoConfiguration.java ├── native-filters/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── imagepipeline/ │ │ ├── nativecode/ │ │ │ ├── NativeBlurFilter.kt │ │ │ ├── NativeFiltersLoader.kt │ │ │ └── NativeRoundingFilter.kt │ │ └── postprocessors/ │ │ ├── IterativeBoxBlurPostProcessor.kt │ │ ├── RoundAsCirclePostprocessor.kt │ │ └── RoundedCornersPostprocessor.kt │ └── jni/ │ ├── Application.mk │ ├── filters/ │ │ ├── Android.mk │ │ ├── blur_filter.c │ │ ├── blur_filter.h │ │ ├── rounding_filter.c │ │ └── rounding_filter.h │ └── native-filters/ │ ├── Android.mk │ ├── exceptions_handling.cpp │ ├── exceptions_handling.h │ ├── init.cpp │ ├── java_globals.h │ └── logging.h ├── native-imagetranscoder/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── imagepipeline/ │ │ └── nativecode/ │ │ ├── NativeJpegTranscoder.java │ │ ├── NativeJpegTranscoderFactory.java │ │ └── NativeJpegTranscoderSoLoader.java │ └── jni/ │ ├── Application.mk │ ├── native-imagetranscoder/ │ │ ├── Android.mk │ │ ├── JpegTranscoder.cpp │ │ ├── JpegTranscoder.h │ │ ├── decoded_image.cpp │ │ ├── decoded_image.h │ │ ├── exceptions_handler.cpp │ │ ├── exceptions_handler.h │ │ ├── init.cpp │ │ ├── java_globals.h │ │ ├── jpeg/ │ │ │ ├── jpeg_codec.cpp │ │ │ ├── jpeg_codec.h │ │ │ ├── jpeg_error_handler.cpp │ │ │ ├── jpeg_error_handler.h │ │ │ ├── jpeg_memory_io.cpp │ │ │ ├── jpeg_memory_io.h │ │ │ ├── jpeg_stream_wrappers.cpp │ │ │ └── jpeg_stream_wrappers.h │ │ ├── logging.h │ │ ├── transformations.cpp │ │ └── transformations.h │ └── third-party/ │ └── libjpeg-turbo-2.1.5.1/ │ ├── Android.mk │ ├── config.h │ ├── jconfig.h │ ├── jconfigint.h │ ├── jversion.h │ └── neon-compat.h ├── run_comparison.py ├── samples/ │ ├── README.md │ ├── animation2/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── facebook/ │ │ │ └── samples/ │ │ │ └── animation2/ │ │ │ ├── AnimationApplication.java │ │ │ ├── MediaControlFragment.java │ │ │ ├── SampleData.java │ │ │ ├── StandaloneActivity.java │ │ │ ├── bitmap/ │ │ │ │ ├── BitmapAnimationCacheSelectorConfigurator.java │ │ │ │ ├── BitmapAnimationDebugFragment.java │ │ │ │ ├── BitmapAnimationFragment.java │ │ │ │ ├── DebugBitmapAnimationFrameListener.java │ │ │ │ ├── ExampleBitmapAnimationFactory.java │ │ │ │ └── NaiveCacheAllFramesCachingBackend.java │ │ │ ├── color/ │ │ │ │ ├── ExampleColorBackend.java │ │ │ │ └── SimpleColorFragment.java │ │ │ ├── local/ │ │ │ │ └── LocalDrawableAnimationBackend.java │ │ │ └── utils/ │ │ │ ├── AnimationBackendUtils.java │ │ │ ├── AnimationControlsManager.java │ │ │ └── SampleAnimationBackendConfigurator.java │ │ └── res/ │ │ ├── drawable/ │ │ │ └── ic_play_pause_24dp.xml │ │ ├── layout/ │ │ │ ├── activity_standalone.xml │ │ │ ├── backends.xml │ │ │ ├── cache_selector.xml │ │ │ ├── controls.xml │ │ │ ├── fragment_debug_bitmap.xml │ │ │ ├── fragment_media_controls.xml │ │ │ ├── fragment_simple_container.xml │ │ │ └── frame_info.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── values-w820dp/ │ │ └── dimens.xml │ ├── comparison/ │ │ ├── build.gradle │ │ ├── proguard.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── facebook/ │ │ │ └── samples/ │ │ │ └── comparison/ │ │ │ ├── ComparisonApp.java │ │ │ ├── Drawables.java │ │ │ ├── MainActivity.kt │ │ │ ├── adapters/ │ │ │ │ ├── AQueryAdapter.java │ │ │ │ ├── FrescoAdapter.java │ │ │ │ ├── GlideAdapter.java │ │ │ │ ├── ImageListAdapter.java │ │ │ │ ├── PicassoAdapter.java │ │ │ │ ├── UilAdapter.java │ │ │ │ └── VolleyAdapter.java │ │ │ ├── configs/ │ │ │ │ ├── ConfigConstants.java │ │ │ │ ├── glide/ │ │ │ │ │ └── SampleGlideModule.java │ │ │ │ ├── imagepipeline/ │ │ │ │ │ └── ImagePipelineConfigFactory.java │ │ │ │ ├── picasso/ │ │ │ │ │ └── SamplePicassoFactory.java │ │ │ │ ├── uil/ │ │ │ │ │ └── SampleUilFactory.java │ │ │ │ └── volley/ │ │ │ │ ├── SampleVolleyFactory.java │ │ │ │ └── VolleyMemoryCache.java │ │ │ ├── holders/ │ │ │ │ ├── AQueryHolder.java │ │ │ │ ├── BaseViewHolder.java │ │ │ │ ├── FrescoHolder.java │ │ │ │ ├── GlideHolder.java │ │ │ │ ├── PicassoHolder.java │ │ │ │ ├── UilHolder.java │ │ │ │ ├── VolleyDraweeHolder.java │ │ │ │ └── VolleyHolder.java │ │ │ ├── instrumentation/ │ │ │ │ ├── Instrumentation.java │ │ │ │ ├── Instrumented.java │ │ │ │ ├── InstrumentedDraweeView.java │ │ │ │ ├── InstrumentedImageView.java │ │ │ │ ├── InstrumentedNetworkImageView.java │ │ │ │ └── PerfListener.java │ │ │ └── urlsfetcher/ │ │ │ ├── ImageFormat.java │ │ │ ├── ImageSize.java │ │ │ ├── ImageUrlsFetcher.java │ │ │ ├── ImageUrlsRequest.java │ │ │ └── ImageUrlsRequestBuilder.java │ │ └── res/ │ │ ├── layout/ │ │ │ └── activity_main.xml │ │ ├── menu/ │ │ │ └── menu_main.xml │ │ └── values/ │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── contrib/ │ │ └── com/ │ │ └── facebook/ │ │ └── drawee/ │ │ └── drawable/ │ │ └── CircleProgressBarDrawable.java │ ├── gestures/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── facebook/ │ │ │ └── samples/ │ │ │ └── gestures/ │ │ │ ├── MultiPointerGestureDetector.java │ │ │ └── TransformGestureDetector.java │ │ └── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── facebook/ │ │ │ └── samples/ │ │ │ └── gestures/ │ │ │ ├── MotionEventTestUtils.java │ │ │ ├── MultiPointerGestureDetectorTest.java │ │ │ └── TransformGestureDetectorTest.java │ │ └── resources/ │ │ └── org.robolectric.Config.properties │ ├── scrollperf/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-scrollperf.pro │ │ └── src/ │ │ ├── androidTest/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── facebook/ │ │ │ │ └── samples/ │ │ │ │ └── scrollperf/ │ │ │ │ └── data/ │ │ │ │ └── impl/ │ │ │ │ ├── InfiniteSimpleAdapterTest.java │ │ │ │ └── LocalResourceSimpleAdapterTest.java │ │ │ └── res/ │ │ │ └── values/ │ │ │ ├── colors.xml │ │ │ └── strings_untranslated.xml │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── facebook/ │ │ │ │ └── samples/ │ │ │ │ └── scrollperf/ │ │ │ │ ├── MainActivity.java │ │ │ │ ├── ScrollPerfApplication.java │ │ │ │ ├── conf/ │ │ │ │ │ ├── Config.java │ │ │ │ │ └── Const.java │ │ │ │ ├── data/ │ │ │ │ │ ├── Decorator.java │ │ │ │ │ ├── SimpleAdapter.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── ContentProviderSimpleAdapter.java │ │ │ │ │ ├── DistinctUriDecorator.java │ │ │ │ │ └── LocalResourceSimpleAdapter.java │ │ │ │ ├── fragments/ │ │ │ │ │ ├── MainFragment.java │ │ │ │ │ ├── SettingsFragment.java │ │ │ │ │ └── recycler/ │ │ │ │ │ ├── VitoViewAdapter.java │ │ │ │ │ ├── VitoViewHolder.java │ │ │ │ │ └── VitoViewListAdapter.java │ │ │ │ ├── instrumentation/ │ │ │ │ │ ├── Instrumentation.java │ │ │ │ │ ├── Instrumented.java │ │ │ │ │ ├── InstrumentedVitoView.java │ │ │ │ │ └── PerfListener.java │ │ │ │ ├── internal/ │ │ │ │ │ └── ScrollPerfExecutorSupplier.java │ │ │ │ ├── postprocessor/ │ │ │ │ │ └── DelayPostprocessor.java │ │ │ │ ├── preferences/ │ │ │ │ │ └── SizePreferences.java │ │ │ │ └── util/ │ │ │ │ ├── PipelineUtil.java │ │ │ │ ├── SizeUtil.java │ │ │ │ ├── TimeWaster.java │ │ │ │ ├── UI.java │ │ │ │ └── VitoUtil.java │ │ │ └── res/ │ │ │ ├── layout/ │ │ │ │ ├── activity_main.xml │ │ │ │ ├── content_listview.xml │ │ │ │ ├── content_recyclerview.xml │ │ │ │ ├── fragment_main.xml │ │ │ │ └── size_preference.xml │ │ │ ├── menu/ │ │ │ │ └── menu.xml │ │ │ ├── values/ │ │ │ │ ├── array.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── strings.xml │ │ │ │ ├── strings_untranslated.xml │ │ │ │ └── styles.xml │ │ │ └── xml/ │ │ │ └── preferences.xml │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── samples/ │ │ └── scrollperf/ │ │ └── util/ │ │ └── TimeWasterTest.java │ ├── showcase/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-showcase.pro │ │ ├── proguard-test.pro │ │ └── src/ │ │ ├── androidTest/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── facebook/ │ │ │ └── fresco/ │ │ │ └── samples/ │ │ │ └── showcase/ │ │ │ └── ShowcaseRunTest.java │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── facebook/ │ │ │ └── fresco/ │ │ │ └── samples/ │ │ │ └── showcase/ │ │ │ ├── BaseShowcaseFragment.java │ │ │ ├── BaseShowcaseKotlinFragment.kt │ │ │ ├── CustomImageFormatConfigurator.java │ │ │ ├── ExampleCategory.kt │ │ │ ├── ExampleDatabase.kt │ │ │ ├── ExampleItem.kt │ │ │ ├── ImageOptionsBottomSheet.kt │ │ │ ├── LithoSample.kt │ │ │ ├── LithoSampleHostFragment.kt │ │ │ ├── MainActivity.kt │ │ │ ├── ShowcaseApplication.kt │ │ │ ├── common/ │ │ │ │ ├── CustomScaleTypes.java │ │ │ │ ├── DimensionUtils.kt │ │ │ │ ├── SimpleScaleTypeAdapter.java │ │ │ │ └── SpinnerUtils.kt │ │ │ ├── imageformat/ │ │ │ │ ├── color/ │ │ │ │ │ ├── ColorImageExample.java │ │ │ │ │ └── ImageFormatColorFragment.java │ │ │ │ ├── datauri/ │ │ │ │ │ └── ImageFormatDataUriFragment.java │ │ │ │ ├── gif/ │ │ │ │ │ └── ImageFormatGifFragment.java │ │ │ │ ├── keyframes/ │ │ │ │ │ ├── AnimatableKeyframesDrawable.java │ │ │ │ │ ├── ImageFormatKeyframesFragment.java │ │ │ │ │ └── KeyframesDecoderExample.java │ │ │ │ ├── override/ │ │ │ │ │ └── ImageFormatOverrideExample.java │ │ │ │ ├── pjpeg/ │ │ │ │ │ └── ImageFormatProgressiveJpegFragment.java │ │ │ │ ├── svg/ │ │ │ │ │ ├── ImageFormatSvgFragment.java │ │ │ │ │ └── SvgDecoderExample.java │ │ │ │ ├── webp/ │ │ │ │ │ └── ImageFormatWebpFragment.java │ │ │ │ └── xml/ │ │ │ │ └── ImageFormatXmlFragment.java │ │ │ ├── imagepipeline/ │ │ │ │ ├── DurationCallback.java │ │ │ │ ├── ImagePipelineBitmapFactoryFragment.java │ │ │ │ ├── ImagePipelineDownsampleFragment.java │ │ │ │ ├── ImagePipelineNotificationFragment.java │ │ │ │ ├── ImagePipelinePostProcessorFragment.java │ │ │ │ ├── ImagePipelinePrefetchFragment.java │ │ │ │ ├── ImagePipelineQualifiedResourceFragment.java │ │ │ │ ├── ImagePipelineRegionDecodingFragment.java │ │ │ │ ├── ImagePipelineResizingFragment.java │ │ │ │ ├── PartialRequestFragment.java │ │ │ │ └── widget/ │ │ │ │ └── ResizableFrameLayout.java │ │ │ ├── misc/ │ │ │ │ ├── CheckerBoardDrawable.java │ │ │ │ ├── DebugImageListener.kt │ │ │ │ ├── DebugOverlaySupplierSingleton.java │ │ │ │ ├── ImageSourceSpinner.kt │ │ │ │ ├── ImageUriProvider.kt │ │ │ │ ├── LogcatImagePerfDataListener.java │ │ │ │ ├── LogcatRequestListener2.kt │ │ │ │ └── WelcomeFragment.java │ │ │ ├── permissions/ │ │ │ │ └── StoragePermissionHelper.kt │ │ │ ├── postprocessor/ │ │ │ │ ├── BasePostprocessorWithDurationCallback.java │ │ │ │ ├── BenchmarkPostprocessorForDuplicatedBitmap.java │ │ │ │ ├── BenchmarkPostprocessorForDuplicatedBitmapInPlace.java │ │ │ │ ├── BenchmarkPostprocessorForManualBitmapHandling.java │ │ │ │ ├── CachedWatermarkPostprocessor.java │ │ │ │ ├── FasterGreyScalePostprocessor.java │ │ │ │ ├── ScalingBlurPostprocessor.java │ │ │ │ ├── SlowGreyScalePostprocessor.java │ │ │ │ └── WatermarkPostprocessor.java │ │ │ ├── settings/ │ │ │ │ └── SettingsFragment.java │ │ │ └── vito/ │ │ │ ├── FrescoVitoImageDecodeOptions.java │ │ │ ├── FrescoVitoImageDecodeOptionsBuilder.java │ │ │ ├── FrescoVitoLithoDrawableImageSourceExample.kt │ │ │ ├── FrescoVitoLithoGalleryFragment.java │ │ │ ├── FrescoVitoLithoImageOptionsConfigFragment.kt │ │ │ ├── FrescoVitoLithoListenerExample.kt │ │ │ ├── FrescoVitoLithoRegionDecodeFragment.java │ │ │ ├── FrescoVitoLithoSectionsFragment.java │ │ │ ├── FrescoVitoLithoSimpleExample.kt │ │ │ ├── FrescoVitoRegionDecoder.java │ │ │ ├── ImageLayersFragment.java │ │ │ ├── ImageSourceConfigurator.kt │ │ │ ├── LithoSlideshowSample.kt │ │ │ ├── MultiUriFragment.kt │ │ │ ├── RetainingDataSourceSupplierFragment.java │ │ │ ├── SimpleGallerySectionSpec.java │ │ │ ├── SimpleListItemSpec.java │ │ │ ├── SimpleListSectionSpec.java │ │ │ ├── VitoMediaPickerFragment.kt │ │ │ ├── VitoRotationFragment.java │ │ │ ├── VitoRoundedCornersFragment.java │ │ │ ├── VitoScaleTypeFragment.java │ │ │ ├── VitoSimpleFragment.java │ │ │ ├── VitoSpanFragment.kt │ │ │ ├── VitoSpinners.kt │ │ │ ├── VitoViewKtxFragment.kt │ │ │ ├── VitoViewPrefetchFragment.kt │ │ │ ├── VitoViewRecyclerFragment.java │ │ │ ├── VitoViewSimpleFragment.java │ │ │ ├── ninepatch/ │ │ │ │ ├── LithoNinePatchSample.kt │ │ │ │ └── NinePatchExample.java │ │ │ ├── renderer/ │ │ │ │ ├── RendererColorFilterExampleFragment.kt │ │ │ │ ├── RendererExampleDrawable.kt │ │ │ │ ├── RendererExampleUtils.kt │ │ │ │ ├── RendererFadeExampleFragment.kt │ │ │ │ ├── RendererShapeExampleFragment.kt │ │ │ │ └── VitoLayerExample.kt │ │ │ ├── source/ │ │ │ │ └── ImageRequestImageSource.kt │ │ │ └── transition/ │ │ │ ├── ImageDetailsActivity.java │ │ │ └── VitoTransitionFragment.java │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── ic_error_black_96dp.xml │ │ │ ├── ic_retry_black_48dp.xml │ │ │ ├── ic_settings_black_24dp.xml │ │ │ ├── resize_outline.xml │ │ │ ├── side_nav_bar.xml │ │ │ ├── xml_bitmap.xml │ │ │ ├── xml_layer_list.xml │ │ │ ├── xml_level_list.xml │ │ │ ├── xml_nine_patch.xml │ │ │ ├── xml_state_list.xml │ │ │ └── xml_vector.xml │ │ ├── drawable-anydpi/ │ │ │ ├── ic_arrow_left.xml │ │ │ ├── ic_arrow_right.xml │ │ │ └── ic_edit.xml │ │ ├── layout/ │ │ │ ├── activity_main.xml │ │ │ ├── activity_vito_transition_detail.xml │ │ │ ├── app_bar_main.xml │ │ │ ├── content_main.xml │ │ │ ├── dialog_fragment_uri_override.xml │ │ │ ├── fragment_format_color.xml │ │ │ ├── fragment_format_datauri.xml │ │ │ ├── fragment_format_gif.xml │ │ │ ├── fragment_format_keyframes.xml │ │ │ ├── fragment_format_override.xml │ │ │ ├── fragment_format_progressive_jpeg.xml │ │ │ ├── fragment_format_svg.xml │ │ │ ├── fragment_format_webp.xml │ │ │ ├── fragment_format_xml.xml │ │ │ ├── fragment_image_layers.xml │ │ │ ├── fragment_imagepipeline_bitmap_factory.xml │ │ │ ├── fragment_imagepipeline_downsample.xml │ │ │ ├── fragment_imagepipeline_notification.xml │ │ │ ├── fragment_imagepipeline_postprocessor.xml │ │ │ ├── fragment_imagepipeline_prefetch.xml │ │ │ ├── fragment_imagepipeline_qualified_resource.xml │ │ │ ├── fragment_imagepipeline_region_decoding.xml │ │ │ ├── fragment_imagepipeline_resizing.xml │ │ │ ├── fragment_litho_host.xml │ │ │ ├── fragment_partial_request.xml │ │ │ ├── fragment_recycler.xml │ │ │ ├── fragment_scrolling_linear_layout.xml │ │ │ ├── fragment_vito_image_options_config.xml │ │ │ ├── fragment_vito_litho_region_decoding.xml │ │ │ ├── fragment_vito_media_picker.xml │ │ │ ├── fragment_vito_multi_uri.xml │ │ │ ├── fragment_vito_retaining_supplier.xml │ │ │ ├── fragment_vito_rotation.xml │ │ │ ├── fragment_vito_rounded_corners.xml │ │ │ ├── fragment_vito_scale_type.xml │ │ │ ├── fragment_vito_simple.xml │ │ │ ├── fragment_vito_text_span.xml │ │ │ ├── fragment_vito_transition.xml │ │ │ ├── fragment_vito_view_ktx.xml │ │ │ ├── fragment_vito_view_prefetch.xml │ │ │ ├── fragment_vito_view_simple.xml │ │ │ ├── fragment_welcome.xml │ │ │ ├── nav_header_main.xml │ │ │ └── vito_recycler_item.xml │ │ ├── menu/ │ │ │ └── main.xml │ │ ├── raw/ │ │ │ ├── custom_color1.color │ │ │ └── custom_color2.color │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── values-v21/ │ │ │ └── styles.xml │ │ ├── values-w820dp/ │ │ │ └── dimens.xml │ │ └── xml/ │ │ └── preferences.xml │ ├── zoomable/ │ │ ├── build.gradle │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── samples/ │ │ └── zoomable/ │ │ ├── AbstractAnimatedZoomableController.java │ │ ├── AnimatedZoomableController.java │ │ ├── DefaultZoomableController.java │ │ ├── DoubleTapGestureListener.java │ │ ├── GestureListenerWrapper.java │ │ ├── MultiGestureListener.java │ │ ├── MultiZoomableControllerListener.java │ │ ├── ZoomableController.java │ │ ├── ZoomableDraweeView.java │ │ └── ZoomableVitoView.java │ └── zoomableapp/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── samples/ │ │ └── zoomableapp/ │ │ ├── MainActivity.java │ │ ├── MyPagerAdapter.java │ │ └── ZoomableApplication.java │ └── res/ │ ├── layout/ │ │ ├── activity_main.xml │ │ └── zoomable_image.xml │ ├── menu/ │ │ └── menu.xml │ └── values/ │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── settings.gradle ├── soloader/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── imagepipeline/ │ └── nativecode/ │ └── NativeCodeInitializer.kt ├── static-webp/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── webpsupport/ │ │ ├── AndroidManifest.xml │ │ ├── WebpBitmapFactoryTest.java │ │ └── WebpDecodingTest.java │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── facebook/ │ │ ├── imagepipeline/ │ │ │ └── nativecode/ │ │ │ └── StaticWebpNativeLoader.kt │ │ └── webpsupport/ │ │ ├── WebPImageDecoder.java │ │ └── WebpBitmapFactoryImpl.java │ └── jni/ │ ├── Application.mk │ ├── static-webp/ │ │ ├── Android.mk │ │ ├── decoded_image.cpp │ │ ├── decoded_image.h │ │ ├── exceptions.cpp │ │ ├── exceptions.h │ │ ├── java_globals.h │ │ ├── jni_helpers.cpp │ │ ├── jni_helpers.h │ │ ├── logging.h │ │ ├── streams.cpp │ │ ├── streams.h │ │ ├── transformations.cpp │ │ ├── transformations.h │ │ ├── webp.cpp │ │ ├── webp.h │ │ └── webp_bitmapfactory.cpp │ └── third-party/ │ ├── libjpeg-turbo-2.1.5.1/ │ │ ├── Android.mk │ │ ├── config.h │ │ ├── jconfig.h │ │ ├── jconfigint.h │ │ ├── jversion.h │ │ └── neon-compat.h │ ├── libpng-1.6.37/ │ │ ├── Android.mk │ │ └── pnglibconf.h │ └── libwebp-1.3.2/ │ └── Android.mk ├── tools/ │ ├── flipper/ │ │ ├── build.gradle │ │ ├── gradle.properties │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── imagepipeline/ │ │ └── debug/ │ │ ├── DebugImageTracker.kt │ │ ├── FlipperCacheKeyFactory.kt │ │ ├── FlipperCloseableReferenceLeakTracker.kt │ │ ├── FlipperImageTracker.kt │ │ ├── LruMap.kt │ │ └── NoOpDebugImageTracker.kt │ ├── flipper-fresco-plugin/ │ │ ├── build.gradle │ │ ├── gradle.properties │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── flipper/ │ │ └── plugins/ │ │ └── fresco/ │ │ ├── FrescoFlipperDebugPrefHelper.java │ │ ├── FrescoFlipperPlugin.java │ │ ├── FrescoFlipperRequestListener.java │ │ └── objecthelper/ │ │ └── FlipperObjectHelper.java │ └── stetho/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── imagepipeline/ │ └── stetho/ │ ├── BaseFrescoStethoPlugin.kt │ └── FrescoStethoPlugin.kt ├── ui-common/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── ui/ │ └── common/ │ ├── BaseControllerListener2.kt │ ├── ControllerListener2.kt │ ├── DimensionsInfo.kt │ ├── ForwardingControllerListener2.kt │ ├── ImageLoadStatus.kt │ ├── ImagePerfData.kt │ ├── ImagePerfDataListener.kt │ ├── ImagePerfDataNotifier.kt │ ├── ImagePerfLoggingState.kt │ ├── ImagePerfNotifier.kt │ ├── ImagePerfNotifierHolder.kt │ ├── ImagePerfState.kt │ ├── ImageRenderingInfra.kt │ ├── LegacyOnFadeListener.kt │ ├── MultiUriHelper.kt │ ├── NoOpImagePerfNotifier.kt │ ├── OnDrawControllerListener.kt │ ├── OnFadeListener.kt │ ├── SimpleImagePerfNotifier.kt │ ├── VisibilityAware.kt │ ├── VisibilityState.kt │ └── VitoUtils.kt ├── ui-core/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ └── java/ │ ├── AndroidManifest.xml │ └── com/ │ └── facebook/ │ └── drawee/ │ └── drawable/ │ ├── ScalingUtils.java │ ├── SizingHint.kt │ └── Viewport.kt ├── urimod/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── urimod/ │ ├── Dimensions.kt │ ├── FetchStrategy.kt │ ├── NopUriModifier.kt │ ├── UriModifier.kt │ └── UriModifierInterface.kt ├── viewport/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── viewport/ │ ├── HasTransform.kt │ └── ViewportData.kt └── vito/ ├── compose/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ └── compose/ │ └── VitoImage.kt ├── core/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── fresco/ │ │ └── vito/ │ │ ├── core/ │ │ │ ├── AnimatedImagePerfLoggingListener.kt │ │ │ ├── BaseVitoImageRequestListener.kt │ │ │ ├── CombinedImageListener.kt │ │ │ ├── ComposedImagePerfLoggingListener.kt │ │ │ ├── DefaultFrescoVitoConfig.kt │ │ │ ├── FrescoController2.kt │ │ │ ├── FrescoDrawableInterface.kt │ │ │ ├── FrescoVitoConfig.kt │ │ │ ├── FrescoVitoPrefetcher.kt │ │ │ ├── ImagePerfLoggingListener.kt │ │ │ ├── ImagePipelineUtils.kt │ │ │ ├── ListVitoImagePerfListener.kt │ │ │ ├── NopDrawable.kt │ │ │ ├── PrefetchConfig.kt │ │ │ ├── PrefetchReason.kt │ │ │ ├── PrefetchTarget.kt │ │ │ ├── ReleaseStrategy.kt │ │ │ ├── VitoImagePerfListener.kt │ │ │ ├── VitoImagePipeline.kt │ │ │ ├── VitoImageRequest.kt │ │ │ └── VitoImageRequestListener.kt │ │ ├── drawable/ │ │ │ ├── ArrayVitoDrawableFactory.kt │ │ │ ├── BitmapDrawableFactory.kt │ │ │ ├── CircularBorderBitmapDrawable.kt │ │ │ └── RoundingUtils.kt │ │ └── listener/ │ │ ├── BaseImageListener.kt │ │ ├── ForwardingImageListener.kt │ │ └── ImageListener.kt │ └── test/ │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ ├── core/ │ │ └── VitoImageRequestTest.kt │ └── drawable/ │ ├── BitmapDrawableFactoryTest.kt │ └── RoundingUtilsTest.kt ├── core-common-impl/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ └── core/ │ └── impl/ │ ├── BaseVitoImagePerfListener.kt │ ├── CombinedImageListenerImpl.kt │ └── debug/ │ ├── DebugDataProvider.kt │ ├── DebugOverlayDrawable.kt │ ├── DebugOverlayImageOriginColor.kt │ └── LightweightDebugOverlayDrawable.kt ├── core-impl/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ └── core/ │ └── impl/ │ ├── ActualImageHandler.kt │ ├── BackgroundLayerHandler.kt │ ├── BorderRenderer.kt │ ├── DebugDataProvider.kt │ ├── DebugOverlayHandler.kt │ ├── DrawableExtensions.kt │ ├── ExtrasUtils.kt │ ├── ImageFetchSubscriber.kt │ ├── ImageLayerDataModel.kt │ ├── ImageLayerDataModelExtensions.kt │ ├── ImageReleaseScheduler.kt │ ├── ImageWithTransformationAndBorderRenderer.kt │ ├── KFrescoController.kt │ ├── KFrescoVitoDrawable.kt │ ├── KImageOptions.kt │ ├── NopImagePerfListener.kt │ ├── ProgressLayerHandler.kt │ ├── PropertyDelegates.kt │ └── ShapeCalculator.kt ├── core-java-impl/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── fresco/ │ │ └── vito/ │ │ └── core/ │ │ └── impl/ │ │ ├── DefaultImageDecodeOptionsProviderImpl.kt │ │ ├── DrawableDataSubscriber.kt │ │ ├── FrescoController2Impl.kt │ │ ├── FrescoDrawable2.kt │ │ ├── FrescoDrawable2Impl.kt │ │ ├── FrescoVitoPrefetcherImpl.kt │ │ ├── Hierarcher.kt │ │ ├── HierarcherImpl.kt │ │ ├── ImagePipelineUtilsImpl.kt │ │ ├── ImageSourceToImagePipelineAdapter.kt │ │ ├── NoOpFrescoVitoPrefetcher.kt │ │ ├── VitoImagePipelineImpl.kt │ │ ├── debug/ │ │ │ ├── BaseDebugOverlayFactory2.kt │ │ │ ├── DebugOverlayFactory2.kt │ │ │ ├── DefaultDebugOverlayFactory2.kt │ │ │ ├── FrescoDrawable2DebugDataProviders.kt │ │ │ ├── LightweightDebugOverlayFactory2.kt │ │ │ └── NoOpDebugOverlayFactory2.kt │ │ └── source/ │ │ ├── DataSourceImageSource.kt │ │ ├── ImagePipelineImageSource.kt │ │ └── RetainingImageSource.kt │ └── test/ │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ └── core/ │ └── impl/ │ ├── FrescoDrawable2ImplTest.kt │ ├── HierarcherImplTest.kt │ ├── ImagePipelineUtilsImplTest.kt │ └── VitoImagePipelineImplTest.kt ├── drawable-holder/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ └── java/ │ ├── AndroidManifest.xml │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ └── drawableholder/ │ └── MultiVitoDrawableHolder.kt ├── drawee-support/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── fresco/ │ │ └── vito/ │ │ └── draweesupport/ │ │ ├── ControllerListenerWrapper.kt │ │ ├── DrawableFactoryWrapper.kt │ │ ├── RoundingParamsWrapper.kt │ │ └── VitoViewInflater.kt │ └── test/ │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ └── draweesupport/ │ └── ControllerListenerWrapperTest.kt ├── init/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ └── init/ │ └── FrescoVito.kt ├── ktx/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ └── ktx/ │ ├── ImageSourceExtensions.kt │ └── ViewExtensions.kt ├── litho/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ └── litho/ │ ├── FrescoVitoImage2Spec.kt │ └── FrescoVitoTapToRetryImageSpec.kt ├── litho-slideshow/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ └── litho/ │ └── slideshow/ │ ├── FrescoVitoSlideshowComponentSpec.kt │ └── FrescoVitoSlideshowDrawable.kt ├── nativecode/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ └── nativecode/ │ ├── CircularBitmapTransformation.kt │ └── NativeCircularBitmapRounding.kt ├── options/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── fresco/ │ │ └── vito/ │ │ └── options/ │ │ ├── AnimatedOptions.kt │ │ ├── BitmapConfig.kt │ │ ├── BorderOptions.kt │ │ ├── DecodedImageOptions.kt │ │ ├── EncodedImageOptions.kt │ │ ├── ImageOptions.kt │ │ ├── ImageOptionsDrawableFactory.kt │ │ └── RoundingOptions.kt │ └── test/ │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ └── options/ │ └── RoundingOptionsTest.kt ├── provider/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ └── provider/ │ ├── FrescoVitoProvider.kt │ ├── components/ │ │ └── FrescoVitoComponents.kt │ ├── impl/ │ │ ├── DefaultFrescoVitoProvider.kt │ │ ├── NoOpCallerContextVerifier.kt │ │ └── kotlin/ │ │ └── KFrescoVitoProvider.kt │ └── setup/ │ └── FrescoVitoSetup.kt ├── renderer/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ └── renderer/ │ ├── CanvasTransformation.kt │ ├── CanvasTransformationHandler.kt │ ├── ImageDataModel.kt │ ├── ImageRenderer.kt │ ├── Shape.kt │ └── util/ │ └── ColorUtils.kt ├── source/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ └── source/ │ ├── BitmapImageSource.kt │ ├── ColorImageSource.kt │ ├── DrawableImageSource.kt │ ├── DrawableResImageSource.kt │ ├── EmptyImageSource.kt │ ├── FirstAvailableImageSource.kt │ ├── ImageSource.kt │ ├── ImageSourceExtras.kt │ ├── ImageSourceProvider.kt │ ├── IncreasingQualityImageSource.kt │ ├── SingleImageSource.kt │ ├── SingleImageSourceImpl.kt │ ├── SmartFetchOptIn.kt │ ├── SmartImageSource.kt │ └── UriImageSource.kt ├── textspan/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ └── textspan/ │ ├── VitoSpan.kt │ └── VitoSpanLoader.kt ├── tools/ │ └── liveeditor/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── fresco/ │ └── vito/ │ └── tools/ │ └── liveeditor/ │ ├── ImageLiveEditor.kt │ ├── ImageOptionsSampleValues.kt │ ├── ImageSelector.kt │ ├── ImageSourceParser.kt │ ├── ImageSourceSampleValues.kt │ ├── ImageSourceSyntaxException.kt │ ├── ImageSourceUiUtil.kt │ ├── ImageTracker.kt │ ├── LiveEditorOnScreenButtonController.kt │ └── LiveEditorUiUtils.kt └── view/ ├── build.gradle ├── gradle.properties └── src/ └── main/ ├── AndroidManifest.xml └── java/ └── com/ └── facebook/ └── fresco/ └── vito/ └── view/ ├── ImageViewWithAspectRatio.kt ├── VitoView.kt ├── impl/ │ └── VitoViewImpl2.kt └── transition/ └── VitoTransition.kt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ We use GitHub Issues for bugs. If you have a non-bug question, please ask on Stack Overflow: http://stackoverflow.com/questions/tagged/fresco --- Please use this template, and delete everything above this line before submitting your issue --- ### Description [FILL THIS OUT: Explain what you did, what you expected to happen, and what actually happens.] ### Reproduction [FILL THIS OUT: How can we reproduce the bug? Provide URLs to relevant images if possible, or a sample project.] ### Solution [OPTIONAL: Do you know what needs to be done to address this issue? Ideally, provide a pull request which fixes this issue.] ### Additional Information * Fresco version: [FILL THIS OUT] * Platform version: [FILL THIS OUT: specific to a particular Android version? Device?] ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ Thanks for submitting a PR! Please read these instructions carefully: - [ ] Explain the **motivation** for making this change. - [ ] Provide a **test plan** demonstrating that the code is solid. - [ ] Match the **code formatting** of the rest of the codebase. - [ ] Target the `main` branch ## Motivation (required) What existing problem does the pull request solve? ## Test Plan (required) A good test plan has the exact commands you ran and their output, provides screenshots or videos if the pull request changes UI or updates the website. See [What is a Test Plan?][1] to learn more. If you have added code that should be tested, add tests. ## Next Steps Sign the [CLA][2], if you haven't already. Small pull requests are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise please split it. Make sure all **tests pass** on [Circle CI][4]. PRs that break tests are unlikely to be merged. For more info, see the [Contributing guide][4]. [1]: https://medium.com/@martinkonicek/what-is-a-test-plan-8bfc840ec171#.y9lcuqqi9 [2]: https://code.facebook.com/cla [3]: http://circleci.com/gh/facebook/fresco [4]: https://github.com/facebook/fresco/blob/main/CONTRIBUTING.md ================================================ FILE: .github/stale.yml ================================================ # Configuration for probot-stale - https://github.com/probot/stale # Number of days of inactivity before an issue becomes stale daysUntilStale: 7 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - bug - enhancement - good first issue - help wanted - question # Label to use when marking an issue as stale staleLabel: stale # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as "bug" or "enhancement" and I will leave it open. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: > Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please feel free to reopen with up-to-date information. only: issues ================================================ FILE: .github/workflows/build.yml ================================================ name: facebook/fresco/build on: push: branches: - main pull_request: branches: - main jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Print pre-setup debug info run: ./ci/print-debug-info.sh - uses: nttld/setup-ndk@v1 id: setup-ndk with: ndk-version: r27b - name: Install JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: | 11 17 cache: gradle - name: Print post-setup debug info run: | ./ci/print-debug-info.sh echo "Printing Gradle Wrapper version" ./gradlew --version env: ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - name: Build & run tests run: ./ci/build-and-test.sh env: ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - name: Copy Results run: | mkdir -p ./gh_actions/test-results/junit find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ./gh_actions/test-results/junit \; - uses: actions/upload-artifact@v6 with: path: "./gh_actions/test-results" ================================================ FILE: .github/workflows/gradle-wrapper-validation.yml ================================================ name: "Validate Gradle Wrapper" on: [push, pull_request] jobs: validation: name: "Validation" runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: gradle/actions/wrapper-validation@v3 ================================================ FILE: .github/workflows/release.yml ================================================ name: Publish on: push: tags: - v* workflow_dispatch: inputs: tag: description: "Tag to upload artifacts to" required: false jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Install JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: | 11 17 cache: gradle - name: Setup Android SDK uses: android-actions/setup-android@v3 - name: Install NDK id: setup-ndk uses: nttld/setup-ndk@v1 with: ndk-version: r27b - name: Register NDK run: | echo $PATH echo 'ndk.path=/opt/hostedtoolcache/ndk/r27b/x64' >> local.properties - name: Write GPG Sec Ring run: echo '${{ secrets.GPG_KEY_CONTENTS }}' | base64 -d > /tmp/secring.gpg - name: Update gradle.properties run: echo -e "signing.secretKeyRingFile=/tmp/secring.gpg\nsigning.keyId=${{ secrets.SIGNING_KEY_ID }}\nsigning.password=${{ secrets.SIGNING_PASSWORD }}\nmavenCentralPassword=${{ secrets.SONATYPE_NEXUS_PASSWORD }}\nmavenCentralUsername=${{ secrets.SONATYPE_NEXUS_USERNAME }}" >> gradle.properties - name: Upload Android Archives run: ./gradlew publish --no-daemon --no-parallel --info --stacktrace env: ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - name: Release and close run: ./gradlew closeAndReleaseRepository env: ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - name: Clean secrets if: always() run: rm /tmp/secring.gpg ================================================ FILE: .gitignore ================================================ .gradle .DS_Store .idea build/ local.properties localhost/ obj/ *.iml Gemfile.lock _site/ # Kotlin 2.0 # https://kotlinlang.org/docs/whatsnew20.html#new-directory-for-kotlin-data-in-gradle-projects .kotlin ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Fresco We want to make contributing to this project as easy and transparent as possible. ## Security bugs Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a GitHub issue. ## Pull Requests We welcome pull requests. 1. Fork the repo and create your branch from `main`. 2. If you've added code that should be tested, add tests. 3. If you've changed APIs, update the documentation. 4. Make sure the test suite passes. 5. Make sure your code passes lint. 6. If you haven't already, complete the [Contributor License Agreement](https://code.facebook.com/cla) ("CLA"). ## Getting started In Android Studio, choose `File > Open..`. and select the `fresco` folder. ### Specify a path to the NDK Fresco uses native code for a few features. To build Fresco you'll need to specify the path to the NDK. In Android Studio, go to `File > Project Structure` and in the dialog set the `Android NDK location`. Android Studio stores the NDK location in to your `local.properties` file. ### Run a sample app Select the **Showcase** app and click run: ![Running a sample Fresco app](https://cloud.githubusercontent.com/assets/346214/24415877/d48d894c-13da-11e7-8601-09627661de67.png) You can use the drawer to select one of the demos: Fresco showcase app Now you can change any code in Fresco and see the changes in the app. Have fun hacking on Fresco! 😎 ## Testing your changes You can check your code compiles using: ``` cd fresco ./gradlew assembleDebug ``` You can run tests locally using: ``` cd fresco ./gradlew test ``` Circle CI will run the same tests and report on your pull request. ## Contributor License Agreement ("CLA") In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects. Complete your CLA here: . ## Our Development Process Each pull request is first submitted into Facebook's internal repositories by a Facebook team member. Once the commit has successfully passed Facebook's internal test suite, it will be exported back out from Facebook's repository. We endeavour to do this as soon as possible for all commits. ## Coding Style * 2 spaces for indentation rather than tabs * 100 character line length * Although officially archived, we still follow the practice of Oracle's [Coding Conventions for the Java Programming Language](http://www.oracle.com/technetwork/java/javase/documentation/codeconvtoc-136057.html). ## License By contributing to Fresco, you agree that your contributions will be licensed under its MIT license. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) Meta Platforms, Inc. and affiliates. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Fresco Fresco Logo [![Build Status](https://github.com/facebook/fresco/actions/workflows/build.yml/badge.svg?event=push)](https://github.com/facebook/fresco/actions/workflows/build.yml?query=event%3Apush) [![License](https://img.shields.io/badge/license-MIT-brightgreen)](https://github.com/facebook/fresco/blob/main/LICENSE) Fresco is a powerful system for displaying images in Android applications. Fresco takes care of image loading and display, so you don't have to. It will load images from the network, local storage, or local resources, and display a placeholder until the image has arrived. It has two levels of cache; one in memory and another in internal storage. In Android 4.x and lower, Fresco puts images in a special region of Android memory. This lets your application run faster - and suffer the dreaded `OutOfMemoryError` much less often. Fresco also supports: * streaming of progressive JPEGs * display of animated GIFs and WebPs * extensive customization of image loading and display * and much more! Find out more at our [website](http://frescolib.org/index.html). ## Requirements Fresco can be included in any Android application. Fresco supports Android 2.3 (Gingerbread) and later. ## Using Fresco in your application If you are building with Gradle, simply add the following line to the `dependencies` section of your `build.gradle` file: ```groovy implementation 'com.facebook.fresco:fresco:3.6.0' ``` For full details, visit the documentation on our web site, available in English and Chinese: ## Join the Fresco community Please use our [issues page](https://github.com/facebook/fresco/issues) to let us know of any problems. For pull requests, please see the [CONTRIBUTING](https://github.com/facebook/fresco/blob/main/CONTRIBUTING.md) file for information on how to help out. See our [documentation](http://frescolib.org/docs/building-from-source.html) for information on how to build from source. ## License Fresco is [MIT-licensed](https://github.com/facebook/fresco/blob/main/LICENSE). ================================================ FILE: animated-base/.gitignore ================================================ nativedeps/ ================================================ FILE: animated-base/build.gradle ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import com.facebook.fresco.buildsrc.Deps import com.facebook.fresco.buildsrc.GradleDeps import com.facebook.fresco.buildsrc.TestDeps apply plugin: 'com.android.library' apply plugin: 'kotlin-android' kotlin { jvmToolchain(11) } dependencies { compileOnly Deps.inferAnnotation compileOnly Deps.javaxAnnotation compileOnly Deps.jsr305 compileOnly Deps.AndroidX.androidxAnnotation api project(':fbcore') api project(':imagepipeline-base') api project(':imagepipeline') api project(':imagepipeline-native') api project(':memory-types:ashmem') api project(':memory-types:nativememory') api project(':memory-types:simple') api project(':animated-drawable') implementation Deps.Bolts.tasks implementation project(':vito:core') implementation project(':vito:options') implementation project(':middleware') testCompileOnly Deps.inferAnnotation testImplementation project(':imagepipeline-test') testImplementation project(':imagepipeline-base-test') testImplementation TestDeps.assertjCore testImplementation TestDeps.junit testImplementation TestDeps.festAssertCore testImplementation TestDeps.mockitoCore3 testImplementation TestDeps.mockitoInline3 testImplementation TestDeps.mockitoKotlin3 testImplementation(TestDeps.robolectric) { exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'org.apache.httpcomponents', module: 'httpclient' } } android { ndkVersion GradleDeps.Native.version buildToolsVersion FrescoConfig.buildToolsVersion compileSdkVersion FrescoConfig.compileSdkVersion namespace "com.facebook.imagepipeline.animated" defaultConfig { minSdkVersion FrescoConfig.minSdkVersion targetSdkVersion FrescoConfig.targetSdkVersion } lintOptions { abortOnError false } testOptions { unitTests.returnDefaultValues = true } } apply plugin: "com.vanniktech.maven.publish" ================================================ FILE: animated-base/gradle.properties ================================================ POM_NAME=AnimatedBase POM_DESCRIPTION=Base classes for animation support POM_ARTIFACT_ID=animated-base POM_PACKAGING=aar ================================================ FILE: animated-base/src/main/AndroidManifest.xml ================================================ ================================================ FILE: animated-base/src/main/java/com/facebook/fresco/animation/bitmap/cache/AnimationFrameCacheKey.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.cache import android.net.Uri import com.facebook.cache.common.CacheKey /* Frame cache key for animation */ class AnimationFrameCacheKey @JvmOverloads constructor(imageId: Int, private val deepEquals: Boolean = false) : CacheKey { private val animationUriString: String = URI_PREFIX + imageId override fun containsUri(uri: Uri): Boolean = uri.toString().startsWith(animationUriString) override fun getUriString(): String = animationUriString override fun isResourceIdForDebugging(): Boolean = false override fun equals(o: Any?): Boolean { if (!deepEquals) { return super.equals(o) } if (this === o) { return true } if (o == null || javaClass != o.javaClass) { return false } val that = o as AnimationFrameCacheKey return animationUriString == that.animationUriString } override fun hashCode(): Int { if (!deepEquals) { return super.hashCode() } return animationUriString.hashCode() } companion object { private const val URI_PREFIX = "anim://" } } ================================================ FILE: animated-base/src/main/java/com/facebook/fresco/animation/bitmap/cache/FrescoFrameCache.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.cache import android.graphics.Bitmap import android.util.SparseArray import androidx.annotation.VisibleForTesting import com.facebook.common.logging.FLog import com.facebook.common.references.CloseableReference import com.facebook.fresco.animation.bitmap.BitmapAnimationBackend import com.facebook.fresco.animation.bitmap.BitmapAnimationBackend.FrameType import com.facebook.fresco.animation.bitmap.BitmapFrameCache import com.facebook.fresco.animation.bitmap.BitmapFrameCache.FrameCacheListener import com.facebook.imagepipeline.animated.impl.AnimatedFrameCache import com.facebook.imagepipeline.image.CloseableBitmap import com.facebook.imagepipeline.image.CloseableImage import com.facebook.imagepipeline.image.CloseableStaticBitmap import com.facebook.imagepipeline.image.ImmutableQualityInfo import com.facebook.imageutils.BitmapUtil import javax.annotation.concurrent.GuardedBy /** Bitmap frame cache that uses Fresco's [AnimatedFrameCache] to cache frames. */ class FrescoFrameCache( private val animatedFrameCache: AnimatedFrameCache, private val enableBitmapReusing: Boolean, ) : BitmapFrameCache { @GuardedBy("this") private val preparedPendingFrames = SparseArray?>() @GuardedBy("this") private var lastRenderedItem: CloseableReference? = null @Synchronized override fun getCachedFrame(frameNumber: Int): CloseableReference? = convertToBitmapReferenceAndClose(animatedFrameCache[frameNumber]) @Synchronized override fun getFallbackFrame(frameNumber: Int): CloseableReference? = convertToBitmapReferenceAndClose(CloseableReference.cloneOrNull(lastRenderedItem)) @Synchronized override fun getBitmapToReuseForFrame( frameNumber: Int, width: Int, height: Int, ): CloseableReference? { if (!enableBitmapReusing) { return null } return convertToBitmapReferenceAndClose(animatedFrameCache.forReuse) } @Synchronized override fun contains(frameNumber: Int): Boolean = animatedFrameCache.contains(frameNumber) @get:Synchronized override val sizeInBytes: Int get() = // This currently does not include the size of the animated frame cache getBitmapSizeBytes(lastRenderedItem) + preparedPendingFramesSizeBytes @Synchronized override fun clear() { CloseableReference.closeSafely(lastRenderedItem) lastRenderedItem = null for (i in 0 until preparedPendingFrames.size()) { CloseableReference.closeSafely(preparedPendingFrames.valueAt(i)) } preparedPendingFrames.clear() // The frame cache will free items when needed } @Synchronized override fun onFrameRendered( frameNumber: Int, bitmapReference: CloseableReference, @BitmapAnimationBackend.FrameType frameType: Int, ) { checkNotNull(bitmapReference) // Close up prepared references. removePreparedReference(frameNumber) // Create the new image reference and cache it. var closableReference: CloseableReference? = null try { closableReference = createImageReference(bitmapReference) if (closableReference != null) { CloseableReference.closeSafely(lastRenderedItem) lastRenderedItem = animatedFrameCache.cache(frameNumber, closableReference) } } finally { CloseableReference.closeSafely(closableReference) } } @Synchronized override fun onFramePrepared( frameNumber: Int, bitmapReference: CloseableReference, @BitmapAnimationBackend.FrameType frameType: Int, ) { checkNotNull(bitmapReference) var closableReference: CloseableReference? = null try { closableReference = createImageReference(bitmapReference) if (closableReference == null) { return } val newReference = animatedFrameCache.cache(frameNumber, closableReference) if (CloseableReference.isValid(newReference)) { val oldReference = preparedPendingFrames[frameNumber] CloseableReference.closeSafely(oldReference) // For performance reasons, we don't clone the reference and close the original one // but cache the reference directly. preparedPendingFrames.put(frameNumber, newReference) FLog.v( TAG, "cachePreparedFrame(%d) cached. Pending frames: %s", frameNumber, preparedPendingFrames, ) } } finally { CloseableReference.closeSafely(closableReference) } } override fun setFrameCacheListener(frameCacheListener: FrameCacheListener?) { // TODO (t15557326) Not supported for now } @get:Synchronized private val preparedPendingFramesSizeBytes: Int get() { var size = 0 for (i in 0 until preparedPendingFrames.size()) { size += getBitmapSizeBytes(preparedPendingFrames.valueAt(i)) } return size } @Synchronized private fun removePreparedReference(frameNumber: Int) { val existingPendingReference = preparedPendingFrames[frameNumber] if (existingPendingReference != null) { preparedPendingFrames.delete(frameNumber) CloseableReference.closeSafely(existingPendingReference) FLog.v( TAG, "removePreparedReference(%d) removed. Pending frames: %s", frameNumber, preparedPendingFrames, ) } } override fun onAnimationPrepared(frameBitmaps: Map>): Boolean = true override fun isAnimationReady(): Boolean = false companion object { private val TAG: Class<*> = FrescoFrameCache::class.java /** * Converts the given image reference to a bitmap reference and closes the original image * reference. * * @param closeableImage the image to convert. It will be closed afterwards and will be invalid * @return the closeable bitmap reference to be used */ @JvmStatic @VisibleForTesting fun convertToBitmapReferenceAndClose( closeableImage: CloseableReference? ): CloseableReference? { try { if ( CloseableReference.isValid(closeableImage) && closeableImage!!.get() is CloseableStaticBitmap ) { val closeableStaticBitmap = closeableImage.get() as CloseableStaticBitmap if (closeableStaticBitmap != null) { // We return a clone of the underlying bitmap reference that has to be manually closed // and then close the passed CloseableStaticBitmap in order to preserve correct // cache size calculations. return closeableStaticBitmap.cloneUnderlyingBitmapReference() } } // Not a bitmap reference, so we return null return null } finally { CloseableReference.closeSafely(closeableImage) } } private fun getBitmapSizeBytes(imageReference: CloseableReference?): Int { if (!CloseableReference.isValid(imageReference)) { return 0 } return getBitmapSizeBytes(imageReference!!.get()) } private fun getBitmapSizeBytes(image: CloseableImage?): Int { if (image !is CloseableBitmap) { return 0 } return BitmapUtil.getSizeInBytes(image.underlyingBitmap) } private fun createImageReference( bitmapReference: CloseableReference ): CloseableReference? { // The given CloseableStaticBitmap will be cached and then released by the resource releaser // of the closeable reference val closeableImage = CloseableStaticBitmap.of(bitmapReference, ImmutableQualityInfo.FULL_QUALITY, 0) return CloseableReference.of(closeableImage) } } } ================================================ FILE: animated-base/src/main/java/com/facebook/fresco/animation/bitmap/wrapper/AnimatedDrawableBackendAnimationInformation.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.wrapper import com.facebook.fresco.animation.backend.AnimationInformation import com.facebook.imagepipeline.animated.base.AnimatedDrawableBackend /** [AnimationInformation] that wraps an [AnimatedDrawableBackend]. */ class AnimatedDrawableBackendAnimationInformation( private val animatedDrawableBackend: AnimatedDrawableBackend ) : AnimationInformation { override fun getFrameCount(): Int = animatedDrawableBackend.frameCount override fun getFrameDurationMs(frameNumber: Int): Int = animatedDrawableBackend.getDurationMsForFrame(frameNumber) override fun getLoopCount(): Int = animatedDrawableBackend.loopCount override fun getLoopDurationMs(): Int = animatedDrawableBackend.durationMs override fun width(): Int = animatedDrawableBackend.width override fun height(): Int = animatedDrawableBackend.height } ================================================ FILE: animated-base/src/main/java/com/facebook/fresco/animation/bitmap/wrapper/AnimatedDrawableBackendFrameRenderer.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.wrapper import android.graphics.Bitmap import android.graphics.Rect import com.facebook.common.logging.FLog import com.facebook.common.references.CloseableReference import com.facebook.fresco.animation.bitmap.BitmapFrameCache import com.facebook.fresco.animation.bitmap.BitmapFrameRenderer import com.facebook.imagepipeline.animated.base.AnimatedDrawableBackend import com.facebook.imagepipeline.animated.impl.AnimatedImageCompositor import com.facebook.imagepipeline.animated.impl.AnimatedImageCompositor.Callback /** [BitmapFrameRenderer] that wraps around an [AnimatedDrawableBackend]. */ class AnimatedDrawableBackendFrameRenderer( private val bitmapFrameCache: BitmapFrameCache, private var animatedDrawableBackend: AnimatedDrawableBackend, private val isNewRenderImplementation: Boolean, ) : BitmapFrameRenderer { private var animatedImageCompositor: AnimatedImageCompositor private val callback: AnimatedImageCompositor.Callback = object : AnimatedImageCompositor.Callback { override fun onIntermediateResult(frameNumber: Int, bitmap: Bitmap) { // We currently don't cache intermediate bitmaps here } override fun getCachedBitmap(frameNumber: Int): CloseableReference? = bitmapFrameCache.getCachedFrame(frameNumber) } override fun setBounds(bounds: Rect?) { val newBackend = animatedDrawableBackend.forNewBounds(bounds) if (newBackend !== animatedDrawableBackend) { animatedDrawableBackend = newBackend animatedImageCompositor = AnimatedImageCompositor(animatedDrawableBackend, isNewRenderImplementation, callback) } } override val intrinsicWidth: Int get() = animatedDrawableBackend.width override val intrinsicHeight: Int get() = animatedDrawableBackend.height init { animatedImageCompositor = AnimatedImageCompositor( this@AnimatedDrawableBackendFrameRenderer.animatedDrawableBackend, isNewRenderImplementation, callback, ) } override fun renderFrame(frameNumber: Int, targetBitmap: Bitmap): Boolean { try { animatedImageCompositor.renderFrame(frameNumber, targetBitmap) } catch (exception: IllegalStateException) { FLog.e(TAG, exception, "Rendering of frame unsuccessful. Frame number: %d", frameNumber) return false } return true } companion object { private val TAG: Class<*> = AnimatedDrawableBackendFrameRenderer::class.java } } ================================================ FILE: animated-base/src/main/java/com/facebook/fresco/animation/drawable/animator/AnimatedDrawableValueAnimatorHelper.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.drawable.animator import android.animation.ValueAnimator import android.animation.ValueAnimator.AnimatorUpdateListener import android.graphics.drawable.Drawable import com.facebook.fresco.animation.drawable.AnimatedDrawable2 import com.facebook.fresco.animation.drawable.animator.AnimatedDrawable2ValueAnimatorHelper.createValueAnimator /** * Helper class to create [ValueAnimator]s for animated drawables. Currently, this class only * supports API 11 (Honeycomb) and above. * * Supported drawable types: - [AnimatedDrawable2] */ object AnimatedDrawableValueAnimatorHelper { /** * Create a value animator for the given animation drawable and max animation duration in ms. * * @param drawable the drawable to create the animator for * @param maxDurationMs the max duration in ms * @return the animator to use */ @JvmStatic fun createValueAnimator(drawable: Drawable?, maxDurationMs: Int): ValueAnimator? = if (drawable is AnimatedDrawable2) { AnimatedDrawable2ValueAnimatorHelper.createValueAnimator( checkNotNull((drawable as AnimatedDrawable2?)), maxDurationMs, ) } else { null } /** * Create a value animator for the given animation drawable. * * @param drawable the drawable to create the animator for * @return the animator to use */ @JvmStatic fun createValueAnimator(drawable: Drawable?): ValueAnimator? { if (drawable is AnimatedDrawable2) { val animatedDrawable2 = drawable return createValueAnimator( animatedDrawable2, animatedDrawable2.loopCount, animatedDrawable2.loopDurationMs, ) } return null } /** * Create an animator update listener to be used to update the drawable to be animated. * * @param drawable the drawable to create the animator update listener for * @return the listener to use */ @JvmStatic fun createAnimatorUpdateListener(drawable: Drawable?): AnimatorUpdateListener? = if (drawable is AnimatedDrawable2) { AnimatedDrawable2ValueAnimatorHelper.createAnimatorUpdateListener( checkNotNull((drawable as AnimatedDrawable2?)) ) } else { null } } ================================================ FILE: animated-base/src/main/java/com/facebook/fresco/animation/factory/AnimatedFactoryV2Impl.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.factory import android.content.Context import android.graphics.Rect import com.facebook.cache.common.CacheKey import com.facebook.common.executors.DefaultSerialExecutorService import com.facebook.common.executors.SerialExecutorService import com.facebook.common.executors.UiThreadImmediateExecutorService import com.facebook.common.internal.DoNotStrip import com.facebook.common.internal.Supplier import com.facebook.common.internal.Suppliers import com.facebook.common.time.RealtimeSinceBootClock import com.facebook.fresco.animation.bitmap.preparation.ondemandanimation.FrameLoaderListener import com.facebook.fresco.animation.drawable.AnimatedDrawable2 import com.facebook.imagepipeline.animated.base.AnimatedDrawableBackend import com.facebook.imagepipeline.animated.base.AnimatedImageResult import com.facebook.imagepipeline.animated.factory.AnimatedFactory import com.facebook.imagepipeline.animated.impl.AnimatedDrawableBackendImpl import com.facebook.imagepipeline.animated.impl.AnimatedDrawableBackendProvider import com.facebook.imagepipeline.animated.util.AnimatedDrawableUtil import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory import com.facebook.imagepipeline.cache.CountingMemoryCache import com.facebook.imagepipeline.core.ExecutorSupplier import com.facebook.imagepipeline.drawable.DrawableFactory import com.facebook.imagepipeline.image.CloseableImage import javax.annotation.concurrent.NotThreadSafe /** * [AnimatedFactory] implementation for animations v2 that creates [AnimatedDrawable2] drawables. * * This factory handles the creation of animated drawables for GIF and WebP formats. It manages the * backend providers and utilities needed for animation processing. */ @NotThreadSafe @DoNotStrip class AnimatedFactoryV2Impl @DoNotStrip constructor( private val platformBitmapFactory: PlatformBitmapFactory, private val executorSupplier: ExecutorSupplier, private val backingCache: CountingMemoryCache, private val downscaleFrameToDrawableDimensions: Boolean, private val useBufferLoaderStrategy: Boolean, var animationFpsLimit: Int, var bufferLengthMilliseconds: Int, var serialExecutorService: SerialExecutorService?, private val enableBufferFrameLoaderFix: Boolean = false, private val frameLoaderListener: FrameLoaderListener? = null, private val enableSingleFrameRendering: Boolean = false, ) : AnimatedFactory { private var animatedDrawableBackendProvider: AnimatedDrawableBackendProvider? = null private var animatedDrawableUtil: AnimatedDrawableUtil? = null private var animatedDrawableFactory: DrawableFactory? = null override fun getAnimatedDrawableFactory(context: Context?): DrawableFactory? { if (animatedDrawableFactory == null) { animatedDrawableFactory = createDrawableFactory() } return animatedDrawableFactory } private fun createDrawableFactory(): DefaultBitmapAnimationDrawableFactory { val cachingStrategySupplier: Supplier = Supplier { DefaultBitmapAnimationDrawableFactory.CACHING_STRATEGY_FRESCO_CACHE_NO_REUSING } val finalSerialExecutorService = serialExecutorService ?: DefaultSerialExecutorService(executorSupplier.forDecode()) val numberOfFramesToPrepareSupplier: Supplier = Supplier { NUMBER_OF_FRAMES_TO_PREPARE } val useDeepEquals = Suppliers.BOOLEAN_FALSE return DefaultBitmapAnimationDrawableFactory( getAnimatedDrawableBackendProvider(), UiThreadImmediateExecutorService.getInstance(), finalSerialExecutorService, RealtimeSinceBootClock.get(), platformBitmapFactory, backingCache, cachingStrategySupplier, numberOfFramesToPrepareSupplier, useDeepEquals, Suppliers.of(useBufferLoaderStrategy), Suppliers.of(downscaleFrameToDrawableDimensions), Suppliers.of(animationFpsLimit), Suppliers.of(bufferLengthMilliseconds), null, enableBufferFrameLoaderFix, frameLoaderListener, enableSingleFrameRendering, ) } private fun getAnimatedDrawableUtil(): AnimatedDrawableUtil { return animatedDrawableUtil ?: AnimatedDrawableUtil().also { animatedDrawableUtil = it } } private fun getAnimatedDrawableBackendProvider(): AnimatedDrawableBackendProvider { if (animatedDrawableBackendProvider == null) { animatedDrawableBackendProvider = object : AnimatedDrawableBackendProvider { override fun get( animatedImageResult: AnimatedImageResult, bounds: Rect?, ): AnimatedDrawableBackend { return AnimatedDrawableBackendImpl( getAnimatedDrawableUtil(), animatedImageResult, bounds, downscaleFrameToDrawableDimensions, ) } } } return animatedDrawableBackendProvider as AnimatedDrawableBackendProvider } companion object { private const val NUMBER_OF_FRAMES_TO_PREPARE = 3 } } ================================================ FILE: animated-base/src/main/java/com/facebook/fresco/animation/factory/DefaultBitmapAnimationDrawableFactory.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.factory import android.content.res.Resources import android.graphics.Bitmap import android.graphics.Rect import android.graphics.drawable.Drawable import com.facebook.cache.common.CacheKey import com.facebook.common.internal.Preconditions import com.facebook.common.internal.Supplier import com.facebook.common.internal.Suppliers import com.facebook.common.time.MonotonicClock import com.facebook.fresco.animation.backend.AnimationBackend import com.facebook.fresco.animation.backend.AnimationBackendDelegateWithInactivityCheck import com.facebook.fresco.animation.bitmap.BitmapAnimationBackend import com.facebook.fresco.animation.bitmap.BitmapFrameCache import com.facebook.fresco.animation.bitmap.BitmapFrameRenderer import com.facebook.fresco.animation.bitmap.cache.AnimationFrameCacheKey import com.facebook.fresco.animation.bitmap.cache.FrescoFrameCache import com.facebook.fresco.animation.bitmap.cache.KeepLastFrameCache import com.facebook.fresco.animation.bitmap.cache.NoOpCache import com.facebook.fresco.animation.bitmap.preparation.BitmapFramePreparationStrategy import com.facebook.fresco.animation.bitmap.preparation.BitmapFramePreparer import com.facebook.fresco.animation.bitmap.preparation.DefaultBitmapFramePreparer import com.facebook.fresco.animation.bitmap.preparation.FixedNumberBitmapFramePreparationStrategy import com.facebook.fresco.animation.bitmap.preparation.FrameLoaderStrategy import com.facebook.fresco.animation.bitmap.preparation.ondemandanimation.FrameLoaderFactory import com.facebook.fresco.animation.bitmap.preparation.ondemandanimation.FrameLoaderListener import com.facebook.fresco.animation.bitmap.wrapper.AnimatedDrawableBackendAnimationInformation import com.facebook.fresco.animation.bitmap.wrapper.AnimatedDrawableBackendFrameRenderer import com.facebook.fresco.animation.drawable.AnimatedDrawable2 import com.facebook.fresco.animation.drawable.KAnimatedDrawable2 import com.facebook.fresco.middleware.HasExtraData import com.facebook.fresco.vito.core.AnimatedImagePerfLoggingListener import com.facebook.fresco.vito.options.ImageOptions import com.facebook.fresco.vito.options.ImageOptionsDrawableFactory import com.facebook.imagepipeline.animated.base.AnimatedDrawableBackend import com.facebook.imagepipeline.animated.base.AnimatedImageResult import com.facebook.imagepipeline.animated.impl.AnimatedDrawableBackendProvider import com.facebook.imagepipeline.animated.impl.AnimatedFrameCache import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory import com.facebook.imagepipeline.cache.CountingMemoryCache import com.facebook.imagepipeline.drawable.DrawableFactory import com.facebook.imagepipeline.image.CloseableAnimatedImage import com.facebook.imagepipeline.image.CloseableImage import java.util.concurrent.ExecutorService import java.util.concurrent.ScheduledExecutorService /** Animation factory for [AnimatedDrawable2]. */ class DefaultBitmapAnimationDrawableFactory( private val animatedDrawableBackendProvider: AnimatedDrawableBackendProvider, private val scheduledExecutorServiceForUiThread: ScheduledExecutorService, private val executorServiceForFramePreparing: ExecutorService, private val monotonicClock: MonotonicClock, private val platformBitmapFactory: PlatformBitmapFactory, private val backingCache: CountingMemoryCache?, private val cachingStrategySupplier: Supplier, private val numberOfFramesToPrepareSupplier: Supplier, private val useDeepEqualsForCacheKey: Supplier, private val useNewBitmapRender: Supplier, private val downscaleFrameToDrawableDimensions: Supplier, private val animationFpsLimit: Supplier, private val bufferLengthMilliseconds: Supplier, private val animatedImagePerfLoggingListener: AnimatedImagePerfLoggingListener? = null, private val enableBufferFrameLoaderFix: Boolean = false, private val frameLoaderListener: FrameLoaderListener? = null, private val enableSingleFrameRendering: Boolean = false, ) : DrawableFactory, ImageOptionsDrawableFactory { // Change the value to true to use KAnimatedDrawable2.kt private val useRendererAnimatedDrawable: Supplier = Suppliers.BOOLEAN_FALSE override fun supportsImageType(image: CloseableImage): Boolean { return image is CloseableAnimatedImage } override fun createDrawable(image: CloseableImage): Drawable? { if (!supportsImageType(image)) { return null } val closeable = image as CloseableAnimatedImage val animatedImage = closeable.image val animationBackend = createAnimationBackend( Preconditions.checkNotNull(closeable.imageResult), animatedImage?.animatedBitmapConfig, null, ) return if (useRendererAnimatedDrawable.get()) { KAnimatedDrawable2(animationBackend) } else { AnimatedDrawable2(animationBackend) } } override fun createDrawable( resources: Resources, closeableImage: CloseableImage, imageOptions: ImageOptions, ): Drawable? { if (!supportsImageType(closeableImage)) { return null } val closeable = closeableImage as CloseableAnimatedImage val animatedImage = closeable.image // Log drawable creation start val imageId = closeable.imageResult?.source ?: "unknown_${System.identityHashCode(closeableImage)}" val startTime = System.nanoTime() animatedImagePerfLoggingListener?.onDrawableCreationStart(imageId, startTime) val animationBackend: AnimationBackend = runCatching { createAnimationBackend( Preconditions.checkNotNull(closeable.imageResult), animatedImage?.animatedBitmapConfig, imageOptions, ) } .getOrElse { e -> when (e) { is NullPointerException -> { val uri = closeableImage.getExtra(HasExtraData.KEY_URI_SOURCE) if (uri != null) { throw NullPointerException("${e.message} uri=${uri}") } else { throw e } } else -> throw e } } val drawable = if (useRendererAnimatedDrawable.get()) { KAnimatedDrawable2(animationBackend) } else { AnimatedDrawable2(animationBackend) } // Log drawable creation success val endTime = System.nanoTime() animatedImagePerfLoggingListener?.onDrawableCreationEnd(imageId, endTime, true) return drawable } /** * Creates an animation backend for the given animated image result. * * @param * animatedImageResult The animated image result to create a backend for * * @param animatedBitmapConfig Optional bitmap configuration for the animation * @param imageOptions Optional image options for customizing the animation * @return An animation backend for the given parameters */ private fun createAnimationBackend( animatedImageResult: AnimatedImageResult, animatedBitmapConfig: Bitmap.Config?, imageOptions: ImageOptions?, ): AnimationBackend { val animatedDrawableBackend = createAnimatedDrawableBackend(animatedImageResult) val animationInfo = AnimatedDrawableBackendAnimationInformation(animatedDrawableBackend) val bitmapFrameCache = createBitmapFrameCache(animatedImageResult) val bitmapFrameRenderer = AnimatedDrawableBackendFrameRenderer( bitmapFrameCache, animatedDrawableBackend, useNewBitmapRender.get(), ) val numberOfFramesToPrefetch = numberOfFramesToPrepareSupplier.get() var bitmapFramePreparationStrategy: BitmapFramePreparationStrategy? = null var bitmapFramePreparer: BitmapFramePreparer? = null if (numberOfFramesToPrefetch > 0) { bitmapFramePreparationStrategy = FixedNumberBitmapFramePreparationStrategy(numberOfFramesToPrefetch) bitmapFramePreparer = createBitmapFramePreparer(bitmapFrameRenderer, animatedBitmapConfig) } val roundingOptions = imageOptions?.roundingOptions val animatedOptions = imageOptions?.animatedOptions if (useNewBitmapRender.get()) { bitmapFramePreparationStrategy = FrameLoaderStrategy( animatedImageResult.source, animationInfo, bitmapFrameRenderer, FrameLoaderFactory( platformBitmapFactory, animationFpsLimit.get(), bufferLengthMilliseconds.get(), enableBufferFrameLoaderFix, frameLoaderListener, enableSingleFrameRendering, ), downscaleFrameToDrawableDimensions.get(), ) } val bitmapAnimationBackend = BitmapAnimationBackend( platformBitmapFactory, bitmapFrameCache, animationInfo, bitmapFrameRenderer, useNewBitmapRender.get(), bitmapFramePreparationStrategy, bitmapFramePreparer, roundingOptions, animatedOptions, ) // Set the animated image performance logging listener bitmapAnimationBackend.setAnimatedImagePerfLoggingListener(animatedImagePerfLoggingListener) return AnimationBackendDelegateWithInactivityCheck.createForBackend( bitmapAnimationBackend, monotonicClock, scheduledExecutorServiceForUiThread, ) } private fun createBitmapFramePreparer( bitmapFrameRenderer: BitmapFrameRenderer, animatedBitmapConfig: Bitmap.Config?, ): BitmapFramePreparer { return DefaultBitmapFramePreparer( platformBitmapFactory, bitmapFrameRenderer, animatedBitmapConfig ?: Bitmap.Config.ARGB_8888, executorServiceForFramePreparing, ) } private fun createAnimatedDrawableBackend( animatedImageResult: AnimatedImageResult ): AnimatedDrawableBackend { val animatedImage = animatedImageResult.image val initialBounds = Rect(0, 0, animatedImage.width, animatedImage.height) return animatedDrawableBackendProvider.get(animatedImageResult, initialBounds) } private fun createBitmapFrameCache(animatedImageResult: AnimatedImageResult): BitmapFrameCache { return when (cachingStrategySupplier.get()) { CACHING_STRATEGY_FRESCO_CACHE -> FrescoFrameCache(createAnimatedFrameCache(animatedImageResult), true) CACHING_STRATEGY_FRESCO_CACHE_NO_REUSING -> FrescoFrameCache(createAnimatedFrameCache(animatedImageResult), false) CACHING_STRATEGY_KEEP_LAST_CACHE -> KeepLastFrameCache() CACHING_STRATEGY_NO_CACHE -> NoOpCache() else -> NoOpCache() } } private fun createAnimatedFrameCache( animatedImageResult: AnimatedImageResult ): AnimatedFrameCache { return AnimatedFrameCache( AnimationFrameCacheKey(animatedImageResult.hashCode(), useDeepEqualsForCacheKey.get()), backingCache ?: throw IllegalStateException("backingCache is null"), ) } companion object { const val CACHING_STRATEGY_NO_CACHE = 0 const val CACHING_STRATEGY_FRESCO_CACHE = 1 const val CACHING_STRATEGY_FRESCO_CACHE_NO_REUSING = 2 const val CACHING_STRATEGY_KEEP_LAST_CACHE = 3 } } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/base/AnimatedDrawableBackend.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.base; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import com.facebook.common.references.CloseableReference; import com.facebook.infer.annotation.Nullsafe; import javax.annotation.Nullable; /** * Interface that {@link com.facebook.fresco.animation.drawable.BaseAnimatedDrawable} uses that * abstracts out the image format. */ @Nullsafe(Nullsafe.Mode.LOCAL) public interface AnimatedDrawableBackend { /** * Gets the original result of the decode. * * @return the original result of the code */ AnimatedImageResult getAnimatedImageResult(); /** * Gets the duration of the animation. * * @return the duration of the animation in milliseconds */ int getDurationMs(); /** * Gets the number of frames in the animation. * * @return the number of frames in the animation */ int getFrameCount(); /** * Gets the number of loops to run the animation for. * * @return the number of loops, or 0 to indicate infinite */ int getLoopCount(); /** * Gets the width of the image. * * @return the width of the image */ int getWidth(); /** * Gets the height of the image. * * @return the height of the image */ int getHeight(); /** * Gets the rendered width of the image. This may be smaller than the underlying image width if * the image is being rendered to a small bounds or to reduce memory requirements. * * @return the rendered width of the image */ int getRenderedWidth(); /** * Gets the rendered height of the image. This may be smaller than the underlying image height if * the image is being rendered to a small bounds or to reduce memory requirements. * * @return the rendered height of the image */ int getRenderedHeight(); /** * Gets info about the specified frame. * * @param frameNumber the frame number (0-based) * @return the frame info */ AnimatedDrawableFrameInfo getFrameInfo(int frameNumber); /** * Renders the specified frame onto the canvas. * * @param frameNumber the frame number (0-based) * @param canvas the canvas to render onto */ void renderFrame(int frameNumber, Canvas canvas); /** * Renders the specified frame onto the canvas. The idea is the same than renderFrame(...) with * this differences: 1) Creates a new bitmap on each call. This allows to not block threads. 2) * Blend is applied here * * @param frameNumber the frame number (0-based) * @param canvas the canvas to render onto */ void renderDeltas(int frameNumber, Canvas canvas); /** * Gets the frame index for specified timestamp. * * @param timestampMs the timestamp * @return the frame index for the timestamp or the last frame number if the timestamp is outside * the duration of the entire animation */ int getFrameForTimestampMs(int timestampMs); /** * Gets the timestamp relative to the first frame that this frame number starts at. * * @param frameNumber the frame number * @return the time in milliseconds */ int getTimestampMsForFrame(int frameNumber); /** * Gets the duration of the specified frame. * * @param frameNumber the frame number * @return the time in milliseconds */ int getDurationMsForFrame(int frameNumber); /** * Gets the frame number to use for the preview frame. * * @return the frame number to use for the preview frame */ int getFrameForPreview(); /** * Creates a new {@link AnimatedDrawableBackend} with the same parameters but with a new bounds. * * @param bounds the bounds * @return an {@link AnimatedDrawableBackend} with the new bounds (this may be the same instance * if the bounds don't require a new backend) */ AnimatedDrawableBackend forNewBounds(@Nullable Rect bounds); /** * Gets the number of bytes currently used by the backend for caching (for debugging) * * @return the number of bytes currently used by the backend for caching */ int getMemoryUsage(); /** * Gets a pre-decoded frame. This will only return non-null if the {@code ImageDecodeOptions} were * configured to decode all frames at decode time. * * @param frameNumber the index of the frame to get * @return a reference to the preview bitmap which must be released by the caller when done or * null if there is no preview bitmap set */ @Nullable CloseableReference getPreDecodedFrame(int frameNumber); /** * Gets whether it has the decoded frame. This will only return true if the {@code * ImageDecodeOptions} were configured to decode all frames at decode time. * * @param frameNumber the index of the frame to get * @return true if the result has the decoded frame */ boolean hasPreDecodedFrame(int frameNumber); /** Instructs the backend to drop its caches. */ void dropCaches(); } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/base/AnimatedDrawableFrameInfo.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.base; import com.facebook.infer.annotation.Nullsafe; /** Info per frame returned by {@link AnimatedDrawableBackend}. */ @Nullsafe(Nullsafe.Mode.LOCAL) public class AnimatedDrawableFrameInfo { /** How to dispose of the current frame before rendering the next frame. */ public enum DisposalMethod { /** Do not dipose the frame. Leave as-is. */ DISPOSE_DO_NOT, /** Dispose to the background color */ DISPOSE_TO_BACKGROUND, /** Dispose to the previous frame */ DISPOSE_TO_PREVIOUS } /** * Indicates how transparent pixels of the current frame are blended with those of the previous * canvas. */ public enum BlendOperation { /** Blend * */ BLEND_WITH_PREVIOUS, /** Do not blend * */ NO_BLEND, } public final int frameNumber; public final int xOffset; public final int yOffset; public final int width; public final int height; public final BlendOperation blendOperation; public final DisposalMethod disposalMethod; public AnimatedDrawableFrameInfo( int frameNumber, int xOffset, int yOffset, int width, int height, BlendOperation blendOperation, DisposalMethod disposalMethod) { this.frameNumber = frameNumber; this.xOffset = xOffset; this.yOffset = yOffset; this.width = width; this.height = height; this.blendOperation = blendOperation; this.disposalMethod = disposalMethod; } } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/base/AnimatedDrawableOptions.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.base; import com.facebook.infer.annotation.Nullsafe; import javax.annotation.concurrent.Immutable; /** Options for creating {@link com.facebook.fresco.animation.drawable.AnimatedDrawable2}. */ @Nullsafe(Nullsafe.Mode.LOCAL) @Immutable public class AnimatedDrawableOptions { /** Default options. */ public static AnimatedDrawableOptions DEFAULTS = AnimatedDrawableOptions.newBuilder().build(); /** Whether all the rendered frames should be held in memory disregarding other constraints. */ public final boolean forceKeepAllFramesInMemory; /** Whether the drawable can use worker threads to optimistically prefetch frames. */ public final boolean allowPrefetching; /** * The maximum bytes that the backend can use to cache image frames in memory or -1 to use the * default */ public final int maximumBytes; /** Whether to enable additional verbose debugging diagnostics. */ public final boolean enableDebugging; /** Creates {@link AnimatedDrawableOptions} with default options. */ public AnimatedDrawableOptions(AnimatedDrawableOptionsBuilder builder) { this.forceKeepAllFramesInMemory = builder.getForceKeepAllFramesInMemory(); this.allowPrefetching = builder.getAllowPrefetching(); this.maximumBytes = builder.getMaximumBytes(); this.enableDebugging = builder.getEnableDebugging(); } /** * Creates a new builder. * * @return the builder */ public static AnimatedDrawableOptionsBuilder newBuilder() { return new AnimatedDrawableOptionsBuilder(); } } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/base/AnimatedDrawableOptionsBuilder.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.base; import com.facebook.infer.annotation.Nullsafe; /** Builder for {@link AnimatedDrawableOptions}. */ @Nullsafe(Nullsafe.Mode.LOCAL) public class AnimatedDrawableOptionsBuilder { private boolean mForceKeepAllFramesInMemory; private boolean mAllowPrefetching = true; private int mMaximumBytes = -1; private boolean mEnableDebugging; /** * Gets whether all the rendered frames should be held in memory disregarding other constraints. * * @return whether all the rendered frames should be held in memory */ public boolean getForceKeepAllFramesInMemory() { return mForceKeepAllFramesInMemory; } /** * Sets whether all the rendered frames should be held in memory disregarding other constraints. * * @param forceKeepAllFramesInMemory whether to force the frames to be held in memory * @return this builder */ public AnimatedDrawableOptionsBuilder setForceKeepAllFramesInMemory( boolean forceKeepAllFramesInMemory) { mForceKeepAllFramesInMemory = forceKeepAllFramesInMemory; return this; } /** * Gets whether the drawable can use worker threads to optimistically prefetch frames. * * @return whether the backend can use worker threads to prefetch frames */ public boolean getAllowPrefetching() { return mAllowPrefetching; } /** * Sets whether the drawable can use worker threads to optimistically prefetch frames. * * @param allowPrefetching whether the backend can use worker threads to prefetch frames * @return this builder */ public AnimatedDrawableOptionsBuilder setAllowPrefetching(boolean allowPrefetching) { mAllowPrefetching = allowPrefetching; return this; } /** * Gets the maximum bytes that the backend can use to cache image frames in memory. * * @return maximumBytes maximum bytes that the backend can use to cache image frames in memory or * -1 to use the default */ public int getMaximumBytes() { return mMaximumBytes; } /** * Sets the maximum bytes that the backend can use to cache image frames in memory. * * @param maximumBytes maximum bytes that the backend can use to cache image frames in memory or * -1 to use the default * @return this builder */ public AnimatedDrawableOptionsBuilder setMaximumBytes(int maximumBytes) { mMaximumBytes = maximumBytes; return this; } /** * Gets whether to enable additional verbose debugging diagnostics. * * @return whether to enable additional verbose debugging diagnostics */ public boolean getEnableDebugging() { return mEnableDebugging; } /** * Sets whether to enable additional verbose debugging diagnostics. * * @param enableDebugging whether to enable additional verbose debugging diagnostics * @return this builder */ public AnimatedDrawableOptionsBuilder setEnableDebugging(boolean enableDebugging) { mEnableDebugging = enableDebugging; return this; } /** * Builds the immutable options instance. * * @return the options instance */ public AnimatedDrawableOptions build() { return new AnimatedDrawableOptions(this); } } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/base/AnimatedImage.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.base; import android.graphics.Bitmap; import com.facebook.infer.annotation.Nullsafe; import javax.annotation.Nullable; /** Common interface for an animated image. */ @Nullsafe(Nullsafe.Mode.LOCAL) public interface AnimatedImage { int LOOP_COUNT_INFINITE = 0; /** * Disposes the instance. This will free native resources held by this instance. Once called, * other methods on this instance may throw. Note, the underlying native resources may not * actually be freed until all associated instances of {@link AnimatedImageFrame} are disposed or * finalized as well. */ void dispose(); /** * Gets the width of the image (also known as the canvas in WebP nomenclature). * * @return the width of the image */ int getWidth(); /** * Gets the height of the image (also known as the canvas in WebP nomenclature). * * @return the height of the image */ int getHeight(); /** * Gets the number of frames in the image. * * @return the number of frames in the image */ int getFrameCount(); /** * Gets the duration of the animated image. * * @return the duration of the animated image in milliseconds */ int getDuration(); /** * Gets the duration of each frame of the animated image. * * @return an array that is the size of the number of frames containing the duration of each frame * in milliseconds */ int[] getFrameDurations(); /** * Gets the number of loops to run the animation for. * * @return the number of loops, or 0 to indicate infinite */ int getLoopCount(); /** * Creates an {@link AnimatedImageFrame} at the specified index. * * @param frameNumber the index of the frame * @return a newly created {@link AnimatedImageFrame} */ AnimatedImageFrame getFrame(int frameNumber); /** * Returns whether {@link AnimatedImageFrame#renderFrame} supports scaling to arbitrary sizes or * whether scaling must be done externally. * * @return whether rendering supports scaling */ boolean doesRenderSupportScaling(); /** * Gets the size of bytes of the encoded image data (which is the data kept in memory for the * image). * * @return the size in bytes of the encoded image data */ int getSizeInBytes(); /** * Gets the frame info for the specified frame. * * @param frameNumber the frame to get the info for * @return the frame info */ AnimatedDrawableFrameInfo getFrameInfo(int frameNumber); /** * Gets the Bitmap.Config to decode the Bitmap of Animated Frames. * * @return Bitmap.Config for Animated Image */ @Nullable Bitmap.Config getAnimatedBitmapConfig(); } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/base/AnimatedImageFrame.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.base; import android.graphics.Bitmap; import com.facebook.infer.annotation.Nullsafe; /** Common interface for a frame of an animated image. */ @Nullsafe(Nullsafe.Mode.LOCAL) public interface AnimatedImageFrame { /** * Disposes the instance. This will free native resources held by this instance. Once called, * other methods on this instance may throw. Note, the underlying native resources may not * actually be freed until all associated instances {@link AnimatedImage} are disposed or * finalized as well. */ void dispose(); /** * Renders the frame to the specified bitmap. The bitmap must have a width and height that is at * least as big as the specified width and height and it must be in RGBA_8888 color format. * * @param width the width to render to (the image is scaled to this width) * @param height the height to render to (the image is scaled to this height) * @param bitmap the bitmap to render into */ void renderFrame(int width, int height, Bitmap bitmap); /** * Gets the duration of the frame. * * @return the duration of the frame in milliseconds */ int getDurationMs(); /** * Gets the width of the frame. * * @return the width of the frame */ int getWidth(); /** * Gets the height of the frame. * * @return the height of the frame */ int getHeight(); /** * Gets the x-offset of the frame relative to the image canvas. * * @return the x-offset of the frame */ int getXOffset(); /** * Gets the y-offset of the frame relative to the image canvas. * * @return the y-offset of the frame */ int getYOffset(); } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/base/AnimatedImageResult.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.base; import android.graphics.Bitmap; import com.facebook.common.internal.Preconditions; import com.facebook.common.references.CloseableReference; import com.facebook.imagepipeline.transformation.BitmapTransformation; import com.facebook.infer.annotation.Nullsafe; import java.util.List; import javax.annotation.Nullable; /** * The result of decoding an animated image. Contains the {@link AnimatedImage} as well as * additional data. */ @Nullsafe(Nullsafe.Mode.LOCAL) public class AnimatedImageResult { private final AnimatedImage mImage; private final int mFrameForPreview; private @Nullable String mSource; private @Nullable CloseableReference mPreviewBitmap; private @Nullable List> mDecodedFrames; private @Nullable BitmapTransformation mBitmapTransformation; AnimatedImageResult(AnimatedImageResultBuilder builder) { mImage = Preconditions.checkNotNull(builder.getImage(), "AnimatedImage cannot be null"); mFrameForPreview = builder.getFrameForPreview(); mPreviewBitmap = builder.getPreviewBitmap(); mDecodedFrames = builder.getDecodedFrames(); mBitmapTransformation = builder.getBitmapTransformation(); mSource = builder.getSource(); } private AnimatedImageResult(AnimatedImage image) { mImage = Preconditions.checkNotNull(image, "AnimatedImage cannot be null"); mFrameForPreview = 0; } /** * Creates an {@link AnimatedImageResult} with no additional options. * * @param image the image * @return the result */ public static AnimatedImageResult forAnimatedImage(AnimatedImage image) { return new AnimatedImageResult(image); } /** * Creates an {@link AnimatedImageResultBuilder} for creating an {@link AnimatedImageResult}. * * @param image the image * @return the builder */ public static AnimatedImageResultBuilder newBuilder(AnimatedImage image) { return new AnimatedImageResultBuilder(image); } /** * Gets the underlying image. * * @return the underlying image */ public AnimatedImage getImage() { return mImage; } /** * Gets the animated result source uri * * @return source uri */ @Nullable public String getSource() { return mSource; } /** * Gets the frame that should be used for the preview image. If the preview bitmap was fetched, * this is the frame that it's for. * * @return the frame that should be used for the preview image */ public int getFrameForPreview() { return mFrameForPreview; } /** * Gets a decoded frame. This will only return non-null if the {@code ImageDecodeOptions} were * configured to decode all frames at decode time. * * @param index the index of the frame to get * @return a reference to the preview bitmap which must be released by the caller when done or * null if there is no preview bitmap set */ public synchronized @Nullable CloseableReference getDecodedFrame(int index) { if (mDecodedFrames != null) { return CloseableReference.cloneOrNull(mDecodedFrames.get(index)); } return null; } /** * Gets whether it has the decoded frame. This will only return true if the {@code * ImageDecodeOptions} were configured to decode all frames at decode time. * * @param index the index of the frame to get * @return true if the result has the decoded frame */ public synchronized boolean hasDecodedFrame(int index) { return mDecodedFrames != null && mDecodedFrames.get(index) != null; } /** * Gets the transformation that is to be applied to the image, or null if none. * * @return the transformation that is to be applied to the image, or null if none */ public @Nullable BitmapTransformation getBitmapTransformation() { return mBitmapTransformation; } /** * Gets the bitmap for the preview frame. This will only return non-null if the {@code * ImageDecodeOptions} were configured to decode the preview frame. * * @return a reference to the preview bitmap which must be released by the caller when done or * null if there is no preview bitmap set */ @Nullable public synchronized CloseableReference getPreviewBitmap() { return CloseableReference.cloneOrNull(mPreviewBitmap); } /** Disposes the result, which releases the reference to any bitmaps. */ public synchronized void dispose() { CloseableReference.closeSafely(mPreviewBitmap); mPreviewBitmap = null; CloseableReference.closeSafely(mDecodedFrames); mDecodedFrames = null; } } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/base/AnimatedImageResultBuilder.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.base; import android.graphics.Bitmap; import com.facebook.common.references.CloseableReference; import com.facebook.imagepipeline.transformation.BitmapTransformation; import com.facebook.infer.annotation.Nullsafe; import java.util.List; import javax.annotation.Nullable; /** Builder for {@link AnimatedImageResult}. */ @Nullsafe(Nullsafe.Mode.LOCAL) public class AnimatedImageResultBuilder { private final AnimatedImage mImage; private @Nullable CloseableReference mPreviewBitmap; private @Nullable List> mDecodedFrames; private int mFrameForPreview; private @Nullable BitmapTransformation mBitmapTransformation; private @Nullable String mSource; AnimatedImageResultBuilder(AnimatedImage image) { mImage = image; } /** * Gets the image for the result. * * @return the image */ public AnimatedImage getImage() { return mImage; } /** * Gets the preview bitmap. This method returns a new reference. The caller must close it. * * @return the reference to the preview bitmap or null if none was set. This returns a reference * that must be released by the caller */ public @Nullable CloseableReference getPreviewBitmap() { return CloseableReference.cloneOrNull(mPreviewBitmap); } /** * Sets a preview bitmap. * * @param previewBitmap the preview. The method clones the reference. * @return this builder */ public AnimatedImageResultBuilder setPreviewBitmap( @Nullable CloseableReference previewBitmap) { mPreviewBitmap = CloseableReference.cloneOrNull(previewBitmap); return this; } /** * Gets the frame that should be used for the preview image. If the preview bitmap was fetched, * this is the frame that it's for. * * @return the frame that should be used for the preview image */ public int getFrameForPreview() { return mFrameForPreview; } /** * Sets the frame that should be used for the preview image. If the preview bitmap was fetched, * this is the frame that it's for. * * @return the frame that should be used for the preview image */ public AnimatedImageResultBuilder setFrameForPreview(int frameForPreview) { mFrameForPreview = frameForPreview; return this; } /** * Gets the decoded frames. Only used if the {@code ImageDecodeOptions} were configured to decode * all frames at decode time. * * @return the references to the decoded frames or null if none was set. This returns references * that must be released by the caller */ public @Nullable List> getDecodedFrames() { return CloseableReference.cloneOrNull(mDecodedFrames); } /** * @return animated image uri path */ @Nullable public String getSource() { return mSource; } /** * Sets the decoded frames. Only used if the {@code ImageDecodeOptions} were configured to decode * all frames at decode time. * * @param decodedFrames the decoded frames. The method clones the references. */ public AnimatedImageResultBuilder setDecodedFrames( @Nullable List> decodedFrames) { mDecodedFrames = CloseableReference.cloneOrNull(decodedFrames); return this; } /** * Gets the transformation that is to be applied to the image, or null if none. * * @return the transformation that is to be applied to the image, or null if none */ @Nullable public BitmapTransformation getBitmapTransformation() { return mBitmapTransformation; } /** * Sets the transformation that is to be applied to the image. * * @param bitmapTransformation the transformation that is to be applied to the image */ public AnimatedImageResultBuilder setBitmapTransformation( @Nullable BitmapTransformation bitmapTransformation) { mBitmapTransformation = bitmapTransformation; return this; } /** * Sets the source of the animated image * * @param source uri path * @return bitmapTransformation the transformation that is to be applied to the image */ public AnimatedImageResultBuilder setSource(@Nullable String source) { mSource = source; return this; } /** * Builds the {@link AnimatedImageResult}. The preview bitmap and the decoded frames are closed * after build is called, so this should not be called more than once or those fields will be lost * after the first call. * * @return the result */ public AnimatedImageResult build() { try { return new AnimatedImageResult(this); } finally { CloseableReference.closeSafely(mPreviewBitmap); mPreviewBitmap = null; CloseableReference.closeSafely(mDecodedFrames); mDecodedFrames = null; } } } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/base/AnimatedImageValidator.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.base import com.facebook.imagepipeline.image.EncodedImage interface AnimatedImageValidator { fun validateImage(encodedImage: EncodedImage): ValidationResult } sealed class ValidationResult(val isValid: Boolean, val message: String? = null) { object Success : ValidationResult(true) class Failure(message: String) : ValidationResult(false, message) } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/base/package-info.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /** Base classes and abstractions for the animation framework. */ package com.facebook.imagepipeline.animated.base; ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/factory/AnimatedImageDecoder.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.factory import com.facebook.imagepipeline.animated.base.AnimatedImage import com.facebook.imagepipeline.common.ImageDecodeOptions import java.nio.ByteBuffer interface AnimatedImageDecoder { /** * Factory method to create the AnimatedImage from a native pointer * * @param nativePtr The native pointer * @param sizeInBytes The size in byte to allocate * @param options The options for decoding * @return The AnimatedImage allocation */ fun decodeFromNativeMemory( nativePtr: Long, sizeInBytes: Int, options: ImageDecodeOptions, ): AnimatedImage? /** * Factory method to create the AnimatedImage from a ByteBuffer * * @param byteBuffer The ByteBuffer containing the image * @param options The options for decoding * @return The AnimatedImage allocation */ fun decodeFromByteBuffer(byteBuffer: ByteBuffer, options: ImageDecodeOptions): AnimatedImage? } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/factory/AnimatedImageDecoderBase.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.factory import android.annotation.SuppressLint import android.graphics.Bitmap import android.graphics.Color import android.graphics.Rect import com.facebook.common.references.CloseableReference import com.facebook.fresco.vito.core.AnimatedImagePerfLoggingListener import com.facebook.imagepipeline.animated.base.AnimatedDrawableBackend import com.facebook.imagepipeline.animated.base.AnimatedImage import com.facebook.imagepipeline.animated.base.AnimatedImageResult import com.facebook.imagepipeline.animated.impl.AnimatedDrawableBackendImpl import com.facebook.imagepipeline.animated.impl.AnimatedDrawableBackendProvider import com.facebook.imagepipeline.animated.impl.AnimatedImageCompositor import com.facebook.imagepipeline.animated.util.AnimatedDrawableUtil import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory import com.facebook.imagepipeline.common.ImageDecodeOptions import com.facebook.imagepipeline.image.CloseableAnimatedImage import com.facebook.imagepipeline.image.CloseableImage import com.facebook.imagepipeline.image.CloseableStaticBitmap import com.facebook.imagepipeline.image.ImmutableQualityInfo /** * Base class for animated image decoders that provides common functionality for decoding animated * images. This class contains the shared logic for creating preview bitmaps, decoding all frames, * and creating closeable images. */ abstract class AnimatedImageDecoderBase( protected val platformBitmapFactory: PlatformBitmapFactory, protected val downscaleFrameToDrawableDimensions: Boolean, protected val isNewRenderImplementation: Boolean, protected val treatAnimatedImagesAsStateful: Boolean = true, ) { protected val animatedDrawableBackendProvider: AnimatedDrawableBackendProvider = createAnimatedDrawableBackendProvider(downscaleFrameToDrawableDimensions) private var animatedImagePerfLoggingListener: AnimatedImagePerfLoggingListener? = null fun setAnimatedImagePerfLoggingListener(listener: AnimatedImagePerfLoggingListener?) { this.animatedImagePerfLoggingListener = listener } companion object { /** Creates an AnimatedDrawableBackendProvider for animated image decoding. */ fun createAnimatedDrawableBackendProvider( downscaleFrameToDrawableDimensions: Boolean ): AnimatedDrawableBackendProvider { return object : AnimatedDrawableBackendProvider { override fun get( animatedImageResult: AnimatedImageResult, bounds: Rect?, ): AnimatedDrawableBackend { return AnimatedDrawableBackendImpl( AnimatedDrawableUtil(), animatedImageResult, bounds, downscaleFrameToDrawableDimensions, ) } } } } protected fun getCloseableImage( sourceUri: String?, options: ImageDecodeOptions, image: AnimatedImage, bitmapConfig: Bitmap.Config, ): CloseableImage { var decodedFrames: List?>? = null var previewBitmap: CloseableReference? = null try { val frameForPreview = if (options.useLastFrameForPreview) (image.frameCount - 1) else 0 if (options.forceStaticImage) { return CloseableStaticBitmap.of( createPreviewBitmap(image, bitmapConfig, frameForPreview), ImmutableQualityInfo.FULL_QUALITY, 0, ) } if (options.decodeAllFrames) { decodedFrames = decodeAllFrames(image, bitmapConfig) previewBitmap = CloseableReference.cloneOrNull(decodedFrames[frameForPreview]) } if (options.decodePreviewFrame && previewBitmap == null) { previewBitmap = createPreviewBitmap(image, bitmapConfig, frameForPreview) } // Log CloseableAnimatedImage creation start val imageId = sourceUri ?: "unknown_${System.identityHashCode(image)}" val startTime = System.nanoTime() animatedImagePerfLoggingListener?.onCloseableAnimatedImageCreationStart(imageId, startTime) val animatedImageResult = AnimatedImageResult.newBuilder(image) .setPreviewBitmap(previewBitmap) .setFrameForPreview(frameForPreview) .setDecodedFrames(decodedFrames) .setBitmapTransformation(options.bitmapTransformation) .setSource(sourceUri) .build() val closeableAnimatedImage = CloseableAnimatedImage(animatedImageResult, treatAnimatedImagesAsStateful) // Log CloseableAnimatedImage creation success val endTime = System.nanoTime() animatedImagePerfLoggingListener?.onCloseableAnimatedImageCreationEnd( imageId, endTime, true, ) return closeableAnimatedImage } finally { CloseableReference.closeSafely(previewBitmap) CloseableReference.closeSafely(decodedFrames) } } protected fun createPreviewBitmap( image: AnimatedImage, bitmapConfig: Bitmap.Config, frameForPreview: Int, ): CloseableReference { val bitmap = createBitmap(image.width, image.height, bitmapConfig) val tempResult = AnimatedImageResult.forAnimatedImage(image) val drawableBackend = animatedDrawableBackendProvider.get(tempResult, null) val animatedImageCompositor = AnimatedImageCompositor( drawableBackend, isNewRenderImplementation, object : AnimatedImageCompositor.Callback { override fun onIntermediateResult(frameNumber: Int, bitmap: Bitmap) { // Don't care. } override fun getCachedBitmap(frameNumber: Int): CloseableReference? = null }, ) animatedImageCompositor.renderFrame(frameForPreview, bitmap.get()) return bitmap } protected fun decodeAllFrames( image: AnimatedImage, bitmapConfig: Bitmap.Config, ): List?> { val tempResult = AnimatedImageResult.forAnimatedImage(image) val drawableBackend = animatedDrawableBackendProvider.get(tempResult, null) val bitmaps: MutableList?> = ArrayList(drawableBackend.frameCount) val animatedImageCompositor = AnimatedImageCompositor( drawableBackend, isNewRenderImplementation, object : AnimatedImageCompositor.Callback { override fun onIntermediateResult(frameNumber: Int, bitmap: Bitmap) { // Don't care. } override fun getCachedBitmap(frameNumber: Int): CloseableReference? = CloseableReference.cloneOrNull(bitmaps[frameNumber]) }, ) for (i in 0.. { val bitmap = platformBitmapFactory.createBitmapInternal(width, height, bitmapConfig) bitmap.get().eraseColor(Color.TRANSPARENT) bitmap.get().setHasAlpha(true) return bitmap } } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/factory/AnimatedImageFactory.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.factory import android.graphics.Bitmap import com.facebook.imagepipeline.common.ImageDecodeOptions import com.facebook.imagepipeline.image.CloseableImage import com.facebook.imagepipeline.image.EncodedImage /** Decoder for animated images. */ interface AnimatedImageFactory { /** * Decodes a GIF into a CloseableImage. * * @param encodedImage encoded image (native byte array holding the encoded bytes and meta data) * @param options the options for the decode * @param bitmapConfig the Bitmap.Config used to generate the output bitmaps * @return a [CloseableImage] for the GIF image */ fun decodeGif( encodedImage: EncodedImage, options: ImageDecodeOptions, bitmapConfig: Bitmap.Config, ): CloseableImage /** * Decode a WebP into a CloseableImage. * * @param encodedImage encoded image (native byte array holding the encoded bytes and meta data) * @param options the options for the decode * @param bitmapConfig the Bitmap.Config used to generate the output bitmaps * @return a [CloseableImage] for the WebP image */ fun decodeWebP( encodedImage: EncodedImage, options: ImageDecodeOptions, bitmapConfig: Bitmap.Config, ): CloseableImage } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/impl/AnimatedDrawableBackendImpl.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.impl; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import com.facebook.common.internal.Preconditions; import com.facebook.common.references.CloseableReference; import com.facebook.imagepipeline.animated.base.AnimatedDrawableBackend; import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo; import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo.BlendOperation; import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo.DisposalMethod; import com.facebook.imagepipeline.animated.base.AnimatedImage; import com.facebook.imagepipeline.animated.base.AnimatedImageFrame; import com.facebook.imagepipeline.animated.base.AnimatedImageResult; import com.facebook.imagepipeline.animated.util.AnimatedDrawableUtil; import com.facebook.infer.annotation.Nullsafe; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; /** An {@link AnimatedDrawableBackend} that renders {@link AnimatedImage}. */ @Nullsafe(Nullsafe.Mode.LOCAL) public class AnimatedDrawableBackendImpl implements AnimatedDrawableBackend { private final AnimatedDrawableUtil mAnimatedDrawableUtil; private final AnimatedImageResult mAnimatedImageResult; private final AnimatedImage mAnimatedImage; private final Rect mRenderedBounds; private final int[] mFrameDurationsMs; private final int[] mFrameTimestampsMs; private final int mDurationMs; private final AnimatedDrawableFrameInfo[] mFrameInfos; private final Rect mRenderSrcRect = new Rect(); private final Rect mRenderDstRect = new Rect(); private final boolean mDownscaleFrameToDrawableDimensions; private final Paint mTransparentPaint; @GuardedBy("this") private @Nullable Bitmap mTempBitmap; public AnimatedDrawableBackendImpl( AnimatedDrawableUtil animatedDrawableUtil, AnimatedImageResult animatedImageResult, @Nullable Rect bounds, boolean downscaleFrameToDrawableDimensions) { mAnimatedDrawableUtil = animatedDrawableUtil; mAnimatedImageResult = animatedImageResult; mAnimatedImage = animatedImageResult.getImage(); mFrameDurationsMs = mAnimatedImage.getFrameDurations(); mAnimatedDrawableUtil.fixFrameDurations(mFrameDurationsMs); mDurationMs = mAnimatedDrawableUtil.getTotalDurationFromFrameDurations(mFrameDurationsMs); mFrameTimestampsMs = mAnimatedDrawableUtil.getFrameTimeStampsFromDurations(mFrameDurationsMs); mRenderedBounds = getBoundsToUse(mAnimatedImage, bounds); mDownscaleFrameToDrawableDimensions = downscaleFrameToDrawableDimensions; mFrameInfos = new AnimatedDrawableFrameInfo[mAnimatedImage.getFrameCount()]; for (int i = 0; i < mAnimatedImage.getFrameCount(); i++) { mFrameInfos[i] = mAnimatedImage.getFrameInfo(i); } mTransparentPaint = new Paint(); mTransparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); } private static Rect getBoundsToUse(AnimatedImage image, @Nullable Rect targetBounds) { if (targetBounds == null) { return new Rect(0, 0, image.getWidth(), image.getHeight()); } return new Rect( 0, 0, Math.min(targetBounds.width(), image.getWidth()), Math.min(targetBounds.height(), image.getHeight())); } @Override public AnimatedImageResult getAnimatedImageResult() { return mAnimatedImageResult; } @Override public int getDurationMs() { return mDurationMs; } @Override public int getFrameCount() { return mAnimatedImage.getFrameCount(); } @Override public int getLoopCount() { return mAnimatedImage.getLoopCount(); } @Override public int getWidth() { return mAnimatedImage.getWidth(); } @Override public int getHeight() { return mAnimatedImage.getHeight(); } @Override public int getRenderedWidth() { return mRenderedBounds.width(); } @Override public int getRenderedHeight() { return mRenderedBounds.height(); } @Override public AnimatedDrawableFrameInfo getFrameInfo(int frameNumber) { return mFrameInfos[frameNumber]; } @Override public int getFrameForTimestampMs(int timestampMs) { return mAnimatedDrawableUtil.getFrameForTimestampMs(mFrameTimestampsMs, timestampMs); } @Override public int getTimestampMsForFrame(int frameNumber) { Preconditions.checkElementIndex(frameNumber, mFrameTimestampsMs.length); return mFrameTimestampsMs[frameNumber]; } @Override public int getDurationMsForFrame(int frameNumber) { return mFrameDurationsMs[frameNumber]; } @Override public int getFrameForPreview() { return mAnimatedImageResult.getFrameForPreview(); } @Override public AnimatedDrawableBackend forNewBounds(@Nullable Rect bounds) { Rect boundsToUse = getBoundsToUse(mAnimatedImage, bounds); if (boundsToUse.equals(mRenderedBounds)) { // Actual bounds aren't changed. return this; } return new AnimatedDrawableBackendImpl( mAnimatedDrawableUtil, mAnimatedImageResult, bounds, mDownscaleFrameToDrawableDimensions); } @Override public synchronized int getMemoryUsage() { int bytes = 0; if (mTempBitmap != null) { bytes += mAnimatedDrawableUtil.getSizeOfBitmap(mTempBitmap); } bytes += mAnimatedImage.getSizeInBytes(); return bytes; } @Override public @Nullable CloseableReference getPreDecodedFrame(int frameNumber) { return mAnimatedImageResult.getDecodedFrame(frameNumber); } @Override public boolean hasPreDecodedFrame(int index) { return mAnimatedImageResult.hasDecodedFrame(index); } @Override public void renderFrame(int frameNumber, Canvas canvas) { AnimatedImageFrame frame = mAnimatedImage.getFrame(frameNumber); try { if (frame.getWidth() <= 0 || frame.getHeight() <= 0) { return; // Frame not visible -> skipping } if (mAnimatedImage.doesRenderSupportScaling()) { renderImageSupportsScaling(canvas, frame); } else { renderImageDoesNotSupportScaling(canvas, frame); } } finally { frame.dispose(); } } @Override public void renderDeltas(int frameNumber, Canvas canvas) { AnimatedImageFrame frame = mAnimatedImage.getFrame(frameNumber); AnimatedDrawableFrameInfo frameInfo = mAnimatedImage.getFrameInfo(frameNumber); AnimatedDrawableFrameInfo previousFrameInfo = frameNumber == 0 ? null : mAnimatedImage.getFrameInfo(frameNumber - 1); try { if (frame.getWidth() <= 0 || frame.getHeight() <= 0) { return; // Frame not visible -> skipping } if (mAnimatedImage.doesRenderSupportScaling()) { renderScalingFrames(canvas, frame, frameInfo, previousFrameInfo); } else { renderNonScalingFrames(canvas, frame, frameInfo, previousFrameInfo); } } finally { frame.dispose(); } } private synchronized Bitmap prepareTempBitmapForThisSize(int width, int height) { // Different gif frames can be different size, // So we need to ensure we can fit next frame to temporary bitmap if (mTempBitmap != null && (mTempBitmap.getWidth() < width || mTempBitmap.getHeight() < height)) { clearTempBitmap(); } if (mTempBitmap == null) { mTempBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); } mTempBitmap.eraseColor(Color.TRANSPARENT); return mTempBitmap; } private void renderImageSupportsScaling(Canvas canvas, AnimatedImageFrame frame) { double xScale = (double) mRenderedBounds.width() / (double) mAnimatedImage.getWidth(); double yScale = (double) mRenderedBounds.height() / (double) mAnimatedImage.getHeight(); int frameWidth = (int) Math.round(frame.getWidth() * xScale); int frameHeight = (int) Math.round(frame.getHeight() * yScale); int xOffset = (int) (frame.getXOffset() * xScale); int yOffset = (int) (frame.getYOffset() * yScale); synchronized (this) { int renderedWidth = mRenderedBounds.width(); int renderedHeight = mRenderedBounds.height(); // Update the temp bitmap to be >= rendered dimensions prepareTempBitmapForThisSize(renderedWidth, renderedHeight); if (mTempBitmap != null) { frame.renderFrame(frameWidth, frameHeight, mTempBitmap); } // Temporary bitmap can be bigger than frame, so we should draw only rendered area of bitmap mRenderSrcRect.set(0, 0, renderedWidth, renderedHeight); mRenderDstRect.set(xOffset, yOffset, xOffset + renderedWidth, yOffset + renderedHeight); if (mTempBitmap != null) { canvas.drawBitmap(mTempBitmap, mRenderSrcRect, mRenderDstRect, null); } } } private void renderScalingFrames( Canvas canvas, AnimatedImageFrame frame, AnimatedDrawableFrameInfo frameInfo, @Nullable AnimatedDrawableFrameInfo previousFrameInfo) { int assetWidth = mAnimatedImage.getWidth(); int assetHeight = mAnimatedImage.getHeight(); // Find the best scale asset size. Maximum scaleSize would be the assetSize. float scaledWidth = assetWidth; float scaledHeight = assetHeight; // Apply the scale to the frame float xScale = 1f; float yScale = 1f; int frameWidth = frame.getWidth(); int frameHeight = frame.getHeight(); int xOffset = frame.getXOffset(); int yOffset = frame.getYOffset(); // Check if we need to down scale the asset to the canvas size if (scaledWidth > canvas.getWidth() || scaledHeight > canvas.getHeight()) { // Canvas could have wrong sizes as 314573336x200. Then we limit the frame sizes int maxCanvasWidth = Math.min(canvas.getWidth(), assetWidth); int maxCanvasHeight = Math.min(canvas.getHeight(), assetHeight); float assetRatio = assetWidth / (float) assetHeight; if (maxCanvasWidth > maxCanvasHeight) { scaledWidth = maxCanvasWidth; scaledHeight = maxCanvasWidth / assetRatio; } else { scaledWidth = maxCanvasHeight * assetRatio; scaledHeight = maxCanvasHeight; } xScale = scaledWidth / (float) assetWidth; yScale = scaledHeight / (float) assetHeight; frameWidth = (int) Math.ceil(frame.getWidth() * xScale); frameHeight = (int) Math.ceil(frame.getHeight() * yScale); xOffset = (int) Math.ceil(frame.getXOffset() * xScale); yOffset = (int) Math.ceil(frame.getYOffset() * yScale); } Rect renderSrcRect = new Rect(0, 0, frameWidth, frameHeight); Rect renderDstRect = new Rect(xOffset, yOffset, xOffset + frameWidth, yOffset + frameHeight); // Clean previous frame surface if that frame was disposable if (previousFrameInfo != null) { maybeDisposeBackground(canvas, xScale, yScale, previousFrameInfo); } // If current frame is no_blend, then we have to clean their surface before rendering if (frameInfo.blendOperation == BlendOperation.NO_BLEND) { canvas.drawRect(renderDstRect, mTransparentPaint); } synchronized (this) { // Impress the frame in the bitmap Bitmap frameBitmap = prepareTempBitmapForThisSize(frameWidth, frameHeight); frame.renderFrame(frameWidth, frameHeight, frameBitmap); canvas.drawBitmap(frameBitmap, renderSrcRect, renderDstRect, null); } } private void maybeDisposeBackground( Canvas canvas, float xScale, float yScale, AnimatedDrawableFrameInfo previousFrameInfo) { if (previousFrameInfo.disposalMethod == DisposalMethod.DISPOSE_TO_BACKGROUND) { int prevFrameWidth = (int) Math.ceil(previousFrameInfo.width * xScale); int prevFrameHeight = (int) Math.ceil(previousFrameInfo.height * yScale); int prevXOffset = (int) Math.ceil(previousFrameInfo.xOffset * xScale); int prevYOffset = (int) Math.ceil(previousFrameInfo.yOffset * yScale); Rect prevFrameSurface = new Rect( prevXOffset, prevYOffset, prevXOffset + prevFrameWidth, prevYOffset + prevFrameHeight); canvas.drawRect(prevFrameSurface, mTransparentPaint); } } private void renderImageDoesNotSupportScaling(Canvas canvas, AnimatedImageFrame frame) { int frameWidth, frameHeight, xOffset, yOffset; if (mDownscaleFrameToDrawableDimensions) { final int fittedWidth = Math.min(frame.getWidth(), canvas.getWidth()); final int fittedHeight = Math.min(frame.getHeight(), canvas.getHeight()); final float scaleX = (float) frame.getWidth() / (float) fittedWidth; final float scaleY = (float) frame.getHeight() / (float) fittedHeight; final float scale = Math.max(scaleX, scaleY); frameWidth = (int) (frame.getWidth() / scale); frameHeight = (int) (frame.getHeight() / scale); xOffset = (int) (frame.getXOffset() / scale); yOffset = (int) (frame.getYOffset() / scale); } else { frameWidth = frame.getWidth(); frameHeight = frame.getHeight(); xOffset = frame.getXOffset(); yOffset = frame.getYOffset(); } synchronized (this) { mTempBitmap = prepareTempBitmapForThisSize(frameWidth, frameHeight); frame.renderFrame(frameWidth, frameHeight, mTempBitmap); canvas.save(); canvas.translate(xOffset, yOffset); canvas.drawBitmap(mTempBitmap, 0, 0, null); canvas.restore(); } } private void renderNonScalingFrames( Canvas canvas, AnimatedImageFrame frame, AnimatedDrawableFrameInfo frameInfo, @Nullable AnimatedDrawableFrameInfo previousFrameInfo) { if (mRenderedBounds == null || mRenderedBounds.width() <= 0 || mRenderedBounds.height() <= 0) { return; } float scale = (float) canvas.getWidth() / mRenderedBounds.width(); // Clean previous frame surface if that frame was disposable if (previousFrameInfo != null) { maybeDisposeBackground(canvas, scale, scale, previousFrameInfo); } // Prepare the new frame int frameWidth = frame.getWidth(); int frameHeight = frame.getHeight(); Rect src = new Rect(0, 0, frameWidth, frameHeight); int resizedWidth = (int) (frameWidth * scale); int resizedHeight = (int) (frameHeight * scale); int xOffset = (int) (frame.getXOffset() * scale); int yOffset = (int) (frame.getYOffset() * scale); // Clear the canvas if this frame doesnt blend Rect renderDstRect = new Rect(xOffset, yOffset, xOffset + resizedWidth, yOffset + resizedHeight); if (frameInfo.blendOperation == BlendOperation.NO_BLEND) { canvas.drawRect(renderDstRect, mTransparentPaint); } synchronized (this) { // Draw canvas frame Bitmap bitmap = prepareTempBitmapForThisSize(frameWidth, frameHeight); frame.renderFrame(frameWidth, frameHeight, bitmap); canvas.drawBitmap(bitmap, src, renderDstRect, null); } } @Override public synchronized void dropCaches() { clearTempBitmap(); } private synchronized void clearTempBitmap() { if (mTempBitmap != null) { mTempBitmap.recycle(); mTempBitmap = null; } } } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/impl/AnimatedDrawableBackendProvider.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.impl; import android.graphics.Rect; import com.facebook.imagepipeline.animated.base.AnimatedDrawableBackend; import com.facebook.imagepipeline.animated.base.AnimatedImageResult; import com.facebook.infer.annotation.Nullsafe; import javax.annotation.Nullable; /** Assisted provider for {@link AnimatedDrawableBackend}. */ @Nullsafe(Nullsafe.Mode.LOCAL) public interface AnimatedDrawableBackendProvider { /** * Creates a new {@link AnimatedDrawableBackend}. * * @param animatedImageResult the image result. * @param bounds the initial bounds for the drawable * @return a new {@link AnimatedDrawableBackend} */ AnimatedDrawableBackend get(AnimatedImageResult animatedImageResult, @Nullable Rect bounds); } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/impl/AnimatedFrameCache.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.impl; import android.net.Uri; import androidx.annotation.VisibleForTesting; import com.facebook.cache.common.CacheKey; import com.facebook.common.internal.Objects; import com.facebook.common.references.CloseableReference; import com.facebook.imagepipeline.cache.CountingMemoryCache; import com.facebook.imagepipeline.image.CloseableImage; import com.facebook.infer.annotation.Nullsafe; import java.util.Iterator; import java.util.LinkedHashSet; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; /** * Facade to the image memory cache for frames of an animated image. * *

Each animated image should have its own instance of this class. */ @Nullsafe(Nullsafe.Mode.LOCAL) public class AnimatedFrameCache { @VisibleForTesting static class FrameKey implements CacheKey { private final CacheKey mImageCacheKey; private final int mFrameIndex; public FrameKey(CacheKey imageCacheKey, int frameIndex) { mImageCacheKey = imageCacheKey; mFrameIndex = frameIndex; } @Override public String toString() { return Objects.toStringHelper(this) .add("imageCacheKey", mImageCacheKey) .add("frameIndex", mFrameIndex) .toString(); } @Override public boolean equals(@Nullable Object o) { if (o == this) { return true; } if (o instanceof FrameKey) { FrameKey that = (FrameKey) o; return this.mFrameIndex == that.mFrameIndex && this.mImageCacheKey.equals(that.mImageCacheKey); } return false; } @Override public int hashCode() { return mImageCacheKey.hashCode() * 1013 + mFrameIndex; } @Override public boolean containsUri(Uri uri) { return mImageCacheKey.containsUri(uri); } @Override // NULLSAFE_FIXME[Inconsistent Subclass Return Annotation] public @Nullable String getUriString() { return null; } @Override public boolean isResourceIdForDebugging() { return false; } } private final CacheKey mImageCacheKey; private final CountingMemoryCache mBackingCache; private final CountingMemoryCache.EntryStateObserver mEntryStateObserver; @GuardedBy("this") private final LinkedHashSet mFreeItemsPool; public AnimatedFrameCache( CacheKey imageCacheKey, final CountingMemoryCache backingCache) { mImageCacheKey = imageCacheKey; mBackingCache = backingCache; mFreeItemsPool = new LinkedHashSet<>(); mEntryStateObserver = new CountingMemoryCache.EntryStateObserver() { @Override public void onExclusivityChanged(CacheKey key, boolean isExclusive) { AnimatedFrameCache.this.onReusabilityChange(key, isExclusive); } }; } public synchronized void onReusabilityChange(CacheKey key, boolean isReusable) { if (isReusable) { mFreeItemsPool.add(key); } else { mFreeItemsPool.remove(key); } } /** * Caches the image for the given frame index. * *

Important: the client should use the returned reference instead of the original one. It is * the caller's responsibility to close the returned reference once not needed anymore. * * @return the new reference to be used, null if the value cannot be cached */ @Nullable public CloseableReference cache( int frameIndex, CloseableReference imageRef) { return mBackingCache.cache(keyFor(frameIndex), imageRef, mEntryStateObserver); } /** * Gets the image for the given frame index. * *

It is the caller's responsibility to close the returned reference once not needed anymore. */ @Nullable public CloseableReference get(int frameIndex) { return mBackingCache.get(keyFor(frameIndex)); } /** Check whether the cache contains an image for the given frame index. */ public boolean contains(int frameIndex) { return mBackingCache.contains(keyFor(frameIndex)); } /** * Gets the image to be reused, or null if there is no such image. * *

The returned image is the least recently used image that has no more clients referencing it, * and it has not yet been evicted from the cache. * *

The client can freely modify the bitmap of the returned image and can cache it again without * any restrictions. */ @Nullable public CloseableReference getForReuse() { while (true) { CacheKey key = popFirstFreeItemKey(); if (key == null) { return null; } CloseableReference imageRef = mBackingCache.reuse(key); if (imageRef != null) { return imageRef; } } } @Nullable private synchronized CacheKey popFirstFreeItemKey() { CacheKey cacheKey = null; Iterator iterator = mFreeItemsPool.iterator(); if (iterator.hasNext()) { cacheKey = iterator.next(); iterator.remove(); } return cacheKey; } private FrameKey keyFor(int frameIndex) { return new FrameKey(mImageCacheKey, frameIndex); } } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/impl/AnimatedImageCompositor.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.impl; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import com.facebook.common.references.CloseableReference; import com.facebook.imagepipeline.animated.base.AnimatedDrawableBackend; import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo; import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo.BlendOperation; import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo.DisposalMethod; import com.facebook.imagepipeline.animated.base.AnimatedImage; import com.facebook.imagepipeline.animated.base.AnimatedImageResult; import com.facebook.imagepipeline.transformation.BitmapTransformation; import com.facebook.infer.annotation.Nullsafe; import javax.annotation.Nullable; /** * Contains the logic for compositing the frames of an {@link AnimatedImage}. Animated image formats * like GIF and WebP support inter-frame compression where a subsequent frame may require being * blended on a previous frame in order to render the full frame. This class encapsulates the * behavior to be able to render any frame of the image. Designed to work with a cache via a * Callback. */ @Nullsafe(Nullsafe.Mode.LOCAL) public class AnimatedImageCompositor { /** Callback for caching. */ public interface Callback { /** * Called from within {@link #renderFrame} to let the caller know that while trying generate the * requested frame, an earlier frame was generated. This allows the caller to optionally cache * the intermediate result. The caller must copy the Bitmap if it wishes to cache it as {@link * #renderFrame} will continue using it generate the requested frame. * * @param frameNumber the frame number of the intermediate result * @param bitmap the bitmap which must not be modified or directly cached */ void onIntermediateResult(int frameNumber, Bitmap bitmap); /** * Called from within {@link #renderFrame} to ask the caller for a cached bitmap for the * specified frame number. If the caller has the bitmap cached, it can greatly reduce the work * required to render the requested frame. * * @param frameNumber the frame number to get * @return a reference to the bitmap. The ownership of the reference is passed to the caller who * must close it. */ @Nullable CloseableReference getCachedBitmap(int frameNumber); } private final AnimatedDrawableBackend mAnimatedDrawableBackend; private final Callback mCallback; private final Paint mTransparentFillPaint; private final boolean mIsNewRenderImplementation; public AnimatedImageCompositor( AnimatedDrawableBackend animatedDrawableBackend, boolean isNewRenderImplementation, Callback callback) { mAnimatedDrawableBackend = animatedDrawableBackend; mCallback = callback; mIsNewRenderImplementation = isNewRenderImplementation; mTransparentFillPaint = new Paint(); mTransparentFillPaint.setColor(Color.TRANSPARENT); mTransparentFillPaint.setStyle(Paint.Style.FILL); mTransparentFillPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); } public void renderDeltas(int frameNumber, Bitmap baseBitmap) { Canvas canvas = new Canvas(baseBitmap); mAnimatedDrawableBackend.renderDeltas(frameNumber, canvas); } /** * Renders the specified frame. Only should be called on the rendering thread. * * @param frameNumber the frame to render * @param bitmap the bitmap to render into */ public void renderFrame(int frameNumber, Bitmap bitmap) { if (mIsNewRenderImplementation) { renderDeltas(frameNumber, bitmap); return; } Canvas canvas = new Canvas(bitmap); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.SRC); // If blending is required, prepare the canvas with the nearest cached frame. int nextIndex; if (!isKeyFrame(frameNumber)) { // Blending is required. nextIndex points to the next index to render onto the canvas. nextIndex = prepareCanvasWithClosestCachedFrame(frameNumber - 1, canvas); } else { // Blending isn't required. Start at the frame we're trying to render. nextIndex = frameNumber; } // Iterate from nextIndex to the frame number just preceding the one we're trying to render // and composite them in order according to the Disposal Method. for (int index = nextIndex; index < frameNumber; index++) { AnimatedDrawableFrameInfo frameInfo = mAnimatedDrawableBackend.getFrameInfo(index); DisposalMethod disposalMethod = frameInfo.disposalMethod; if (disposalMethod == DisposalMethod.DISPOSE_TO_PREVIOUS) { continue; } if (frameInfo.blendOperation == BlendOperation.NO_BLEND) { disposeToBackground(canvas, frameInfo); } mAnimatedDrawableBackend.renderFrame(index, canvas); mCallback.onIntermediateResult(index, bitmap); if (disposalMethod == DisposalMethod.DISPOSE_TO_BACKGROUND) { disposeToBackground(canvas, frameInfo); } } AnimatedDrawableFrameInfo frameInfo = mAnimatedDrawableBackend.getFrameInfo(frameNumber); if (frameInfo.blendOperation == BlendOperation.NO_BLEND) { disposeToBackground(canvas, frameInfo); } // Finally, we render the current frame. We don't dispose it. mAnimatedDrawableBackend.renderFrame(frameNumber, canvas); maybeApplyTransformation(bitmap); } /** Return value for {@link #isFrameNeededForRendering} used in the compositing logic. */ private enum FrameNeededResult { /** The frame is required to render the next frame */ REQUIRED, /** The frame is not required to render the next frame. */ NOT_REQUIRED, /** Skip this frame and keep going. Used for GIF's DISPOSE_TO_PREVIOUS */ SKIP, /** Stop processing at this frame. This means the image didn't specify the disposal method */ ABORT } /** * Given a frame number, prepares the canvas to render based on the nearest cached frame at or * before the frame. On return the canvas will be prepared as if the nearest cached frame had been * rendered and disposed. The returned index is the next frame that needs to be composited onto * the canvas. * * @param previousFrameNumber the frame number that is ones less than the one we're rendering * @param canvas the canvas to prepare * @return the index of the the next frame to process */ private int prepareCanvasWithClosestCachedFrame(int previousFrameNumber, Canvas canvas) { for (int index = previousFrameNumber; index >= 0; index--) { FrameNeededResult neededResult = isFrameNeededForRendering(index); switch (neededResult) { case REQUIRED: AnimatedDrawableFrameInfo frameInfo = mAnimatedDrawableBackend.getFrameInfo(index); CloseableReference startBitmap = mCallback.getCachedBitmap(index); if (startBitmap != null) { try { canvas.drawBitmap(startBitmap.get(), 0, 0, null); if (frameInfo.disposalMethod == DisposalMethod.DISPOSE_TO_BACKGROUND) { disposeToBackground(canvas, frameInfo); } return index + 1; } finally { if (!mIsNewRenderImplementation) { startBitmap.close(); } } } else { if (isKeyFrame(index)) { return index; } else { // Keep going. break; } } case NOT_REQUIRED: return index + 1; case ABORT: return index; case SKIP: default: // Keep going. } } return 0; } private void disposeToBackground(Canvas canvas, AnimatedDrawableFrameInfo frameInfo) { canvas.drawRect( frameInfo.xOffset, frameInfo.yOffset, frameInfo.xOffset + frameInfo.width, frameInfo.yOffset + frameInfo.height, mTransparentFillPaint); } /** * Returns whether the specified frame is needed for rendering the next frame. This is part of the * compositing logic. See {@link FrameNeededResult} for more info about the results. * * @param index the frame to check * @return whether the frame is required taking into account special conditions */ private FrameNeededResult isFrameNeededForRendering(int index) { AnimatedDrawableFrameInfo frameInfo = mAnimatedDrawableBackend.getFrameInfo(index); DisposalMethod disposalMethod = frameInfo.disposalMethod; if (disposalMethod == DisposalMethod.DISPOSE_DO_NOT) { // Need this frame so keep going. return FrameNeededResult.REQUIRED; } else if (disposalMethod == DisposalMethod.DISPOSE_TO_BACKGROUND) { if (isFullFrame(frameInfo)) { // The frame covered the whole image and we're disposing to background, // so we don't even need to draw this frame. return FrameNeededResult.NOT_REQUIRED; } else { // We need to draw the image. Then erase the part the previous frame covered. // So keep going. return FrameNeededResult.REQUIRED; } } else if (disposalMethod == DisposalMethod.DISPOSE_TO_PREVIOUS) { return FrameNeededResult.SKIP; } else { return FrameNeededResult.ABORT; } } private boolean isKeyFrame(int index) { if (index == 0) { return true; } AnimatedDrawableFrameInfo currFrameInfo = mAnimatedDrawableBackend.getFrameInfo(index); AnimatedDrawableFrameInfo prevFrameInfo = mAnimatedDrawableBackend.getFrameInfo(index - 1); if (currFrameInfo.blendOperation == BlendOperation.NO_BLEND && isFullFrame(currFrameInfo)) { return true; } else return prevFrameInfo.disposalMethod == DisposalMethod.DISPOSE_TO_BACKGROUND && isFullFrame(prevFrameInfo); } private boolean isFullFrame(AnimatedDrawableFrameInfo frameInfo) { return frameInfo.xOffset == 0 && frameInfo.yOffset == 0 && frameInfo.width == mAnimatedDrawableBackend.getRenderedWidth() && frameInfo.height == mAnimatedDrawableBackend.getRenderedHeight(); } private void maybeApplyTransformation(Bitmap bitmap) { AnimatedImageResult animatedImageResult = mAnimatedDrawableBackend.getAnimatedImageResult(); if (animatedImageResult == null) { return; } BitmapTransformation tr = animatedImageResult.getBitmapTransformation(); if (tr == null) { return; } tr.transform(bitmap); } } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/impl/package-info.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /** Base implementations for the animation framework abstractions. */ package com.facebook.imagepipeline.animated.impl; ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/util/AnimatedDrawableUtil.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.util import android.graphics.Bitmap import java.util.Arrays /** Utility methods for AnimatedDrawable. */ class AnimatedDrawableUtil { /** * Adjusts the frame duration array to respect logic for minimum frame duration time. * * @param frameDurationMs the frame duration array */ fun fixFrameDurations(frameDurationMs: IntArray) { // We follow Chrome's behavior which comes from Firefox. // Comment from Chrome's ImageSource.cpp follows: // We follow Firefox's behavior and use a duration of 100 ms for any frames that specify // a duration of <= 10 ms. See and // for more information. for (i in frameDurationMs.indices) { if (frameDurationMs[i] < MIN_FRAME_DURATION_MS) { frameDurationMs[i] = FRAME_DURATION_MS_FOR_MIN } } } /** * Gets the total duration of an image by summing up the duration of the frames. * * @param frameDurationMs the frame duration array * @return the total duration in milliseconds */ fun getTotalDurationFromFrameDurations(frameDurationMs: IntArray): Int { var totalMs = 0 for (i in frameDurationMs.indices) { totalMs += frameDurationMs[i] } return totalMs } /** * Given an array of frame durations, generate an array of timestamps corresponding to when each * frame beings. * * @param frameDurationsMs an array of frame durations * @return an array of timestamps */ fun getFrameTimeStampsFromDurations(frameDurationsMs: IntArray): IntArray { val frameTimestampsMs = IntArray(frameDurationsMs.size) var accumulatedDurationMs = 0 for (i in frameDurationsMs.indices) { frameTimestampsMs[i] = accumulatedDurationMs accumulatedDurationMs += frameDurationsMs[i] } return frameTimestampsMs } /** * Gets the frame index for specified timestamp. * * @param frameTimestampsMs an array of timestamps generated by [getFrameForTimestampMs)] * @param timestampMs the timestamp * @return the frame index for the timestamp or the last frame number if the timestamp is outside * the duration of the entire animation */ fun getFrameForTimestampMs(frameTimestampsMs: IntArray?, timestampMs: Int): Int { val index = Arrays.binarySearch(frameTimestampsMs, timestampMs) return if (index < 0) { -index - 1 - 1 } else { index } } fun getSizeOfBitmap(bitmap: Bitmap): Int = bitmap.allocationByteCount companion object { // See comment in fixFrameDurations below. private const val MIN_FRAME_DURATION_MS = 11 private const val FRAME_DURATION_MS_FOR_MIN = 100 /** * Checks whether the specified frame number is outside the range inclusive of both start and * end. If start <= end, start is within, end is within, and everything in between is within. If * start * > end, start is within, end is within, everything less than start is within and everything * > greater than end is within. This behavior is useful for handling the wrapping case. * * @param startFrame the start frame * @param endFrame the end frame * @param frameNumber the frame number * @return whether the frame is outside the range of [start, end] */ @JvmStatic fun isOutsideRange(startFrame: Int, endFrame: Int, frameNumber: Int): Boolean { if (startFrame == -1 || endFrame == -1) { // This means nothing should pass. return true } val outsideRange = if (startFrame <= endFrame) { frameNumber < startFrame || frameNumber > endFrame } else { // Wrapping frameNumber < startFrame && frameNumber > endFrame } return outsideRange } } } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/animated/util/package-info.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /** Utility classes for the animation framework */ package com.facebook.imagepipeline.animated.util ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/image/CloseableAnimatedImage.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.image; import com.facebook.imagepipeline.animated.base.AnimatedImage; import com.facebook.imagepipeline.animated.base.AnimatedImageResult; import com.facebook.infer.annotation.Nullsafe; import javax.annotation.Nullable; /** * Encapsulates the data needed in order for {@code AnimatedDrawable} to render a {@code * AnimatedImage}. */ @Nullsafe(Nullsafe.Mode.LOCAL) public class CloseableAnimatedImage extends DefaultCloseableImage { private @Nullable AnimatedImageResult mImageResult; private boolean mIsStateful; public CloseableAnimatedImage(AnimatedImageResult imageResult) { this(imageResult, true); } public CloseableAnimatedImage(AnimatedImageResult imageResult, boolean isStateful) { mImageResult = imageResult; mIsStateful = isStateful; } @Override public synchronized int getWidth() { return mImageResult == null ? 0 : mImageResult.getImage().getWidth(); } @Override public synchronized int getHeight() { return mImageResult == null ? 0 : mImageResult.getImage().getHeight(); } @Override public void close() { AnimatedImageResult imageResult; synchronized (this) { if (mImageResult == null) { return; } imageResult = mImageResult; mImageResult = null; } imageResult.dispose(); } @Override public synchronized boolean isClosed() { return mImageResult == null; } @Override public synchronized int getSizeInBytes() { return mImageResult == null ? 0 : mImageResult.getImage().getSizeInBytes(); } @Override public boolean isStateful() { return mIsStateful; } public synchronized @Nullable AnimatedImageResult getImageResult() { return mImageResult; } public synchronized @Nullable AnimatedImage getImage() { return mImageResult == null ? null : mImageResult.getImage(); } } ================================================ FILE: animated-base/src/main/java/com/facebook/imagepipeline/image/package-info.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /** CloseableReference implementation for objects used into the animation framework. */ package com.facebook.imagepipeline.image; ================================================ FILE: animated-base/src/test/java/android/net/http/AndroidHttpClient.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package android.net.http; public class AndroidHttpClient {} ================================================ FILE: animated-base/src/test/java/com/facebook/fresco/animation/bitmap/cache/FrescoFrameCacheTest.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.cache import android.graphics.Bitmap import com.facebook.common.references.CloseableReference import com.facebook.imagepipeline.image.CloseableImage import com.facebook.imagepipeline.image.CloseableStaticBitmap import org.assertj.core.api.Assertions import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner /** Tests [FrescoFrameCache]. */ @RunWith(RobolectricTestRunner::class) class FrescoFrameCacheTest { private lateinit var imageReference: CloseableReference private lateinit var closeableStaticBitmap: CloseableStaticBitmap private lateinit var bitmapReference: CloseableReference private lateinit var bitmapReferenceClone: CloseableReference private lateinit var underlyingBitmap: Bitmap @Before fun setup() { imageReference = mock() closeableStaticBitmap = mock() bitmapReference = mock() bitmapReferenceClone = mock() underlyingBitmap = mock() whenever(bitmapReference.isValid).thenReturn(true) whenever(bitmapReference.get()).thenReturn(underlyingBitmap) whenever(bitmapReferenceClone.isValid).thenReturn(true) whenever(bitmapReferenceClone.get()).thenReturn(underlyingBitmap) whenever(closeableStaticBitmap.isClosed()).thenReturn(false) whenever(closeableStaticBitmap.getUnderlyingBitmap()).thenReturn(underlyingBitmap) whenever(closeableStaticBitmap.convertToBitmapReference()).thenReturn(bitmapReference) whenever(closeableStaticBitmap.cloneUnderlyingBitmapReference()) .thenReturn(bitmapReferenceClone) whenever(imageReference.isValid).thenReturn(true) whenever(imageReference.get()).thenReturn(closeableStaticBitmap) } @Test @Throws(Exception::class) fun testExtractAndClose() { val extractedReference: CloseableReference? = FrescoFrameCache.convertToBitmapReferenceAndClose(imageReference) Assertions.assertThat(extractedReference).isNotNull() extractedReference?.let { ref -> Assertions.assertThat(ref.get()).isEqualTo(underlyingBitmap) ref.close() } verify(imageReference).close() } @Test @Throws(Exception::class) fun testExtractAndClose_whenBitmapRecycled_thenReturnReference() { whenever(underlyingBitmap.isRecycled).thenReturn(true) val extractedReference: CloseableReference? = FrescoFrameCache.convertToBitmapReferenceAndClose(imageReference) // We only detach the reference and do not care if the bitmap is valid Assertions.assertThat(extractedReference).isNotNull() extractedReference?.let { ref -> Assertions.assertThat(ref.get()).isEqualTo(underlyingBitmap) ref.close() } verify(imageReference).close() } @Test @Throws(Exception::class) fun testExtractAndClose_whenBitmapReferenceInvalid_thenReturnReference() { whenever(bitmapReference.isValid).thenReturn(false) val extractedReference: CloseableReference? = FrescoFrameCache.convertToBitmapReferenceAndClose(imageReference) // We only detach the reference and do not care if the bitmap reference is valid Assertions.assertThat(extractedReference).isNotNull() extractedReference?.let { ref -> Assertions.assertThat(ref.get()).isEqualTo(underlyingBitmap) ref.close() } verify(imageReference).close() } @Test @Throws(Exception::class) fun testExtractAndClose_whenCloseableStaticBitmapClosed_thenReturnNull() { whenever(closeableStaticBitmap.isClosed()).thenReturn(true) whenever(closeableStaticBitmap.cloneUnderlyingBitmapReference()).thenReturn(null) val extractedReference: CloseableReference? = FrescoFrameCache.convertToBitmapReferenceAndClose(imageReference) // We only detach the reference and do not care if the bitmap is valid Assertions.assertThat(extractedReference).isNull() verify(imageReference).close() } @Test @Throws(Exception::class) fun testExtractAndClose_whenImageReferenceInvalid_thenReturnNull() { whenever(imageReference.isValid).thenReturn(false) val extractedReference: CloseableReference? = FrescoFrameCache.convertToBitmapReferenceAndClose(imageReference) // We only detach the reference and do not care if the bitmap is valid Assertions.assertThat(extractedReference).isNull() verify(imageReference).close() } @Test @Throws(Exception::class) fun testExtractAndClose_whenInputNull_thenReturnNull() { val extractedReference: CloseableReference? = FrescoFrameCache.convertToBitmapReferenceAndClose(null) Assertions.assertThat(extractedReference).isNull() verifyNoMoreInteractions(imageReference) } @Test @Throws(Exception::class) fun testExtractAndClose_whenCloseableStaticBitmapNull_thenReturnNull() { whenever(imageReference.get()).thenReturn(null) val extractedReference: CloseableReference? = FrescoFrameCache.convertToBitmapReferenceAndClose(imageReference) Assertions.assertThat(extractedReference).isNull() verify(imageReference).close() } } ================================================ FILE: animated-base/src/test/java/com/facebook/fresco/animation/bitmap/wrapper/AnimatedDrawableBackendAnimationInformationTest.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.wrapper import com.facebook.fresco.animation.backend.AnimationBackend import com.facebook.imagepipeline.animated.base.AnimatedDrawableBackend import org.assertj.core.api.Assertions import org.junit.Before import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.whenever /** Tests [AnimatedDrawableBackendAnimationInformation]. */ class AnimatedDrawableBackendAnimationInformationTest { private lateinit var animatedDrawableBackend: AnimatedDrawableBackend private lateinit var animatedDrawableBackendAnimationInformation: AnimatedDrawableBackendAnimationInformation @Before fun setup() { animatedDrawableBackend = mock() animatedDrawableBackendAnimationInformation = AnimatedDrawableBackendAnimationInformation(animatedDrawableBackend) } @Test @Throws(Exception::class) fun testGetFrameCount() { whenever(animatedDrawableBackend.getFrameCount()).thenReturn(123) Assertions.assertThat(animatedDrawableBackendAnimationInformation.getFrameCount()) .isEqualTo(123) } @Test @Throws(Exception::class) fun testGetFrameDurationMs() { whenever(animatedDrawableBackend.getDurationMsForFrame(1)).thenReturn(123) whenever(animatedDrawableBackend.getDurationMsForFrame(2)).thenReturn(200) Assertions.assertThat(animatedDrawableBackendAnimationInformation.getFrameDurationMs(1)) .isEqualTo(123) Assertions.assertThat(animatedDrawableBackendAnimationInformation.getFrameDurationMs(2)) .isEqualTo(200) } @Test @Throws(Exception::class) fun testGetLoopCount() { whenever(animatedDrawableBackend.getLoopCount()).thenReturn(123) Assertions.assertThat(animatedDrawableBackendAnimationInformation.getLoopCount()).isEqualTo(123) } @Test @Throws(Exception::class) fun testGetLoopCountInfinite() { whenever(animatedDrawableBackend.getLoopCount()) .thenReturn(AnimationBackend.LOOP_COUNT_INFINITE) Assertions.assertThat(animatedDrawableBackendAnimationInformation.getLoopCount()) .isEqualTo(AnimationBackend.LOOP_COUNT_INFINITE) } } ================================================ FILE: animated-base/src/test/java/com/facebook/fresco/animation/bitmap/wrapper/AnimatedDrawableBackendFrameRendererTest.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.wrapper import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Rect import com.facebook.fresco.animation.bitmap.BitmapFrameCache import com.facebook.imagepipeline.animated.base.AnimatedDrawableBackend import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo import org.assertj.core.api.Assertions import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner /** Tests [AnimatedDrawableBackendFrameRenderer] */ @RunWith(RobolectricTestRunner::class) class AnimatedDrawableBackendFrameRendererTest { private lateinit var animatedDrawableBackendFrameRenderer: AnimatedDrawableBackendFrameRenderer private lateinit var animatedDrawableBackend: AnimatedDrawableBackend private lateinit var bitmapFrameCache: BitmapFrameCache @Before fun setup() { animatedDrawableBackend = mock() bitmapFrameCache = mock() animatedDrawableBackendFrameRenderer = AnimatedDrawableBackendFrameRenderer(bitmapFrameCache, animatedDrawableBackend, false) } @Test fun testSetBounds() { whenever(animatedDrawableBackend.forNewBounds(any())).thenReturn(animatedDrawableBackend) val bounds: Rect = mock() animatedDrawableBackendFrameRenderer.setBounds(bounds) verify(animatedDrawableBackend).forNewBounds(bounds) } @Test fun testGetIntrinsicWidth() { whenever(animatedDrawableBackend.getWidth()).thenReturn(123) Assertions.assertThat(animatedDrawableBackendFrameRenderer.intrinsicWidth).isEqualTo(123) Assertions.assertThat(animatedDrawableBackendFrameRenderer.intrinsicHeight).isNotEqualTo(123) } @Test fun testGetIntrinsicHeight() { whenever(animatedDrawableBackend.getHeight()).thenReturn(1200) Assertions.assertThat(animatedDrawableBackendFrameRenderer.intrinsicHeight).isEqualTo(1200) Assertions.assertThat(animatedDrawableBackendFrameRenderer.intrinsicWidth).isNotEqualTo(1200) } @Test fun testRenderFrame() { whenever(animatedDrawableBackend.getHeight()).thenReturn(1200) val bitmap: Bitmap = mockBitmap() val animatedDrawableFrameInfo: AnimatedDrawableFrameInfo = mock() whenever(animatedDrawableBackend.getFrameInfo(any())).thenReturn(animatedDrawableFrameInfo) val rendered = animatedDrawableBackendFrameRenderer.renderFrame(0, bitmap) Assertions.assertThat(rendered).isTrue() } @Test fun testRenderFrameUnsuccessful() { val frameNumber = 0 whenever(animatedDrawableBackend.getHeight()).thenReturn(1200) val bitmap: Bitmap = mockBitmap() val animatedDrawableFrameInfo: AnimatedDrawableFrameInfo = mock() whenever(animatedDrawableBackend.getFrameInfo(any())).thenReturn(animatedDrawableFrameInfo) doThrow(IllegalStateException()) .whenever(animatedDrawableBackend) .renderFrame(eq(frameNumber), any()) val rendered = animatedDrawableBackendFrameRenderer.renderFrame(frameNumber, bitmap) Assertions.assertThat(rendered).isFalse() } companion object { private fun mockBitmap(): Bitmap { val mock: Bitmap = mock() whenever(mock.isMutable).thenReturn(true) return mock } } } ================================================ FILE: animated-base/src/test/java/com/facebook/imagepipeline/animated/impl/AnimatedDrawableBackendImplTest.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.impl import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Rect import com.facebook.imagepipeline.animated.base.AnimatedImage import com.facebook.imagepipeline.animated.base.AnimatedImageFrame import com.facebook.imagepipeline.animated.base.AnimatedImageResult import com.facebook.imagepipeline.animated.util.AnimatedDrawableUtil import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.MockedStatic import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class AnimatedDrawableBackendImplTest { private lateinit var animatedDrawableUtil: AnimatedDrawableUtil private lateinit var animatedImageResult: AnimatedImageResult private lateinit var canvas: Canvas private lateinit var image: AnimatedImage private lateinit var frame: AnimatedImageFrame private lateinit var bitmap: Bitmap private lateinit var rect: Rect private lateinit var mockedBitmap: MockedStatic @Before @Throws(Exception::class) fun setUp() { animatedDrawableUtil = mock() animatedImageResult = mock() canvas = mock() image = mock() frame = mock() bitmap = mock() rect = mock() mockedBitmap = Mockito.mockStatic(Bitmap::class.java) whenever(animatedImageResult.image).thenReturn(image) whenever(image.doesRenderSupportScaling()).thenReturn(false) whenever(image.getFrame(any())).thenReturn(frame) mockedBitmap .`when` { Bitmap.createBitmap( Mockito.anyInt(), Mockito.anyInt(), Mockito.any(Bitmap.Config::class.java), ) } .thenReturn(bitmap) } private fun verifyBasic( canvasWidth: Int, canvasHeight: Int, frameOriginalWidth: Int, frameOriginalHeight: Int, frameExpectedRenderedWidth: Int, frameExpectedRenderedHeight: Int, ) { whenever(canvas.width).thenReturn(canvasWidth) whenever(canvas.height).thenReturn(canvasHeight) whenever(frame.width).thenReturn(frameOriginalWidth) whenever(frame.height).thenReturn(frameOriginalHeight) val animatedDrawableBackendImpl = AnimatedDrawableBackendImpl(animatedDrawableUtil, animatedImageResult, rect, true) animatedDrawableBackendImpl.renderFrame(0, canvas) verify(frame).renderFrame(frameExpectedRenderedWidth, frameExpectedRenderedHeight, bitmap) } @After fun tearDownStaticMocks() { mockedBitmap.close() } @Test fun testSimple() { verifyBasic(128, 128, 512, 512, 128, 128) } @Test fun testNoUpscaling() { verifyBasic(128, 128, 16, 16, 16, 16) } @Test fun testNarrow() { verifyBasic(64, 128, 256, 256, 64, 64) } @Test fun testOffsets() { val frameSide = 1024 val canvasSide = 256 val scale = frameSide / canvasSide val frameOffset = 512 whenever(frame.xOffset).thenReturn(frameOffset) whenever(frame.yOffset).thenReturn(frameOffset) verifyBasic(canvasSide, canvasSide, frameSide, frameSide, frameSide / scale, frameSide / scale) verify(canvas).translate((frameOffset / scale).toFloat(), (frameOffset / scale).toFloat()) } } ================================================ FILE: animated-base/src/test/java/com/facebook/imagepipeline/animated/impl/AnimatedFrameCacheTest.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.impl import com.facebook.cache.common.CacheKey import com.facebook.cache.common.SimpleCacheKey import com.facebook.common.internal.Supplier import com.facebook.common.memory.MemoryTrimmableRegistry import com.facebook.common.references.CloseableReference import com.facebook.common.util.ByteConstants import com.facebook.imagepipeline.cache.BitmapMemoryCacheTrimStrategy import com.facebook.imagepipeline.cache.CountingLruBitmapMemoryCacheFactory import com.facebook.imagepipeline.cache.CountingMemoryCache import com.facebook.imagepipeline.cache.MemoryCacheParams import com.facebook.imagepipeline.image.CloseableImage import java.util.concurrent.TimeUnit import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class AnimatedFrameCacheTest { private lateinit var memoryTrimmableRegistry: MemoryTrimmableRegistry private lateinit var memoryCacheParamsSupplier: Supplier private lateinit var cacheKey: CacheKey private lateinit var animatedFrameCache: AnimatedFrameCache private lateinit var frame1: CloseableReference private lateinit var frame2: CloseableReference @Before fun setUp() { memoryTrimmableRegistry = mock() memoryCacheParamsSupplier = mock() val params: MemoryCacheParams = MemoryCacheParams( 4 * ByteConstants.MB, 256, Int.Companion.MAX_VALUE, Int.Companion.MAX_VALUE, Int.Companion.MAX_VALUE, TimeUnit.MINUTES.toMillis(5), ) whenever(memoryCacheParamsSupplier.get()).thenReturn(params) val countingMemoryCache: CountingMemoryCache = CountingLruBitmapMemoryCacheFactory() .create( memoryCacheParamsSupplier, memoryTrimmableRegistry, BitmapMemoryCacheTrimStrategy(), false, false, null, ) cacheKey = SimpleCacheKey("key") animatedFrameCache = AnimatedFrameCache(cacheKey, countingMemoryCache) frame1 = CloseableReference.of(mock()) frame2 = CloseableReference.of(mock()) } @Test fun testBasic() { val ret = animatedFrameCache.cache(1, frame1) assertThat(ret?.get()).isSameAs(frame1.get()) } @Test fun testMultipleFrames() { animatedFrameCache.cache(1, frame1) animatedFrameCache.cache(2, frame2) assertThat(animatedFrameCache.get(1)?.get()).isSameAs(frame1.get()) assertThat(animatedFrameCache.get(2)?.get()).isSameAs(frame2.get()) } @Test fun testReplace() { animatedFrameCache.cache(1, frame1) animatedFrameCache.cache(1, frame2) assertThat(animatedFrameCache.get(1)?.get()).isNotSameAs(frame1.get()) assertThat(animatedFrameCache.get(1)?.get()).isSameAs(frame2.get()) } @Test fun testReuse() { val ret = animatedFrameCache.cache(1, frame1) ret?.close() val free = animatedFrameCache.getForReuse() assertThat(free).isNotNull() } @Test fun testCantReuseIfNotClosed() { val ret = animatedFrameCache.cache(1, frame1) val free = animatedFrameCache.getForReuse() assertThat(free).isNull() } @Test fun testStillThereIfClosed() { val ret = animatedFrameCache.cache(1, frame1) ret?.close() assertThat(animatedFrameCache.get(1)).isNotNull() } @Test fun testContains() { assertThat(animatedFrameCache.contains(1)).isFalse() val ret = animatedFrameCache.cache(1, frame1) assertThat(animatedFrameCache.contains(1)).isTrue() assertThat(animatedFrameCache.contains(2)).isFalse() ret?.close() assertThat(animatedFrameCache.contains(1)).isTrue() assertThat(animatedFrameCache.contains(2)).isFalse() } @Test fun testContainsWhenReused() { val ret = animatedFrameCache.cache(1, frame1) ret?.close() assertThat(animatedFrameCache.contains(1)).isTrue() assertThat(animatedFrameCache.contains(2)).isFalse() val free = animatedFrameCache.getForReuse() free?.close() assertThat(animatedFrameCache.contains(1)).isFalse() assertThat(animatedFrameCache.contains(2)).isFalse() } @Test fun testContainsFullReuseFlowWithMultipleItems() { assertThat(animatedFrameCache.contains(1)).isFalse() assertThat(animatedFrameCache.contains(2)).isFalse() val ret = animatedFrameCache.cache(1, frame1) val ret2 = animatedFrameCache.cache(2, frame2) assertThat(animatedFrameCache.contains(1)).isTrue() assertThat(animatedFrameCache.contains(2)).isTrue() ret?.close() ret2?.close() assertThat(animatedFrameCache.contains(1)).isTrue() assertThat(animatedFrameCache.contains(2)).isTrue() var free = animatedFrameCache.getForReuse() free?.close() assertThat(animatedFrameCache.contains(1)).isFalse() assertThat(animatedFrameCache.contains(2)).isTrue() free = animatedFrameCache.getForReuse() free?.close() assertThat(animatedFrameCache.contains(1)).isFalse() assertThat(animatedFrameCache.contains(2)).isFalse() } } ================================================ FILE: animated-base/src/test/java/com/facebook/imagepipeline/animated/testing/TestAnimatedDrawableBackend.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.testing; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import com.facebook.common.references.CloseableReference; import com.facebook.imagepipeline.animated.base.AnimatedDrawableBackend; import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo; import com.facebook.imagepipeline.animated.base.AnimatedImageResult; import com.facebook.infer.annotation.Nullsafe; import javax.annotation.Nullable; /** Implementation of {@link AnimatedDrawableBackend} for unit tests. */ @Nullsafe(Nullsafe.Mode.LOCAL) public class TestAnimatedDrawableBackend implements AnimatedDrawableBackend { private final int mWidth; private final int mHeight; private final int[] mFrameDurations; private final int[] mAccumulatedDurationsMs; private int mDropCachesCallCount; public TestAnimatedDrawableBackend(int width, int height, int[] frameDurations) { mWidth = width; mHeight = height; mFrameDurations = frameDurations; mAccumulatedDurationsMs = new int[mFrameDurations.length]; int accumulatedDurationMs = 0; for (int i = 0; i < mAccumulatedDurationsMs.length; i++) { mAccumulatedDurationsMs[i] = accumulatedDurationMs + mFrameDurations[i]; accumulatedDurationMs = mAccumulatedDurationsMs[i]; } } public static int pixelValue(int frameNumber, int x, int y) { return ((frameNumber & 0xff) << 16) | ((x & 0xff) << 8) | ((y & 0xff)); } @Nullable @Override // NULLSAFE_FIXME[Inconsistent Subclass Return Annotation] public AnimatedImageResult getAnimatedImageResult() { return null; } @Override public int getDurationMs() { return mAccumulatedDurationsMs[mAccumulatedDurationsMs.length - 1]; } @Override public int getFrameCount() { return mFrameDurations.length; } @Override public int getLoopCount() { return 0; } @Override public int getWidth() { return mWidth; } @Override public int getHeight() { return mHeight; } @Override public int getRenderedWidth() { return mWidth; } @Override public int getRenderedHeight() { return mHeight; } @Override public AnimatedDrawableFrameInfo getFrameInfo(int frameNumber) { return new AnimatedDrawableFrameInfo( frameNumber, 0, 0, mWidth, mHeight, AnimatedDrawableFrameInfo.BlendOperation.NO_BLEND, AnimatedDrawableFrameInfo.DisposalMethod.DISPOSE_DO_NOT); } @Override public void renderFrame(int frameNumber, Canvas canvas) { int[] pixels = new int[mWidth * mHeight]; for (int i = 0; i < pixels.length; i++) { // We store the frame number in the R, the x in the G, and the y in the B. int x = i % mWidth; int y = i / mWidth; pixels[i] = pixelValue(frameNumber, x, y); } Bitmap bitmap = Bitmap.createBitmap(pixels, mWidth, mHeight, Bitmap.Config.ARGB_8888); canvas.drawBitmap(bitmap, 0, 0, null); } @Override public void renderDeltas(int frameNumber, Canvas canvas) { renderFrame(frameNumber, canvas); } @Override public int getFrameForTimestampMs(int timestampMs) { int accumulator = 0; for (int i = 0; i < mFrameDurations.length; i++) { if (timestampMs < accumulator + mFrameDurations[i]) { return i; } accumulator += mFrameDurations[i]; } return mFrameDurations.length - 1; } @Override public int getTimestampMsForFrame(int frameNumber) { return frameNumber == 0 ? 0 : mAccumulatedDurationsMs[frameNumber - 1]; } @Override public int getDurationMsForFrame(int frameNumber) { return mFrameDurations[frameNumber]; } @Override public int getFrameForPreview() { return 0; } @Override public AnimatedDrawableBackend forNewBounds(@Nullable Rect bounds) { return this; } @Override public int getMemoryUsage() { return 0; } @Nullable @Override public CloseableReference getPreDecodedFrame(int frameNumber) { return null; } @Override public boolean hasPreDecodedFrame(int frameNumber) { return false; } public int getDropCachesCallCount() { return mDropCachesCallCount; } @Override public void dropCaches() { mDropCachesCallCount++; } } ================================================ FILE: animated-base/src/test/java/com/facebook/imagepipeline/animated/util/AnimatedDrawableUtilTest.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.animated.util import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner /** Tests for [AnimatedDrawableUtil]. */ @RunWith(RobolectricTestRunner::class) class AnimatedDrawableUtilTest { @Test fun testGetFrameTimeStampsFromDurations() { val frameDurationsMs = intArrayOf(30, 30, 60, 30, 30) val util = AnimatedDrawableUtil() val frameTimestampsMs = util.getFrameTimeStampsFromDurations(frameDurationsMs) val expected = intArrayOf(0, 30, 60, 120, 150) assertThat(frameTimestampsMs).isEqualTo(expected) } @Test fun testGetFrameTimeStampsFromDurationsWithEmptyArray() { val frameDurationsMs = IntArray(0) val util = AnimatedDrawableUtil() val frameTimestampsMs = util.getFrameTimeStampsFromDurations(frameDurationsMs) assertThat(frameTimestampsMs.size.toLong()).isEqualTo(0) } @Test fun testGetFrameForTimestampMs() { val frameTimestampsMs = intArrayOf(0, 50, 75, 100, 200) val util = AnimatedDrawableUtil() assertThat(util.getFrameForTimestampMs(frameTimestampsMs, 0).toLong()).isEqualTo(0) assertThat(util.getFrameForTimestampMs(frameTimestampsMs, 1).toLong()).isEqualTo(0) assertThat(util.getFrameForTimestampMs(frameTimestampsMs, 49).toLong()).isEqualTo(0) assertThat(util.getFrameForTimestampMs(frameTimestampsMs, 50).toLong()).isEqualTo(1) assertThat(util.getFrameForTimestampMs(frameTimestampsMs, 74).toLong()).isEqualTo(1) assertThat(util.getFrameForTimestampMs(frameTimestampsMs, 75).toLong()).isEqualTo(2) assertThat(util.getFrameForTimestampMs(frameTimestampsMs, 76).toLong()).isEqualTo(2) assertThat(util.getFrameForTimestampMs(frameTimestampsMs, 99).toLong()).isEqualTo(2) assertThat(util.getFrameForTimestampMs(frameTimestampsMs, 100).toLong()).isEqualTo(3) assertThat(util.getFrameForTimestampMs(frameTimestampsMs, 101).toLong()).isEqualTo(3) assertThat(util.getFrameForTimestampMs(frameTimestampsMs, 200).toLong()).isEqualTo(4) } @Test fun testIsOutsideRange() { assertThat(AnimatedDrawableUtil.isOutsideRange(-1, -1, 1)).isTrue() // Always outside range // Test before, within, and after 2 through 5. var start = 2 var end = 5 assertThat(AnimatedDrawableUtil.isOutsideRange(start, end, 1)).isTrue() assertThat(AnimatedDrawableUtil.isOutsideRange(start, end, 2)).isFalse() assertThat(AnimatedDrawableUtil.isOutsideRange(start, end, 3)).isFalse() assertThat(AnimatedDrawableUtil.isOutsideRange(start, end, 4)).isFalse() assertThat(AnimatedDrawableUtil.isOutsideRange(start, end, 5)).isFalse() assertThat(AnimatedDrawableUtil.isOutsideRange(start, end, 6)).isTrue() // Test wrapping case when start is greater than end // Test before, within, and after 4 through 1 start = 4 end = 1 assertThat(AnimatedDrawableUtil.isOutsideRange(start, end, 0)).isFalse() assertThat(AnimatedDrawableUtil.isOutsideRange(start, end, 1)).isFalse() assertThat(AnimatedDrawableUtil.isOutsideRange(start, end, 2)).isTrue() assertThat(AnimatedDrawableUtil.isOutsideRange(start, end, 3)).isTrue() assertThat(AnimatedDrawableUtil.isOutsideRange(start, end, 4)).isFalse() assertThat(AnimatedDrawableUtil.isOutsideRange(start, end, 5)).isFalse() // Test cases where start == end start = 2 end = 2 assertThat(AnimatedDrawableUtil.isOutsideRange(start, end, 1)).isTrue() assertThat(AnimatedDrawableUtil.isOutsideRange(start, end, 2)).isFalse() assertThat(AnimatedDrawableUtil.isOutsideRange(start, end, 3)).isTrue() } } ================================================ FILE: animated-base/src/test/java/com/facebook/imagepipeline/producers/AnimatedRepeatedPostprocessorProducerTest.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.producers import android.graphics.Bitmap import com.facebook.common.internal.ImmutableMap import com.facebook.common.references.CloseableReference import com.facebook.common.references.ResourceReleaser import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory import com.facebook.imagepipeline.common.Priority import com.facebook.imagepipeline.core.ImagePipelineConfig import com.facebook.imagepipeline.image.CloseableAnimatedImage import com.facebook.imagepipeline.image.CloseableImage import com.facebook.imagepipeline.image.CloseableStaticBitmap import com.facebook.imagepipeline.producers.PostprocessorProducer.RepeatedPostprocessorConsumer import com.facebook.imagepipeline.request.ImageRequest import com.facebook.imagepipeline.request.RepeatedPostprocessor import com.facebook.imagepipeline.request.RepeatedPostprocessorRunner import com.facebook.imagepipeline.testing.FakeClock import com.facebook.imagepipeline.testing.TestExecutorService import java.util.ArrayList import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.InOrder import org.mockito.Mock import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config(manifest = Config.NONE) class AnimatedRepeatedPostprocessorProducerTest { companion object { private const val POSTPROCESSOR_NAME = "postprocessor_name" private val extraMap = ImmutableMap.of(PostprocessorProducer.POSTPROCESSOR, POSTPROCESSOR_NAME) } @Mock lateinit var platformBitmapFactory: PlatformBitmapFactory @Mock lateinit var producerListener: ProducerListener2 @Mock lateinit var inputProducer: Producer> @Mock lateinit var consumer: Consumer> @Mock lateinit var postprocessor: RepeatedPostprocessor @Mock lateinit var bitmapResourceReleaser: ResourceReleaser @Mock lateinit var imageRequest: ImageRequest @Mock lateinit var config: ImagePipelineConfig private lateinit var producerContext: SettableProducerContext private val requestId = "requestId" private lateinit var sourceBitmap: Bitmap private lateinit var sourceCloseableStaticBitmap: CloseableStaticBitmap private lateinit var sourceCloseableImageRef: CloseableReference private lateinit var destinationBitmap: Bitmap private lateinit var destinationCloseableBitmapRef: CloseableReference private lateinit var testExecutorService: TestExecutorService private lateinit var postprocessorProducer: PostprocessorProducer private lateinit var results: MutableList> private lateinit var inOrder: InOrder @Before fun setUp() { MockitoAnnotations.initMocks(this) testExecutorService = TestExecutorService(FakeClock()) postprocessorProducer = PostprocessorProducer(inputProducer, platformBitmapFactory, testExecutorService) producerContext = SettableProducerContext( imageRequest, requestId, producerListener, mock(), ImageRequest.RequestLevel.FULL_FETCH, false /* isPrefetch */, false /* isIntermediateResultExpected */, Priority.MEDIUM, config, ) whenever(imageRequest.postprocessor).thenReturn(postprocessor) results = ArrayList() whenever(postprocessor.name).thenReturn(POSTPROCESSOR_NAME) whenever(producerListener.requiresExtraMap(producerContext, POSTPROCESSOR_NAME)) .thenReturn(true) doAnswer { invocation -> results.add((invocation.arguments[0] as CloseableReference).clone()) null } .whenever(consumer) .onNewResult(any(), any()) inOrder = inOrder(postprocessor, producerListener, consumer) } @Test fun testNonStaticBitmapIsPassedOn() { val postprocessorConsumer = produceResults() val repeatedPostprocessorRunner = getRunner() val sourceCloseableAnimatedImage = mock() val sourceCloseableImageRef = CloseableReference.of(sourceCloseableAnimatedImage) postprocessorConsumer.onNewResult(sourceCloseableImageRef, Consumer.IS_LAST) sourceCloseableImageRef.close() testExecutorService.runUntilIdle() inOrder .verify(consumer) .onNewResult(any>(), eq(Consumer.NO_FLAGS)) inOrder.verifyNoMoreInteractions() assertThat(results).hasSize(1) val res0 = results[0] assertThat(CloseableReference.isValid(res0)).isTrue() assertThat(res0.get()).isSameAs(sourceCloseableAnimatedImage) res0.close() performCancelAndVerifyOnCancellation() verify(sourceCloseableAnimatedImage).close() } private fun setupNewSourceImage() { sourceBitmap = mock() sourceCloseableStaticBitmap = mock() whenever(sourceCloseableStaticBitmap.underlyingBitmap).thenReturn(sourceBitmap) sourceCloseableImageRef = CloseableReference.of(sourceCloseableStaticBitmap) } private fun setupNewDestinationImage() { destinationBitmap = mock() destinationCloseableBitmapRef = CloseableReference.of(destinationBitmap, bitmapResourceReleaser) doReturn(destinationCloseableBitmapRef) .whenever(postprocessor) .process(sourceBitmap, platformBitmapFactory) } private fun produceResults(): RepeatedPostprocessorConsumer { postprocessorProducer.produceResults(consumer, producerContext) // Use argumentCaptor from Mockito Kotlin val consumerCaptor = argumentCaptor>>() verify(inputProducer).produceResults(consumerCaptor.capture(), eq(producerContext)) return consumerCaptor.firstValue as RepeatedPostprocessorConsumer } private fun getRunner(): RepeatedPostprocessorRunner { val captor = argumentCaptor() inOrder.verify(postprocessor).setCallback(captor.capture()) return captor.firstValue } private fun performNewResult(postprocessorConsumer: RepeatedPostprocessorConsumer, run: Boolean) { setupNewSourceImage() setupNewDestinationImage() postprocessorConsumer.onNewResult(sourceCloseableImageRef, Consumer.IS_LAST) sourceCloseableImageRef.close() if (run) { testExecutorService.runUntilIdle() } } private fun performUpdate( repeatedPostprocessorRunner: RepeatedPostprocessorRunner, run: Boolean, ) { setupNewDestinationImage() repeatedPostprocessorRunner.update() if (run) { testExecutorService.runUntilIdle() } } private fun performUpdateDuringTheNextPostprocessing( repeatedPostprocessorRunner: RepeatedPostprocessorRunner ) { doAnswer { val destBitmapRef = destinationCloseableBitmapRef performUpdate(repeatedPostprocessorRunner, false) // the following call should be ignored performUpdate(repeatedPostprocessorRunner, false) destBitmapRef } .whenever(postprocessor) .process(sourceBitmap, platformBitmapFactory) } private fun performFailure(repeatedPostprocessorRunner: RepeatedPostprocessorRunner) { setupNewDestinationImage() doThrow(RuntimeException()).whenever(postprocessor).process(sourceBitmap, platformBitmapFactory) repeatedPostprocessorRunner.update() testExecutorService.runUntilIdle() } private fun performCancelAndVerifyOnCancellation() { performCancel() inOrder.verify(consumer).onCancellation() } private fun performCancelAfterFinished() { performCancel() inOrder.verify(consumer, never()).onCancellation() } private fun performCancel() { producerContext.cancel() testExecutorService.runUntilIdle() } private fun verifyNewResultProcessed(index: Int) { verifyNewResultProcessed(index, destinationBitmap) } private fun verifyNewResultProcessed(index: Int, destBitmap: Bitmap) { inOrder.verify(producerListener).onProducerStart(producerContext, PostprocessorProducer.NAME) inOrder.verify(postprocessor).process(sourceBitmap, platformBitmapFactory) inOrder.verify(producerListener).requiresExtraMap(producerContext, PostprocessorProducer.NAME) inOrder .verify(producerListener) .onProducerFinishWithSuccess(producerContext, PostprocessorProducer.NAME, extraMap) inOrder .verify(consumer) .onNewResult(any>(), eq(Consumer.NO_FLAGS)) inOrder.verifyNoMoreInteractions() assertThat(results).hasSize(index + 1) val res0 = results[index] assertThat(CloseableReference.isValid(res0)).isTrue() assertThat((res0.get() as CloseableStaticBitmap).underlyingBitmap).isSameAs(destBitmap) res0.close() verify(bitmapResourceReleaser).release(destBitmap) } } ================================================ FILE: animated-base/src/test/java/com/facebook/imagepipeline/producers/AnimatedSingleUsePostprocessorProducerTest.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.imagepipeline.producers import android.graphics.Bitmap import com.facebook.common.internal.ImmutableMap import com.facebook.common.references.CloseableReference import com.facebook.common.references.ResourceReleaser import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory import com.facebook.imagepipeline.image.CloseableAnimatedImage import com.facebook.imagepipeline.image.CloseableImage import com.facebook.imagepipeline.image.CloseableStaticBitmap import com.facebook.imagepipeline.producers.PostprocessorProducer.SingleUsePostprocessorConsumer import com.facebook.imagepipeline.request.ImageRequest import com.facebook.imagepipeline.request.Postprocessor import com.facebook.imagepipeline.testing.FakeClock import com.facebook.imagepipeline.testing.TestExecutorService import java.util.ArrayList import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.InOrder import org.mockito.Mock import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer import org.mockito.kotlin.eq import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config(manifest = Config.NONE) class AnimatedSingleUsePostprocessorProducerTest { companion object { private const val POSTPROCESSOR_NAME = "postprocessor_name" private val extraMap = ImmutableMap.of(PostprocessorProducer.POSTPROCESSOR, POSTPROCESSOR_NAME) } @Mock lateinit var platformBitmapFactory: PlatformBitmapFactory @Mock lateinit var producerContext: ProducerContext @Mock lateinit var producerListener: ProducerListener2 @Mock lateinit var inputProducer: Producer> @Mock lateinit var consumer: Consumer> @Mock lateinit var postprocessor: Postprocessor @Mock lateinit var bitmapResourceReleaser: ResourceReleaser @Mock lateinit var imageRequest: ImageRequest private val requestId = "mRequestId" private lateinit var sourceBitmap: Bitmap private lateinit var sourceCloseableStaticBitmap: CloseableStaticBitmap private lateinit var sourceCloseableImageRef: CloseableReference private lateinit var destinationBitmap: Bitmap private lateinit var destinationCloseableBitmapRef: CloseableReference private lateinit var testExecutorService: TestExecutorService private lateinit var postprocessorProducer: PostprocessorProducer private lateinit var results: MutableList> private lateinit var inOrder: InOrder @Before fun setUp() { MockitoAnnotations.initMocks(this) testExecutorService = TestExecutorService(FakeClock()) postprocessorProducer = PostprocessorProducer(inputProducer, platformBitmapFactory, testExecutorService) whenever(imageRequest.postprocessor).thenReturn(postprocessor) whenever(producerContext.id).thenReturn(requestId) whenever(producerContext.producerListener).thenReturn(producerListener) whenever(producerContext.imageRequest).thenReturn(imageRequest) results = ArrayList() whenever(postprocessor.name).thenReturn(POSTPROCESSOR_NAME) whenever(producerListener.requiresExtraMap(eq(producerContext), eq(POSTPROCESSOR_NAME))) .thenReturn(true) doAnswer { invocation -> results.add((invocation.arguments[0] as CloseableReference).clone()) null } .whenever(consumer) .onNewResult(any(), any()) inOrder = inOrder(postprocessor, producerListener, consumer) sourceBitmap = mock() sourceCloseableStaticBitmap = mock() whenever(sourceCloseableStaticBitmap.underlyingBitmap).thenReturn(sourceBitmap) sourceCloseableImageRef = CloseableReference.of(sourceCloseableStaticBitmap) destinationBitmap = mock() destinationCloseableBitmapRef = CloseableReference.of(destinationBitmap, bitmapResourceReleaser) } @Test fun testNonStaticBitmapIsPassedOn() { val postprocessorConsumer = produceResults() val sourceCloseableAnimatedImage = mock() val sourceCloseableImageRef = CloseableReference.of(sourceCloseableAnimatedImage) postprocessorConsumer.onNewResult(sourceCloseableImageRef, Consumer.IS_LAST) sourceCloseableImageRef.close() testExecutorService.runUntilIdle() inOrder .verify(consumer) .onNewResult(any>(), eq(Consumer.IS_LAST)) inOrder.verifyNoMoreInteractions() assertThat(results).hasSize(1) val res0 = results[0] assertThat(CloseableReference.isValid(res0)).isTrue() assertThat(res0.get()).isSameAs(sourceCloseableAnimatedImage) res0.close() verify(sourceCloseableAnimatedImage).close() } private fun produceResults(): SingleUsePostprocessorConsumer { postprocessorProducer.produceResults(consumer, producerContext) val consumerCaptor = argumentCaptor>>() verify(inputProducer).produceResults(consumerCaptor.capture(), eq(producerContext)) return consumerCaptor.firstValue as SingleUsePostprocessorConsumer } } ================================================ FILE: animated-drawable/.gitignore ================================================ /build nativedeps/ ================================================ FILE: animated-drawable/README.md ================================================ # Experimental Implementation for Animated Images This is an experimental new animation implementation that is still work in progress. The APIs & design might change significantly in the future. ================================================ FILE: animated-drawable/build.gradle ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import com.facebook.fresco.buildsrc.Deps import com.facebook.fresco.buildsrc.GradleDeps import com.facebook.fresco.buildsrc.TestDeps apply plugin: 'com.android.library' apply plugin: 'kotlin-android' kotlin { jvmToolchain(11) } android { ndkVersion GradleDeps.Native.version buildToolsVersion FrescoConfig.buildToolsVersion compileSdkVersion FrescoConfig.compileSdkVersion namespace "com.facebook.animated.drawable" defaultConfig { minSdkVersion FrescoConfig.minSdkVersion targetSdkVersion FrescoConfig.targetSdkVersion } lintOptions { abortOnError false } testOptions { unitTests.returnDefaultValues = true } } dependencies { compileOnly Deps.AndroidX.androidxAnnotation compileOnly Deps.inferAnnotation compileOnly Deps.jsr305 compileOnly Deps.javaxAnnotation testCompileOnly Deps.inferAnnotation testImplementation Deps.AndroidX.androidxAnnotation testImplementation Deps.jsr305 testImplementation TestDeps.assertjCore testImplementation TestDeps.junit testImplementation TestDeps.festAssertCore testImplementation TestDeps.mockitoCore3 testImplementation TestDeps.mockitoInline3 testImplementation TestDeps.mockitoKotlin3 testImplementation(TestDeps.robolectric) { exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'org.apache.httpcomponents', module: 'httpclient' } testImplementation project(':imagepipeline-test') testImplementation project(':imagepipeline-base-test') implementation project(':imagepipeline-base') implementation project(':drawee') implementation project(':fbcore') implementation project(':urimod') implementation project(':vito:core') implementation project(':vito:options') implementation project(':vito:provider') implementation project(':vito:renderer') implementation project(':vito:source') } apply plugin: "com.vanniktech.maven.publish" ================================================ FILE: animated-drawable/gradle.properties ================================================ POM_NAME=AnimatedDrawable POM_DESCRIPTION=Animated drawable that renders GIFs, WebPs and other Animations POM_ARTIFACT_ID=animated-drawable POM_PACKAGING=aar ================================================ FILE: animated-drawable/src/main/AndroidManifest.xml ================================================ ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/backend/AnimationBackend.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.backend; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Rect; import android.graphics.drawable.Drawable; import androidx.annotation.IntRange; import javax.annotation.Nullable; /** Animation backend interface that is used to draw frames. */ public interface AnimationBackend extends AnimationInformation { interface Listener { /** Trigger when full animation is loaded and ready to play */ void onAnimationLoaded(); } /** * Default value if the intrinsic dimensions are not set. * * @see #getIntrinsicWidth() * @see #getIntrinsicHeight() */ int INTRINSIC_DIMENSION_UNSET = -1; /** * Draw the frame for the given frame number on the canvas. * * @param parent the parent that draws the frame * @param canvas the canvas to draw an * @param frameNumber the frame number of the frame to draw * @return true if successful, false if the frame could not be rendered */ boolean drawFrame(Drawable parent, Canvas canvas, int frameNumber); /** * Set the alpha value to be used for drawing frames in {@link #drawFrame(Drawable, Canvas, int)} * if supported. * * @param alpha the alpha value between 0 and 255 */ void setAlpha(@IntRange(from = 0, to = 255) int alpha); /** * The color filter to be used for drawing frames in {@link #drawFrame(Drawable, Canvas, int)} if * supported. * * @param colorFilter the color filter to use */ void setColorFilter(@Nullable ColorFilter colorFilter); /** * Called when the bounds of the parent drawable are updated. This can be used to perform some * ahead-of-time computations if needed. * *

The supplied bounds do not have to be stored. It is possible to just use {@link * Drawable#getBounds()} of the parent drawable of {@link #drawFrame(Drawable, Canvas, int)} * instead. * * @param bounds the bounds to be used for drawing frames */ void setBounds(Rect bounds); /** * Get the intrinsic width of the underlying animation or {@link #INTRINSIC_DIMENSION_UNSET} if * not available. * *

This value is used by the underlying drawable for aspect ratio computations, similar to * {@link Drawable#getIntrinsicWidth()}. * * @return the width or {@link #INTRINSIC_DIMENSION_UNSET} if unset */ int getIntrinsicWidth(); /** * Get the intrinsic height of the underlying animation or {@link #INTRINSIC_DIMENSION_UNSET} if * not available. * *

This value is used by the underlying drawable for aspect ratio computations, similar to * {@link Drawable#getIntrinsicHeight()}. * * @return the height or {@link #INTRINSIC_DIMENSION_UNSET} if unset */ int getIntrinsicHeight(); /** * Get the size of the animation backend. * * @return the size in bytes */ int getSizeInBytes(); /** * Clean up animation data. This will be called when the backing drawable is cleared as well. For * example, drop all cached frames. */ void clear(); /** Load animation bitmaps using the animation frame as canvas size. */ void preloadAnimation(); /** Set listener for animation events */ void setAnimationListener(@Nullable Listener listener); } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/backend/AnimationBackendDelegate.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.backend import android.annotation.SuppressLint import android.graphics.Canvas import android.graphics.ColorFilter import android.graphics.Rect import android.graphics.drawable.Drawable import androidx.annotation.IntRange /** Animation backend delegate that forwards all calls to a given [AnimationBackend] */ open class AnimationBackendDelegate( /** Current animation backend in use */ private var _animationBackend: T? ) : AnimationBackend { // Animation backend parameters @IntRange(from = -1, to = 255) private var alpha = ALPHA_UNSET private var colorFilter: ColorFilter? = null private var bounds: Rect? = null override fun getFrameCount(): Int = if (_animationBackend == null) 0 else _animationBackend!!.frameCount override fun getFrameDurationMs(frameNumber: Int): Int = if (_animationBackend == null) 0 else _animationBackend!!.getFrameDurationMs(frameNumber) override fun getLoopDurationMs(): Int = if (_animationBackend == null) 0 else _animationBackend!!.loopDurationMs override fun width(): Int = if (_animationBackend == null) 0 else _animationBackend!!.width() override fun height(): Int = if (_animationBackend == null) 0 else _animationBackend!!.height() override fun getLoopCount(): Int = if (_animationBackend == null) AnimationInformation.LOOP_COUNT_INFINITE else _animationBackend!!.loopCount override fun drawFrame(parent: Drawable, canvas: Canvas, frameNumber: Int): Boolean = _animationBackend?.drawFrame(parent, canvas, frameNumber) == true override fun setAlpha(@IntRange(from = 0, to = 255) alpha: Int) { _animationBackend?.setAlpha(alpha) this.alpha = alpha } override fun setColorFilter(colorFilter: ColorFilter?) { _animationBackend?.setColorFilter(colorFilter) this.colorFilter = colorFilter } override fun setBounds(bounds: Rect) { _animationBackend?.setBounds(bounds) this.bounds = bounds } override fun getSizeInBytes(): Int = if (_animationBackend == null) 0 else _animationBackend!!.sizeInBytes override fun clear() { _animationBackend?.clear() } override fun preloadAnimation() { _animationBackend?.preloadAnimation() } override fun setAnimationListener(listener: AnimationBackend.Listener?) { _animationBackend?.setAnimationListener(listener) } override fun getIntrinsicWidth(): Int = if (_animationBackend == null) AnimationBackend.INTRINSIC_DIMENSION_UNSET else _animationBackend!!.intrinsicWidth override fun getIntrinsicHeight(): Int = if (_animationBackend == null) AnimationBackend.INTRINSIC_DIMENSION_UNSET else _animationBackend!!.intrinsicHeight var animationBackend: T? /** * Get the current animation backend. * * @return the current animation backend in use or null if not set */ get() = _animationBackend /** * Set the animation backend to forward calls to. If called with null, the current backend will * be removed. * * @param animationBackend the backend to use or null to remove the current backend */ set(animationBackend) { this._animationBackend = animationBackend if (this._animationBackend != null) { applyBackendProperties(_animationBackend!!) } } @SuppressLint("Range") private fun applyBackendProperties(backend: AnimationBackend) { if (bounds != null) { backend.setBounds(bounds) } if (alpha >= 0 && alpha <= 255) { backend.setAlpha(alpha) } if (colorFilter != null) { backend.setColorFilter(colorFilter) } } companion object { private const val ALPHA_UNSET = -1 } } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/backend/AnimationBackendDelegateWithInactivityCheck.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.backend; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import androidx.annotation.VisibleForTesting; import com.facebook.common.time.MonotonicClock; import com.facebook.infer.annotation.Nullsafe; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; /** * Animation backend delegate for animation backends that implement {@link InactivityListener}. * After a certain inactivity period (default = {@link #INACTIVITY_THRESHOLD_MS}, {@link * InactivityListener#onInactive()} will be called. * *

This can for example be used to drop caches if needed. * *

New instances can be created with {@link #createForBackend(AnimationBackend, MonotonicClock, * ScheduledExecutorService)}. */ @Nullsafe(Nullsafe.Mode.LOCAL) public class AnimationBackendDelegateWithInactivityCheck extends AnimationBackendDelegate { public interface InactivityListener { /** * Called when the animation backend has not been used to draw frames within the given * threshold. */ void onInactive(); } public static < T extends AnimationBackend & AnimationBackendDelegateWithInactivityCheck.InactivityListener> AnimationBackendDelegate createForBackend( T backend, MonotonicClock monotonicClock, ScheduledExecutorService scheduledExecutorServiceForUiThread) { return createForBackend(backend, backend, monotonicClock, scheduledExecutorServiceForUiThread); } public static AnimationBackendDelegate createForBackend( T backend, InactivityListener inactivityListener, MonotonicClock monotonicClock, ScheduledExecutorService scheduledExecutorServiceForUiThread) { return new AnimationBackendDelegateWithInactivityCheck<>( backend, inactivityListener, monotonicClock, scheduledExecutorServiceForUiThread); } @VisibleForTesting static final long INACTIVITY_THRESHOLD_MS = 2000; @VisibleForTesting static final long INACTIVITY_CHECK_POLLING_TIME_MS = 1000; private final MonotonicClock mMonotonicClock; private final ScheduledExecutorService mScheduledExecutorServiceForUiThread; private boolean mInactivityCheckScheduled = false; private long mLastDrawnTimeMs; private long mInactivityThresholdMs = INACTIVITY_THRESHOLD_MS; private long mInactivityCheckPollingTimeMs = INACTIVITY_CHECK_POLLING_TIME_MS; @Nullable private InactivityListener mInactivityListener; /** * Watchdog runnable that calls {@link InactivityListener#onInactive()} if necessary or schedules * a new watchdog task otherwise. */ private final Runnable mIsInactiveCheck = new Runnable() { @Override public void run() { synchronized (AnimationBackendDelegateWithInactivityCheck.this) { mInactivityCheckScheduled = false; if (isInactive()) { if (mInactivityListener != null) { mInactivityListener.onInactive(); } } else { maybeScheduleInactivityCheck(); } } } }; private AnimationBackendDelegateWithInactivityCheck( @Nullable T animationBackend, @Nullable InactivityListener inactivityListener, MonotonicClock monotonicClock, ScheduledExecutorService scheduledExecutorServiceForUiThread) { super(animationBackend); mInactivityListener = inactivityListener; mMonotonicClock = monotonicClock; mScheduledExecutorServiceForUiThread = scheduledExecutorServiceForUiThread; } @Override public boolean drawFrame(Drawable parent, Canvas canvas, int frameNumber) { mLastDrawnTimeMs = mMonotonicClock.now(); boolean result = super.drawFrame(parent, canvas, frameNumber); maybeScheduleInactivityCheck(); return result; } public void setInactivityListener(@Nullable InactivityListener inactivityListener) { mInactivityListener = inactivityListener; } public long getInactivityCheckPollingTimeMs() { return mInactivityCheckPollingTimeMs; } public void setInactivityCheckPollingTimeMs(long inactivityCheckPollingTimeMs) { mInactivityCheckPollingTimeMs = inactivityCheckPollingTimeMs; } public long getInactivityThresholdMs() { return mInactivityThresholdMs; } public void setInactivityThresholdMs(long inactivityThresholdMs) { mInactivityThresholdMs = inactivityThresholdMs; } private boolean isInactive() { return mMonotonicClock.now() - mLastDrawnTimeMs > mInactivityThresholdMs; } private synchronized void maybeScheduleInactivityCheck() { if (!mInactivityCheckScheduled) { mInactivityCheckScheduled = true; mScheduledExecutorServiceForUiThread.schedule( mIsInactiveCheck, mInactivityCheckPollingTimeMs, TimeUnit.MILLISECONDS); } } } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/backend/AnimationInformation.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.backend; import com.facebook.infer.annotation.Nullsafe; /** Basic animation metadata: Frame and loop count & duration */ @Nullsafe(Nullsafe.Mode.LOCAL) public interface AnimationInformation { /** * Loop count to be returned by {@link #getLoopCount()} when the animation should be repeated * indefinitely. */ int LOOP_COUNT_INFINITE = 0; /** * Get the number of frames for the animation * * @return the number of frames */ int getFrameCount(); /** * Get the frame duration for a given frame number in milliseconds. * * @param frameNumber the frame to get the duration for * @return the duration in ms */ int getFrameDurationMs(int frameNumber); /** * Loop duration in ms * * @return duration in ms */ int getLoopDurationMs(); /** * @return Animation asset width */ int width(); /** * @return Animation asset height */ int height(); /** * Get the number of loops the animation has or {@link #LOOP_COUNT_INFINITE} for infinite looping. * * @return the loop count or {@link #LOOP_COUNT_INFINITE} */ int getLoopCount(); } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/BitmapAnimationBackend.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap import android.content.res.Resources import android.graphics.Bitmap import android.graphics.BitmapShader import android.graphics.Canvas import android.graphics.ColorFilter import android.graphics.Matrix import android.graphics.Paint import android.graphics.Path import android.graphics.Rect import android.graphics.RectF import android.graphics.Shader import android.graphics.drawable.Drawable import androidx.annotation.IntDef import androidx.annotation.IntRange import com.facebook.common.logging.FLog import com.facebook.common.references.CloseableReference import com.facebook.fresco.animation.backend.AnimationBackend import com.facebook.fresco.animation.backend.AnimationBackendDelegateWithInactivityCheck.InactivityListener import com.facebook.fresco.animation.backend.AnimationInformation import com.facebook.fresco.animation.bitmap.preparation.BitmapFramePreparationStrategy import com.facebook.fresco.animation.bitmap.preparation.BitmapFramePreparer import com.facebook.fresco.ui.common.DimensionsInfo import com.facebook.fresco.vito.core.AnimatedImagePerfLoggingListener import com.facebook.fresco.vito.core.FrescoDrawableInterface import com.facebook.fresco.vito.listener.ImageListener import com.facebook.fresco.vito.options.AnimatedOptions import com.facebook.fresco.vito.options.ImageOptions import com.facebook.fresco.vito.options.RoundingOptions import com.facebook.fresco.vito.provider.FrescoVitoProvider import com.facebook.fresco.vito.source.ImageSourceProvider import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory import com.facebook.imagepipeline.image.ImageInfo /** * Bitmap animation backend that renders bitmap frames. * * The given [BitmapFrameCache] is used to cache frames and create new bitmaps. * [AnimationInformation] defines the main animation parameters, like frame and loop count. * [BitmapFrameRenderer] is used to render frames to the bitmaps acquired from the * [BitmapFrameCache]. */ class BitmapAnimationBackend @JvmOverloads constructor( private val platformBitmapFactory: PlatformBitmapFactory, private val bitmapFrameCache: BitmapFrameCache, private val animationInformation: AnimationInformation, private val bitmapFrameRenderer: BitmapFrameRenderer, private val isNewRenderImplementation: Boolean, private val bitmapFramePreparationStrategy: BitmapFramePreparationStrategy?, private val bitmapFramePreparer: BitmapFramePreparer?, val roundingOptions: RoundingOptions? = null, val animatedOptions: AnimatedOptions? = null, ) : AnimationBackend, InactivityListener { private val isCircular: Boolean = roundingOptions?.isCircular == true private val isAntiAliased: Boolean = roundingOptions?.isAntiAliased == true val cornerRadii: FloatArray? = roundingOptions?.let { roundingOptions -> if (isCircular) { null } else if (roundingOptions.cornerRadius != RoundingOptions.CORNER_RADIUS_UNSET) { val corners = FloatArray(8) corners.fill(roundingOptions.cornerRadius) corners } else { roundingOptions.cornerRadii } } interface FrameListener { /** * Called when the backend started drawing the given frame. * * @param backend the backend * @param frameNumber the frame number to be drawn */ fun onDrawFrameStart(backend: BitmapAnimationBackend, frameNumber: Int) /** * Called when the given frame has been drawn. * * @param backend the backend * @param frameNumber the frame number that has been drawn * @param frameType the [FrameType] that has been drawn */ fun onFrameDrawn(backend: BitmapAnimationBackend, frameNumber: Int, @FrameType frameType: Int) /** * Called when no bitmap could be drawn by the backend for the given frame number. * * @param backend the backend * @param frameNumber the frame number that could not be drawn */ fun onFrameDropped(backend: BitmapAnimationBackend, frameNumber: Int) } /** Frame type that has been drawn. Can be used for logging. */ @Retention(AnnotationRetention.SOURCE) @IntDef( FRAME_TYPE_UNKNOWN, FRAME_TYPE_CACHED, FRAME_TYPE_REUSED, FRAME_TYPE_CREATED, FRAME_TYPE_FALLBACK, ) annotation class FrameType private val bitmapConfig = Bitmap.Config.ARGB_8888 private val paint: Paint = Paint(Paint.FILTER_BITMAP_FLAG or Paint.DITHER_FLAG) private var bounds: Rect? = null private var bitmapWidth = 0 private var bitmapHeight = 0 private val path: Path = Path() private val matrix: Matrix = Matrix() private var pathFrameNumber: Int = -1 private var frameListener: FrameListener? = null private var animationListener: AnimationBackend.Listener? = null private var animatedImagePerfLoggingListener: AnimatedImagePerfLoggingListener? = null // Thumbnail fallback functionality private var thumbnailDrawable: FrescoDrawableInterface? = null private var animationCompleted: Boolean = false private var totalFramesProcessed: Int = 0 private var lastFrameNumber: Int = -1 init { initializeThumbnailDrawable() updateBitmapDimensions() } fun setFrameListener(frameListener: FrameListener?) { this.frameListener = frameListener } fun setAnimatedImagePerfLoggingListener(listener: AnimatedImagePerfLoggingListener?) { this.animatedImagePerfLoggingListener = listener } override fun getFrameCount(): Int = animationInformation.frameCount override fun getFrameDurationMs(frameNumber: Int): Int = animationInformation.getFrameDurationMs(frameNumber) override fun width(): Int = animationInformation.width() override fun height(): Int = animationInformation.height() override fun getLoopDurationMs(): Int = animationInformation.loopDurationMs override fun getLoopCount(): Int { // If no animated options are set, use the default loop count if (animatedOptions == null) { return animationInformation.loopCount } return when (animatedOptions.loopCount) { AnimatedOptions.LOOP_COUNT_INFINITE -> AnimationInformation.LOOP_COUNT_INFINITE else -> animatedOptions.loopCount } } override fun drawFrame(parent: Drawable, canvas: Canvas, frameNumber: Int): Boolean { frameListener?.onDrawFrameStart(this, frameNumber) // Check if we should show thumbnail instead of animation frame if (showThumbnail()) { val thumbnailDrawn = drawThumbnail(canvas) if (thumbnailDrawn) { return true } } val drawn = drawFrameOrFallback(canvas, frameNumber, FRAME_TYPE_CACHED) // Track animation progress for thumbnail fallback trackAnimationProgress(frameNumber) // We could not draw anything if (!drawn) { frameListener?.onFrameDropped(this, frameNumber) // Log frame drop for performance monitoring val imageId = animationInformation.toString() // Use animation info as identifier val timestamp = System.currentTimeMillis() animatedImagePerfLoggingListener?.onFrameDropped(imageId, frameNumber, timestamp) } // Prepare next frames if (!isNewRenderImplementation && bitmapFramePreparer != null) { bitmapFramePreparationStrategy?.prepareFrames( bitmapFramePreparer, bitmapFrameCache, this, frameNumber, ) } return drawn } private fun drawFrameOrFallback( canvas: Canvas, frameNumber: Int, @FrameType frameType: Int, ): Boolean { var bitmapReference: CloseableReference? = null val drawn: Boolean var nextFrameType = FRAME_TYPE_UNKNOWN try { if (isNewRenderImplementation) { bitmapReference = bitmapFramePreparationStrategy?.getBitmapFrame(frameNumber, canvas.width, canvas.height) if (bitmapReference != null && bitmapReference.isValid) { drawBitmap(frameNumber, bitmapReference.get(), canvas) return true } // If bitmap could not be drawn, then fetch frames bitmapFramePreparationStrategy?.prepareFrames(canvas.width, canvas.height, null) return false } when (frameType) { FRAME_TYPE_CACHED -> { bitmapReference = bitmapFrameCache.getCachedFrame(frameNumber) drawn = drawBitmapAndCache(frameNumber, bitmapReference, canvas, FRAME_TYPE_CACHED) nextFrameType = FRAME_TYPE_REUSED } FRAME_TYPE_REUSED -> { bitmapReference = bitmapFrameCache.getBitmapToReuseForFrame(frameNumber, bitmapWidth, bitmapHeight) // Try to render the frame and draw on the canvas immediately after drawn = (renderFrameInBitmap(frameNumber, bitmapReference) && drawBitmapAndCache(frameNumber, bitmapReference, canvas, FRAME_TYPE_REUSED)) nextFrameType = FRAME_TYPE_CREATED } FRAME_TYPE_CREATED -> { bitmapReference = try { platformBitmapFactory.createBitmap(bitmapWidth, bitmapHeight, bitmapConfig) } catch (e: RuntimeException) { // Failed to create the bitmap for the frame, return and report that we could not // draw the frame. FLog.w(TAG, "Failed to create frame bitmap", e) return false } // Try to render the frame and draw on the canvas immediately after drawn = (renderFrameInBitmap(frameNumber, bitmapReference) && drawBitmapAndCache(frameNumber, bitmapReference, canvas, FRAME_TYPE_CREATED)) nextFrameType = FRAME_TYPE_FALLBACK } FRAME_TYPE_FALLBACK -> { bitmapReference = bitmapFrameCache.getFallbackFrame(frameNumber) drawn = drawBitmapAndCache(frameNumber, bitmapReference, canvas, FRAME_TYPE_FALLBACK) } else -> return false } } finally { CloseableReference.closeSafely(bitmapReference) } return if (drawn || nextFrameType == FRAME_TYPE_UNKNOWN) { drawn } else { drawFrameOrFallback(canvas, frameNumber, nextFrameType) } } override fun setAlpha(@IntRange(from = 0, to = 255) alpha: Int) { paint.alpha = alpha } override fun setColorFilter(colorFilter: ColorFilter?) { paint.colorFilter = colorFilter } override fun setBounds(bounds: Rect?) { this.bounds = bounds bitmapFrameRenderer.setBounds(bounds) updateBitmapDimensions() // Set bounds on thumbnail drawable when backend bounds change thumbnailDrawable?.let { drawable -> val thumbnailBounds = bounds ?: Rect(0, 0, width(), height()) (drawable as Drawable).setBounds(thumbnailBounds) } } override fun getIntrinsicWidth(): Int = bitmapWidth override fun getIntrinsicHeight(): Int = bitmapHeight override fun getSizeInBytes(): Int = bitmapFrameCache.sizeInBytes override fun clear() { if (isNewRenderImplementation) { bitmapFramePreparationStrategy?.clearFrames() } else { bitmapFrameCache.clear() } } override fun preloadAnimation() { if (!isNewRenderImplementation && bitmapFramePreparer != null) { bitmapFramePreparationStrategy?.prepareFrames( bitmapFramePreparer, bitmapFrameCache, this, 0, ) { animationListener?.onAnimationLoaded() } } else { bitmapFramePreparationStrategy?.prepareFrames( animationInformation.width(), animationInformation.height(), ) { animationListener?.onAnimationLoaded() } } } override fun onInactive() { if (isNewRenderImplementation) { bitmapFramePreparationStrategy?.onStop() } else { clear() } thumbnailDrawable?.let { FrescoVitoProvider.getController().releaseImmediately(it) } thumbnailDrawable = null } override fun setAnimationListener(listener: AnimationBackend.Listener?) { animationListener = listener } private fun updateBitmapDimensions() { // Calculate the correct bitmap dimensions bitmapWidth = bitmapFrameRenderer.intrinsicWidth if (bitmapWidth == AnimationBackend.INTRINSIC_DIMENSION_UNSET) { bitmapWidth = bounds?.width() ?: AnimationBackend.INTRINSIC_DIMENSION_UNSET } bitmapHeight = bitmapFrameRenderer.intrinsicHeight if (bitmapHeight == AnimationBackend.INTRINSIC_DIMENSION_UNSET) { bitmapHeight = bounds?.height() ?: AnimationBackend.INTRINSIC_DIMENSION_UNSET } } /** * Try to render the frame to the given target bitmap. If the rendering fails, the target bitmap * reference will be closed and false is returned. If rendering succeeds, the target bitmap * reference can be drawn and has to be manually closed after drawing has been completed. * * @param frameNumber the frame number to render * @param targetBitmap the target bitmap * @return true if rendering successful */ private fun renderFrameInBitmap( frameNumber: Int, targetBitmap: CloseableReference?, ): Boolean { if (targetBitmap == null || !targetBitmap.isValid) { return false } // Render the image val frameRendered = bitmapFrameRenderer.renderFrame(frameNumber, targetBitmap.get()) if (!frameRendered) { CloseableReference.closeSafely(targetBitmap) } return frameRendered } private fun updatePath( frameNumber: Int, bitmap: Bitmap, currentBoundsWidth: Float, currentBoundsHeight: Float, ): Boolean { if (!isCircular && cornerRadii == null) { return false } if (frameNumber == pathFrameNumber) { return true } val bitmapShader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) val src = RectF(0f, 0f, bitmapWidth.toFloat(), bitmapHeight.toFloat()) val dst = RectF(0f, 0f, currentBoundsWidth, currentBoundsHeight) matrix.setRectToRect(src, dst, Matrix.ScaleToFit.FILL) bitmapShader.setLocalMatrix(matrix) paint.shader = bitmapShader paint.isAntiAlias = isAntiAliased path.reset() if (isCircular) { val centerX = currentBoundsWidth / 2f val centerY = currentBoundsHeight / 2f val radius = minOf(centerX, centerY) path.addCircle(centerX, centerY, radius, Path.Direction.CW) } else { path.addRoundRect( RectF(0f, 0f, currentBoundsWidth, currentBoundsHeight), cornerRadii ?: floatArrayOf(), Path.Direction.CW, ) } pathFrameNumber = frameNumber return true } private fun drawBitmap(frameNumber: Int, bitmap: Bitmap, canvas: Canvas) { val currentBounds = bounds if (currentBounds == null) { canvas.drawBitmap(bitmap, 0f, 0f, paint) } else { if ( updatePath( frameNumber, bitmap, currentBounds.width().toFloat(), currentBounds.height().toFloat(), ) ) { canvas.drawPath(path, paint) } else { canvas.drawBitmap(bitmap, null, currentBounds, paint) } } } /** * Helper method that draws the given bitmap on the canvas respecting the bounds (if set). * * If rendering was successful, it notifies the cache that the frame has been rendered with the * given bitmap. In addition, it will notify the [FrameListener] if set. * * @param frameNumber the current frame number passed to the cache * @param bitmapReference the bitmap to draw * @param canvas the canvas to draw an * @param frameType the [FrameType] to be rendered * @return true if the bitmap has been drawn */ private fun drawBitmapAndCache( frameNumber: Int, bitmapReference: CloseableReference?, canvas: Canvas, @FrameType frameType: Int, ): Boolean { if (bitmapReference == null || !CloseableReference.isValid(bitmapReference)) { return false } this.drawBitmap(frameNumber, bitmapReference.get(), canvas) // Notify the cache that a frame has been rendered. // We should not cache fallback frames since they do not represent the actual frame. if (frameType != FRAME_TYPE_FALLBACK && !isNewRenderImplementation) { bitmapFrameCache.onFrameRendered(frameNumber, bitmapReference, frameType) } frameListener?.onFrameDrawn(this, frameNumber, frameType) return true } private fun initializeThumbnailDrawable() { val options = animatedOptions if (options?.useFallbackThumbnail() == true && !options.thumbnailUrl.isNullOrEmpty()) { try { thumbnailDrawable = FrescoVitoProvider.getController().createDrawable("bitmap-animation-thumbnail") // Load the thumbnail through Fresco's pipeline options.thumbnailUrl?.let { url -> loadFrescoThumbnail(url) } } catch (e: Exception) { FLog.w(TAG, "Failed to initialize thumbnail drawable", e) thumbnailDrawable = null } } } private fun loadFrescoThumbnail(thumbnailUrl: String) { val drawable = thumbnailDrawable ?: return try { val imageOptions = ImageOptions.defaults().extend().round(roundingOptions).build() val imageRequest = FrescoVitoProvider.getImagePipeline() .createImageRequest( Resources.getSystem(), ImageSourceProvider.forUri(thumbnailUrl), imageOptions, callerContext = CALLER_CONTEXT, ) val imageListener = object : ImageListener { override fun onFinalImageSet( id: Long, imageOrigin: Int, imageInfo: ImageInfo?, drawable: Drawable?, ) = Unit override fun onFailure(id: Long, error: Drawable?, throwable: Throwable?) { FLog.w(TAG, "Failed to load thumbnail from URL: $thumbnailUrl", throwable) thumbnailDrawable = null } override fun onImageDrawn( id: String, imageInfo: ImageInfo, dimensionsInfo: DimensionsInfo, ) { // Image drawn } } FrescoVitoProvider.getController() .fetch( drawable = drawable, imageRequest = imageRequest, callerContext = CALLER_CONTEXT, contextChain = null, listener = imageListener, onFadeListener = null, viewportDimensions = null, ) } catch (e: Exception) { FLog.w(TAG, "Failed to load thumbnail through Fresco: $thumbnailUrl", e) // Release the image thumbnailDrawable?.let { FrescoVitoProvider.getController().releaseImmediately(it) } thumbnailDrawable = null } } private fun showThumbnail(): Boolean { val options = animatedOptions ?: return false // Only show thumbnail if we should use fallback thumbnail and we have a thumbnail drawable if (!options.useFallbackThumbnail() || thumbnailDrawable?.hasImage() != true) { return false } if (animationCompleted) { return true } return false } private fun drawThumbnail(canvas: Canvas): Boolean { val frescoDrawable = thumbnailDrawable ?: return false if (!frescoDrawable.hasImage()) { return false } try { val drawable = frescoDrawable as Drawable drawable.draw(canvas) return true } catch (e: Exception) { FLog.w(TAG, "Failed to draw thumbnail drawable", e) return false } } // Track animation progress for thumbnail fallback private fun trackAnimationProgress(frameNumber: Int) { val options = animatedOptions ?: return if (!options.useFallbackThumbnail()) { return } val totalLoops = getLoopCount() val framesPerLoop = getFrameCount() if (totalLoops == AnimationInformation.LOOP_COUNT_INFINITE) { return } if (frameNumber < lastFrameNumber) { totalFramesProcessed += framesPerLoop } lastFrameNumber = frameNumber // Calculate current loop and frame within loop val currentLoop = totalFramesProcessed / framesPerLoop val frameInLoop = frameNumber // Last loop reached, mark animation as completed if (currentLoop >= totalLoops - 1 && frameInLoop == framesPerLoop - 1) { animationCompleted = true } } companion object { const val FRAME_TYPE_UNKNOWN: Int = -1 const val FRAME_TYPE_CACHED = 0 const val FRAME_TYPE_REUSED = 1 const val FRAME_TYPE_CREATED = 2 const val FRAME_TYPE_FALLBACK = 3 private val TAG = BitmapAnimationBackend::class.java private const val CALLER_CONTEXT = "BitmapAnimationBackend" } } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/BitmapFrameCache.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap import android.graphics.Bitmap import com.facebook.common.references.CloseableReference import com.facebook.fresco.animation.bitmap.BitmapAnimationBackend.FrameType /** Bitmap frame cache that is used for animated images. */ interface BitmapFrameCache { interface FrameCacheListener { /** * Called when the frame for the given frame number has been put in the frame cache. * * @param bitmapFrameCache the frame cache that holds the frame * @param frameNumber the cached frame number */ fun onFrameCached(bitmapFrameCache: BitmapFrameCache, frameNumber: Int) /** * Called when the frame for the given frame number has been evicted from the frame cache. * * @param bitmapFrameCache the frame cache that evicted the frame * @param frameNumber the frame number of the evicted frame */ fun onFrameEvicted(bitmapFrameCache: BitmapFrameCache, frameNumber: Int) } /** * Get the cached frame for the given frame number. * * @param frameNumber the frame number to get the cached frame for * @return the cached frame or null if not cached */ fun getCachedFrame(frameNumber: Int): CloseableReference? /** * Get a fallback frame for the given frame number. This method is called if all other attempts to * draw a frame failed. The bitmap returned could for example be the last drawn frame (if any). * * @param frameNumber the frame number to get the fallback * @return the fallback frame or null if not cached */ fun getFallbackFrame(frameNumber: Int): CloseableReference? /** * Return a reusable bitmap that should be used to render the given frame. * * @param frameNumber the frame number to be rendered * @param width the width of the target bitmap * @param height the height of the target bitmap * @return the reusable bitmap or null if no reusable bitmaps available */ fun getBitmapToReuseForFrame( frameNumber: Int, width: Int, height: Int, ): CloseableReference? /** * Check whether the cache contains a certain frame. * * @param frameNumber the frame number to check * @return true if the frame is cached */ operator fun contains(frameNumber: Int): Boolean /** @return the size in bytes of all cached data */ val sizeInBytes: Int /** Send the list of frames when the animation frames are loaded */ fun onAnimationPrepared(frameBitmaps: Map>): Boolean = true /** Clear the cache. */ fun clear() /** Indicates if animation is loaded in cache and ready for usage */ fun isAnimationReady(): Boolean = false /** * Callback when the given bitmap has been drawn to a canvas. This bitmap can either be a reused * bitmap returned by [getBitmapToReuseForFrame(int, int, int)] or a new bitmap. * * Note: the implementation of this interface must manually clone the given bitmap reference if it * wants to hold on to the bitmap. The original reference will be automatically closed after this * call. * * @param frameNumber the frame number that has been rendered * @param bitmapReference the bitmap reference that has been rendered * @param frameType the frame type that has been rendered */ fun onFrameRendered( frameNumber: Int, bitmapReference: CloseableReference, @FrameType frameType: Int, ) /** * Callback when a bitmap reference for a given frame has been prepared for future rendering. * * This method is called ahead of render time (i.e. when future frames have been prepared in the * background), whereas [onFrameRendered(int, CloseableReference, int)] is invoked when the actual * frame has been drawn on a Canvas. * * The supplied bitmap reference can either hold a reused bitmap returned by * [getBitmapToReuseForFrame(int, int, int)] or a new bitmap as indicated by the frame type * parameter. * * Note: the implementation of this interface must manually clone the given bitmap reference if it * wants to hold on to the bitmap. The original reference will be automatically closed after this * call. * * @param frameNumber the frame number of the passed bitmapReference * @param bitmapReference the bitmap reference that has been prepared for future rendering * @param frameType the frame type of the prepared frame * @return true if the frame has been successfully cached */ fun onFramePrepared( frameNumber: Int, bitmapReference: CloseableReference, @FrameType frameType: Int, ) /** * Set a frame cache listener that gets notified about caching events. * * @param frameCacheListener the listener to use */ fun setFrameCacheListener(frameCacheListener: FrameCacheListener?) } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/BitmapFrameRenderer.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap import android.graphics.Bitmap import android.graphics.Rect /** * Bitmap frame renderer used by [BitmapAnimationBackend] to render animated images (e.g. GIFs or * animated WebPs). */ interface BitmapFrameRenderer { /** * Render the frame for the given frame number to the target bitmap. * * @param frameNumber the frame number to render * @param targetBitmap the bitmap to render the frame in * @return true if successful */ fun renderFrame(frameNumber: Int, targetBitmap: Bitmap): Boolean /** * Set the parent drawable bounds to be used for frame rendering. * * @param bounds the bounds to use */ fun setBounds(bounds: Rect?) /** * Return the intrinsic width of bitmap frames. Return * [AnimationBackend#INTRINSIC_DIMENSION_UNSET] if no specific width is set. * * @return the intrinsic width */ val intrinsicWidth: Int /** * Return the intrinsic height of bitmap frames. Return * [AnimationBackend#INTRINSIC_DIMENSION_UNSET] if no specific height is set. * * @return the intrinsic height */ val intrinsicHeight: Int } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/cache/KeepLastFrameCache.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.cache import android.graphics.Bitmap import com.facebook.common.references.CloseableReference import com.facebook.fresco.animation.bitmap.BitmapAnimationBackend.FrameType import com.facebook.fresco.animation.bitmap.BitmapFrameCache import com.facebook.fresco.animation.bitmap.BitmapFrameCache.FrameCacheListener import com.facebook.imageutils.BitmapUtil import javax.annotation.concurrent.GuardedBy /** Simple bitmap cache that keeps the last frame and reuses it if possible. */ class KeepLastFrameCache : BitmapFrameCache { private var lastFrameNumber = FRAME_NUMBER_UNSET private var frameCacheListener: FrameCacheListener? = null @GuardedBy("this") private var lastBitmapReference: CloseableReference? = null @Synchronized override fun getCachedFrame(frameNumber: Int): CloseableReference? = if (lastFrameNumber == frameNumber) { CloseableReference.cloneOrNull(lastBitmapReference) } else { null } @Synchronized override fun getFallbackFrame(frameNumber: Int): CloseableReference? = CloseableReference.cloneOrNull(lastBitmapReference) @Synchronized override fun getBitmapToReuseForFrame( frameNumber: Int, width: Int, height: Int, ): CloseableReference? = try { CloseableReference.cloneOrNull(lastBitmapReference) } finally { closeAndResetLastBitmapReference() } @Synchronized override fun contains(frameNumber: Int): Boolean = frameNumber == lastFrameNumber && CloseableReference.isValid(lastBitmapReference) @get:Synchronized override val sizeInBytes: Int get() = if (lastBitmapReference == null) 0 else BitmapUtil.getSizeInBytes(lastBitmapReference!!.get()) @Synchronized override fun clear() { closeAndResetLastBitmapReference() } @Synchronized override fun onFrameRendered( frameNumber: Int, bitmapReference: CloseableReference, @FrameType frameType: Int, ) { if (lastBitmapReference != null && bitmapReference.get() == lastBitmapReference?.get()) { return } CloseableReference.closeSafely(lastBitmapReference) if (lastFrameNumber != FRAME_NUMBER_UNSET) { frameCacheListener?.onFrameEvicted(this, lastFrameNumber) } lastBitmapReference = CloseableReference.cloneOrNull(bitmapReference) frameCacheListener?.onFrameCached(this, frameNumber) lastFrameNumber = frameNumber } override fun onFramePrepared( frameNumber: Int, bitmapReference: CloseableReference, @FrameType frameType: Int, ) = Unit override fun setFrameCacheListener(frameCacheListener: FrameCacheListener?) { this.frameCacheListener = frameCacheListener } @Synchronized private fun closeAndResetLastBitmapReference() { if (lastFrameNumber != FRAME_NUMBER_UNSET) { frameCacheListener?.onFrameEvicted(this, lastFrameNumber) } CloseableReference.closeSafely(lastBitmapReference) lastBitmapReference = null lastFrameNumber = FRAME_NUMBER_UNSET } companion object { private const val FRAME_NUMBER_UNSET = -1 } } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/cache/NoOpCache.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.cache import android.graphics.Bitmap import com.facebook.common.references.CloseableReference import com.facebook.fresco.animation.bitmap.BitmapAnimationBackend.FrameType import com.facebook.fresco.animation.bitmap.BitmapFrameCache import com.facebook.fresco.animation.bitmap.BitmapFrameCache.FrameCacheListener /** No-op bitmap cache that doesn't do anything. */ class NoOpCache : BitmapFrameCache { override fun getCachedFrame(frameNumber: Int): CloseableReference? = null override fun getFallbackFrame(frameNumber: Int): CloseableReference? = null override fun getBitmapToReuseForFrame( frameNumber: Int, width: Int, height: Int, ): CloseableReference? = null override fun contains(frameNumber: Int): Boolean = false override val sizeInBytes: Int = 0 override fun clear() { // no-op } override fun onFrameRendered( frameNumber: Int, bitmapReference: CloseableReference, @FrameType frameType: Int, ) { // no-op } override fun onFramePrepared( frameNumber: Int, bitmapReference: CloseableReference, @FrameType frameType: Int, ) { // Does not cache anything } override fun setFrameCacheListener(frameCacheListener: FrameCacheListener?) { // Does not cache anything } } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/preparation/BitmapFramePreparationStrategy.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.preparation import android.graphics.Bitmap import com.facebook.common.references.CloseableReference import com.facebook.fresco.animation.backend.AnimationBackend import com.facebook.fresco.animation.bitmap.BitmapFrameCache /** Frame preparation strategy to prepare next animation frames. */ interface BitmapFramePreparationStrategy { /** * Decide whether frames should be prepared ahead of time when a frame is drawn. * * @param bitmapFramePreparer the preparer to be used to create frames * @param bitmapFrameCache the cache to pass to the preparer * @param animationBackend the animation backend to prepare frames for * @param lastDrawnFrameNumber the last drawn frame number */ fun prepareFrames( bitmapFramePreparer: BitmapFramePreparer, bitmapFrameCache: BitmapFrameCache, animationBackend: AnimationBackend, lastDrawnFrameNumber: Int, onAnimationLoaded: (() -> Unit)? = null, ) = Unit /** * Prepare the frames for the animation giving the size of the animation and the size of the * canvas * * @param onAnimationLoaded This callback is invoked every time that bitmaps are loaded in memory * successfully. This can be triggered several times because animation could be dropped from * memory and then loaded again */ fun prepareFrames(canvasWidth: Int, canvasHeight: Int, onAnimationLoaded: (() -> Unit)?) = Unit /** Force stop any task running on the strategy */ fun onStop() = Unit /** Clear the frames from cache */ fun clearFrames() = Unit fun getBitmapFrame( frameNumber: Int, canvasWidth: Int, canvasHeight: Int, ): CloseableReference? = null } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/preparation/BitmapFramePreparer.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.preparation import com.facebook.fresco.animation.backend.AnimationBackend import com.facebook.fresco.animation.bitmap.BitmapFrameCache /** Prepare frames for animated images ahead of time. */ interface BitmapFramePreparer { /** * Prepare the frame with the given frame number and notify the supplied bitmap frame cache once * the frame is ready by calling [BitmapFrameCache#onFramePrepared(int, CloseableReference, int)] * * @param bitmapFrameCache the cache to notify for prepared frames * @param animationBackend the backend to prepare frames for * @param frameNumber * @return */ fun prepareFrame( bitmapFrameCache: BitmapFrameCache, animationBackend: AnimationBackend, frameNumber: Int, ): Boolean } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/preparation/DefaultBitmapFramePreparer.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.preparation import android.graphics.Bitmap import android.util.SparseArray import com.facebook.common.logging.FLog import com.facebook.common.references.CloseableReference import com.facebook.fresco.animation.backend.AnimationBackend import com.facebook.fresco.animation.bitmap.BitmapAnimationBackend import com.facebook.fresco.animation.bitmap.BitmapAnimationBackend.FrameType import com.facebook.fresco.animation.bitmap.BitmapFrameCache import com.facebook.fresco.animation.bitmap.BitmapFrameRenderer import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory import java.lang.RuntimeException import java.util.concurrent.ExecutorService /** * Default bitmap frame preparer that uses the given [ExecutorService] to schedule jobs. An instance * of this class can be shared between multiple animated images. */ class DefaultBitmapFramePreparer( private val platformBitmapFactory: PlatformBitmapFactory, private val bitmapFrameRenderer: BitmapFrameRenderer, private val bitmapConfig: Bitmap.Config, private val executorService: ExecutorService, ) : BitmapFramePreparer { private val TAG = DefaultBitmapFramePreparer::class.java private val pendingFrameDecodeJobs: SparseArray = SparseArray() override fun prepareFrame( bitmapFrameCache: BitmapFrameCache, animationBackend: AnimationBackend, frameNumber: Int, ): Boolean { // Create a unique ID to identify the frame for the given backend. val frameId = getUniqueId(animationBackend, frameNumber) synchronized(pendingFrameDecodeJobs) { // Check if already scheduled. if (pendingFrameDecodeJobs[frameId] != null) { FLog.v(TAG, "Already scheduled decode job for frame %d", frameNumber) return true } // Check if already cached. if (bitmapFrameCache.contains(frameNumber)) { FLog.v(TAG, "Frame %d is cached already.", frameNumber) return true } val frameDecodeRunnable = FrameDecodeRunnable(animationBackend, bitmapFrameCache, frameNumber, frameId) pendingFrameDecodeJobs.put(frameId, frameDecodeRunnable) executorService.execute(frameDecodeRunnable) } return true } private inner class FrameDecodeRunnable( private val animationBackend: AnimationBackend, private val bitmapFrameCache: BitmapFrameCache, private val frameNumber: Int, private val frameId: Int, ) : Runnable { override fun run() { try { // If we have a cached frame already, we don't need to do anything. if (bitmapFrameCache.contains(frameNumber)) { FLog.v(TAG, "Frame %d is cached already.", frameNumber) return } // Prepare the frame. if (prepareFrameAndCache(frameNumber, BitmapAnimationBackend.FRAME_TYPE_REUSED)) { FLog.v(TAG, "Prepared frame %d.", frameNumber) } else { FLog.e(TAG, "Could not prepare frame %d.", frameNumber) } } finally { synchronized(pendingFrameDecodeJobs) { pendingFrameDecodeJobs.remove(frameId) } } } private fun prepareFrameAndCache(frameNumber: Int, @FrameType frameType: Int): Boolean { var bitmapReference: CloseableReference? = null val created: Boolean val nextFrameType: Int try { when (frameType) { BitmapAnimationBackend.FRAME_TYPE_REUSED -> { bitmapReference = bitmapFrameCache.getBitmapToReuseForFrame( frameNumber, animationBackend.intrinsicWidth, animationBackend.intrinsicHeight, ) nextFrameType = BitmapAnimationBackend.FRAME_TYPE_CREATED } BitmapAnimationBackend.FRAME_TYPE_CREATED -> { bitmapReference = try { platformBitmapFactory.createBitmap( animationBackend.intrinsicWidth, animationBackend.intrinsicHeight, bitmapConfig, ) } catch (e: RuntimeException) { // Failed to create the bitmap for the frame, return and report that we could not // prepare the frame. FLog.w(TAG, "Failed to create frame bitmap", e) return false } nextFrameType = BitmapAnimationBackend.FRAME_TYPE_UNKNOWN } else -> return false } // Try to render and cache the frame created = renderFrameAndCache(frameNumber, bitmapReference, frameType) } finally { CloseableReference.closeSafely(bitmapReference) } return if (created || nextFrameType == BitmapAnimationBackend.FRAME_TYPE_UNKNOWN) { created } else { prepareFrameAndCache(frameNumber, nextFrameType) } } private fun renderFrameAndCache( frameNumber: Int, bitmapReference: CloseableReference?, @FrameType frameType: Int, ): Boolean { // Check if the bitmap is valid if (!CloseableReference.isValid(bitmapReference)) { return false } // Try to render the frame if ( bitmapReference == null || !bitmapFrameRenderer.renderFrame(frameNumber, bitmapReference.get()) ) { return false } FLog.v(TAG, "Frame %d ready.", frameNumber) // Cache the frame synchronized(pendingFrameDecodeJobs) { bitmapFrameCache.onFramePrepared(frameNumber, bitmapReference, frameType) } return true } } private fun getUniqueId(backend: AnimationBackend, frameNumber: Int): Int = 31 * backend.hashCode() + frameNumber } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/preparation/FixedNumberBitmapFramePreparationStrategy.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.preparation import com.facebook.common.logging.FLog import com.facebook.fresco.animation.backend.AnimationBackend import com.facebook.fresco.animation.bitmap.BitmapFrameCache /** Frame preparation strategy to prepare the next n frames */ class FixedNumberBitmapFramePreparationStrategy @JvmOverloads constructor(private val framesToPrepare: Int = 3) : BitmapFramePreparationStrategy { private val TAG = FixedNumberBitmapFramePreparationStrategy::class.java override fun prepareFrames( bitmapFramePreparer: BitmapFramePreparer, bitmapFrameCache: BitmapFrameCache, animationBackend: AnimationBackend, lastDrawnFrameNumber: Int, onAnimationLoaded: (() -> Unit)?, ) { for (i in 1..framesToPrepare) { val nextFrameNumber = (lastDrawnFrameNumber + i) % animationBackend.frameCount if (FLog.isLoggable(FLog.VERBOSE)) { FLog.v(TAG, "Preparing frame %d, last drawn: %d", nextFrameNumber, lastDrawnFrameNumber) } if (!bitmapFramePreparer.prepareFrame(bitmapFrameCache, animationBackend, nextFrameNumber)) { // We cannot prepare more frames, so we return early return } } onAnimationLoaded?.invoke() } } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/preparation/FrameLoaderStrategy.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.preparation import android.graphics.Bitmap import androidx.annotation.UiThread import com.facebook.common.references.CloseableReference import com.facebook.fresco.animation.backend.AnimationInformation import com.facebook.fresco.animation.bitmap.BitmapFrameRenderer import com.facebook.fresco.animation.bitmap.preparation.ondemandanimation.AnimationCoordinator import com.facebook.fresco.animation.bitmap.preparation.ondemandanimation.DynamicRenderingFps import com.facebook.fresco.animation.bitmap.preparation.ondemandanimation.FrameLoader import com.facebook.fresco.animation.bitmap.preparation.ondemandanimation.FrameLoaderFactory import java.util.concurrent.TimeUnit /** Use a [FrameLoader] strategy to render the animaion */ class FrameLoaderStrategy( source: String?, private val animationInformation: AnimationInformation, private val bitmapFrameRenderer: BitmapFrameRenderer, private val frameLoaderFactory: FrameLoaderFactory, private val downscaleFrameToDrawableDimensions: Boolean, ) : BitmapFramePreparationStrategy { private val cacheKey = source ?: this.hashCode().toString() private val animationWidth: Int = animationInformation.width() private val animationHeight: Int = animationInformation.height() private var frameLoader: FrameLoader? = null get() { if (field == null) { field = frameLoaderFactory.createBufferLoader( cacheKey, bitmapFrameRenderer, animationInformation, ) } return field } private val maxAnimationFps = animationInformation.fps() private var currentFps = maxAnimationFps private var isRunning = true private val dynamicFpsRender = object : DynamicRenderingFps { override val animationFps: Int = maxAnimationFps override val renderingFps: Int get() = currentFps override fun setRenderingFps(renderingFps: Int) { if (renderingFps != currentFps && isRunning) { currentFps = renderingFps.coerceIn(1, maxAnimationFps) frameLoader?.compressToFps(currentFps) } } } @UiThread override fun prepareFrames( canvasWidth: Int, canvasHeight: Int, onAnimationLoaded: (() -> Unit)?, ) { // Validate inputs if (canvasWidth <= 0 || canvasHeight <= 0 || animationWidth <= 0 || animationHeight <= 0) { return } isRunning = true val frameSize = calculateFrameSize(canvasWidth, canvasHeight) frameLoader?.prepareFrames(frameSize.width, frameSize.height, onAnimationLoaded ?: {}) } @UiThread override fun getBitmapFrame( frameNumber: Int, canvasWidth: Int, canvasHeight: Int, ): CloseableReference? { val frameSize = calculateFrameSize(canvasWidth, canvasHeight) val frame = frameLoader?.getFrame(frameNumber, frameSize.width, frameSize.height) frame?.let { AnimationCoordinator.onRenderFrame(dynamicFpsRender, it) } isRunning = true return frame?.bitmapRef } override fun onStop() { frameLoader?.onStop() clearFrames() } override fun clearFrames() { frameLoader?.let { FrameLoaderFactory.saveUnusedFrame(cacheKey, it) } frameLoader = null isRunning = false } private fun calculateFrameSize(canvasWidth: Int, canvasHeight: Int): FrameSize { if (!downscaleFrameToDrawableDimensions) { return FrameSize(animationWidth, animationHeight) } var bitmapWidth: Int = animationWidth var bitmapHeight: Int = animationHeight // The maximum size for the bitmap is the size of the animation if the canvas is bigger if (canvasWidth < animationWidth || canvasHeight < animationHeight) { val ratioW = animationWidth.toDouble().div(animationHeight) if (canvasHeight > canvasWidth) { bitmapHeight = canvasHeight.coerceAtMost(animationHeight) bitmapWidth = bitmapHeight.times(ratioW).toInt() } else { bitmapWidth = canvasWidth.coerceAtMost(animationWidth) bitmapHeight = bitmapWidth.div(ratioW).toInt() } } return FrameSize(bitmapWidth, bitmapHeight) } private fun AnimationInformation.fps(): Int = TimeUnit.SECONDS.toMillis(1).div(loopDurationMs.div(frameCount)).coerceAtLeast(1).toInt() } private class FrameSize(val width: Int, val height: Int) ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/preparation/loadframe/AnimationLoaderExecutor.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.preparation.loadframe import java.util.concurrent.Executors import java.util.concurrent.ThreadFactory object AnimationLoaderExecutor { private val frameThreadFactory = ThreadFactory { runnable: Runnable? -> val thread = Thread(runnable) thread.priority = Thread.MIN_PRIORITY thread } private val executor = Executors.newCachedThreadPool(frameThreadFactory) fun execute(task: Runnable) { executor.execute(task) } } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/preparation/loadframe/FpsCompressorInfo.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.preparation.loadframe import android.graphics.Bitmap import com.facebook.common.references.CloseableReference class FpsCompressorInfo(private val maxFpsLimit: Int) { /** * @param frameBitmaps has the bitmaps of the animation. FrameNumber -> Bitmap * @param targetFps compressed animation fps * @return AnimationFrame reference if the animation was saved in memory. Returns null if * animation couldn't be allocated */ fun compress( durationMs: Int, frameBitmaps: Map>, targetFps: Int, ): CompressionResult { val realToCompressIndex = calculateReducedIndexes(durationMs, frameBitmaps.size, targetFps) return compressAnimation(frameBitmaps, realToCompressIndex) } /** * Create a Map calculated based on the maximum FPS allowed * * @param durationMs duration of the animation in ms * @param frameCount Number of frames extracted from the animation asset * @param targetFps * @return Map of equivalences between the original frame number and compress frame number */ fun calculateReducedIndexes(durationMs: Int, frameCount: Int, targetFps: Int): Map { val sanitiseFps = targetFps.coerceAtLeast(1).coerceAtMost(maxFpsLimit) val maxAllowedFrames = sanitiseFps.times(durationMs.millisecondsToSeconds()).coerceAtLeast(0f) val skipRatio = frameCount.div(maxAllowedFrames.coerceAtMost(frameCount.toFloat())) var prevFrame = 0 return (0 until frameCount).associateWith { frameIndex -> if ((frameIndex % skipRatio).toInt() == 0) { prevFrame = frameIndex } prevFrame } } /** * Compress animation releasing those bitmaps which wont be in the final animation * * @param frameBitmaps relation frameNumber->bitmap of the original animation * @param realToReducedIndex map of equivalences between originalFrameNumber->compressFrameNumber * @return map associating compressFrameNumber->bitmap */ private fun compressAnimation( frameBitmaps: Map>, realToReducedIndex: Map, ): CompressionResult { val compressedAnim = mutableMapOf>() val removedFrames = mutableListOf>() frameBitmaps.forEach { (i, bitmapRef) -> val reducedIndex = realToReducedIndex[i] ?: return@forEach if (compressedAnim.contains(reducedIndex)) { removedFrames.add(bitmapRef) } else { compressedAnim[reducedIndex] = bitmapRef } } return CompressionResult(compressedAnim, realToReducedIndex, removedFrames) } /** * Contain the result of the compression * * @param compressedAnim Contains the bitmaps associated with frame number * * ``` * ________________________ * | Frame 1 ----> Bitmap1 | ________________________ * | Frame 2 ----> Bitmap2 | ====> | Compress 1 ----> Bitmap1 | * ------------------------- ------------------------- * ``` * * @param realToReducedIndex contains the association between old frame numbers to new frame * numbers * * ``` * ________________________________ * | Frame 1 ----> Compress Frame 1 | * | Frame 2 ----> Compress Frame 1 | * --------------------------------- * * ``` * * @param removedFrames contains the bitmap that are not useful. "Bitmap2" */ class CompressionResult( val compressedAnim: Map>, val realToReducedIndex: Map, val removedFrames: List>, ) fun Int.millisecondsToSeconds(): Float = this.div(1000f) } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/preparation/ondemandanimation/AnimationBitmapFrame.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.preparation.ondemandanimation import android.graphics.Bitmap import com.facebook.common.references.CloseableReference import java.io.Closeable class AnimationBitmapFrame(var frameNumber: Int, val bitmap: CloseableReference) : Closeable { fun isValidFor(frameNumber: Int): Boolean = this.frameNumber == frameNumber && bitmap.isValid fun isValid(): Boolean = bitmap.isValid override fun close() { bitmap.close() } } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/preparation/ondemandanimation/AnimationCoordinator.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.preparation.ondemandanimation import android.os.Handler import android.os.HandlerThread import com.facebook.fresco.animation.bitmap.preparation.ondemandanimation.FrameResult.FrameType import java.util.Date import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicInteger object AnimationCoordinator { /** Frequency in ms to adjust the rendering fps based con device performance */ private const val FREQUENCY_PERFORMANCE_MS = 2000L private const val FREQUENCY_LOADERS_MS = 10000L /** This is the % of fps that animation will increase or decrease based on device performance */ private const val FPS_STEP_PERCENTAGE = 0.20f /** Minimum rendering fps percentage */ private const val MIN_RENDERING_FPS_PERCENTAGE = 0.50f private val successCounter = AtomicInteger(0) private val failuresCounter = AtomicInteger(0) private val criticalCounter = AtomicInteger(0) private val runningAnimations = ConcurrentHashMap() private val handler: Handler by lazy { val handlerThread = HandlerThread("FrescoAnimationWorker") handlerThread.start() Handler(handlerThread.looper) } private val calculatePerformance: Runnable = Runnable { val success = successCounter.getAndSet(0).toFloat() val failures = failuresCounter.getAndSet(0).toFloat() val critical = criticalCounter.getAndSet(0).toFloat() val totalFrames = success + failures + critical if (totalFrames > 0) { val successRatio = success.div(totalFrames) val failuresRatio = failures.div(totalFrames) val criticalRatio = critical.div(totalFrames) // Verify that device performance can render all needed frames if (failuresRatio > 0.25f || criticalRatio > 0.1f) { // If animation performance is not enough, then decrease the rendering fps runningAnimations.forEach { (animation, fpsStep) -> updateRenderingFps(animation, -fpsStep) } } else if (successRatio > 0.98f) { // Animation performance is good, then we can increase the rendering fps runningAnimations.forEach { (animation, fpsStep) -> updateRenderingFps(animation, fpsStep) } } else { // Performance is good enough, but to increase rendering fps could be risky } runningAnimations.clear() } schedulePerformance() } private val clearUnusedFrameLoaders: Runnable = Runnable { val maxUnusedTime = System.currentTimeMillis() - FREQUENCY_LOADERS_MS FrameLoaderFactory.clearUnusedUntil(Date(maxUnusedTime)) scheduleLoaders() } init { handler.post(calculatePerformance) handler.post(clearUnusedFrameLoaders) } private fun schedulePerformance() = handler.postDelayed(calculatePerformance, FREQUENCY_PERFORMANCE_MS) private fun scheduleLoaders() = handler.postDelayed(clearUnusedFrameLoaders, FREQUENCY_LOADERS_MS) private fun updateRenderingFps(animation: DynamicRenderingFps, delta: Int) { val minRenderingFps = animation.animationFps.times(MIN_RENDERING_FPS_PERCENTAGE).coerceAtLeast(1f).toInt() val renderingFps = (animation.renderingFps + delta).coerceIn(minRenderingFps, animation.animationFps) if (renderingFps != animation.renderingFps) { animation.setRenderingFps(renderingFps) } } /** * This method is executed everytime that a frame is render in one animation. This allow to * collect what is the running animation performance per [FREQUENCY_PERFORMANCE_MS] We will adjust * the animation performance based on this data. */ fun onRenderFrame(animation: DynamicRenderingFps, frameResult: FrameResult) { if (!runningAnimations.contains(animation)) { val fps = animation.animationFps val fpsStep = fps.times(FPS_STEP_PERCENTAGE).toInt() runningAnimations[animation] = fpsStep } when (frameResult.type) { FrameType.SUCCESS -> successCounter.incrementAndGet() FrameType.NEAREST -> failuresCounter.incrementAndGet() FrameType.MISSING -> criticalCounter.incrementAndGet() } } } /** This interface allow animations to adjust their fps according with the device performance. */ interface DynamicRenderingFps { /** Animation FPS provided by the original asset */ val animationFps: Int /** Render animation FPS. These are time-varying based on the performance of the device. */ val renderingFps: Int /** * Update the render fps to [renderingFps]. This number is calculated based on the range from * [AnimationCoordinator.MIN_RENDERING_FPS_PERCENTAGE] to [animationFps] */ fun setRenderingFps(renderingFps: Int) } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/preparation/ondemandanimation/AnimationLoaderFactory.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.preparation.ondemandanimation import com.facebook.fresco.animation.backend.AnimationInformation import com.facebook.fresco.animation.bitmap.BitmapFrameRenderer import com.facebook.fresco.animation.bitmap.preparation.loadframe.FpsCompressorInfo import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory import java.util.Date import java.util.concurrent.ConcurrentHashMap class FrameLoaderFactory( private val platformBitmapFactory: PlatformBitmapFactory, private val maxFpsRender: Int, private val bufferLengthMilliseconds: Int, private val enableBufferFrameLoaderFix: Boolean = false, private val frameLoaderListener: FrameLoaderListener? = null, private val enableSingleFrameRendering: Boolean = false, ) { fun createBufferLoader( cacheKey: String, bitmapFrameRenderer: BitmapFrameRenderer, animationInformation: AnimationInformation, ): FrameLoader { synchronized(UNUSED_FRAME_LOADERS) { val unusedFrameLoader = UNUSED_FRAME_LOADERS[cacheKey] if (unusedFrameLoader != null) { UNUSED_FRAME_LOADERS.remove(cacheKey) return unusedFrameLoader.frameLoader } } return BufferFrameLoader( platformBitmapFactory, bitmapFrameRenderer, FpsCompressorInfo(maxFpsRender), animationInformation, bufferLengthMilliseconds, enableBufferFrameLoaderFix, frameLoaderListener, enableSingleFrameRendering, ) } companion object { private val UNUSED_FRAME_LOADERS = ConcurrentHashMap() fun saveUnusedFrame(cacheKey: String, frameLoader: FrameLoader) { UNUSED_FRAME_LOADERS[cacheKey] = UnusedFrameLoader(frameLoader, Date()) } fun clearUnusedUntil(until: Date) { synchronized(UNUSED_FRAME_LOADERS) { val oldItems = UNUSED_FRAME_LOADERS.filter { it.value.insertedTime < until } oldItems.forEach { entry -> entry.value.frameLoader.clear() UNUSED_FRAME_LOADERS.remove(entry.key) } } } } } private class UnusedFrameLoader(val frameLoader: FrameLoader, val insertedTime: Date) ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/preparation/ondemandanimation/BufferFrameLoader.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.preparation.ondemandanimation import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.PorterDuff import androidx.annotation.UiThread import androidx.annotation.WorkerThread import com.facebook.common.references.CloseableReference import com.facebook.fresco.animation.backend.AnimationInformation import com.facebook.fresco.animation.bitmap.BitmapFrameRenderer import com.facebook.fresco.animation.bitmap.preparation.loadframe.AnimationLoaderExecutor import com.facebook.fresco.animation.bitmap.preparation.loadframe.FpsCompressorInfo import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory import java.util.ArrayDeque import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit import kotlin.collections.set /** * This frame loader uses a fixed number of bitmap. The buffer loads the next bunch of frames when * the animation render an specific threshold frame */ class BufferFrameLoader( private val platformBitmapFactory: PlatformBitmapFactory, private val bitmapFrameRenderer: BitmapFrameRenderer, private val fpsCompressor: FpsCompressorInfo, override val animationInformation: AnimationInformation, private val bufferLengthMilliseconds: Int, private val enableBufferFrameLoaderFix: Boolean = false, private val frameLoaderListener: FrameLoaderListener? = null, private val enableSingleFrameRendering: Boolean = false, ) : FrameLoader { private val bufferSize = ((animationInformation.fps() * bufferLengthMilliseconds) / 1000).coerceAtLeast(1) private val isSingleFrame = enableSingleFrameRendering && animationInformation.frameCount == 1 @Volatile private var singleFrameRef: CloseableReference? = null private val bufferFramesHash = ConcurrentHashMap() @Volatile private var thresholdFrame: Int @Volatile private var isFetching = false private val frameSequence = CircularList(animationInformation.frameCount) private var lastRenderedFrameNumber: Int = -1 private var compressionFrameMap: Map = emptyMap() private var renderableFrameIndexes: Set = emptySet() init { compressToFps(animationInformation.fps()) thresholdFrame = bufferSize.times(THRESHOLD_PERCENTAGE).toInt() } @UiThread override fun getFrame(frameNumber: Int, width: Int, height: Int): FrameResult { if (isSingleFrame) { return getSingleFrame(width, height) } val cachedFrameIndex = compressionFrameMap[frameNumber] // Return the nearest frame if the frame is not in the buffer OR width or height is 0 if (enableBufferFrameLoaderFix && (width == 0 || height == 0)) { frameLoaderListener?.onZeroFrameDimensions( origin = "BufferFrameLoader.getFrame", frameNumber, width, height, ) return findNearestToRender(frameNumber) } if (cachedFrameIndex == null) { return findNearestToRender(frameNumber) } lastRenderedFrameNumber = cachedFrameIndex val cachedFrame = bufferFramesHash[cachedFrameIndex]?.takeIf { it.isFrameAvailable } if (cachedFrame != null) { val isTargetAhead = frameSequence.isTargetAhead(thresholdFrame, cachedFrameIndex, bufferSize) if (isTargetAhead) { loadNextFrames(width, height) } return FrameResult(cachedFrame.bitmapRef.clone(), FrameResult.FrameType.SUCCESS) } loadNextFrames(width, height) return findNearestToRender(cachedFrameIndex) } @UiThread private fun getSingleFrame(width: Int, height: Int): FrameResult { frameLoaderListener?.onSingleFrameRender( origin = "BufferFrameLoader.getSingleFrame", width, height, ) singleFrameRef?.let { ref -> val clone = ref.cloneOrNull() if (clone != null) { return FrameResult(clone, FrameResult.FrameType.SUCCESS) } } if (width == 0 || height == 0) { return FrameResult(null, FrameResult.FrameType.MISSING) } val bitmapRef = platformBitmapFactory.createBitmap(width, height) bitmapFrameRenderer.renderFrame(0, bitmapRef.get()) singleFrameRef = bitmapRef return FrameResult(bitmapRef.clone(), FrameResult.FrameType.SUCCESS) } @UiThread private fun findNearestToRender(targetFrame: Int): FrameResult { val nearestFrame = findNearestFrame(targetFrame) return if (nearestFrame != null) { val bitmapRef = nearestFrame.bitmap.clone() lastRenderedFrameNumber = nearestFrame.frameNumber FrameResult(bitmapRef, FrameResult.FrameType.NEAREST) } else { FrameResult(null, FrameResult.FrameType.MISSING) } } @UiThread override fun prepareFrames(width: Int, height: Int, onAnimationLoaded: () -> Unit) { if (isSingleFrame) { onAnimationLoaded() return } loadNextFrames(width, height) onAnimationLoaded() } override fun compressToFps(fps: Int) { val durationMs = animationInformation.loopDurationMs.times(animationInformation.loopCount.coerceAtLeast(1)) compressionFrameMap = fpsCompressor.calculateReducedIndexes( durationMs = durationMs, frameCount = animationInformation.frameCount, targetFps = fps.coerceAtMost(animationInformation.fps()), ) renderableFrameIndexes = compressionFrameMap.values.toSet() } /** Release all bitmaps */ override fun clear() { CloseableReference.closeSafely(singleFrameRef) singleFrameRef = null bufferFramesHash.values.forEach { it.release() } bufferFramesHash.clear() lastRenderedFrameNumber = -1 } private fun loadNextFrames(width: Int, height: Int) { // Skip frame if width or height is 0 OR if the buffer is already loading if ((enableBufferFrameLoaderFix && (width == 0 || height == 0)) || isFetching) { frameLoaderListener?.onZeroFrameDimensions( origin = "BufferFrameLoader.loadNextFrames", frameNumber = lastRenderedFrameNumber.coerceAtLeast(0), width, height, ) return } isFetching = true AnimationLoaderExecutor.execute { do { val targetFrame = lastRenderedFrameNumber.coerceAtLeast(0) val success = extractDemandedFrame(targetFrame, width, height) } while (!success) isFetching = false } } @WorkerThread private fun extractDemandedFrame( targetFrame: Int, width: Int, height: Int, count: Int = 0, ): Boolean { val nextWindow = frameSequence.sublist(targetFrame, bufferSize).filter { renderableFrameIndexes.contains(it) } val nextWindowIndexes = nextWindow.toSet() val oldFramesNumbers = ArrayDeque(bufferFramesHash.keys.minus(nextWindowIndexes)) // Load new frames nextWindow.forEach { newFrameNumber -> if (bufferFramesHash[newFrameNumber] != null) { return@forEach } if (lastRenderedFrameNumber != -1 && !nextWindowIndexes.contains(lastRenderedFrameNumber)) { return false } val deprecatedFrameNumber = oldFramesNumbers.pollFirst() ?: -1 val cachedFrame = bufferFramesHash[deprecatedFrameNumber] val bufferFrame: BufferFrame val bitmapRef: CloseableReference val ref = cachedFrame?.bitmapRef?.cloneOrNull() if (ref != null) { bufferFrame = cachedFrame bitmapRef = ref } else { bufferFrame = BufferFrame(platformBitmapFactory.createBitmap(width, height)) bitmapRef = bufferFrame.bitmapRef.clone() } bufferFrame.isUpdatingFrame = true bitmapRef.use { obtainFrame(it, newFrameNumber, width, height) } bufferFramesHash.remove(deprecatedFrameNumber) bufferFrame.isUpdatingFrame = false bufferFramesHash[newFrameNumber] = bufferFrame } thresholdFrame = if (nextWindow.isEmpty()) bufferSize.times(THRESHOLD_PERCENTAGE).toInt() else { val windowSize = nextWindow.size val middlePoint = windowSize.times(THRESHOLD_PERCENTAGE).toInt().coerceIn(0, windowSize - 1) nextWindow[middlePoint] } return true } private fun obtainFrame( targetBitmap: CloseableReference, targetFrame: Int, width: Int, height: Int, ) { val nearestFrame = findNearestFrame(targetFrame) nearestFrame?.bitmap?.cloneOrNull()?.use { nearestBitmap -> val from = nearestFrame.frameNumber if (from < targetFrame) { targetBitmap.set(nearestBitmap.get()) (from + 1..targetFrame).forEach { bitmapFrameRenderer.renderFrame(it, targetBitmap.get()) } return } } targetBitmap.clear() (0..targetFrame).forEach { bitmapFrameRenderer.renderFrame(it, targetBitmap.get()) } } private fun findNearestFrame(targetFrame: Int): AnimationBitmapFrame? = (0..frameSequence.size).firstNotNullOfOrNull { delta -> val closestFrame = frameSequence.getPosition(targetFrame - delta) bufferFramesHash[closestFrame] ?.takeIf { it.isFrameAvailable } ?.let { AnimationBitmapFrame(closestFrame, it.bitmapRef) } } private fun CloseableReference.clear() { if (isValid) { Canvas(get()).drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) } } private fun CloseableReference.set(src: Bitmap): CloseableReference { if (isValid && get() != src) { val canvas = Canvas(get()) canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) canvas.drawBitmap(src, 0f, 0f, null) } return this } private fun AnimationInformation.fps(): Int = TimeUnit.SECONDS.toMillis(1).div(loopDurationMs.div(frameCount)).coerceAtLeast(1).toInt() private class BufferFrame(val bitmapRef: CloseableReference) { var isUpdatingFrame: Boolean = false val isFrameAvailable: Boolean get(): Boolean = !isUpdatingFrame && bitmapRef.isValid fun release() { CloseableReference.closeSafely(bitmapRef) } } companion object { /** * Used to calculate the threshold frame for triggering the next buffer load from the last * render frame */ private const val THRESHOLD_PERCENTAGE = 0.5f } } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/preparation/ondemandanimation/CircularList.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.preparation.ondemandanimation class CircularList(val size: Int) { fun isTargetAhead(from: Int, target: Int, length: Int): Boolean { val endPosition = getPosition(from + length) return if (from < endPosition) { target in from..endPosition } else { target in from..size || target in 0..endPosition } } fun getPosition(target: Int): Int { val circularPosition = target % size return circularPosition.takeIf { it >= 0 } ?: (circularPosition + size) } fun sublist(from: Int, length: Int): List = (0 until length).map { getPosition(from + it) } } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/bitmap/preparation/ondemandanimation/FrameLoader.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.preparation.ondemandanimation import android.graphics.Bitmap import androidx.annotation.UiThread import com.facebook.common.references.CloseableReference import com.facebook.fresco.animation.backend.AnimationInformation /** * Callback interface for logging frame loading events. This allows app-specific error reporting * implementations to be injected for various frame loading scenarios. */ interface FrameLoaderListener { /** * Called when frame loading is skipped due to zero frame dimensions. * * @param origin the origin of the call (e.g., "BufferFrameLoader.getFrame") * @param frameNumber the frame number being loaded (-1 if not applicable) * @param width the frame width * @param height the frame height */ fun onZeroFrameDimensions(origin: String, frameNumber: Int, width: Int, height: Int) /** * Called when a single frame render is performed. * * @param origin the origin of the call (e.g., "BufferFrameLoader.getSingleFrame") * @param width the frame width * @param height the frame height */ fun onSingleFrameRender(origin: String, width: Int, height: Int) } /** This interface provides the basic O(1) methods to extract and prepare bitmap animations */ interface FrameLoader { /** Animation info */ val animationInformation: AnimationInformation /** * Return the [frameNumber] bitmap to render, given the [width] and [height] of the view. * * This method is always executed by the main thread, so its performance needs to be O(1) */ @UiThread fun getFrame(frameNumber: Int, width: Int, height: Int): FrameResult /** * Prepare the frames to be rendered. [onAnimationLoaded] is executed once the preparation is done * * This method is always executed by the main thread, so its performance needs to be O(1). */ @UiThread fun prepareFrames(width: Int, height: Int, onAnimationLoaded: () -> Unit) /** * Force the animation to run to indicated fps * * @param fps fps that animation should run */ fun compressToFps(fps: Int): Unit = Unit fun onStop() = Unit /** Release resources */ fun clear() } class FrameResult(val bitmapRef: CloseableReference?, val type: FrameType) { enum class FrameType { SUCCESS, NEAREST, MISSING, } } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/drawable/AnimatedDrawable2.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.drawable import android.graphics.Canvas import android.graphics.ColorFilter import android.graphics.PixelFormat import android.graphics.Rect import android.graphics.drawable.Animatable import android.graphics.drawable.Drawable import android.os.SystemClock import com.facebook.common.logging.FLog import com.facebook.drawable.base.DrawableWithCaches import com.facebook.drawee.drawable.DrawableProperties import com.facebook.fresco.animation.backend.AnimationBackend import com.facebook.fresco.animation.bitmap.BitmapAnimationBackend import com.facebook.fresco.animation.frame.DropFramesFrameScheduler import com.facebook.fresco.animation.frame.FrameScheduler import kotlin.concurrent.Volatile import kotlin.math.max /** * Experimental new animated drawable that uses a supplied [AnimationBackend] for drawing frames. */ open class AnimatedDrawable2 @JvmOverloads constructor(private var _animationBackend: AnimationBackend? = null) : Drawable(), Animatable, DrawableWithCaches { /** [draw(Canvas)] listener that is notified for each draw call. Can be used for debugging. */ fun interface DrawListener { fun onDraw( animatedDrawable: AnimatedDrawable2, frameScheduler: FrameScheduler, frameNumberToDraw: Int, frameDrawn: Boolean, isAnimationRunning: Boolean, animationStartTimeMs: Long, animationTimeMs: Long, lastFrameAnimationTimeMs: Long, actualRenderTimeStartMs: Long, actualRenderTimeEndMs: Long, startRenderTimeForNextFrameMs: Long, scheduledRenderTimeForNextFrameMs: Long, ) } private var frameScheduler: FrameScheduler? // Animation parameters @Volatile private var _isRunning = false var startTimeMs: Long = 0 private set private var lastFrameAnimationTimeMs: Long = 0 private var expectedRenderTimeMs: Long = 0 private var lastDrawnFrameNumber = 0 private var pausedStartTimeMsDifference: Long = 0 private var pausedLastFrameAnimationTimeMsDifference: Long = 0 private var pausedLastDrawnFrameNumber = 0 private var frameSchedulingDelayMs = DEFAULT_FRAME_SCHEDULING_DELAY_MS.toLong() private var frameSchedulingOffsetMs = DEFAULT_FRAME_SCHEDULING_OFFSET_MS.toLong() // Animation statistics private var _droppedFrames = 0 // Listeners @Volatile private var animationListener = NO_OP_LISTENER @Volatile private var drawListener: DrawListener? = null private val animationBackendListener = AnimationBackend.Listener { animationListener.onAnimationLoaded() } // Holder for drawable properties like alpha to be able to re-apply if the backend changes. // The instance is created lazily only if needed. private var drawableProperties: DrawableProperties? = null /** * Runnable that invalidates the drawable that will be scheduled according to the next target * frame. */ private val invalidateRunnable: Runnable = object : Runnable { override fun run() { // Remove all potential other scheduled runnables // (e.g. if the view has been invalidated a lot) unscheduleSelf(this) // Draw the next frame invalidateSelf() } } override fun getIntrinsicWidth(): Int { return _animationBackend?.intrinsicWidth ?: super.getIntrinsicWidth() } override fun getIntrinsicHeight(): Int { return _animationBackend?.intrinsicHeight ?: super.getIntrinsicHeight() } /** Start the animation. */ override fun start() { if (_isRunning || _animationBackend == null || _animationBackend!!.frameCount <= 1) { return } _isRunning = true val now = now() startTimeMs = now - pausedStartTimeMsDifference expectedRenderTimeMs = startTimeMs lastFrameAnimationTimeMs = now - pausedLastFrameAnimationTimeMsDifference lastDrawnFrameNumber = pausedLastDrawnFrameNumber invalidateSelf() animationListener.onAnimationStart(this) } /** Stop the animation at the current frame. It can be resumed by calling [start()] again. */ override fun stop() { if (!_isRunning) { return } val now = now() pausedStartTimeMsDifference = now - startTimeMs pausedLastFrameAnimationTimeMsDifference = now - lastFrameAnimationTimeMs pausedLastDrawnFrameNumber = lastDrawnFrameNumber _isRunning = false startTimeMs = 0 expectedRenderTimeMs = startTimeMs lastFrameAnimationTimeMs = -1 lastDrawnFrameNumber = -1 _animationBackend?.clear() unscheduleSelf(invalidateRunnable) animationListener.onAnimationStop(this) } /** * Check whether the animation is running. * * @return true if the animation is currently running */ override fun isRunning(): Boolean = _isRunning override fun onBoundsChange(bounds: Rect) { super.onBoundsChange(bounds) _animationBackend?.setBounds(bounds) } override fun draw(canvas: Canvas) { if (_animationBackend == null || frameScheduler == null) { return } val actualRenderTimeStartMs = now() val animationTimeMs = if (_isRunning) (actualRenderTimeStartMs - startTimeMs + frameSchedulingOffsetMs) else max(lastFrameAnimationTimeMs.toDouble(), 0.0).toLong() // What frame should be drawn? var frameNumberToDraw = frameScheduler!!.getFrameNumberToRender(animationTimeMs, lastFrameAnimationTimeMs) // Check if the animation is finished and draw last frame if (frameNumberToDraw == FrameScheduler.FRAME_NUMBER_DONE) { frameNumberToDraw = _animationBackend!!.frameCount - 1 // Check if the animation is finished and draw the fallback thumbnail if // useFallbackThumbnail is true val bitmapBackend = _animationBackend as? BitmapAnimationBackend val animatedOptions = bitmapBackend?.animatedOptions val useFallbackThumbnail = animatedOptions?.useFallbackThumbnail() == true if (!useFallbackThumbnail) { animationListener.onAnimationStop(this) _isRunning = false } } else if (frameNumberToDraw == 0) { if (lastDrawnFrameNumber != -1 && actualRenderTimeStartMs >= expectedRenderTimeMs) { animationListener.onAnimationRepeat(this) } } // Draw the frame val frameDrawn = _animationBackend!!.drawFrame(this, canvas, frameNumberToDraw) if (frameDrawn) { // Notify listeners that we draw a new frame and // that the animation might be repeated animationListener.onAnimationFrame(this, frameNumberToDraw) lastDrawnFrameNumber = frameNumberToDraw } // Log potential dropped frames if (!frameDrawn) { onFrameDropped() } var targetRenderTimeForNextFrameMs = FrameScheduler.NO_NEXT_TARGET_RENDER_TIME.toLong() var scheduledRenderTimeForNextFrameMs: Long = -1 val actualRenderTimeEnd = now() if (_isRunning) { // Schedule the next frame if needed. targetRenderTimeForNextFrameMs = frameScheduler!!.getTargetRenderTimeForNextFrameMs(actualRenderTimeEnd - startTimeMs) if (targetRenderTimeForNextFrameMs != FrameScheduler.NO_NEXT_TARGET_RENDER_TIME.toLong()) { scheduledRenderTimeForNextFrameMs = targetRenderTimeForNextFrameMs + frameSchedulingDelayMs scheduleNextFrame(scheduledRenderTimeForNextFrameMs) } else { animationListener.onAnimationStop(this) _isRunning = false } } val listener = drawListener listener?.onDraw( this, checkNotNull(frameScheduler), frameNumberToDraw, frameDrawn, _isRunning, startTimeMs, animationTimeMs, lastFrameAnimationTimeMs, actualRenderTimeStartMs, actualRenderTimeEnd, targetRenderTimeForNextFrameMs, scheduledRenderTimeForNextFrameMs, ) lastFrameAnimationTimeMs = animationTimeMs } override fun setAlpha(alpha: Int) { if (drawableProperties == null) { drawableProperties = DrawableProperties() } drawableProperties!!.setAlpha(alpha) _animationBackend?.setAlpha(alpha) } override fun setColorFilter(colorFilter: ColorFilter?) { if (drawableProperties == null) { drawableProperties = DrawableProperties() } drawableProperties!!.setColorFilter(colorFilter) _animationBackend?.setColorFilter(colorFilter) } override fun getOpacity(): Int = PixelFormat.TRANSLUCENT fun preloadAnimation() { _animationBackend?.preloadAnimation() } var animationBackend: AnimationBackend? get() = _animationBackend /** * Update the animation backend to be used for the animation. This will also stop the animation. * In order to remove the current animation backend, call this method with null. * * @param animationBackend the animation backend to be used or null */ set(animationBackend) { if (this._animationBackend != null) { _animationBackend!!.setAnimationListener(null) } this._animationBackend = animationBackend if (this._animationBackend != null) { frameScheduler = DropFramesFrameScheduler(_animationBackend!!) _animationBackend!!.setAnimationListener(animationBackendListener) _animationBackend!!.setBounds(bounds) drawableProperties?.applyTo(this) } frameScheduler = createSchedulerForBackendAndDelayMethod(this._animationBackend) stop() } val droppedFrames: Long get() = _droppedFrames.toLong() val isInfiniteAnimation: Boolean get() = frameScheduler?.isInfiniteAnimation == true /** * Jump immediately to the given frame number. The animation will not be paused if it is running. * If the animation is not running, the animation will not be started. * * @param targetFrameNumber the frame number to jump to */ fun jumpToFrame(targetFrameNumber: Int) { if (_animationBackend == null || frameScheduler == null) { return } // In order to jump to a given frame, we have to compute the correct start time lastFrameAnimationTimeMs = frameScheduler!!.getTargetRenderTimeMs(targetFrameNumber) // Reset the paused timing as we broke the animation frame flow pausedLastDrawnFrameNumber = targetFrameNumber pausedStartTimeMsDifference = 0 pausedLastFrameAnimationTimeMsDifference = 0 startTimeMs = now() - lastFrameAnimationTimeMs expectedRenderTimeMs = startTimeMs invalidateSelf() } val loopDurationMs: Long /** * Get the animation duration for 1 loop by summing all frame durations. * * @return the duration of 1 animation loop in ms */ get() { if (_animationBackend == null) { return 0L } if (frameScheduler != null) { return frameScheduler!!.loopDurationMs } var loopDurationMs = 0 for (i in 0..<_animationBackend!!.frameCount) { loopDurationMs += _animationBackend!!.getFrameDurationMs(i) } return loopDurationMs.toLong() } val frameCount: Int /** * Get the number of frames for the animation. If no animation backend is set, 0 will be * returned. * * @return the number of frames of the animation */ get() = if (_animationBackend == null) 0 else _animationBackend!!.frameCount /** * Get the duration of a specific frame. If not animation backend is set, 0 will be returned. * * @param frameNumber the requested frame * @return the duration of the frame */ fun getFrameDurationMs(frameNumber: Int): Int = if (_animationBackend == null) 0 else _animationBackend!!.getFrameDurationMs(frameNumber) val loopCount: Int /** * Get the loop count of the animation. The returned value is either * [AnimationInformation#LOOP_COUNT_INFINITE] if the animation is repeated infinitely or a * positive integer that corresponds to the number of loops. If no animation backend is set, * [AnimationInformation#LOOP_COUNT_INFINITE] will be returned. * * @return the loop count of the animation or [AnimationInformation#LOOP_COUNT_INFINITE] */ get() = if (_animationBackend == null) 0 else _animationBackend!!.loopCount init { frameScheduler = createSchedulerForBackendAndDelayMethod(this._animationBackend) _animationBackend?.setAnimationListener(animationBackendListener) } /** * Frame scheduling delay to shift the target render time for a frame within the frame's visible * window. If the value is set to 0, the frame will be scheduled right at the beginning of the * frame's visible window. * * @param frameSchedulingDelayMs the delay to use in ms */ fun setFrameSchedulingDelayMs(frameSchedulingDelayMs: Long) { this.frameSchedulingDelayMs = frameSchedulingDelayMs } /** * Frame scheduling offset to shift the animation time by the given offset. This is similar to * [mFrameSchedulingDelayMs] but instead of delaying the invalidation, this offsets the animation * time by the given value. * * @param frameSchedulingOffsetMs the offset to use in ms */ fun setFrameSchedulingOffsetMs(frameSchedulingOffsetMs: Long) { this.frameSchedulingOffsetMs = frameSchedulingOffsetMs } /** * Set an animation listener that is notified for various animation events. * * @param animationListener the listener to use */ fun setAnimationListener(animationListener: AnimationListener?) { this.animationListener = animationListener ?: NO_OP_LISTENER } /** * Set a draw listener that is notified for each [draw(Canvas)] call. * * @param drawListener the listener to use */ fun setDrawListener(drawListener: DrawListener?) { this.drawListener = drawListener } /** * Schedule the next frame to be rendered after the given delay. * * @param targetAnimationTimeMs the time in ms to update the frame */ private fun scheduleNextFrame(targetAnimationTimeMs: Long) { expectedRenderTimeMs = startTimeMs + targetAnimationTimeMs scheduleSelf(invalidateRunnable, expectedRenderTimeMs) } private fun onFrameDropped() { _droppedFrames++ // we need to drop frames if (FLog.isLoggable(FLog.VERBOSE)) { FLog.v(TAG, "Dropped a frame. Count: %s", _droppedFrames) } } /** @return the current uptime in ms */ private fun now(): Long = // This call has to return [SystemClock#uptimeMillis()] in order to preserve correct // frame scheduling. SystemClock.uptimeMillis() /** * Set the animation to the given level. The level represents the animation time in ms. If the * animation time is greater than the last frame time for the last loop, the last frame will be * displayed. * * If the animation is running (e.g. if [start()] has been called, the level change will be * ignored. In this case, [stop()] the animation first. * * @param level the animation time in ms * @return true if the level change could be performed */ override fun onLevelChange(level: Int): Boolean { if (_isRunning) { // If the client called start on us, they expect us to run the animation. In that case, // we ignore level changes. return false } if (lastFrameAnimationTimeMs != level.toLong()) { lastFrameAnimationTimeMs = level.toLong() invalidateSelf() return true } return false } override fun dropCaches() { _animationBackend?.clear() } companion object { private val TAG: Class<*> = AnimatedDrawable2::class.java private val NO_OP_LISTENER: AnimationListener = BaseAnimationListener() private const val DEFAULT_FRAME_SCHEDULING_DELAY_MS = 8 private const val DEFAULT_FRAME_SCHEDULING_OFFSET_MS = 0 private fun createSchedulerForBackendAndDelayMethod( animationBackend: AnimationBackend? ): FrameScheduler? { if (animationBackend == null) { return null } return DropFramesFrameScheduler(animationBackend) } } } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/drawable/AnimatedDrawable2DebugDrawListener.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.drawable import com.facebook.common.logging.FLog import com.facebook.fresco.animation.frame.FrameScheduler /** * [com.facebook.fresco.animation.drawable.AnimatedDrawable2.DrawListener] for debugging * [AnimatedDrawable2]. */ class AnimatedDrawable2DebugDrawListener : AnimatedDrawable2.DrawListener { private var lastFrameNumber = -1 private var skippedFrames = 0 private var duplicateFrames = 0 private var drawCalls = 0 override fun onDraw( animatedDrawable: AnimatedDrawable2, frameScheduler: FrameScheduler, frameNumberToDraw: Int, frameDrawn: Boolean, isAnimationRunning: Boolean, animationStartTimeMs: Long, animationTimeMs: Long, lastFrameAnimationTimeMs: Long, actualRenderTimeStartMs: Long, actualRenderTimeEndMs: Long, startRenderTimeForNextFrameMs: Long, scheduledRenderTimeForNextFrameMs: Long, ) { val frameCount = animatedDrawable.animationBackend?.getFrameCount() ?: return val animationTimeDifference = animationTimeMs - lastFrameAnimationTimeMs drawCalls++ val expectedNextFrameNumber = (lastFrameNumber + 1) % frameCount if (expectedNextFrameNumber != frameNumberToDraw) { // something went wrong... if (lastFrameNumber == frameNumberToDraw) { duplicateFrames++ } else { var skippedFrameCount = (frameNumberToDraw - expectedNextFrameNumber) % frameCount if (skippedFrameCount < 0) { skippedFrameCount += frameCount } skippedFrames += skippedFrameCount } } lastFrameNumber = frameNumberToDraw FLog.d( TAG, ("draw: frame: %2d, drawn: %b, delay: %3d ms, rendering: %3d ms, prev: %3d ms ago, duplicates: %3d, skipped: %3d, draw calls: %4d, anim time: %6d ms, next start: %6d ms, next scheduled: %6d ms"), frameNumberToDraw, frameDrawn, animationTimeMs % frameScheduler.loopDurationMs - frameScheduler.getTargetRenderTimeMs(frameNumberToDraw), actualRenderTimeEndMs - actualRenderTimeStartMs, animationTimeDifference, duplicateFrames, skippedFrames, drawCalls, animationTimeMs, startRenderTimeForNextFrameMs, scheduledRenderTimeForNextFrameMs, ) } companion object { private val TAG: Class<*> = AnimatedDrawable2DebugDrawListener::class.java } } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/drawable/AnimationFrameScheduler.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.drawable import android.os.SystemClock import com.facebook.fresco.animation.frame.FrameScheduler import com.facebook.fresco.animation.frame.FrameScheduler.NO_NEXT_TARGET_RENDER_TIME import kotlin.math.max const val INVALID_FRAME_TIME = -1L private const val DEFAULT_FRAME_SCHEDULING_DELAY_MS = 8L private const val DEFAULT_FRAME_SCHEDULING_OFFSET_MS = 0L class AnimationFrameScheduler(private val frameScheduler: FrameScheduler) { var running: Boolean = false // Frame scheduling timer in milliseconds var frameSchedulingDelayMs: Long = DEFAULT_FRAME_SCHEDULING_DELAY_MS var frameSchedulingOffsetMs: Long = DEFAULT_FRAME_SCHEDULING_OFFSET_MS private var pauseTimeMs = 0L private var startMs = 0L private var expectedRenderTimeMs = 0L private var lastFrameAnimationTimeMs = 0L private var lastFrameAnimationTimeDifferenceMs = 0L // Frame management var lastDrawnFrameNumber = -1 private val pausedLastDrawnFrameNumber = -1 // Stats private var framesDropped = 0 private fun now() = SystemClock.uptimeMillis() fun start() { if (!running) { val now = now() startMs = now - pauseTimeMs expectedRenderTimeMs = startMs lastFrameAnimationTimeMs = now - lastFrameAnimationTimeDifferenceMs lastDrawnFrameNumber = pausedLastDrawnFrameNumber running = true } } fun stop() { if (running) { val now = now() pauseTimeMs = now - startMs lastFrameAnimationTimeDifferenceMs = now - lastFrameAnimationTimeMs startMs = 0L expectedRenderTimeMs = 0L lastFrameAnimationTimeMs = -1L lastDrawnFrameNumber = -1 running = false } } fun frameToDraw(): Int { val renderTimeMillis = now() val animationTimeMillis = if (running) { renderTimeMillis - startMs + frameSchedulingOffsetMs } else { max(lastFrameAnimationTimeMs, 0) } // What frame should be drawn? val frameNumberToDraw = frameScheduler.getFrameNumberToRender(animationTimeMillis, lastFrameAnimationTimeMs) lastFrameAnimationTimeMs = animationTimeMillis return frameNumberToDraw } // Returns -1 if it's an valid next render time fun nextRenderTime(): Long { if (!running) { return INVALID_FRAME_TIME } val actualRenderTimeEnd = now() val targetRenderTimeForNextFrameMs = frameScheduler.getTargetRenderTimeForNextFrameMs(actualRenderTimeEnd - startMs) if (targetRenderTimeForNextFrameMs != NO_NEXT_TARGET_RENDER_TIME.toLong()) { val nextFrameTime = targetRenderTimeForNextFrameMs + frameSchedulingDelayMs expectedRenderTimeMs = startMs + nextFrameTime return nextFrameTime } running = false return INVALID_FRAME_TIME } fun shouldRepeatAnimation(): Boolean { return lastDrawnFrameNumber != -1 && now() >= expectedRenderTimeMs } fun onFrameDropped() { framesDropped++ // TODO Add log info here } fun infinite(): Boolean = frameScheduler.isInfiniteAnimation fun loopDuration(): Long = frameScheduler.loopDurationMs } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/drawable/AnimationListener.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.drawable import android.graphics.drawable.Drawable /** * Animation listener that can be used to get notified about [AnimatedDrawable2] events. Call * [AnimatedDrawable2#setAnimationListener(AnimationListener)] to set a listener. */ interface AnimationListener { /** * Called when the animation is started for the given drawable. * * @param drawable the affected drawable */ fun onAnimationStart(drawable: Drawable) /** * Called when the animation is stopped for the given drawable. * * @param drawable the affected drawable */ fun onAnimationStop(drawable: Drawable) /** * Called when the animation is reset for the given drawable. * * @param drawable the affected drawable */ fun onAnimationReset(drawable: Drawable) /** * Called when the animation is repeated for the given drawable. Animations have a loop count, and * frame count, so this is called when the frame count is 0 and the loop count is increased. * * @param drawable the affected drawable */ fun onAnimationRepeat(drawable: Drawable) /** * Called when a frame of the animation is about to be rendered. * * @param drawable the affected drawable * @param frameNumber the frame number to be rendered */ fun onAnimationFrame(drawable: Drawable, frameNumber: Int) /** Triggered when animation is loaded in memory and ready to play */ fun onAnimationLoaded() = Unit } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/drawable/BaseAnimationListener.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.drawable import android.graphics.drawable.Drawable /** * Base animation listener. This convenience class can be used to simplify the code if the extending * class is not interested in all events. Just override the ones you need. * * See [AnimationListener] for more information. */ open class BaseAnimationListener : AnimationListener { override fun onAnimationStart(drawable: Drawable) = Unit override fun onAnimationStop(drawable: Drawable) = Unit override fun onAnimationReset(drawable: Drawable) = Unit override fun onAnimationRepeat(drawable: Drawable) = Unit override fun onAnimationFrame(drawable: Drawable, frameNumber: Int) = Unit } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/drawable/KAnimatedDrawable2.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.drawable import android.graphics.Canvas import android.graphics.ColorFilter import android.graphics.PixelFormat import android.graphics.Rect import android.graphics.drawable.Animatable import android.graphics.drawable.Drawable import com.facebook.drawable.base.DrawableWithCaches import com.facebook.drawee.drawable.DrawableProperties import com.facebook.fresco.animation.backend.AnimationBackend import com.facebook.fresco.animation.frame.DropFramesFrameScheduler import com.facebook.fresco.animation.frame.FrameScheduler open class KAnimatedDrawable2(private var animationBackend: AnimationBackend) : Drawable(), Animatable, DrawableWithCaches { private val animatedFrameScheduler = AnimationFrameScheduler(DropFramesFrameScheduler(animationBackend)) private var animationListener: AnimationListener = BaseAnimationListener() private var drawListener: DrawListener? = null private val drawableProperties = DrawableProperties().apply { applyTo(this@KAnimatedDrawable2) } @Volatile private var isRunning = false /** * Runnable that invalidates the drawable that will be scheduled according to the next target * frame. */ private val invalidateRunnable = object : Runnable { override fun run() { // Remove all potential other scheduled runnables // (e.g. if the view has been invalidated a lot) unscheduleSelf(this) // Draw the next frame invalidateSelf() } } override fun setAlpha(alpha: Int) { drawableProperties.setAlpha(alpha) animationBackend.setAlpha(alpha) } override fun setColorFilter(colorFilter: ColorFilter?) { drawableProperties.setColorFilter(colorFilter) animationBackend.setColorFilter(colorFilter) } override fun getOpacity(): Int = PixelFormat.TRANSLUCENT /** Start the animation. */ override fun start() { if (animationBackend.frameCount <= 0) { return } animatedFrameScheduler.start() animationListener.onAnimationStart(this) invalidateSelf() } /** Stop the animation at the current frame. It can be resumed by calling [start()] again. */ override fun stop() { animatedFrameScheduler.stop() animationListener.onAnimationStop(this) unscheduleSelf(invalidateRunnable) } /** * Check whether the animation is running. * * @return true if the animation is currently running */ override fun isRunning(): Boolean = animatedFrameScheduler.running override fun dropCaches() { animationBackend.clear() } override fun onBoundsChange(bounds: Rect) { animationBackend.setBounds(bounds) } override fun getIntrinsicWidth(): Int = animationBackend.intrinsicWidth override fun getIntrinsicHeight(): Int = animationBackend.intrinsicHeight /** * Get the animation duration for 1 loop by summing all frame durations. * * @return the duration of 1 animation loop in ms */ fun loopDurationMs(): Int = animationBackend.loopDurationMs /** * Get the number of frames for the animation. If no animation backend is set, 0 will be returned. * * @return the number of frames of the animation */ fun getFrameCount(): Int = animationBackend.frameCount /** * Get the loop count of the animation. The returned value is either * [AnimationInformation#LOOP_COUNT_INFINITE] if the animation is repeated infinitely or a * positive integer that corresponds to the number of loops. If no animation backend is set, * [AnimationInformation#LOOP_COUNT_INFINITE] will be returned. * * @return the loop count of the animation or [AnimationInformation#LOOP_COUNT_INFINITE] */ fun loopCount(): Int = animationBackend.loopCount /** * Frame scheduling delay to shift the target render time for a frame within the frame's visible * window. If the value is set to 0, the frame will be scheduled right at the beginning of the * frame's visible window. * * @param delayMs the delay to use in ms */ fun setFrameSchedulingDelayMs(delayMs: Long) { animatedFrameScheduler.frameSchedulingDelayMs = delayMs } /** * Frame scheduling offset to shift the animation time by the given offset. This is similar to * [frameSchedulingDelayMs] but instead of delaying the invalidation, this offsets the animation * time by the given value. * * @param offsetMs the offset to use in ms */ fun setFrameSchedulingOffsetMs(offsetMs: Long) { animatedFrameScheduler.frameSchedulingOffsetMs = offsetMs } /** * Set an animation listener that is notified for various animation events. * * @param listener the listener to use */ fun setAnimationListener(listener: AnimationListener?) { animationListener = listener ?: animationListener } /** * Set a draw listener that is notified for each [draw(Canvas)] call. * * @param listener the listener to use */ fun setDrawListener(listener: DrawListener?) { drawListener = listener } /** * Update the animation backend to be used for the animation. This will also stop the animation. * In order to remove the current animation backend, call this method with null. * * @param animationBackend the animation backend to be used or null */ fun setAnimationBackend(animationBackend: AnimationBackend?) { animationBackend ?: return stop() animationBackend.setBounds(bounds) drawableProperties.applyTo(this) this.animationBackend = animationBackend } override fun draw(canvas: Canvas) { var frameNumber = animatedFrameScheduler.frameToDraw() // Check if the animation is finished and draw last frame if so if (frameNumber == FrameScheduler.FRAME_NUMBER_DONE) { frameNumber = animationBackend.frameCount - 1 animatedFrameScheduler.running = false animationListener.onAnimationStop(this) } else if (frameNumber == 0 && animatedFrameScheduler.shouldRepeatAnimation()) { animationListener.onAnimationRepeat(this) } val frameDrawn = animationBackend.drawFrame(this, canvas, frameNumber) if (frameDrawn) { // Notify listeners that we draw a new frame and // that the animation might be repeated animationListener.onAnimationFrame(this, frameNumber) animatedFrameScheduler.lastDrawnFrameNumber = frameNumber } else { animatedFrameScheduler.onFrameDropped() } val nextFrameTime = animatedFrameScheduler.nextRenderTime() if (nextFrameTime != INVALID_FRAME_TIME) { scheduleSelf(invalidateRunnable, nextFrameTime) } else { animationListener.onAnimationStop(this) animatedFrameScheduler.running = false } } interface DrawListener { fun onDraw( animatedDrawable: KAnimatedDrawable2, frameScheduler: FrameScheduler, frameNumberToDraw: Int, frameDrawn: Boolean, isAnimationRunning: Boolean, animationStartTimeMs: Long, animationTimeMs: Long, lastFrameAnimationTimeMs: Long, actualRenderTimeStartMs: Long, actualRenderTimeEndMs: Long, startRenderTimeForNextFrameMs: Long, scheduledRenderTimeForNextFrameMs: Long, ) } } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/drawable/animator/AnimatedDrawable2ValueAnimatorHelper.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.drawable.animator import android.animation.ValueAnimator import android.animation.ValueAnimator.AnimatorUpdateListener import android.graphics.drawable.Drawable import com.facebook.fresco.animation.backend.AnimationInformation import com.facebook.fresco.animation.drawable.AnimatedDrawable2 /** Helper class to create [ValueAnimator]s for [AnimatedDrawable2]. */ object AnimatedDrawable2ValueAnimatorHelper { @JvmStatic fun createValueAnimator(animatedDrawable: AnimatedDrawable2, maxDurationMs: Int): ValueAnimator? { val animator = createValueAnimator( animatedDrawable, animatedDrawable.loopCount, animatedDrawable.loopDurationMs, ) ?: return null val repeatCount = Math.max(maxDurationMs / animatedDrawable.loopDurationMs, 1).toInt() animator.repeatCount = repeatCount return animator } @JvmStatic fun createValueAnimator( animatedDrawable: Drawable, loopCount: Int, loopDurationMs: Long, ): ValueAnimator { val animator = ValueAnimator() animator.setIntValues(0, loopDurationMs.toInt()) animator.duration = loopDurationMs animator.repeatCount = if (loopCount != AnimationInformation.LOOP_COUNT_INFINITE) loopCount else ValueAnimator.INFINITE animator.repeatMode = ValueAnimator.RESTART // Use a linear interpolator animator.interpolator = null val animatorUpdateListener = createAnimatorUpdateListener(animatedDrawable) animator.addUpdateListener(animatorUpdateListener) return animator } @JvmStatic fun createAnimatorUpdateListener(drawable: Drawable): AnimatorUpdateListener = AnimatorUpdateListener { animation: ValueAnimator -> drawable.level = (animation.animatedValue as Int) } } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/frame/DropFramesFrameScheduler.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.frame import androidx.annotation.VisibleForTesting import com.facebook.fresco.animation.backend.AnimationInformation /** Frame scheduler that maps time values to frames. */ class DropFramesFrameScheduler(private val animationInformation: AnimationInformation) : FrameScheduler { private var _loopDurationMs = UNSET.toLong() override fun getFrameNumberToRender(animationTimeMs: Long, lastFrameTimeMs: Long): Int { val loopDurationMs = this.loopDurationMs if (loopDurationMs == 0L) { return getFrameNumberWithinLoop(0) } if (!isInfiniteAnimation) { val loopCount = animationTimeMs / loopDurationMs if (loopCount >= animationInformation.loopCount) { return FrameScheduler.FRAME_NUMBER_DONE } } val timeInCurrentLoopMs = animationTimeMs % loopDurationMs return getFrameNumberWithinLoop(timeInCurrentLoopMs) } override fun getLoopDurationMs(): Long { if (_loopDurationMs != UNSET.toLong()) { return _loopDurationMs } _loopDurationMs = 0 val frameCount = animationInformation.frameCount for (i in 0 until frameCount) { _loopDurationMs += animationInformation.getFrameDurationMs(i).toLong() } return _loopDurationMs } override fun getTargetRenderTimeMs(frameNumber: Int): Long { var targetRenderTimeMs = 0L for (i in 0 until frameNumber) { targetRenderTimeMs += animationInformation.getFrameDurationMs(i).toLong() } return targetRenderTimeMs } override fun getTargetRenderTimeForNextFrameMs(animationTimeMs: Long): Long { val loopDurationMs = this.loopDurationMs // Sanity check. if (loopDurationMs == 0L) { return FrameScheduler.NO_NEXT_TARGET_RENDER_TIME.toLong() } if (!isInfiniteAnimation) { val loopCount = animationTimeMs / loopDurationMs if (loopCount >= animationInformation.loopCount) { return FrameScheduler.NO_NEXT_TARGET_RENDER_TIME.toLong() } } // The animation time in the current loop val timePassedInCurrentLoopMs = animationTimeMs % loopDurationMs // The animation time in the current loop for the next frame var timeOfNextFrameInLoopMs = 0L val frameCount = animationInformation.frameCount var i = 0 while (i < frameCount && timeOfNextFrameInLoopMs <= timePassedInCurrentLoopMs) { timeOfNextFrameInLoopMs += animationInformation.getFrameDurationMs(i).toLong() i++ } // Difference between current time in loop and next frame in loop val timeUntilNextFrameInLoopMs = timeOfNextFrameInLoopMs - timePassedInCurrentLoopMs // Add the difference to the current animation time return animationTimeMs + timeUntilNextFrameInLoopMs } override fun isInfiniteAnimation(): Boolean = animationInformation.loopCount == AnimationInformation.LOOP_COUNT_INFINITE @VisibleForTesting fun getFrameNumberWithinLoop(timeInCurrentLoopMs: Long): Int { var frame = 0 var currentDuration = 0L do { currentDuration += animationInformation.getFrameDurationMs(frame).toLong() frame++ } while (timeInCurrentLoopMs >= currentDuration) return frame - 1 } companion object { private const val UNSET = -1 } } ================================================ FILE: animated-drawable/src/main/java/com/facebook/fresco/animation/frame/FrameScheduler.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.frame; import com.facebook.infer.annotation.Nullsafe; /** Frame scheduler used to calculate which frame to display for given animation times. */ @Nullsafe(Nullsafe.Mode.LOCAL) public interface FrameScheduler { int FRAME_NUMBER_DONE = -1; int NO_NEXT_TARGET_RENDER_TIME = -1; /** * Get the frame number for the given animation time or {@link #FRAME_NUMBER_DONE} if the * animation is over. * * @param animationTimeMs the animation time to get the frame number for * @param lastFrameTimeMs the time of the last draw before * @return the frame number to render or {@link #FRAME_NUMBER_DONE} */ int getFrameNumberToRender(long animationTimeMs, long lastFrameTimeMs); /** * Get the loop duration of 1 full loop. * * @return the loop duration in ms */ long getLoopDurationMs(); /** * Get the target render time for the given frame number in ms. * * @param frameNumber the frame number to use * @return the target render time */ long getTargetRenderTimeMs(int frameNumber); /** * For a given animation time, calculate the target render time for the next frame in ms. If the * animation is over, this will return {@link #NO_NEXT_TARGET_RENDER_TIME} * * @param animationTimeMs the current animation time in ms * @return the target animation time in ms for the next frame after the given animation time or * {@link #NO_NEXT_TARGET_RENDER_TIME} if the animation is over */ long getTargetRenderTimeForNextFrameMs(long animationTimeMs); /** * @return true if the animation is infinite */ boolean isInfiniteAnimation(); } ================================================ FILE: animated-drawable/src/test/java/com/facebook/fresco/animation/backend/AnimationBackendDelegateTest.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.backend import android.graphics.Canvas import android.graphics.ColorFilter import android.graphics.Rect import android.graphics.drawable.Drawable import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner /** Tests [AnimationBackendDelegate] */ @RunWith(RobolectricTestRunner::class) class AnimationBackendDelegateTest { private lateinit var animationBackendDelegate: AnimationBackendDelegate private lateinit var animationBackend: AnimationBackend private lateinit var parent: Drawable private lateinit var canvas: Canvas @Before fun setup() { animationBackend = mock() parent = mock() canvas = mock() animationBackendDelegate = AnimationBackendDelegate(animationBackend) } @Test fun testForwardProperties() { val colorFilter = mock() val bounds = mock() val alphaValue = 123 verifyNoMoreInteractions(animationBackend) // Set values to be persisted animationBackendDelegate.setAlpha(alphaValue) animationBackendDelegate.setColorFilter(colorFilter) animationBackendDelegate.setBounds(bounds) // Verify that values have been restored verify(animationBackend).setAlpha(alphaValue) verify(animationBackend).setColorFilter(colorFilter) verify(animationBackend).setBounds(bounds) } @Test fun testGetProperties() { val width = 123 val height = 234 val sizeInBytes = 2000 val frameCount = 20 val loopCount = 1000 val frameDurationMs = 200 whenever(animationBackend.intrinsicWidth).thenReturn(width) whenever(animationBackend.intrinsicHeight).thenReturn(height) whenever(animationBackend.sizeInBytes).thenReturn(sizeInBytes) whenever(animationBackend.frameCount).thenReturn(frameCount) whenever(animationBackend.loopCount).thenReturn(loopCount) whenever(animationBackend.getFrameDurationMs(any())).thenReturn(frameDurationMs) assertThat(animationBackendDelegate.intrinsicWidth).isEqualTo(width) assertThat(animationBackendDelegate.intrinsicHeight).isEqualTo(height) assertThat(animationBackendDelegate.sizeInBytes).isEqualTo(sizeInBytes) assertThat(animationBackendDelegate.frameCount).isEqualTo(frameCount) assertThat(animationBackendDelegate.loopCount).isEqualTo(loopCount) assertThat(animationBackendDelegate.getFrameDurationMs(1)).isEqualTo(frameDurationMs) } @Test fun testGetDefaultProperties() { // We don't set an animation backend animationBackendDelegate.animationBackend = null assertThat(animationBackendDelegate.intrinsicWidth) .isEqualTo(AnimationBackend.INTRINSIC_DIMENSION_UNSET) assertThat(animationBackendDelegate.intrinsicHeight) .isEqualTo(AnimationBackend.INTRINSIC_DIMENSION_UNSET) assertThat(animationBackendDelegate.sizeInBytes).isEqualTo(0) assertThat(animationBackendDelegate.frameCount).isEqualTo(0) assertThat(animationBackendDelegate.loopCount).isEqualTo(0) assertThat(animationBackendDelegate.getFrameDurationMs(1)).isEqualTo(0) } @Test fun testSetAnimationBackend() { val backend2 = mock() val colorFilter = mock() val bounds = mock() val alphaValue = 123 verifyNoMoreInteractions(backend2) // Set values to be persisted animationBackendDelegate.setAlpha(alphaValue) animationBackendDelegate.setColorFilter(colorFilter) animationBackendDelegate.setBounds(bounds) animationBackendDelegate.animationBackend = backend2 // Verify that values have been restored verify(backend2).setAlpha(alphaValue) verify(backend2).setColorFilter(colorFilter) verify(backend2).setBounds(bounds) } @Test fun testDrawFrame() { animationBackendDelegate.drawFrame(parent, canvas, 1) verify(animationBackend).drawFrame(parent, canvas, 1) } @Test fun testClear() { animationBackendDelegate.clear() verify(animationBackend).clear() } } ================================================ FILE: animated-drawable/src/test/java/com/facebook/fresco/animation/backend/AnimationBackendDelegateWithInactivityCheckTest.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.backend import android.graphics.Canvas import android.graphics.drawable.Drawable import com.facebook.imagepipeline.testing.FakeClock import com.facebook.imagepipeline.testing.TestScheduledExecutorService import org.junit.Before import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions /** Tests [AnimationBackendDelegateWithInactivityCheck] */ class AnimationBackendDelegateWithInactivityCheckTest { private lateinit var animationBackendDelegateWithInactivityCheck: AnimationBackendDelegate private lateinit var animationBackend: AnimationBackend private lateinit var inactivityListener: AnimationBackendDelegateWithInactivityCheck.InactivityListener private lateinit var parent: Drawable private lateinit var canvas: Canvas private lateinit var fakeClock: FakeClock private lateinit var testScheduledExecutorService: TestScheduledExecutorService @Before fun setup() { animationBackend = mock() inactivityListener = mock() parent = mock() canvas = mock() fakeClock = FakeClock() testScheduledExecutorService = TestScheduledExecutorService(fakeClock) animationBackendDelegateWithInactivityCheck = AnimationBackendDelegateWithInactivityCheck.createForBackend( animationBackend, inactivityListener, fakeClock, testScheduledExecutorService, ) } @Test fun testNotifyInactive() { verifyNoMoreInteractions(inactivityListener) animationBackendDelegateWithInactivityCheck.drawFrame(parent, canvas, 0) verifyNoMoreInteractions(inactivityListener) fakeClock.incrementBy(100) verifyNoMoreInteractions(inactivityListener) fakeClock.incrementBy(AnimationBackendDelegateWithInactivityCheck.INACTIVITY_THRESHOLD_MS) verify(inactivityListener).onInactive() } } ================================================ FILE: animated-drawable/src/test/java/com/facebook/fresco/animation/bitmap/BitmapAnimationBackendTest.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Paint import android.graphics.Rect import android.graphics.drawable.Drawable import com.facebook.common.references.CloseableReference import com.facebook.common.references.ResourceReleaser import com.facebook.fresco.animation.backend.AnimationBackend import com.facebook.fresco.animation.backend.AnimationInformation import com.facebook.fresco.animation.bitmap.preparation.BitmapFramePreparationStrategy import com.facebook.fresco.animation.bitmap.preparation.BitmapFramePreparer import com.facebook.fresco.vito.options.AnimatedOptions import com.facebook.fresco.vito.options.RoundingOptions import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers.anyInt import org.mockito.Captor import org.mockito.Mock import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.capture import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner /** Tests [BitmapAnimationBackend] */ @RunWith(RobolectricTestRunner::class) class BitmapAnimationBackendTest { @Mock lateinit var platformBitmapFactory: PlatformBitmapFactory @Mock lateinit var bitmapFrameCache: BitmapFrameCache @Mock lateinit var animationInformation: AnimationInformation @Mock lateinit var bitmapFrameRenderer: BitmapFrameRenderer @Mock lateinit var bounds: Rect @Mock lateinit var parentDrawable: Drawable @Mock lateinit var canvas: Canvas @Mock lateinit var bitmap: Bitmap @Mock lateinit var bitmapResourceReleaser: ResourceReleaser @Mock lateinit var frameListener: BitmapAnimationBackend.FrameListener @Mock lateinit var bitmapFramePreparationStrategy: BitmapFramePreparationStrategy @Mock lateinit var bitmapFramePreparer: BitmapFramePreparer @Captor lateinit var capturedBitmapReference: ArgumentCaptor> private lateinit var bitmapReference: CloseableReference private lateinit var bitmapAnimationBackend: BitmapAnimationBackend @Before fun setup() { MockitoAnnotations.initMocks(this) bitmapReference = CloseableReference.of(bitmap, bitmapResourceReleaser) bitmapAnimationBackend = BitmapAnimationBackend( platformBitmapFactory, bitmapFrameCache, animationInformation, bitmapFrameRenderer, false, /* isNewRenderImplementation */ bitmapFramePreparationStrategy, bitmapFramePreparer, null, ) bitmapAnimationBackend.setFrameListener(frameListener) } @Test fun testSetBounds() { bitmapAnimationBackend.setBounds(bounds) verify(bitmapFrameRenderer).setBounds(bounds) } @Test fun testSetBoundsUpdatesIntrinsicDimensionsWhenBackendDimensionsUnset() { val boundsWidth = 160 val boundsHeight = 90 val backendIntrinsicWidth = AnimationBackend.INTRINSIC_DIMENSION_UNSET val backendIntrinsicHeight = AnimationBackend.INTRINSIC_DIMENSION_UNSET setupBoundsAndRendererDimensions( boundsWidth, boundsHeight, backendIntrinsicWidth, backendIntrinsicHeight, ) bitmapAnimationBackend.setBounds(bounds) assertThat(bitmapAnimationBackend.intrinsicWidth).isEqualTo(boundsWidth) assertThat(bitmapAnimationBackend.intrinsicHeight).isEqualTo(boundsHeight) } @Test fun testSetBoundsUpdatesIntrinsicDimensionsWhenBackendDimensionWidthSet() { val boundsWidth = 160 val boundsHeight = 90 val backendIntrinsicWidth = 260 val backendIntrinsicHeight = AnimationBackend.INTRINSIC_DIMENSION_UNSET setupBoundsAndRendererDimensions( boundsWidth, boundsHeight, backendIntrinsicWidth, backendIntrinsicHeight, ) bitmapAnimationBackend.setBounds(bounds) assertThat(bitmapAnimationBackend.intrinsicWidth).isEqualTo(backendIntrinsicWidth) assertThat(bitmapAnimationBackend.intrinsicHeight).isEqualTo(boundsHeight) } @Test fun testSetBoundsUpdatesIntrinsicDimensionsWhenBackendDimensionHeightSet() { val boundsWidth = 160 val boundsHeight = 90 val backendIntrinsicWidth = AnimationBackend.INTRINSIC_DIMENSION_UNSET val backendIntrinsicHeight = 260 setupBoundsAndRendererDimensions( boundsWidth, boundsHeight, backendIntrinsicWidth, backendIntrinsicHeight, ) bitmapAnimationBackend.setBounds(bounds) assertThat(bitmapAnimationBackend.intrinsicWidth).isEqualTo(boundsWidth) assertThat(bitmapAnimationBackend.intrinsicHeight).isEqualTo(backendIntrinsicHeight) } @Test fun testSetBoundsUpdatesIntrinsicDimensionsWhenBackendDimensionsSet() { val boundsWidth = 160 val boundsHeight = 90 val backendIntrinsicWidth = 260 val backendIntrinsicHeight = 300 setupBoundsAndRendererDimensions( boundsWidth, boundsHeight, backendIntrinsicWidth, backendIntrinsicHeight, ) bitmapAnimationBackend.setBounds(bounds) assertThat(bitmapAnimationBackend.intrinsicWidth).isEqualTo(backendIntrinsicWidth) assertThat(bitmapAnimationBackend.intrinsicHeight).isEqualTo(backendIntrinsicHeight) } @Test fun testSetBoundsUpdatesIntrinsicDimensionsWhenBackendDimensionsUnsetAndNullBounds() { val boundsWidth = 160 val boundsHeight = 90 val backendIntrinsicWidth = AnimationBackend.INTRINSIC_DIMENSION_UNSET val backendIntrinsicHeight = AnimationBackend.INTRINSIC_DIMENSION_UNSET setupBoundsAndRendererDimensions( boundsWidth, boundsHeight, backendIntrinsicWidth, backendIntrinsicHeight, ) bitmapAnimationBackend.setBounds(null) assertThat(bitmapAnimationBackend.intrinsicWidth) .isEqualTo(AnimationBackend.INTRINSIC_DIMENSION_UNSET) assertThat(bitmapAnimationBackend.intrinsicHeight) .isEqualTo(AnimationBackend.INTRINSIC_DIMENSION_UNSET) } @Test fun testSetBoundsUpdatesIntrinsicDimensionsWhenBackendDimensionsSetAndNullBounds() { val boundsWidth = 160 val boundsHeight = 90 val backendIntrinsicWidth = 260 val backendIntrinsicHeight = 300 setupBoundsAndRendererDimensions( boundsWidth, boundsHeight, backendIntrinsicWidth, backendIntrinsicHeight, ) bitmapAnimationBackend.setBounds(null) assertThat(bitmapAnimationBackend.intrinsicWidth).isEqualTo(backendIntrinsicWidth) assertThat(bitmapAnimationBackend.intrinsicHeight).isEqualTo(backendIntrinsicHeight) } @Test fun testSetBoundsUpdatesIntrinsicDimensionsWhenBackendWidthSetAndNullBounds() { val boundsWidth = 160 val boundsHeight = 90 val backendIntrinsicWidth = 260 val backendIntrinsicHeight = AnimationBackend.INTRINSIC_DIMENSION_UNSET setupBoundsAndRendererDimensions( boundsWidth, boundsHeight, backendIntrinsicWidth, backendIntrinsicHeight, ) bitmapAnimationBackend.setBounds(null) assertThat(bitmapAnimationBackend.intrinsicWidth).isEqualTo(backendIntrinsicWidth) assertThat(bitmapAnimationBackend.intrinsicHeight).isEqualTo(backendIntrinsicHeight) } @Test fun testSetBoundsUpdatesIntrinsicDimensionsWhenBackendHeightSetAndNullBounds() { val boundsWidth = 160 val boundsHeight = 90 val backendIntrinsicWidth = AnimationBackend.INTRINSIC_DIMENSION_UNSET val backendIntrinsicHeight = 400 setupBoundsAndRendererDimensions( boundsWidth, boundsHeight, backendIntrinsicWidth, backendIntrinsicHeight, ) bitmapAnimationBackend.setBounds(null) assertThat(bitmapAnimationBackend.intrinsicWidth).isEqualTo(backendIntrinsicWidth) assertThat(bitmapAnimationBackend.intrinsicHeight).isEqualTo(backendIntrinsicHeight) } @Test fun testGetFrameCount() { whenever(animationInformation.frameCount).thenReturn(123) assertThat(bitmapAnimationBackend.frameCount).isEqualTo(123) } @Test fun testGetLoopCount() { whenever(animationInformation.loopCount).thenReturn(AnimationInformation.LOOP_COUNT_INFINITE) assertThat(bitmapAnimationBackend.loopCount).isEqualTo(AnimationInformation.LOOP_COUNT_INFINITE) whenever(animationInformation.loopCount).thenReturn(123) assertThat(bitmapAnimationBackend.loopCount).isEqualTo(123) } @Test fun testGetFrameDuration() { whenever(animationInformation.getFrameDurationMs(1)).thenReturn(50) whenever(animationInformation.getFrameDurationMs(2)).thenReturn(100) assertThat(bitmapAnimationBackend.getFrameDurationMs(1)).isEqualTo(50) assertThat(bitmapAnimationBackend.getFrameDurationMs(2)).isEqualTo(100) } @Test fun testDrawCachedBitmap() { whenever(bitmapFrameCache.getCachedFrame(anyInt())).thenReturn(bitmapReference) bitmapAnimationBackend.drawFrame(parentDrawable, canvas, 1) verify(frameListener).onDrawFrameStart(bitmapAnimationBackend, 1) verify(bitmapFrameCache).getCachedFrame(1) verify(canvas).drawBitmap(eq(bitmap), eq(0f), eq(0f), any()) verifyFramePreparationStrategyCalled(1) assertReferencesClosed() } @Test fun testDrawReusedBitmap() { whenever(bitmapFrameCache.getBitmapToReuseForFrame(anyInt(), anyInt(), anyInt())) .thenReturn(bitmapReference) whenever(bitmapFrameRenderer.renderFrame(anyInt(), any())).thenReturn(true) bitmapAnimationBackend.drawFrame(parentDrawable, canvas, 1) verify(frameListener).onDrawFrameStart(bitmapAnimationBackend, 1) verify(bitmapFrameCache).getCachedFrame(1) verify(bitmapFrameCache).getBitmapToReuseForFrame(1, 0, 0) verify(bitmapFrameRenderer).renderFrame(1, bitmap) verify(canvas).drawBitmap(eq(bitmap), eq(0f), eq(0f), any()) verifyFramePreparationStrategyCalled(1) verifyListenersAndCacheNotified(1, BitmapAnimationBackend.FRAME_TYPE_REUSED) assertReferencesClosed() } @Test fun testDrawNewBitmap() { whenever(platformBitmapFactory.createBitmap(anyInt(), anyInt(), any())) .thenReturn(bitmapReference) whenever(bitmapFrameRenderer.renderFrame(anyInt(), any())).thenReturn(true) bitmapAnimationBackend.drawFrame(parentDrawable, canvas, 2) verify(frameListener).onDrawFrameStart(bitmapAnimationBackend, 2) verify(bitmapFrameCache).getCachedFrame(2) verify(bitmapFrameCache).getBitmapToReuseForFrame(2, 0, 0) verify(platformBitmapFactory).createBitmap(0, 0, Bitmap.Config.ARGB_8888) verify(bitmapFrameRenderer).renderFrame(2, bitmap) verify(canvas).drawBitmap(eq(bitmap), eq(0f), eq(0f), any()) verifyFramePreparationStrategyCalled(2) verifyListenersAndCacheNotified(2, BitmapAnimationBackend.FRAME_TYPE_CREATED) assertReferencesClosed() } @Test fun testDrawNewBitmapWithBounds() { val width = 160 val height = 90 whenever(bounds.width()).thenReturn(width) whenever(bounds.height()).thenReturn(height) whenever(platformBitmapFactory.createBitmap(anyInt(), anyInt(), any())) .thenReturn(bitmapReference) whenever(bitmapFrameRenderer.renderFrame(anyInt(), any())).thenReturn(true) whenever(bitmapFrameRenderer.intrinsicWidth) .thenReturn(AnimationBackend.INTRINSIC_DIMENSION_UNSET) whenever(bitmapFrameRenderer.intrinsicHeight) .thenReturn(AnimationBackend.INTRINSIC_DIMENSION_UNSET) bitmapAnimationBackend.setBounds(bounds) bitmapAnimationBackend.drawFrame(parentDrawable, canvas, 2) verify(frameListener).onDrawFrameStart(bitmapAnimationBackend, 2) verify(bitmapFrameCache).getCachedFrame(2) verify(bitmapFrameCache).getBitmapToReuseForFrame(2, width, height) verify(platformBitmapFactory).createBitmap(width, height, Bitmap.Config.ARGB_8888) verify(bitmapFrameRenderer).renderFrame(2, bitmap) verify(canvas) .drawBitmap(eq(bitmap), ArgumentMatchers.isNull(Rect::class.java), eq(bounds), any()) verifyFramePreparationStrategyCalled(2) verifyListenersAndCacheNotified(2, BitmapAnimationBackend.FRAME_TYPE_CREATED) assertReferencesClosed() } @Test fun testDrawFallbackBitmapWhenCreateBitmapNotWorking() { whenever(bitmapFrameCache.getFallbackFrame(anyInt())).thenReturn(bitmapReference) whenever(bitmapFrameRenderer.renderFrame(anyInt(), any())).thenReturn(true) bitmapAnimationBackend.drawFrame(parentDrawable, canvas, 3) verify(frameListener).onDrawFrameStart(bitmapAnimationBackend, 3) verify(bitmapFrameCache).getCachedFrame(3) verify(bitmapFrameCache).getBitmapToReuseForFrame(3, 0, 0) verify(platformBitmapFactory).createBitmap(0, 0, Bitmap.Config.ARGB_8888) verify(bitmapFrameCache).getFallbackFrame(3) verify(canvas).drawBitmap(eq(bitmap), eq(0f), eq(0f), any()) verifyFramePreparationStrategyCalled(3) verifyListenersNotifiedWithoutCache(3, BitmapAnimationBackend.FRAME_TYPE_FALLBACK) assertReferencesClosed() } @Test fun testDrawFallbackBitmapWhenRenderFrameNotWorking() { whenever(bitmapFrameCache.getFallbackFrame(anyInt())).thenReturn(bitmapReference) // Return a different bitmap for PlatformBitmapFactory val temporaryBitmap = CloseableReference.of(bitmap, bitmapResourceReleaser) whenever(platformBitmapFactory.createBitmap(anyInt(), anyInt(), any())) .thenReturn(temporaryBitmap) whenever(bitmapFrameRenderer.renderFrame(anyInt(), any())).thenReturn(false) bitmapAnimationBackend.drawFrame(parentDrawable, canvas, 3) verify(frameListener).onDrawFrameStart(bitmapAnimationBackend, 3) verify(bitmapFrameCache).getCachedFrame(3) verify(bitmapFrameCache).getBitmapToReuseForFrame(3, 0, 0) verify(platformBitmapFactory).createBitmap(0, 0, Bitmap.Config.ARGB_8888) // Verify that the bitmap has been closed assertThat(temporaryBitmap.isValid).isFalse() verify(bitmapFrameCache).getFallbackFrame(3) verify(canvas).drawBitmap(eq(bitmap), eq(0f), eq(0f), any()) verifyFramePreparationStrategyCalled(3) verifyListenersNotifiedWithoutCache(3, BitmapAnimationBackend.FRAME_TYPE_FALLBACK) assertReferencesClosed() } @Test fun testDrawNoFrame() { bitmapAnimationBackend.drawFrame(parentDrawable, canvas, 4) verify(frameListener).onDrawFrameStart(bitmapAnimationBackend, 4) verify(bitmapFrameCache).getCachedFrame(4) verify(bitmapFrameCache).getBitmapToReuseForFrame(4, 0, 0) verify(platformBitmapFactory).createBitmap(0, 0, Bitmap.Config.ARGB_8888) verify(bitmapFrameCache).getFallbackFrame(4) verifyNoMoreInteractions(canvas, bitmapFrameCache) verifyFramePreparationStrategyCalled(4) verify(frameListener).onFrameDropped(bitmapAnimationBackend, 4) } private fun verifyFramePreparationStrategyCalled(frameNumber: Int) { verify(bitmapFramePreparationStrategy) .prepareFrames( bitmapFramePreparer, bitmapFrameCache, bitmapAnimationBackend, frameNumber, null, ) } private fun verifyListenersAndCacheNotified( frameNumber: Int, @BitmapAnimationBackend.FrameType frameType: Int, ) { // Verify cache callback verify(bitmapFrameCache) .onFrameRendered(eq(frameNumber), capture(capturedBitmapReference), eq(frameType)) assertThat(capturedBitmapReference.value).isEqualTo(bitmapReference) // Verify frame listener verify(frameListener).onFrameDrawn(bitmapAnimationBackend, frameNumber, frameType) } private fun verifyListenersNotifiedWithoutCache( frameNumber: Int, @BitmapAnimationBackend.FrameType frameType: Int, ) { // Verify cache callback verify(bitmapFrameCache, never()).onFrameRendered(anyInt(), any(), eq(frameType)) // Verify frame listener verify(frameListener).onFrameDrawn(bitmapAnimationBackend, frameNumber, frameType) } private fun assertReferencesClosed() { assertThat(bitmapReference.isValid).isFalse() } private fun setupBoundsAndRendererDimensions( boundsWidth: Int, boundsHeight: Int, backendIntrinsicWidth: Int, backendIntrinsicHeight: Int, ) { whenever(bounds.width()).thenReturn(boundsWidth) whenever(bounds.height()).thenReturn(boundsHeight) whenever(bitmapFrameRenderer.intrinsicWidth).thenReturn(backendIntrinsicWidth) whenever(bitmapFrameRenderer.intrinsicHeight).thenReturn(backendIntrinsicHeight) } fun createBackendWithRounding(roundingOptions: RoundingOptions?): BitmapAnimationBackend { return BitmapAnimationBackend( platformBitmapFactory, bitmapFrameCache, animationInformation, bitmapFrameRenderer, false, bitmapFramePreparationStrategy, bitmapFramePreparer, roundingOptions, ) } fun createBackendWithNewRenderImplementation( isNewRenderImplementation: Boolean ): BitmapAnimationBackend { return BitmapAnimationBackend( platformBitmapFactory, bitmapFrameCache, animationInformation, bitmapFrameRenderer, isNewRenderImplementation, bitmapFramePreparationStrategy, bitmapFramePreparer, null, ) } /** Verifies circular rounding options are preserved and cornerRadii is null */ @Test fun testCircularRoundingInitialization() { val circularRounding = RoundingOptions.asCircle() val backend = createBackendWithRounding(circularRounding) assertThat(backend.roundingOptions).isEqualTo(circularRounding) assertThat(backend.cornerRadii).isNull() } /** Verifies rectangular rounding creates an 8-element cornerRadii array with correct values */ @Test fun testRectangularRoundingInitialization() { val rectangularRounding = RoundingOptions.forCornerRadiusPx(20f) val backend = createBackendWithRounding(rectangularRounding) assertThat(backend.roundingOptions?.isCircular).isFalse() assertThat(backend.cornerRadii).isNotNull() assertThat(backend.cornerRadii).hasSize(8) backend.cornerRadii?.forEach { radius -> assertThat(radius).isEqualTo(20f) } } /** Verifies null rounding options result in null cornerRadii */ @Test fun testNoRoundingInitialization() { val backend = createBackendWithRounding(null) assertThat(backend.roundingOptions).isNull() assertThat(backend.cornerRadii).isNull() } /** Verifies custom corner radii arrays are preserved correctly */ @Test fun testRoundingWithCornerRadiiArray() { val cornerRadii = floatArrayOf(10f, 10f, 20f, 20f, 30f, 30f, 40f, 40f) val roundingOptions = RoundingOptions.forCornerRadii(cornerRadii) val backend = createBackendWithRounding(roundingOptions) assertThat(backend.roundingOptions?.isCircular).isFalse() assertThat(backend.cornerRadii).isEqualTo(cornerRadii) } /** Verifies unset corner radius results in null cornerRadii */ @Test fun testRoundingWithUnsetCornerRadius() { val roundingOptions = RoundingOptions.forCornerRadiusPx(RoundingOptions.CORNER_RADIUS_UNSET) val backend = createBackendWithRounding(roundingOptions) assertThat(backend.roundingOptions?.isCircular).isFalse() assertThat(backend.cornerRadii).isNull() } /** Verifies circular rounding options can be accessed and isCircular flag is true */ @Test fun testRoundingOptionsAccessibilityForCircular() { val circularRounding = RoundingOptions.asCircle() val backend = createBackendWithRounding(circularRounding) assertThat(backend.roundingOptions).isEqualTo(circularRounding) assertThat(backend.roundingOptions?.isCircular).isTrue() } /** Verifies rectangular rounding options can be accessed and isCircular flag is false */ @Test fun testRoundingOptionsAccessibilityForRectangular() { val cornerRadius = 25f val rectangularRounding = RoundingOptions.forCornerRadiusPx(cornerRadius) val backend = createBackendWithRounding(rectangularRounding) assertThat(backend.roundingOptions?.cornerRadius).isEqualTo(cornerRadius) assertThat(backend.roundingOptions?.isCircular).isFalse() } /** Verifies frame drawing works correctly with circular rounding applied */ @Test fun testDrawFrameWithCircularRounding() { val circularRounding = RoundingOptions.asCircle() val backend = createBackendWithRounding(circularRounding) backend.setFrameListener(frameListener) whenever(bitmapFrameCache.getCachedFrame(anyInt())).thenReturn(bitmapReference) val result = backend.drawFrame(parentDrawable, canvas, 1) assertThat(result).isTrue() verify(frameListener).onDrawFrameStart(backend, 1) } /** Verifies frame drawing works correctly with rectangular rounding applied */ @Test fun testDrawFrameWithRectangularRounding() { val rectangularRounding = RoundingOptions.forCornerRadiusPx(20f) val backend = createBackendWithRounding(rectangularRounding) backend.setFrameListener(frameListener) whenever(bitmapFrameCache.getCachedFrame(anyInt())).thenReturn(bitmapReference) val result = backend.drawFrame(parentDrawable, canvas, 1) assertThat(result).isTrue() verify(frameListener).onDrawFrameStart(backend, 1) } /** Verifies bounds setting works correctly with circular rounding */ @Test fun testCircularRoundingWithBounds() { val circularRounding = RoundingOptions.asCircle() val backend = createBackendWithRounding(circularRounding) val testBounds = Rect(0, 0, 150, 150) backend.setBounds(testBounds) verify(bitmapFrameRenderer).setBounds(testBounds) } /** Verifies empty corner radii arrays are handled correctly */ @Test fun testRoundingOptionsWithEmptyCornerRadiiArray() { val emptyCornerRadii = floatArrayOf() val roundingOptions = RoundingOptions.forCornerRadii(emptyCornerRadii) val backend = createBackendWithRounding(roundingOptions) assertThat(backend.roundingOptions?.isCircular).isFalse() assertThat(backend.cornerRadii).isEqualTo(emptyCornerRadii) } /** Verifies cornerRadii is null specifically for circular rounding */ @Test fun testCornerRadiiNullForCircularRounding() { val circularRounding = RoundingOptions.asCircle() val backend = createBackendWithRounding(circularRounding) // For circular rounding, cornerRadii should be null assertThat(backend.cornerRadii).isNull() assertThat(backend.roundingOptions?.isCircular).isTrue() } /** Verifies cornerRadii is a proper 8-element array for rectangular rounding */ @Test fun testCornerRadiiArrayForRectangularRounding() { val cornerRadius = 15f val rectangularRounding = RoundingOptions.forCornerRadiusPx(cornerRadius) val backend = createBackendWithRounding(rectangularRounding) // For rectangular rounding, cornerRadii should be an array of 8 elements assertThat(backend.cornerRadii).isNotNull() assertThat(backend.cornerRadii).hasSize(8) backend.cornerRadii?.forEach { radius -> assertThat(radius).isEqualTo(cornerRadius) } } /** Verifies rounding options are preserved during backend creation */ @Test fun testRoundingOptionsPreservation() { val originalRounding = RoundingOptions.asCircle() val backend = createBackendWithRounding(originalRounding) assertThat(backend.roundingOptions).isEqualTo(originalRounding) } /** Verifies custom corner radii arrays are preserved in both backend and options */ @Test fun testRoundingWithCustomCornerRadiiPreservation() { val customRadii = floatArrayOf(5f, 5f, 10f, 10f, 15f, 15f, 20f, 20f) val roundingOptions = RoundingOptions.forCornerRadii(customRadii) val backend = createBackendWithRounding(roundingOptions) assertThat(backend.cornerRadii).isEqualTo(customRadii) assertThat(backend.roundingOptions?.cornerRadii).isEqualTo(customRadii) } /** Verifies circular rounding behavior when bounds are set and frames are drawn */ @Test fun testCircularRoundingBehaviorWithBounds() { val circularRounding = RoundingOptions.asCircle() val backend = createBackendWithRounding(circularRounding) backend.setFrameListener(frameListener) val testBounds = Rect(0, 0, 100, 100) backend.setBounds(testBounds) whenever(bitmapFrameCache.getCachedFrame(anyInt())).thenReturn(bitmapReference) val result = backend.drawFrame(parentDrawable, canvas, 1) assertThat(result).isTrue() verify(bitmapFrameRenderer).setBounds(testBounds) verify(frameListener).onDrawFrameStart(backend, 1) } /** Verifies rectangular rounding behavior when bounds are set and frames are drawn */ @Test fun testRectangularRoundingBehaviorWithBounds() { val rectangularRounding = RoundingOptions.forCornerRadiusPx(10f) val backend = createBackendWithRounding(rectangularRounding) backend.setFrameListener(frameListener) val testBounds = Rect(0, 0, 200, 150) backend.setBounds(testBounds) whenever(bitmapFrameCache.getCachedFrame(anyInt())).thenReturn(bitmapReference) val result = backend.drawFrame(parentDrawable, canvas, 1) assertThat(result).isTrue() verify(bitmapFrameRenderer).setBounds(testBounds) verify(frameListener).onDrawFrameStart(backend, 1) } private fun createThumbnailBackend( thumbnailUrl: String?, loopCount: Int, roundingOptions: RoundingOptions? = null, ): BitmapAnimationBackend { val animatedOptions = if (thumbnailUrl != null) { AnimatedOptions.loop(loopCount, thumbnailUrl) } else { AnimatedOptions.loop(loopCount) } return BitmapAnimationBackend( platformBitmapFactory, bitmapFrameCache, animationInformation, bitmapFrameRenderer, false, bitmapFramePreparationStrategy, bitmapFramePreparer, roundingOptions, animatedOptions, ) } private fun createBackendWithAnimatedOptions( animatedOptions: AnimatedOptions? ): BitmapAnimationBackend { return BitmapAnimationBackend( platformBitmapFactory, bitmapFrameCache, animationInformation, bitmapFrameRenderer, false, bitmapFramePreparationStrategy, bitmapFramePreparer, null, animatedOptions, ) } private fun setupAnimationInformation(frameCount: Int = 3, loopCount: Int = 1) { whenever(animationInformation.frameCount).thenReturn(frameCount) whenever(animationInformation.loopCount).thenReturn(loopCount) } /** * Tests that thumbnail fallback is enabled when valid thumbnail URL and finite loop count are * provided. */ @Test fun testThumbnailInitializationWithValidOptions() { val thumbnailUrl = "https://example.com/thumbnail.jpg" val backend = createThumbnailBackend(thumbnailUrl, 3) assertThat(backend.animatedOptions?.useFallbackThumbnail()).isTrue() assertThat(backend.animatedOptions?.thumbnailUrl).isEqualTo(thumbnailUrl) } /** Tests that backend handles null AnimatedOptions without errors. */ @Test fun testThumbnailInitializationWithNullOptions() { val backend = createBackendWithAnimatedOptions(null) assertThat(backend.animatedOptions).isNull() } /** Tests that thumbnail fallback is disabled when empty thumbnail URL is provided. */ @Test fun testThumbnailInitializationWithEmptyUrl() { val backend = createThumbnailBackend("", 3) assertThat(backend.animatedOptions?.useFallbackThumbnail()).isFalse() } /** Tests that infinite loop animations don't use thumbnail fallback even with valid URL. */ @Test fun testThumbnailInitializationWithInfiniteLoop() { val backend = createThumbnailBackend("https://example.com/thumb.jpg", AnimatedOptions.LOOP_COUNT_INFINITE) assertThat(backend.animatedOptions?.useFallbackThumbnail()).isFalse() } /** Tests that AnimatedOptions loop count overrides AnimationInformation loop count. */ @Test fun testLoopCountWithAnimatedOptions() { setupAnimationInformation(loopCount = 10) val backend = createBackendWithAnimatedOptions(AnimatedOptions.loop(5)) assertThat(backend.loopCount).isEqualTo(5) } /** Tests that infinite AnimatedOptions correctly returns infinite loop count. */ @Test fun testLoopCountWithInfiniteAnimatedOptions() { setupAnimationInformation(loopCount = 10) val backend = createBackendWithAnimatedOptions(AnimatedOptions.infinite()) assertThat(backend.loopCount).isEqualTo(AnimationInformation.LOOP_COUNT_INFINITE) } /** * Tests that backend falls back to AnimationInformation loop count when no AnimatedOptions * provided. */ @Test fun testLoopCountWithoutAnimatedOptions() { setupAnimationInformation(loopCount = 7) val backend = createBackendWithAnimatedOptions(null) assertThat(backend.loopCount).isEqualTo(7) } /** Tests that normal frame drawing works when animation hasn't completed yet. */ @Test fun testDrawFrameWithThumbnailFallback() { setupAnimationInformation(frameCount = 3) whenever(bitmapFrameCache.getCachedFrame(anyInt())).thenReturn(bitmapReference) val backend = createThumbnailBackend("https://example.com/thumb.jpg", 2) backend.setFrameListener(frameListener) val result = backend.drawFrame(parentDrawable, canvas, 0) assertThat(result).isTrue() verify(frameListener).onDrawFrameStart(backend, 0) verify(canvas).drawBitmap(eq(bitmap), eq(0f), eq(0f), any()) } /** * Tests the useFallbackThumbnail() logic with various URL and loop configurations including * static options. */ @Test fun testAnimatedOptionsUseFallbackThumbnail() { val validOptions = AnimatedOptions.loop(3, "https://example.com/thumb.jpg") assertThat(validOptions.useFallbackThumbnail()).isTrue() val emptyUrlOptions = AnimatedOptions.loop(3, "") assertThat(emptyUrlOptions.useFallbackThumbnail()).isFalse() val nullUrlOptions = AnimatedOptions.loop(3, null) assertThat(nullUrlOptions.useFallbackThumbnail()).isFalse() val infiniteOptions = AnimatedOptions.infinite() assertThat(infiniteOptions.useFallbackThumbnail()).isFalse() } /** Tests equality and hashCode methods for AnimatedOptions with thumbnail URLs. */ @Test fun testAnimatedOptionsEquality() { val options1 = AnimatedOptions.loop(3, "https://example.com/thumb.jpg") val options2 = AnimatedOptions.loop(3, "https://example.com/thumb.jpg") val options3 = AnimatedOptions.loop(3, "https://different.com/thumb.jpg") val options4 = AnimatedOptions.loop(5, "https://example.com/thumb.jpg") assertThat(options1).isEqualTo(options2) assertThat(options1).isNotEqualTo(options3) assertThat(options1).isNotEqualTo(options4) assertThat(options1.hashCode()).isEqualTo(options2.hashCode()) } /** Tests isAnimationDisabled() method returns correct values. */ @Test fun testIsAnimationDisabled() { val normalOptions = AnimatedOptions.loop(3) assertThat(normalOptions.isAnimationDisabled()).isFalse() val infiniteOptions = AnimatedOptions.infinite() assertThat(infiniteOptions.isAnimationDisabled()).isFalse() val disabledOptions = AnimatedOptions.disableAnimation() assertThat(disabledOptions.isAnimationDisabled()).isTrue() val thumbnailOptions = AnimatedOptions.loop(2, "https://example.com/thumb.jpg") assertThat(thumbnailOptions.isAnimationDisabled()).isFalse() } /** Tests that disableAnimation() creates options with correct properties. */ @Test fun testDisableAnimationOptions() { val disabledOptions = AnimatedOptions.disableAnimation() assertThat(disabledOptions.isAnimationDisabled()).isTrue() assertThat(disabledOptions.loopCount).isEqualTo(-1) assertThat(disabledOptions.thumbnailUrl).isNull() assertThat(disabledOptions.isInfinite()).isFalse() assertThat(disabledOptions.useFallbackThumbnail()).isFalse() } /** Tests equality and hashCode for AnimatedOptions with disableAnimation flag. */ @Test fun testAnimatedOptionsEqualityWithDisableAnimation() { val disabledOptions1 = AnimatedOptions.disableAnimation() val disabledOptions2 = AnimatedOptions.disableAnimation() val normalOptions = AnimatedOptions.loop(3) assertThat(disabledOptions1).isEqualTo(disabledOptions2) assertThat(disabledOptions1).isNotEqualTo(normalOptions) assertThat(disabledOptions1.hashCode()).isEqualTo(disabledOptions2.hashCode()) } /** Tests that disabled animation options don't use thumbnail fallback. */ @Test fun testDisabledAnimationWithThumbnailFallback() { val disabledOptions = AnimatedOptions.disableAnimation() assertThat(disabledOptions.useFallbackThumbnail()).isFalse() } /** Tests backend behavior with disabled animation options. */ @Test fun testBackendWithDisabledAnimationOptions() { val disabledOptions = AnimatedOptions.disableAnimation() val backend = createBackendWithAnimatedOptions(disabledOptions) assertThat(backend.animatedOptions?.isAnimationDisabled()).isTrue() assertThat(backend.animatedOptions?.useFallbackThumbnail()).isFalse() } /** Tests that thumbnail and rounding options work together correctly. */ @Test fun testAnimatedOptionsWithRoundingOptions() { val roundingOptions = RoundingOptions.asCircle() val backend = createThumbnailBackend("https://example.com/thumb.jpg", 3, roundingOptions) assertThat(backend.animatedOptions?.useFallbackThumbnail()).isTrue() assertThat(backend.roundingOptions).isEqualTo(roundingOptions) } /** Tests that thumbnail resources are properly cleaned up when backend becomes inactive. */ @Test fun testOnInactiveReleasesResources() { val backend = createThumbnailBackend("https://example.com/thumb.jpg", 3) backend.onInactive() verify(bitmapFrameCache).clear() } /** Tests that setting bounds works correctly when thumbnail drawable is present. */ @Test fun testSetBoundsWithThumbnailDrawable() { val testBounds = Rect(10, 20, 110, 120) val backend = createThumbnailBackend("https://example.com/thumb.jpg", 3) backend.setBounds(testBounds) verify(bitmapFrameRenderer).setBounds(testBounds) } /** Tests that setting null bounds with thumbnail drawable doesn't cause errors. */ @Test fun testSetBoundsWithNullBoundsAndThumbnail() { val backend = createThumbnailBackend("https://example.com/thumb.jpg", 3) backend.setBounds(null) verify(bitmapFrameRenderer).setBounds(null) } /** Tests that roundingOptions is accessible as a property for circular rounding. */ @Test fun testRoundingOptionsAccessibility() { val roundingOptions = RoundingOptions.asCircle() val backend = createThumbnailBackend("https://example.com/thumb.jpg", 3, roundingOptions) assertThat(backend.roundingOptions).isEqualTo(roundingOptions) assertThat(backend.roundingOptions?.isCircular).isTrue() } /** Tests that corner radius rounding options are properly accessible and configured. */ @Test fun testRoundingOptionsWithCornerRadius() { val cornerRadius = 15f val roundingOptions = RoundingOptions.forCornerRadiusPx(cornerRadius) val backend = createThumbnailBackend("https://example.com/thumb.jpg", 3, roundingOptions) assertThat(backend.roundingOptions?.cornerRadius).isEqualTo(cornerRadius) assertThat(backend.roundingOptions?.isCircular).isFalse() } /** Tests that null rounding options are handled correctly without errors. */ @Test fun testRoundingOptionsWithNullRounding() { val backend = createThumbnailBackend("https://example.com/thumb.jpg", 3, null) assertThat(backend.roundingOptions).isNull() } /** Tests that circular rounding and thumbnail fallback work together seamlessly. */ @Test fun testThumbnailWithCircularRounding() { val roundingOptions = RoundingOptions.asCircle() val backend = createThumbnailBackend("https://example.com/thumb.jpg", 2, roundingOptions) assertThat(backend.roundingOptions?.isCircular).isTrue() assertThat(backend.animatedOptions?.useFallbackThumbnail()).isTrue() } /** Tests that thumbnail fallback is disabled when null thumbnail URL is provided. */ @Test fun testThumbnailFallbackDisabledWithoutUrl() { val backend = createThumbnailBackend(null, 3) assertThat(backend.animatedOptions?.useFallbackThumbnail()).isFalse() } /** Tests that empty string thumbnail URL disables thumbnail fallback functionality. */ @Test fun testThumbnailFallbackDisabledWithEmptyUrl() { val backend = createThumbnailBackend("", 3) assertThat(backend.animatedOptions?.useFallbackThumbnail()).isFalse() } /** Tests setAnimationListener and callback functionality */ @Test fun testSetAnimationListener() { val mockListener = mock() bitmapAnimationBackend.setAnimationListener(mockListener) // Trigger preload to test listener callback bitmapAnimationBackend.preloadAnimation() verify(bitmapFramePreparationStrategy) .prepareFrames( eq(bitmapFramePreparer), eq(bitmapFrameCache), eq(bitmapAnimationBackend), eq(0), any(), ) } /** Tests animation progress tracking for finite animations */ @Test fun testAnimationProgressTrackingFiniteAnimation() { setupAnimationInformation(frameCount = 3, loopCount = 2) val backend = createThumbnailBackend("https://example.com/thumb.jpg", 2) backend.setFrameListener(frameListener) whenever(bitmapFrameCache.getCachedFrame(anyInt())).thenReturn(bitmapReference) // Draw frames for first loop backend.drawFrame(parentDrawable, canvas, 0) backend.drawFrame(parentDrawable, canvas, 1) backend.drawFrame(parentDrawable, canvas, 2) // Draw frames for second loop backend.drawFrame(parentDrawable, canvas, 0) backend.drawFrame(parentDrawable, canvas, 1) backend.drawFrame(parentDrawable, canvas, 2) // This should complete the animation // Verify frames were drawn verify(frameListener, times(6)).onDrawFrameStart(eq(backend), anyInt()) } /** Tests that infinite animations don't trigger animation completion */ @Test fun testAnimationProgressTrackingInfiniteAnimation() { setupAnimationInformation(frameCount = 3) val backend = createThumbnailBackend( "https://example.com/thumb.jpg", AnimationInformation.LOOP_COUNT_INFINITE, ) backend.setFrameListener(frameListener) whenever(bitmapFrameCache.getCachedFrame(anyInt())).thenReturn(bitmapReference) for (i in 0..10) { backend.drawFrame(parentDrawable, canvas, i % 3) } verify(frameListener, org.mockito.kotlin.times(11)).onDrawFrameStart(eq(backend), anyInt()) } /** Tests preloadAnimation with old render implementation */ @Test fun testPreloadAnimationOldImplementation() { val mockListener = mock() bitmapAnimationBackend.setAnimationListener(mockListener) bitmapAnimationBackend.preloadAnimation() verify(bitmapFramePreparationStrategy) .prepareFrames( eq(bitmapFramePreparer), eq(bitmapFrameCache), eq(bitmapAnimationBackend), eq(0), any(), ) } /** Tests preloadAnimation with new render implementation */ @Test fun testPreloadAnimationNewImplementation() { val newBackend = createBackendWithNewRenderImplementation(true) whenever(animationInformation.width()).thenReturn(100) whenever(animationInformation.height()).thenReturn(200) newBackend.preloadAnimation() verify(bitmapFramePreparationStrategy).prepareFrames(eq(100), eq(200), any()) } /** Tests new render implementation draw frame path */ @Test fun testDrawFrameNewImplementation() { val newBackend = createBackendWithNewRenderImplementation(true) whenever(canvas.width).thenReturn(100) whenever(canvas.height).thenReturn(200) whenever(bitmapFramePreparationStrategy.getBitmapFrame(anyInt(), anyInt(), anyInt())) .thenReturn(bitmapReference) val result = newBackend.drawFrame(parentDrawable, canvas, 1) assertThat(result).isTrue() verify(bitmapFramePreparationStrategy).getBitmapFrame(1, 100, 200) verify(canvas).drawBitmap(eq(bitmap), eq(0f), eq(0f), any()) } /** Tests new render implementation when getBitmapFrame returns null */ @Test fun testDrawFrameNewImplementationNoBitmap() { val newBackend = createBackendWithNewRenderImplementation(true) whenever(canvas.width).thenReturn(100) whenever(canvas.height).thenReturn(200) whenever(bitmapFramePreparationStrategy.getBitmapFrame(anyInt(), anyInt(), anyInt())) .thenReturn(null) val result = newBackend.drawFrame(parentDrawable, canvas, 1) assertThat(result).isFalse() verify(bitmapFramePreparationStrategy).getBitmapFrame(1, 100, 200) verify(bitmapFramePreparationStrategy).prepareFrames(100, 200, null) } /** Tests onInactive with old render implementation */ @Test fun testOnInactiveOldImplementation() { bitmapAnimationBackend.onInactive() verify(bitmapFrameCache).clear() verify(bitmapFramePreparationStrategy, never()).onStop() } /** Tests onInactive with new render implementation */ @Test fun testOnInactiveNewImplementation() { val newBackend = createBackendWithNewRenderImplementation(true) newBackend.onInactive() verify(bitmapFramePreparationStrategy).onStop() verify(bitmapFrameCache, never()).clear() } /** Tests clear method with old render implementation */ @Test fun testClearOldImplementation() { bitmapAnimationBackend.clear() verify(bitmapFrameCache).clear() verify(bitmapFramePreparationStrategy, never()).clearFrames() } /** Tests clear method with new render implementation */ @Test fun testClearNewImplementation() { val newBackend = createBackendWithNewRenderImplementation(true) newBackend.clear() verify(bitmapFramePreparationStrategy).clearFrames() verify(bitmapFrameCache, never()).clear() } /** Tests setAlpha method updates paint alpha correctly */ @Test fun testSetAlpha() { val alpha = 128 bitmapAnimationBackend.setAlpha(alpha) // Verify alpha is applied when drawing whenever(bitmapFrameCache.getCachedFrame(anyInt())).thenReturn(bitmapReference) bitmapAnimationBackend.drawFrame(parentDrawable, canvas, 1) // Verify the frame as drawn verify(canvas).drawBitmap(eq(bitmap), eq(0f), eq(0f), any()) } /** Tests setColorFilter with null removes color filter */ @Test fun testSetColorFilterNull() { bitmapAnimationBackend.setColorFilter(null) whenever(bitmapFrameCache.getCachedFrame(anyInt())).thenReturn(bitmapReference) bitmapAnimationBackend.drawFrame(parentDrawable, canvas, 1) verify(canvas).drawBitmap(eq(bitmap), eq(0f), eq(0f), any()) } /** Tests getSizeInBytes delegates to bitmap frame cache */ @Test fun testGetSizeInBytes() { val expectedSize = 1024 whenever(bitmapFrameCache.sizeInBytes).thenReturn(expectedSize) assertThat(bitmapAnimationBackend.sizeInBytes).isEqualTo(expectedSize) } /** Tests that width and height delegate to animation information */ @Test fun testWidthAndHeight() { whenever(animationInformation.width()).thenReturn(150) whenever(animationInformation.height()).thenReturn(250) assertThat(bitmapAnimationBackend.width()).isEqualTo(150) assertThat(bitmapAnimationBackend.height()).isEqualTo(250) } /** Tests getLoopDurationMs delegates to animation information */ @Test fun testGetLoopDurationMs() { whenever(animationInformation.loopDurationMs).thenReturn(5000) assertThat(bitmapAnimationBackend.loopDurationMs).isEqualTo(5000) } } ================================================ FILE: animated-drawable/src/test/java/com/facebook/fresco/animation/bitmap/preparation/DefaultBitmapFramePreparerTest.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.preparation import android.graphics.Bitmap import com.facebook.common.references.CloseableReference import com.facebook.fresco.animation.backend.AnimationBackend import com.facebook.fresco.animation.bitmap.BitmapAnimationBackend import com.facebook.fresco.animation.bitmap.BitmapFrameCache import com.facebook.fresco.animation.bitmap.BitmapFrameRenderer import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory import com.facebook.imagepipeline.testing.FakeClock import com.facebook.imagepipeline.testing.TestExecutorService import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.mockito.kotlin.reset import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner /** Tests [DefaultBitmapFramePreparer]. */ @RunWith(RobolectricTestRunner::class) class DefaultBitmapFramePreparerTest { companion object { private const val FRAME_COUNT = 10 private const val BACKEND_INTRINSIC_WIDTH = 160 private const val BACKEND_INTRINSIC_HEIGHT = 90 private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888 } private lateinit var animationBackend: AnimationBackend private lateinit var bitmapFrameCache: BitmapFrameCache private lateinit var platformBitmapFactory: PlatformBitmapFactory private lateinit var bitmapFrameRenderer: BitmapFrameRenderer private lateinit var bitmapReference: CloseableReference private lateinit var bitmap: Bitmap private lateinit var fakeClock: FakeClock private lateinit var executorService: TestExecutorService private lateinit var defaultBitmapFramePreparer: DefaultBitmapFramePreparer @Before fun setup() { animationBackend = mock() bitmapFrameCache = mock() platformBitmapFactory = mock() bitmapFrameRenderer = mock() bitmapReference = mock() bitmap = mock() fakeClock = FakeClock() executorService = TestExecutorService(fakeClock) defaultBitmapFramePreparer = DefaultBitmapFramePreparer( platformBitmapFactory, bitmapFrameRenderer, BITMAP_CONFIG, executorService, ) whenever(animationBackend.frameCount).thenReturn(FRAME_COUNT) whenever(animationBackend.intrinsicWidth).thenReturn(BACKEND_INTRINSIC_WIDTH) whenever(animationBackend.intrinsicHeight).thenReturn(BACKEND_INTRINSIC_HEIGHT) whenever(bitmapReference.isValid).thenReturn(true) whenever(bitmapReference.get()).thenReturn(bitmap) } @Test fun testPrepareFrame_whenBitmapAlreadyCached_thenDoNothing() { whenever(bitmapFrameCache.contains(1)).thenReturn(true) whenever(bitmapFrameRenderer.renderFrame(1, bitmap)).thenReturn(true) defaultBitmapFramePreparer.prepareFrame(bitmapFrameCache, animationBackend, 1) assertThat(executorService.scheduledQueue.isIdle).isTrue() verify(bitmapFrameCache).contains(1) verifyNoMoreInteractions(bitmapFrameCache) verifyNoMoreInteractions(platformBitmapFactory, bitmapFrameRenderer, bitmapReference) } @Test fun testPrepareFrame_whenNoBitmapAvailable_thenDoNothing() { defaultBitmapFramePreparer.prepareFrame(bitmapFrameCache, animationBackend, 1) verify(bitmapFrameCache).contains(1) verifyNoMoreInteractions(bitmapFrameCache) reset(bitmapFrameCache) executorService.scheduledQueue.runNextPendingCommand() verify(bitmapFrameCache).contains(1) verify(bitmapFrameCache) .getBitmapToReuseForFrame(1, BACKEND_INTRINSIC_WIDTH, BACKEND_INTRINSIC_HEIGHT) verifyNoMoreInteractions(bitmapFrameCache) verify(platformBitmapFactory) .createBitmap(BACKEND_INTRINSIC_WIDTH, BACKEND_INTRINSIC_HEIGHT, BITMAP_CONFIG) verifyNoMoreInteractions(bitmapFrameRenderer) } @Test fun testPrepareFrame_whenReusedBitmapAvailable_thenCacheReusedBitmap() { whenever( bitmapFrameCache.getBitmapToReuseForFrame( 1, BACKEND_INTRINSIC_WIDTH, BACKEND_INTRINSIC_HEIGHT, ) ) .thenReturn(bitmapReference) whenever(bitmapFrameRenderer.renderFrame(1, bitmap)).thenReturn(true) defaultBitmapFramePreparer.prepareFrame(bitmapFrameCache, animationBackend, 1) executorService.scheduledQueue.runNextPendingCommand() verify(bitmapFrameCache, times(2)).contains(1) verify(bitmapFrameCache) .getBitmapToReuseForFrame(1, BACKEND_INTRINSIC_WIDTH, BACKEND_INTRINSIC_HEIGHT) verify(bitmapFrameRenderer).renderFrame(1, bitmap) verify(bitmapFrameCache) .onFramePrepared(1, bitmapReference, BitmapAnimationBackend.FRAME_TYPE_REUSED) verifyNoMoreInteractions(platformBitmapFactory) } @Test fun testPrepareFrame_whenPlatformBitmapAvailable_thenCacheCreatedBitmap() { whenever( platformBitmapFactory.createBitmap( BACKEND_INTRINSIC_WIDTH, BACKEND_INTRINSIC_HEIGHT, BITMAP_CONFIG, ) ) .thenReturn(bitmapReference) whenever(bitmapFrameRenderer.renderFrame(1, bitmap)).thenReturn(true) defaultBitmapFramePreparer.prepareFrame(bitmapFrameCache, animationBackend, 1) executorService.scheduledQueue.runNextPendingCommand() verify(bitmapFrameCache, times(2)).contains(1) verify(bitmapFrameCache) .getBitmapToReuseForFrame(1, BACKEND_INTRINSIC_WIDTH, BACKEND_INTRINSIC_HEIGHT) verify(platformBitmapFactory) .createBitmap(BACKEND_INTRINSIC_WIDTH, BACKEND_INTRINSIC_HEIGHT, BITMAP_CONFIG) verify(bitmapFrameRenderer).renderFrame(1, bitmap) verify(bitmapFrameCache) .onFramePrepared(1, bitmapReference, BitmapAnimationBackend.FRAME_TYPE_CREATED) verifyNoMoreInteractions(platformBitmapFactory) } @Test fun testPrepareFrame_whenReusedAndPlatformBitmapAvailable_thenCacheReusedBitmap() { whenever( bitmapFrameCache.getBitmapToReuseForFrame( 1, BACKEND_INTRINSIC_WIDTH, BACKEND_INTRINSIC_HEIGHT, ) ) .thenReturn(bitmapReference) whenever( platformBitmapFactory.createBitmap( BACKEND_INTRINSIC_WIDTH, BACKEND_INTRINSIC_HEIGHT, BITMAP_CONFIG, ) ) .thenReturn(bitmapReference) whenever(bitmapFrameRenderer.renderFrame(1, bitmap)).thenReturn(true) defaultBitmapFramePreparer.prepareFrame(bitmapFrameCache, animationBackend, 1) executorService.scheduledQueue.runNextPendingCommand() verify(bitmapFrameCache, times(2)).contains(1) verify(bitmapFrameCache) .getBitmapToReuseForFrame(1, BACKEND_INTRINSIC_WIDTH, BACKEND_INTRINSIC_HEIGHT) verify(bitmapFrameRenderer).renderFrame(1, bitmap) verify(bitmapFrameCache) .onFramePrepared(1, bitmapReference, BitmapAnimationBackend.FRAME_TYPE_REUSED) verifyNoMoreInteractions(platformBitmapFactory) } @Test fun testPrepareFrame_whenRenderingFails_thenDoNothing() { whenever( bitmapFrameCache.getBitmapToReuseForFrame( 1, BACKEND_INTRINSIC_WIDTH, BACKEND_INTRINSIC_HEIGHT, ) ) .thenReturn(bitmapReference) whenever( platformBitmapFactory.createBitmap( BACKEND_INTRINSIC_WIDTH, BACKEND_INTRINSIC_HEIGHT, BITMAP_CONFIG, ) ) .thenReturn(bitmapReference) whenever(bitmapFrameRenderer.renderFrame(1, bitmap)).thenReturn(false) defaultBitmapFramePreparer.prepareFrame(bitmapFrameCache, animationBackend, 1) executorService.scheduledQueue.runNextPendingCommand() verify(bitmapFrameCache, times(2)).contains(1) verify(bitmapFrameCache) .getBitmapToReuseForFrame(1, BACKEND_INTRINSIC_WIDTH, BACKEND_INTRINSIC_HEIGHT) verify(platformBitmapFactory) .createBitmap(BACKEND_INTRINSIC_WIDTH, BACKEND_INTRINSIC_HEIGHT, BITMAP_CONFIG) verify(bitmapFrameRenderer, times(2)).renderFrame(1, bitmap) verifyNoMoreInteractions(bitmapFrameCache) } } ================================================ FILE: animated-drawable/src/test/java/com/facebook/fresco/animation/bitmap/preparation/FixedNumberBitmapFramePreparationStrategyTest.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.bitmap.preparation import com.facebook.fresco.animation.backend.AnimationBackend import com.facebook.fresco.animation.bitmap.BitmapFrameCache import kotlin.Unit import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.inOrder import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner /** Tests [FixedNumberBitmapFramePreparationStrategy]. */ @RunWith(RobolectricTestRunner::class) class FixedNumberBitmapFramePreparationStrategyTest { companion object { const val NUMBER_OF_FRAMES_TO_PREPARE = 3 const val FRAME_COUNT = 10 } @Mock lateinit var animationBackend: AnimationBackend @Mock lateinit var bitmapFramePreparer: BitmapFramePreparer @Mock lateinit var bitmapFrameCache: BitmapFrameCache @Mock lateinit var onAnimationLoaded: () -> Unit private lateinit var bitmapFramePreparationStrategy: BitmapFramePreparationStrategy @Before fun setup() { MockitoAnnotations.initMocks(this) bitmapFramePreparationStrategy = FixedNumberBitmapFramePreparationStrategy(NUMBER_OF_FRAMES_TO_PREPARE) whenever(animationBackend.frameCount).thenReturn(FRAME_COUNT) whenever(bitmapFramePreparer.prepareFrame(eq(bitmapFrameCache), eq(animationBackend), any())) .thenReturn(true) } @Test fun testPrepareFrames_FromFirstFrame() { bitmapFramePreparationStrategy.prepareFrames( bitmapFramePreparer, bitmapFrameCache, animationBackend, 0, onAnimationLoaded, ) verifyPrepareCalledForFramesInOrder(1, 2, 3) } @Test fun testPrepareFrames_FromLastFrame() { bitmapFramePreparationStrategy.prepareFrames( bitmapFramePreparer, bitmapFrameCache, animationBackend, 9, onAnimationLoaded, ) verifyPrepareCalledForFramesInOrder(0, 1, 2) } @Test fun testPrepareFrames_ExactlyLastFrames() { bitmapFramePreparationStrategy.prepareFrames( bitmapFramePreparer, bitmapFrameCache, animationBackend, 6, onAnimationLoaded, ) verifyPrepareCalledForFramesInOrder(7, 8, 9) } @Test fun testPrepareFrames_FrameOverflow() { bitmapFramePreparationStrategy.prepareFrames( bitmapFramePreparer, bitmapFrameCache, animationBackend, 8, onAnimationLoaded, ) verifyPrepareCalledForFramesInOrder(9, 0, 1) } @Test fun testPrepareFrames_FromFirstFrame_WhenBitmapFramePreparerAlwaysFails() { whenever(bitmapFramePreparer.prepareFrame(eq(bitmapFrameCache), eq(animationBackend), any())) .thenReturn(false) bitmapFramePreparationStrategy.prepareFrames( bitmapFramePreparer, bitmapFrameCache, animationBackend, 0, onAnimationLoaded, ) verifyPrepareCalledForFramesInOrder(1) } @Test fun testPrepareFrames_FromFirstFrame_WhenBitmapFramePreparerFailsForSelectedFrames() { whenever(bitmapFramePreparer.prepareFrame(eq(bitmapFrameCache), eq(animationBackend), eq(2))) .thenReturn(false) whenever(bitmapFramePreparer.prepareFrame(eq(bitmapFrameCache), eq(animationBackend), eq(3))) .thenReturn(false) bitmapFramePreparationStrategy.prepareFrames( bitmapFramePreparer, bitmapFrameCache, animationBackend, 0, onAnimationLoaded, ) verifyPrepareCalledForFramesInOrder(1, 2) } @Test fun testPrepareFrames_onAnimationLoadedIsTrigger_WhenFramesAreLoaded() { bitmapFramePreparationStrategy.prepareFrames( bitmapFramePreparer, bitmapFrameCache, animationBackend, 0, onAnimationLoaded, ) verify(onAnimationLoaded).invoke() } private fun verifyPrepareCalledForFramesInOrder(vararg frameNumbers: Int) { val inOrderBitmapFramePreparer = inOrder(bitmapFramePreparer) for (frameNumber in frameNumbers) { inOrderBitmapFramePreparer .verify(bitmapFramePreparer) .prepareFrame(bitmapFrameCache, animationBackend, frameNumber) } inOrderBitmapFramePreparer.verifyNoMoreInteractions() } } ================================================ FILE: animated-drawable/src/test/java/com/facebook/fresco/animation/frame/DropFramesFrameSchedulerTest.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.animation.frame import android.graphics.Canvas import android.graphics.ColorFilter import android.graphics.Rect import android.graphics.drawable.Drawable import androidx.annotation.IntRange import com.facebook.fresco.animation.backend.AnimationBackend import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test /** Tests [DropFramesFrameScheduler]. */ class DropFramesFrameSchedulerTest { private lateinit var dummyAnimationBackend: DummyAnimationBackend private lateinit var frameScheduler: DropFramesFrameScheduler @Before fun setUp() { dummyAnimationBackend = DummyAnimationBackend(5) frameScheduler = DropFramesFrameScheduler(dummyAnimationBackend) } @Test fun testGetFrameNumberToRender() { assertThat(frameScheduler.getFrameNumberToRender(0, -1)).isEqualTo(0) assertThat(frameScheduler.getFrameNumberToRender(50, -1)).isEqualTo(0) assertThat(frameScheduler.getFrameNumberToRender(100, -1)).isEqualTo(1) assertThat(frameScheduler.getFrameNumberToRender(499, -1)).isEqualTo(4) assertThat(frameScheduler.getFrameNumberToRender(500, -1)).isEqualTo(0) assertThat(frameScheduler.getFrameNumberToRender(600, -1)).isEqualTo(1) assertThat(frameScheduler.getFrameNumberToRender(601, -1)).isEqualTo(1) } @Test fun testGetLoopDurationMs() { assertThat(frameScheduler.loopDurationMs).isEqualTo(500) } @Test fun testGetTargetRenderTimeMs() { assertThat(frameScheduler.getTargetRenderTimeMs(0)).isEqualTo(0) assertThat(frameScheduler.getTargetRenderTimeMs(1)).isEqualTo(100) assertThat(frameScheduler.getTargetRenderTimeMs(2)).isEqualTo(200) assertThat(frameScheduler.getTargetRenderTimeMs(3)).isEqualTo(300) assertThat(frameScheduler.getTargetRenderTimeMs(4)).isEqualTo(400) } @Test fun testGetTargetRenderTimeForNextFrameMs() { assertThat(frameScheduler.getTargetRenderTimeForNextFrameMs(0)).isEqualTo(100) assertThat(frameScheduler.getTargetRenderTimeForNextFrameMs(1)).isEqualTo(100) assertThat(frameScheduler.getTargetRenderTimeForNextFrameMs(50)).isEqualTo(100) assertThat(frameScheduler.getTargetRenderTimeForNextFrameMs(100)).isEqualTo(200) assertThat(frameScheduler.getTargetRenderTimeForNextFrameMs(170)).isEqualTo(200) assertThat(frameScheduler.getTargetRenderTimeForNextFrameMs(460)).isEqualTo(500) assertThat(frameScheduler.getTargetRenderTimeForNextFrameMs(499)).isEqualTo(500) assertThat(frameScheduler.getTargetRenderTimeForNextFrameMs(500)).isEqualTo(600) assertThat(frameScheduler.getTargetRenderTimeForNextFrameMs(501)).isEqualTo(600) assertThat(frameScheduler.getTargetRenderTimeForNextFrameMs(510)).isEqualTo(600) } @Test fun testGetTargetRenderTimeForNextFrameMsWhenAnimationOver() { val animationDurationMs = dummyAnimationBackend.getAnimationDurationMs() assertThat(frameScheduler.getTargetRenderTimeForNextFrameMs(animationDurationMs - 1)) .isEqualTo(animationDurationMs) assertThat(frameScheduler.getTargetRenderTimeForNextFrameMs(animationDurationMs)) .isEqualTo(FrameScheduler.NO_NEXT_TARGET_RENDER_TIME.toLong()) assertThat(frameScheduler.getTargetRenderTimeForNextFrameMs(animationDurationMs + 1)) .isEqualTo(FrameScheduler.NO_NEXT_TARGET_RENDER_TIME.toLong()) assertThat(frameScheduler.getTargetRenderTimeForNextFrameMs(animationDurationMs + 100)) .isEqualTo(FrameScheduler.NO_NEXT_TARGET_RENDER_TIME.toLong()) assertThat(frameScheduler.getTargetRenderTimeForNextFrameMs(animationDurationMs * 100)) .isEqualTo(FrameScheduler.NO_NEXT_TARGET_RENDER_TIME.toLong()) } @Test fun testIsInfiniteAnimation() { assertThat(frameScheduler.isInfiniteAnimation).isFalse() } @Test fun testLoopCount() { val animationDurationMs = dummyAnimationBackend.getAnimationDurationMs() val lastFrameNumber = dummyAnimationBackend.frameCount - 1 assertThat(frameScheduler.getFrameNumberToRender(animationDurationMs, -1)) .isEqualTo(FrameScheduler.FRAME_NUMBER_DONE) assertThat(frameScheduler.getFrameNumberToRender(animationDurationMs + 1, -1)) .isEqualTo(FrameScheduler.FRAME_NUMBER_DONE) assertThat( frameScheduler.getFrameNumberToRender( animationDurationMs + dummyAnimationBackend.getFrameDurationMs(lastFrameNumber), -1, ) ) .isEqualTo(FrameScheduler.FRAME_NUMBER_DONE) assertThat( frameScheduler.getFrameNumberToRender( animationDurationMs + dummyAnimationBackend.getFrameDurationMs(lastFrameNumber) + 100, -1, ) ) .isEqualTo(FrameScheduler.FRAME_NUMBER_DONE) } @Test fun testGetFrameNumberWithinLoop() { assertThat(frameScheduler.getFrameNumberWithinLoop(0)).isEqualTo(0) assertThat(frameScheduler.getFrameNumberWithinLoop(1)).isEqualTo(0) assertThat(frameScheduler.getFrameNumberWithinLoop(99)).isEqualTo(0) assertThat(frameScheduler.getFrameNumberWithinLoop(100)).isEqualTo(1) assertThat(frameScheduler.getFrameNumberWithinLoop(101)).isEqualTo(1) assertThat(frameScheduler.getFrameNumberWithinLoop(250)).isEqualTo(2) assertThat(frameScheduler.getFrameNumberWithinLoop(499)).isEqualTo(4) } @Test fun testGetFrameNumberToRender_whenNoFrames_thenReturnFirstFrame() { val backend = DummyAnimationBackend(0) val frameScheduler = DropFramesFrameScheduler(backend) assertThat(frameScheduler.loopDurationMs).isEqualTo(0) assertThat(frameScheduler.getFrameNumberToRender(0, 0)).isEqualTo(0) } private class DummyAnimationBackend(private val frameCount: Int) : AnimationBackend { override fun getLoopDurationMs(): Int { var loopDuration = 0L for (i in 0 until frameCount) { loopDuration += getFrameDurationMs(i) } return loopDuration.toInt() } override fun width(): Int = intrinsicWidth override fun height(): Int = intrinsicHeight fun getAnimationDurationMs(): Long = getLoopDurationMs().toLong() * getLoopCount() override fun getFrameCount(): Int = frameCount override fun getFrameDurationMs(frameNumber: Int): Int = 100 override fun getLoopCount(): Int = 7 override fun drawFrame(parent: Drawable, canvas: Canvas, frameNumber: Int): Boolean = false override fun setAlpha(@IntRange(from = 0, to = 255) alpha: Int) = Unit override fun setColorFilter(colorFilter: ColorFilter?) = Unit override fun setBounds(bounds: Rect) = Unit override fun getIntrinsicWidth(): Int = AnimationBackend.INTRINSIC_DIMENSION_UNSET override fun getIntrinsicHeight(): Int = AnimationBackend.INTRINSIC_DIMENSION_UNSET override fun getSizeInBytes(): Int = 0 override fun clear() = Unit override fun preloadAnimation() = Unit override fun setAnimationListener(listener: AnimationBackend.Listener?) = Unit } } ================================================ FILE: animated-drawable/src/test/java/javax/microedition/khronos/opengles/GL.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package javax.microedition.khronos.opengles; /** Dummy interface to get Canvas mocks to work. */ public interface GL {} ================================================ FILE: animated-gif/.gitignore ================================================ nativedeps/ ================================================ FILE: animated-gif/build.gradle ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import com.facebook.fresco.buildsrc.Deps import com.facebook.fresco.buildsrc.GradleDeps import com.facebook.fresco.buildsrc.TestDeps apply plugin: 'com.android.library' apply plugin: 'kotlin-android' kotlin { jvmToolchain(11) } dependencies { compileOnly Deps.inferAnnotation compileOnly Deps.jsr305 implementation Deps.Bolts.tasks compileOnly Deps.javaxAnnotation implementation Deps.SoLoader.nativeloader implementation project(':fbcore') implementation project(':animated-base') implementation project(':animated-gif-lite') implementation project(':middleware') testCompileOnly Deps.inferAnnotation testImplementation project(':imagepipeline-base-test') testImplementation project(':imagepipeline-test') testImplementation project(':middleware') testImplementation TestDeps.junit testImplementation TestDeps.assertjCore testImplementation TestDeps.mockitoCore3 testImplementation TestDeps.mockitoInline3 testImplementation TestDeps.mockitoKotlin3 testImplementation(TestDeps.robolectric) { exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'org.apache.httpcomponents', module: 'httpclient' } } // We download various C++ open-source dependencies from SourceForge into nativedeps/downloads. // We then copy both downloaded code and our custom makefiles and headers into nativedeps/merge. task fetchNativeDeps(dependsOn: [copyGiflib]) { } android { ndkVersion GradleDeps.Native.version def ndkLibs = [['gifimage', [copyGiflib]]] buildToolsVersion FrescoConfig.buildToolsVersion compileSdkVersion FrescoConfig.compileSdkVersion namespace "com.facebook.animated.gif" defaultConfig { minSdkVersion FrescoConfig.minSdkVersion targetSdkVersion FrescoConfig.targetSdkVersion } sourceSets { main { jni.srcDirs = [] jniLibs.srcDirs = ndkLibs.collect { "$buildDir/${it[0]}" } } } ndkLibs.each { lib -> makeNdkTasks lib[0], lib[1] } preBuild.dependsOn("ndk_build_gifimage") } apply plugin: "com.vanniktech.maven.publish" ================================================ FILE: animated-gif/gradle.properties ================================================ POM_NAME=AnimatedGif POM_DESCRIPTION=The classes to support animated gif POM_ARTIFACT_ID=animated-gif POM_PACKAGING=aar ================================================ FILE: animated-gif/src/main/AndroidManifest.xml ================================================ ================================================ FILE: animated-gif/src/main/java/com/facebook/animated/gif/AnimatedImageGifValidator.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.animated.gif import com.facebook.animated.giflite.decoder.GifMetadataDecoder import com.facebook.imagepipeline.animated.base.AnimatedImageValidator import com.facebook.imagepipeline.animated.base.ValidationResult import com.facebook.imagepipeline.image.EncodedImage private const val MAX_GIF_TOTAL_PIXELS = 100_000_000 object AnimatedImageGifValidator : AnimatedImageValidator { override fun validateImage(encodedImage: EncodedImage): ValidationResult { val inputStream = encodedImage.inputStream ?: return ValidationResult.Failure("No input stream available") try { inputStream.use { stream -> val decoder = GifMetadataDecoder.create(stream, null) val width = decoder.screenWidth val height = decoder.screenHeight if (width <= 0 || height <= 0) { return ValidationResult.Failure("GIF invalid logical screen size: $width x $height") } val totalPixels = width * height * decoder.frameCount if (totalPixels > MAX_GIF_TOTAL_PIXELS) { return ValidationResult.Failure( "GIF too large: $width x $height x ${decoder.frameCount} frames = $totalPixels pixels" ) } } return ValidationResult.Success } catch (e: Exception) { return ValidationResult.Failure("Error parsing GIF: ${e.message}") } } } ================================================ FILE: animated-gif/src/main/java/com/facebook/animated/gif/GifFrame.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.animated.gif; import android.graphics.Bitmap; import com.facebook.common.internal.DoNotStrip; import com.facebook.imagepipeline.animated.base.AnimatedImageFrame; import com.facebook.infer.annotation.Nullsafe; import javax.annotation.concurrent.ThreadSafe; /** A single frame of a {@link GifImage}. */ @Nullsafe(Nullsafe.Mode.LOCAL) @ThreadSafe public class GifFrame implements AnimatedImageFrame { // Accessed by native methods @SuppressWarnings("unused") @DoNotStrip private long mNativeContext; /** * Constructs the frame with the native pointer. This is called by native code. * * @param nativeContext the native pointer */ @DoNotStrip GifFrame(long nativeContext) { mNativeContext = nativeContext; } // This is a valid use of finalize. No other mechanism is appropriate. @Override protected void finalize() { nativeFinalize(); } @Override public void dispose() { nativeDispose(); } @Override public void renderFrame(int width, int height, Bitmap bitmap) { nativeRenderFrame(width, height, bitmap); } @Override public int getDurationMs() { return nativeGetDurationMs(); } @Override public int getWidth() { return nativeGetWidth(); } @Override public int getHeight() { return nativeGetHeight(); } @Override public int getXOffset() { return nativeGetXOffset(); } @Override public int getYOffset() { return nativeGetYOffset(); } public boolean hasTransparency() { return nativeHasTransparency(); } public int getTransparentPixelColor() { return nativeGetTransparentPixelColor(); } public int getDisposalMode() { return nativeGetDisposalMode(); } @DoNotStrip private native void nativeRenderFrame(int width, int height, Bitmap bitmap); @DoNotStrip private native int nativeGetDurationMs(); @DoNotStrip private native int nativeGetWidth(); @DoNotStrip private native int nativeGetHeight(); @DoNotStrip private native int nativeGetXOffset(); @DoNotStrip private native int nativeGetYOffset(); @DoNotStrip private native int nativeGetDisposalMode(); @DoNotStrip private native int nativeGetTransparentPixelColor(); @DoNotStrip private native boolean nativeHasTransparency(); @DoNotStrip private native void nativeDispose(); @DoNotStrip private native void nativeFinalize(); } ================================================ FILE: animated-gif/src/main/java/com/facebook/animated/gif/GifImage.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.animated.gif; import android.graphics.Bitmap; import com.facebook.common.internal.DoNotStrip; import com.facebook.common.internal.Preconditions; import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo; import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo.BlendOperation; import com.facebook.imagepipeline.animated.base.AnimatedImage; import com.facebook.imagepipeline.animated.factory.AnimatedImageDecoder; import com.facebook.imagepipeline.common.ImageDecodeOptions; import com.facebook.infer.annotation.Nullsafe; import com.facebook.soloader.nativeloader.NativeLoader; import java.nio.ByteBuffer; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; /** * A representation of a GIF image. An instance of this class will hold a copy of the encoded data * in memory along with the parsed header data. Frames are decoded on demand via {@link GifFrame}. */ @Nullsafe(Nullsafe.Mode.LOCAL) @ThreadSafe @DoNotStrip public class GifImage implements AnimatedImage, AnimatedImageDecoder { private static final int LOOP_COUNT_FOREVER = 0; private static final int LOOP_COUNT_MISSING = -1; private static volatile boolean sInitialized; // Accessed by native methods @SuppressWarnings("unused") @DoNotStrip private long mNativeContext; @Nullable private Bitmap.Config mDecodeBitmapConfig = null; private static synchronized void ensure() { if (!sInitialized) { sInitialized = true; NativeLoader.loadLibrary("gifimage"); } } /** * Creates a {@link GifImage} from the specified encoded data. This will throw if it fails to * create. This is meant to be called on a worker thread. * * @param source the data to the image (a copy will be made) */ public static GifImage createFromByteArray(byte[] source) { Preconditions.checkNotNull(source, "Source byte array cannot be null"); ByteBuffer byteBuffer = ByteBuffer.allocateDirect(source.length); byteBuffer.put(source); byteBuffer.rewind(); return createFromByteBuffer(byteBuffer, ImageDecodeOptions.defaults()); } /** * Creates a {@link GifImage} from a ByteBuffer containing the image. This will throw if it fails * to create. * * @param byteBuffer the ByteBuffer containing the image (a copy will be made) */ public static GifImage createFromByteBuffer(ByteBuffer byteBuffer) { return createFromByteBuffer(byteBuffer, ImageDecodeOptions.defaults()); } /** * Creates a {@link GifImage} from a ByteBuffer containing the image. This will throw if it fails * to create. * * @param byteBuffer the ByteBuffer containing the image (a copy will be made) */ public static GifImage createFromByteBuffer(ByteBuffer byteBuffer, ImageDecodeOptions options) { ensure(); byteBuffer.rewind(); GifImage image = nativeCreateFromDirectByteBuffer( byteBuffer, options.maxDimensionPx, options.forceStaticImage); image.mDecodeBitmapConfig = options.animatedBitmapConfig; return image; } public static GifImage createFromNativeMemory( long nativePtr, int sizeInBytes, ImageDecodeOptions options) { ensure(); Preconditions.checkArgument(nativePtr != 0); GifImage image = nativeCreateFromNativeMemory( nativePtr, sizeInBytes, options.maxDimensionPx, options.forceStaticImage); image.mDecodeBitmapConfig = options.animatedBitmapConfig; return image; } /** * Creates a {@link GifImage} from a file descriptor containing the image. This will throw if it * fails to create. * * @param fileDescriptor the file descriptor containing the image (a copy will be made) */ public static GifImage createFromFileDescriptor(int fileDescriptor, ImageDecodeOptions options) { ensure(); return nativeCreateFromFileDescriptor( fileDescriptor, options.maxDimensionPx, options.forceStaticImage); } @Override public AnimatedImage decodeFromNativeMemory( long nativePtr, int sizeInBytes, ImageDecodeOptions options) { return GifImage.createFromNativeMemory(nativePtr, sizeInBytes, options); } @Override public AnimatedImage decodeFromByteBuffer(ByteBuffer byteBuffer, ImageDecodeOptions options) { return GifImage.createFromByteBuffer(byteBuffer, options); } @DoNotStrip public GifImage() {} /** * Constructs the image with the native pointer. This is called by native code. * * @param nativeContext the native pointer */ @DoNotStrip GifImage(long nativeContext) { mNativeContext = nativeContext; } // This is a valid use of finalize. No other mechanism is appropriate. @Override protected void finalize() { nativeFinalize(); } @Override public void dispose() { nativeDispose(); } @Override public int getWidth() { return nativeGetWidth(); } @Override public int getHeight() { return nativeGetHeight(); } @Override public int getFrameCount() { return nativeGetFrameCount(); } @Override public int getDuration() { return nativeGetDuration(); } @Override public int[] getFrameDurations() { return nativeGetFrameDurations(); } @Override public int getLoopCount() { // If a GIF image has no Netscape 2.0 loop extension, it is meant to play once and then stop. A // loop count of 0 indicates an endless looping of the animation. Any loop count X>0 indicates // that the animation shall be repeated X times, resulting in the animation to play X+1 times. final int loopCount = nativeGetLoopCount(); switch (loopCount) { case LOOP_COUNT_FOREVER: return AnimatedImage.LOOP_COUNT_INFINITE; case LOOP_COUNT_MISSING: return 1; default: return loopCount + 1; } } @Override public GifFrame getFrame(int frameNumber) { return nativeGetFrame(frameNumber); } @Override public boolean doesRenderSupportScaling() { return false; } @Override public int getSizeInBytes() { return nativeGetSizeInBytes(); } public boolean isAnimated() { return nativeIsAnimated(); } @Override public AnimatedDrawableFrameInfo getFrameInfo(int frameNumber) { GifFrame frame = getFrame(frameNumber); try { return new AnimatedDrawableFrameInfo( frameNumber, frame.getXOffset(), frame.getYOffset(), frame.getWidth(), frame.getHeight(), BlendOperation.BLEND_WITH_PREVIOUS, fromGifDisposalMethod(frame.getDisposalMode())); } finally { frame.dispose(); } } @Override @Nullable public Bitmap.Config getAnimatedBitmapConfig() { return mDecodeBitmapConfig; } private static AnimatedDrawableFrameInfo.DisposalMethod fromGifDisposalMethod(int disposalMode) { if (disposalMode == 0 /* DISPOSAL_UNSPECIFIED */) { return AnimatedDrawableFrameInfo.DisposalMethod.DISPOSE_DO_NOT; } else if (disposalMode == 1 /* DISPOSE_DO_NOT */) { return AnimatedDrawableFrameInfo.DisposalMethod.DISPOSE_DO_NOT; } else if (disposalMode == 2 /* DISPOSE_BACKGROUND */) { return AnimatedDrawableFrameInfo.DisposalMethod.DISPOSE_TO_BACKGROUND; } else if (disposalMode == 3 /* DISPOSE_PREVIOUS */) { return AnimatedDrawableFrameInfo.DisposalMethod.DISPOSE_TO_PREVIOUS; } else { return AnimatedDrawableFrameInfo.DisposalMethod.DISPOSE_DO_NOT; } } @DoNotStrip private static native GifImage nativeCreateFromDirectByteBuffer( ByteBuffer buffer, int maxDimension, boolean forceStatic); @DoNotStrip private static native GifImage nativeCreateFromNativeMemory( long nativePtr, int sizeInBytes, int maxDimension, boolean forceStatic); @DoNotStrip private static native GifImage nativeCreateFromFileDescriptor( int fileDescriptor, int maxDimension, boolean forceStatic); @DoNotStrip private native int nativeGetWidth(); @DoNotStrip private native int nativeGetHeight(); @DoNotStrip private native int nativeGetDuration(); @DoNotStrip private native int nativeGetFrameCount(); @DoNotStrip private native int[] nativeGetFrameDurations(); @DoNotStrip private native int nativeGetLoopCount(); @DoNotStrip private native GifFrame nativeGetFrame(int frameNumber); @DoNotStrip private native int nativeGetSizeInBytes(); @DoNotStrip private native boolean nativeIsAnimated(); @DoNotStrip private native void nativeDispose(); @DoNotStrip private native void nativeFinalize(); } ================================================ FILE: animated-gif/src/main/java/com/facebook/animated/gif/GifImageDecoder.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.animated.gif import com.facebook.common.logging.FLog import com.facebook.imagepipeline.animated.factory.AnimatedImageDecoderBase import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory import com.facebook.imagepipeline.common.ImageDecodeOptions import com.facebook.imagepipeline.decoder.ImageDecoder import com.facebook.imagepipeline.image.CloseableImage import com.facebook.imagepipeline.image.EncodedImage import com.facebook.imagepipeline.image.QualityInfo class GifImageDecoder( platformBitmapFactory: PlatformBitmapFactory, isNewRenderImplementation: Boolean, downscaleFrameToDrawableDimensions: Boolean, treatAnimatedImagesAsStateful: Boolean = true, ) : AnimatedImageDecoderBase( platformBitmapFactory, downscaleFrameToDrawableDimensions, isNewRenderImplementation, treatAnimatedImagesAsStateful, ), ImageDecoder { /** * Decodes an animated GIF image into a CloseableImage. * * @param encodedImage encoded image (native byte array holding the encoded bytes and meta data) * @param length the length of the encoded data * @param qualityInfo quality information about the image * @param options decode options specifying how the image should be decoded * @return a CloseableImage */ override fun decode( encodedImage: EncodedImage, length: Int, qualityInfo: QualityInfo, options: ImageDecodeOptions, ): CloseableImage? { val bytesRef = encodedImage.byteBufferRef checkNotNull(bytesRef) bytesRef.use { val validationResult = AnimatedImageGifValidator.validateImage(encodedImage) if (validationResult.isValid == false) { FLog.w(TAG, "Image validation failed: ${validationResult.message}") throw UnsupportedOperationException("Invalid image: ${validationResult.message}") } val input = bytesRef.get() val image = input.byteBuffer?.let { byteBuffer -> GifImage.createFromByteBuffer(byteBuffer, options) } ?: GifImage.createFromNativeMemory(input.nativePtr, input.size(), options) return getCloseableImage( encodedImage.source, options, checkNotNull(image), options.animatedBitmapConfig, ) } } companion object { private const val TAG = "GifImageDecoder" } } ================================================ FILE: animated-gif/src/main/jni/Application.mk ================================================ # Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. APP_BUILD_SCRIPT := Android.mk APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 APP_MK_DIR := $(dir $(lastword $(MAKEFILE_LIST))) NDK_MODULE_PATH := $(APP_MK_DIR)$(HOST_DIRSEP)$(APP_MK_DIR)../../../nativedeps/merge APP_STL := c++_static APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true # Make sure every shared lib includes a .note.gnu.build-id header APP_LDFLAGS := -Wl,--build-id NDK_TOOLCHAIN_VERSION := clang # We link our libs with static stl implementation. Because of that we need to # hide all stl related symbols to make them unaccessible from the outside. # We also need to make sure that our library does not use any stl functions # coming from other stl implementations as well # This hides all symbols exported from libgnustl_static FRESCO_CPP_LDFLAGS := -Wl,--gc-sections,--exclude-libs,libc++_static.a ================================================ FILE: animated-gif/src/main/jni/gifimage/Android.mk ================================================ # Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := gifimage LOCAL_SRC_FILES := \ OnLoad.cpp \ gif.cpp \ jni_helpers.cpp \ CXX11_FLAGS := -std=c++11 LOCAL_CFLAGS += $(CXX11_FLAGS) LOCAL_CFLAGS += -fvisibility=hidden LOCAL_CFLAGS += $(FRESCO_CPP_CFLAGS) LOCAL_EXPORT_CPPFLAGS := $(CXX11_FLAGS) LOCAL_LDLIBS += -latomic -ljnigraphics LOCAL_LDFLAGS += $(FRESCO_CPP_LDFLAGS) LOCAL_LDLIBS += -llog -ldl -landroid LOCAL_STATIC_LIBRARIES += gif include $(BUILD_SHARED_LIBRARY) $(call import-module, giflib) ================================================ FILE: animated-gif/src/main/jni/gifimage/OnLoad.cpp ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include #include #include #include #include #include int initGifImage(JNIEnv* env); // Registers jni methods. __attribute__((visibility("default"))) jint JNI_OnLoad(JavaVM* vm, void* reserved) { // get the current env JNIEnv* env = nullptr; if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } int result = initGifImage(env); if (result != JNI_OK) { return result; } return JNI_VERSION_1_6; } ================================================ FILE: animated-gif/src/main/jni/gifimage/gif.cpp ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #define LOG_TAG "GifImage" #include #include #include #include #include #include #include #include #include #include #include "gif_lib.h" #include "locks.h" #include "secure_memcpy.h" #ifdef __ANGLIRU__ #include #else #define __ANGLIRU_SOURCE__ #endif // For Angliru analysis #if defined(__has_include) && __has_include() #include #else #include "jni_helpers.h" #endif using namespace facebook; #define APPLICATION_EXT_NETSCAPE "NETSCAPE2.0" #define APPLICATION_EXT_NETSCAPE_LEN sizeof(APPLICATION_EXT_NETSCAPE) - 1 #define EXTRA_LOGGING false #define LOOP_COUNT_MISSING -1; static void DGifCloseFile2(GifFileType* pGifFile) { int errorCode; DGifCloseFile(pGifFile, &errorCode); } class DataWrapper { public: DataWrapper() {} virtual ~DataWrapper() {} virtual size_t read(GifByteType* dest, size_t size) = 0; virtual size_t getBufferSize() = 0; virtual size_t getPosition() = 0; virtual bool setPosition(size_t position) = 0; }; class BytesDataWrapper : public DataWrapper { public: BytesDataWrapper(std::vector&& pBuffer) : DataWrapper(), m_pBuffer(std::move(pBuffer)), m_position(0) { m_length = m_pBuffer.size(); } inline static size_t rangeAdd(size_t current, size_t increment, size_t max) { size_t end = current + increment; if (end < current || // integer overflow end > max // buffer overflow ) { end = max; } return end; } size_t read(GifByteType* dest, size_t size) override { size_t endPosition = rangeAdd(m_position, size, m_length); size_t readSize = endPosition - m_position; if (try_checked_memcpy( dest, size, m_pBuffer.data() + m_position, readSize) != 0) { return 0; // memcpy error } else { m_position = endPosition; return readSize; } } size_t getBufferSize() override { return m_length; } size_t getPosition() override { return m_position; } bool setPosition(size_t position) override { if (position < m_length) { m_position = position; return true; } else { return false; } } private: std::vector m_pBuffer; size_t m_position; size_t m_length; }; class FileDataWrapper : public DataWrapper { public: static FileDataWrapper* create(JNIEnv* pEnv, int fd) { fd = dup(fd); FILE* file = fdopen(fd, "rb"); if (file == nullptr) { throwIllegalStateException( pEnv, "Unable to open file: %s", strerror(errno)); return nullptr; } if (fseek(file, 0, SEEK_END) != 0) { throwIllegalStateException( pEnv, "Unable to seek to end of file: %s", strerror(errno)); return nullptr; } long size = ftell(file); if (size < 0) { throwIllegalStateException( pEnv, "Unable to get file size: %s", strerror(errno)); return nullptr; } if (fseek(file, 0, SEEK_SET) != 0) { throwIllegalStateException( pEnv, "Unable to seek to beginning of file: %s", strerror(errno)); return nullptr; } return new FileDataWrapper(file, size); } FileDataWrapper(FILE* file, size_t length) : DataWrapper(), m_file(file), m_length(length) {} ~FileDataWrapper() override { fclose(m_file); } size_t read(GifByteType* dest, size_t size) override { return fread(dest, 1, size, m_file); } size_t getBufferSize() override { return m_length; } size_t getPosition() override { long position = ftell(m_file); return position >= 0 ? position : 0; } bool setPosition(size_t position) override { return fseek(m_file, position, SEEK_SET) == 0; } private: FILE* m_file; size_t m_length; }; class GifWrapper { public: GifWrapper( std::unique_ptr&& pGifFile, std::shared_ptr& pData) : m_spGifFile(std::move(pGifFile)), m_spData(pData), m_rasterBits(m_spGifFile->SWidth * m_spGifFile->SHeight) {} virtual ~GifWrapper() { // FBLOGD("Deleting GifWrapper"); } GifFileType* get() { return m_spGifFile.get(); } DataWrapper* getData() { return m_spData.get(); } void addFrameByteOffset(size_t offset) { m_vectorFrameByteOffsets.push_back(offset); } size_t getFrameByteOffset(int frameNum) { return m_vectorFrameByteOffsets[frameNum]; } size_t getFrameSize() { return m_vectorFrameByteOffsets.size(); } int getLoopCount() { return m_loopCount; } uint8_t* getRasterBits() { return m_rasterBits.data(); } size_t getRasterBitsCapacity() { return m_rasterBits.capacity(); } void resizeRasterBuffer(size_t bufferSize) { m_rasterBits.resize(bufferSize); } std::mutex& getRasterMutex() { return m_rasterMutex; } void setLoopCount(int pLoopCount) { m_loopCount = pLoopCount; } bool isAnimated() { return m_animated; } void setAnimated(bool animated) { m_animated = animated; } RWLock* getSavedImagesRWLock() { return &m_savedImagesRWLock; } private: int m_loopCount = LOOP_COUNT_MISSING; bool m_animated = false; std::unique_ptr m_spGifFile; std::shared_ptr m_spData; std::vector m_vectorFrameByteOffsets; std::vector m_rasterBits; std::mutex m_rasterMutex; mutable RWLock m_savedImagesRWLock; }; /** * Native context for GifImage. */ struct GifImageNativeContext { /** Reference to the GifWrapper */ std::shared_ptr spGifWrapper; /** Cached width of the image */ int pixelWidth; /** Cached height of the image */ int pixelHeight; /** Cached number of the frames in the image */ int numFrames; /** Cached loop count for the image. 0 means infinite. */ int loopCount; /** Duration of all the animation (the sum of all the frames duration) */ int durationMs; /** Array of each frame's duration (size of array is numFrames) */ std::vector frameDurationsMs; /** Reference counter. Instance is deleted when it goes from 1 to 0 */ size_t refCount; #if EXTRA_LOGGING ~GifImageNativeContext() { __android_log_write( ANDROID_LOG_DEBUG, LOG_TAG, "GifImageNativeContext destructor"); } #endif }; /** * Native context for GifFrame. */ struct GifFrameNativeContext { /* Reference to the GifWrapper */ std::shared_ptr spGifWrapper; /** Frame number for the image. Starts at 0. */ int frameNum; /** X offset for the frame relative to the image canvas */ int xOffset; /** Y offset for the frame relative to the image canvas */ int yOffset; /** Display duration for the frame in ms */ int durationMs; /** Width of this frame */ int width; /** Height of this frame */ int height; /** How the GIF is disposed. See DISPOSAL_* constants in gif_lib.h */ int disposalMode; /** Palette index of the transparency color, or -1 for none */ int transparentIndex; /** Reference counter. Instance is deleted when it goes from 1 to 0 */ size_t refCount; #if EXTRA_LOGGING ~GifFrameNativeContext() { __android_log_write( ANDROID_LOG_DEBUG, LOG_TAG, "GifFrameNativeContext destructor"); } #endif }; /** * giflib takes a callback function for reading from the file. */ static int directByteBufferReadFun( GifFileType* gifFileType, GifByteType* bytes, int size) { DataWrapper* pData = (DataWrapper*)gifFileType->UserData; if (size > 0) { return pData->read(bytes, size); } return 0; } /** * Struct to represent a 32-bit ARGB in the correct order. */ struct PixelType32 { uint8_t red; uint8_t green; uint8_t blue; uint8_t alpha; }; /** * Transparent pixel constant. */ static const PixelType32 TRANSPARENT{0, 0, 0, 0}; // Class Names. static const char* const kGifImageClassPathName = "com/facebook/animated/gif/GifImage"; static const char* const kGifFrameClassPathName = "com/facebook/animated/gif/GifFrame"; // Cached fields related to GifImage static jclass sClazzGifImage; static jmethodID sGifImageConstructor; static jfieldID sGifImageFieldNativeContext; // Cached fields related to GifFrame static jclass sClazzGifFrame; static jmethodID sGifFrameConstructor; static jfieldID sGifFrameFieldNativeContext; // Static default color map. static ColorMapObject* sDefaultColorMap; static ColorMapObject* genDefColorMap(void) { ColorMapObject* pColorMap = GifMakeMapObject(256, NULL); if (pColorMap != NULL) { int iColor; for (iColor = 0; iColor < 256; iColor++) { pColorMap->Colors[iColor].Red = (GifByteType)iColor; pColorMap->Colors[iColor].Green = (GifByteType)iColor; pColorMap->Colors[iColor].Blue = (GifByteType)iColor; } } return pColorMap; } //////////////////////////////////////////////////////////////// /// Related to GifImage //////////////////////////////////////////////////////////////// bool getGraphicsControlBlockForImage( SavedImage* pSavedImage, GraphicsControlBlock* pGcp) { int resultCode = GIF_ERROR; // If a GIF has multiple graphic control extension blocks, we use the last one for (int i = 0; i < pSavedImage->ExtensionBlockCount; i++) { ExtensionBlock* pExtensionBlock = &pSavedImage->ExtensionBlocks[i]; if (pExtensionBlock->Function == GRAPHICS_EXT_FUNC_CODE) { resultCode = DGifExtensionToGCB( pExtensionBlock->ByteCount, pExtensionBlock->Bytes, pGcp); } } return resultCode == GIF_OK; } /** * Reads a single frame by reading data using giflib. The method expects the data source * referenced by pGifFile to point to the first byte of the encoded frame data. When the method * returns, the data source will point to the byte just past the encoded frame data. Unlike standard * decoding with giflib, the raster data is written to the passed-in buffer instead of being * written to the SavedImage structure. This is the key to how we avoid caching all the decoded * frame pixels in memory. * * @param pGifWrapper the gif wrapper containing the giflib struct and additional data * @param decodeFrame if set to true, next frame will be decoded to pGifWrapper bits buffer, otherwise it will only decode frame data and skip it * @param addToSavedImages if set to true, will add an additional SavedImage to * pGifFile->SavedImages * @param maxDimension Maximum allowed dimension of each decoded frame * @return a gif error code */ int readSingleFrame( GifWrapper* pGifWrapper, bool decodeFramePixels, bool addToSavedImages, int maxDimension) { GifFileType* pGifFile = pGifWrapper->get(); int imageCount = pGifFile->ImageCount; int imageDescResult = GIF_ERROR; { WriterLock wlock_{pGifWrapper->getSavedImagesRWLock()}; imageDescResult = DGifGetImageDesc(pGifFile); } // DGifGetImageDesc may have changed the count, temporarily restoring until we // know whether the frame was read successfully. pGifFile->ImageCount = imageCount; if (imageDescResult == GIF_ERROR) { return GIF_ERROR; } ReaderLock rlock_{pGifWrapper->getSavedImagesRWLock()}; SavedImage* pSavedImage = &pGifFile->SavedImages[imageCount]; // Check size of image. Note: Frames with 0 width or height should be allowed. if (pSavedImage->ImageDesc.Width < 0 || pSavedImage->ImageDesc.Height < 0 || pSavedImage->ImageDesc.Width > maxDimension || pSavedImage->ImageDesc.Height > maxDimension) { return GIF_ERROR; } // Check for image size overflow. if (pSavedImage->ImageDesc.Height != 0 && pSavedImage->ImageDesc.Width > (INT_MAX / pSavedImage->ImageDesc.Height)) { return GIF_ERROR; } if (decodeFramePixels) { // Reserve larger raster bits buffer if needed size_t imageSize = pSavedImage->ImageDesc.Width * pSavedImage->ImageDesc.Height; pGifWrapper->resizeRasterBuffer(imageSize); // Decode frame image and save it to temporary raster bits buffer uint8_t* pRasterBits = pGifWrapper->getRasterBits(); if (pSavedImage->ImageDesc.Interlace) { // The way an interlaced image should be read - offsets and jumps... int interlacedOffset[] = {0, 4, 2, 1}; int interlacedJumps[] = {8, 8, 4, 2}; // Need to perform 4 passes on the image. for (int i = 0; i < 4; i++) { for (int j = interlacedOffset[i]; j < pSavedImage->ImageDesc.Height; j += interlacedJumps[i]) { GifPixelType* pLine = pRasterBits + j * pSavedImage->ImageDesc.Width; int lineLength = pSavedImage->ImageDesc.Width; if (DGifGetLine(pGifFile, pLine, lineLength) == GIF_ERROR) { return GIF_ERROR; } } } } else { if (DGifGetLine(pGifFile, pRasterBits, imageSize) == GIF_ERROR) { return GIF_ERROR; } } } else { // Don't decode. Just read the encoded data to skip past it. int codeSize; GifByteType* pCodeBlock; if (DGifGetCode(pGifFile, &codeSize, &pCodeBlock) == GIF_ERROR) { return GIF_ERROR; } while (pCodeBlock != NULL) { if (DGifGetCodeNext(pGifFile, &pCodeBlock) == GIF_ERROR) { return GIF_ERROR; } } } if (pGifFile->ExtensionBlocks) { pSavedImage->ExtensionBlocks = pGifFile->ExtensionBlocks; pSavedImage->ExtensionBlockCount = pGifFile->ExtensionBlockCount; pGifFile->ExtensionBlocks = nullptr; pGifFile->ExtensionBlockCount = 0; } if (addToSavedImages) { // giflib wasn't designed to work with decoding arbitrary frames on the fly. // By default, it keeps adding more images to the SavedImages array, and we // reset the value after calling DGifGetImageDesc. Now, as the result of // decoding is known to be successful, we can increment the value to // represent correct number of images. pGifFile->ImageCount = imageCount + 1; } return GIF_OK; } /** * Decodes an extension as part of modifiedDGifSlurp. * * @param pGifFile the gif data structure to read to and write to * @return a gif error code */ int decodeExtension(GifFileType* pGifFile) { GifByteType* pExtData; int extFunction; if (DGifGetExtension(pGifFile, &extFunction, &pExtData) == GIF_ERROR) { return GIF_ERROR; } // Create an extension block with our data. if (pExtData != nullptr) { if (GifAddExtensionBlock( &pGifFile->ExtensionBlockCount, &pGifFile->ExtensionBlocks, extFunction, pExtData[0], &pExtData[1]) == GIF_ERROR) { return GIF_ERROR; } } while (pExtData != nullptr) { if (DGifGetExtensionNext(pGifFile, &pExtData) == GIF_ERROR) { return GIF_ERROR; } // Continue the extension block. if (pExtData != NULL) { if (GifAddExtensionBlock( &pGifFile->ExtensionBlockCount, &pGifFile->ExtensionBlocks, CONTINUE_EXT_FUNC_CODE, pExtData[0], &pExtData[1]) == GIF_ERROR) { return GIF_ERROR; } } } return GIF_OK; } /** * Tries to parse known application extensions of a given SavedImage and adds * the information to the GifWrapper accordingly. Currently, this method only * parses the Netscape 2.0 looping extension which can indicate how often a GIF * animation shall be played. * * @param pSavedImage saved image that might contain several ExtensionBlocks * @param pGifWrapper gif wrapper containing the giflib struct and additional * data */ void parseApplicationExtensions( SavedImage* pSavedImage, GifWrapper* pGifWrapper) { const int extensionCount = pSavedImage->ExtensionBlockCount; for (int j = 0; j < extensionCount; j++) { const ExtensionBlock* extensionBlock = &pSavedImage->ExtensionBlocks[j]; if (extensionBlock->Function != APPLICATION_EXT_FUNC_CODE) { continue; } // Check for Netscape 2.0 looping block if (extensionBlock->ByteCount == APPLICATION_EXT_NETSCAPE_LEN && strncmp( APPLICATION_EXT_NETSCAPE, (const char*)extensionBlock->Bytes, APPLICATION_EXT_NETSCAPE_LEN) == 0) { // The data sub-block has been added as the following extension block ExtensionBlock* subBlock = NULL; if (j + 1 < extensionCount) { subBlock = &pSavedImage->ExtensionBlocks[j + 1]; } if (subBlock != NULL && subBlock->Function == CONTINUE_EXT_FUNC_CODE && subBlock->ByteCount == 3) { // The loop count is stored little endian const int loopCount = subBlock->Bytes[1] | subBlock->Bytes[2] << 8; pGifWrapper->setLoopCount(loopCount); // The looping extension is the only block that we are interested in break; } } } } /** * A heavily modified version of giflib's DGifSlurp. This uses some hacks to * avoid caching the decoded pixel data for each frame in memory. Like * DGifSlurp, GifFileType will contain the results of slurping the GIF but there * will be no frame pixel data cached in SavedImage.RasterBits. * * @param pGifWrapper the gif wrapper containing the giflib struct and * additional data * @param maxDimension Maximum allowed dimension of each frame * @param forceStatic whether GIF will be loaded as static image * @return a gif error code */ int modifiedDGifSlurp( GifWrapper* pGifWrapper, int maxDimension, bool forceStatic) { GifFileType* pGifFile = pGifWrapper->get(); GifRecordType recordType; pGifFile->ExtensionBlocks = NULL; pGifFile->ExtensionBlockCount = 0; bool isStop = false; do { if (DGifGetRecordType(pGifFile, &recordType) == GIF_ERROR) { break; } switch (recordType) { case IMAGE_DESC_RECORD_TYPE: // Set the flag whether gif is animated, but give up slurping after the // first frame, when static image is requested. if (pGifFile->ImageCount >= 1) { pGifWrapper->setAnimated(true); if (forceStatic) { isStop = true; break; } } // We save the byte offset where each frame begins. This allows us to // avoid storing the pixel data for each frame and instead decode it on // the fly. pGifWrapper->addFrameByteOffset(pGifWrapper->getData()->getPosition()); if (readSingleFrame( pGifWrapper, false, // Don't decode frame pixels true, // Add to saved images maxDimension // Max dimension ) == GIF_ERROR) { isStop = true; } break; case EXTENSION_RECORD_TYPE: if (decodeExtension(pGifFile) == GIF_ERROR) { isStop = true; } break; case TERMINATE_RECORD_TYPE: isStop = true; break; default: // Should be trapped by DGifGetRecordType. break; } } while (!isStop); isStop = false; // parse application extensions const int imageCount = pGifFile->ImageCount; ReaderLock rlock_{pGifWrapper->getSavedImagesRWLock()}; for (int i = 0; i < imageCount; i++) { parseApplicationExtensions(&pGifFile->SavedImages[i], pGifWrapper); } return pGifWrapper->getFrameSize() > 0 ? GIF_OK : GIF_ERROR; } /** * Creates a new GifImage from the specified data. * * @param spDataWrapper the wrapper providing bytes * @param maxDimension Maximum allowed dimension of canvas and each decoded * frame * @param forceStatic Whether GIF should be decoded as static image * @return a newly allocated GifImage */ jobject createFromDataWrapper( JNIEnv* pEnv, std::shared_ptr spDataWrapper, int maxDimension, bool forceStatic) { std::unique_ptr spNativeContext( new GifImageNativeContext()); if (!spNativeContext) { throwOutOfMemoryError(pEnv, "Unable to allocate native context"); return 0; } int gifError = 0; auto spGifFileIn = std::unique_ptr{ DGifOpen((void*)spDataWrapper.get(), &directByteBufferReadFun, &gifError), DGifCloseFile2}; if (spGifFileIn == nullptr) { throwIllegalStateException(pEnv, "Error %d", gifError); return nullptr; } int width = spGifFileIn->SWidth; int height = spGifFileIn->SHeight; size_t wxh = width * height; if (wxh < 1 || wxh > SIZE_MAX || width > maxDimension || height > maxDimension) { throwIllegalStateException(pEnv, "Invalid dimensions"); return nullptr; } // Create the GifWrapper spNativeContext->spGifWrapper = std::shared_ptr( new GifWrapper(std::move(spGifFileIn), spDataWrapper)); GifFileType* pGifFile = spNativeContext->spGifWrapper->get(); spNativeContext->pixelWidth = width; spNativeContext->pixelHeight = height; int error = modifiedDGifSlurp( spNativeContext->spGifWrapper.get(), maxDimension, forceStatic); if (error != GIF_OK) { throwIllegalStateException(pEnv, "Failed to slurp image %d", error); return nullptr; } if (pGifFile->ImageCount < 1) { throwIllegalStateException(pEnv, "No frames in image"); return nullptr; } spNativeContext->numFrames = pGifFile->ImageCount; // Compute cached fields that require iterating the frames. int durationMs = 0; std::vector frameDurationsMs; ReaderLock rlock_{spNativeContext->spGifWrapper->getSavedImagesRWLock()}; for (int i = 0; i < pGifFile->ImageCount; i++) { SavedImage* pSavedImage = &pGifFile->SavedImages[i]; GraphicsControlBlock gcp; if (getGraphicsControlBlockForImage(pSavedImage, &gcp)) { int frameDurationMs = gcp.DelayTime * 10; durationMs += frameDurationMs; frameDurationsMs.push_back(frameDurationMs); } else { frameDurationsMs.push_back(0); } } spNativeContext->durationMs = durationMs; spNativeContext->frameDurationsMs = frameDurationsMs; // Cache loop count spNativeContext->loopCount = spNativeContext->spGifWrapper->getLoopCount(); // Create the GifImage with the native context. jobject ret = pEnv->NewObject( sClazzGifImage, sGifImageConstructor, (jlong)spNativeContext.get()); if (ret != nullptr) { // Ownership was transferred. spNativeContext->refCount = 1; spNativeContext.release(); } return ret; } /** * Creates a new GifImage from the specified buffer. * * @param vBuffer the vector containing the bytes * @return a newly allocated GifImage */ jobject GifImage_nativeCreateFromByteVector( JNIEnv* pEnv, __ANGLIRU_SOURCE__ std::vector& vBuffer, int maxDimension, bool forceStatic) { // Create the DataWrapper std::shared_ptr spDataWrapper = std::shared_ptr(new BytesDataWrapper(std::move(vBuffer))); return createFromDataWrapper(pEnv, spDataWrapper, maxDimension, forceStatic); } /** * Releases a reference to the GifPImageNativeContext and deletes it when the * reference count reaches 0 */ void GifImageNativeContext_releaseRef( JNIEnv* pEnv, jobject thiz, GifImageNativeContext* p) { pEnv->MonitorEnter(thiz); p->refCount--; if (p->refCount == 0) { delete p; } pEnv->MonitorExit(thiz); } /** * Functor for getGifImageNativeContext that releases the reference. */ struct GifImageNativeContextReleaser { JNIEnv* pEnv; jobject gifImage; GifImageNativeContextReleaser(JNIEnv* pEnv, jobject gifImage) : pEnv(pEnv), gifImage(gifImage) {} void operator()(GifImageNativeContext* pNativeContext) { GifImageNativeContext_releaseRef(pEnv, gifImage, pNativeContext); } }; /** * Gets the GifImageNativeContext from the mNativeContext of the GifImage * object. This returns a reference counted pointer. * * @return the referenced counted pointer which will be a nullptr in the case * where the object has already been disposed */ std::unique_ptr getGifImageNativeContext(JNIEnv* pEnv, jobject thiz) { GifImageNativeContextReleaser releaser(pEnv, thiz); std::unique_ptr ret( nullptr, releaser); pEnv->MonitorEnter(thiz); GifImageNativeContext* pNativeContext = (GifImageNativeContext*)pEnv->GetLongField( thiz, sGifImageFieldNativeContext); if (pNativeContext != nullptr) { pNativeContext->refCount++; ret.reset(pNativeContext); } pEnv->MonitorExit(thiz); return ret; } /** * Creates a new GifImage from the specified byte buffer. The data from the byte * buffer is copied into native memory managed by GifImage. * * @param byteBuffer A java.nio.ByteBuffer. Must be direct. Assumes data is the * entire capacity of the buffer * @param maxDimension Maximum allowed dimension of canvas and each decoded * frame * @param forceStatic Whether GIF should be decoded as static image * @return a newly allocated GifImage */ jobject GifImage_nativeCreateFromDirectByteBuffer( JNIEnv* pEnv, jclass clazz, jobject byteBuffer, jint maxDimension, jboolean forceStatic) { jbyte* bbufInput = (jbyte*)pEnv->GetDirectBufferAddress(byteBuffer); if (!bbufInput) { throwIllegalArgumentException(pEnv, "ByteBuffer must be direct"); return 0; } jlong capacity = pEnv->GetDirectBufferCapacity(byteBuffer); if (pEnv->ExceptionCheck()) { return 0; } std::vector vBuffer(bbufInput, bbufInput + capacity); return GifImage_nativeCreateFromByteVector( pEnv, vBuffer, maxDimension, forceStatic); } /** * Creates a new GifImage from the specified native pointer. The data is copied into memory managed by GifImage. * * @param nativePtr the native memory pointer * @param sizeInBytes size in bytes of the buffer * @param maxDimension Maximum allowed dimension of canvas and each decoded frame * @param forceStatic whether GIF will be loaded as static image * @return a newly allocated GifImage */ jobject GifImage_nativeCreateFromNativeMemory( JNIEnv* pEnv, jclass clazz, jlong nativePtr, jint sizeInBytes, jint maxDimension, jboolean forceStatic) { jbyte* const pointer = (jbyte*)nativePtr; if (sizeInBytes < 0) { throwIllegalArgumentException(pEnv, "Size must be non-negative"); return 0; } std::vector vBuffer(pointer, pointer + sizeInBytes); return GifImage_nativeCreateFromByteVector( pEnv, vBuffer, maxDimension, forceStatic); } /** * Creates a new GifImage from the specified byte buffer. The data from the byte * buffer is copied into native memory managed by GifImage. * * @param fileDescriptor File descriptor to open * @param maxDimension Maximum allowed dimension of canvas and each decoded * frame * @param forceStatic Whether GIF should be decoded as static image * @return a newly allocated GifImage */ jobject GifImage_nativeCreateFromFileDescriptor( JNIEnv* pEnv, jclass clazz, jint fileDescriptor, jint maxDimension, jboolean forceStatic) { // Create the DataWrapper std::shared_ptr spDataWrapper = std::shared_ptr( FileDataWrapper::create(pEnv, fileDescriptor)); if (pEnv->ExceptionCheck() || !spDataWrapper) { return 0; } return createFromDataWrapper(pEnv, spDataWrapper, maxDimension, forceStatic); } /** * Gets the width of the image. * * @return the width of the image */ jint GifImage_nativeGetWidth(JNIEnv* pEnv, jobject thiz) { auto spNativeContext = getGifImageNativeContext(pEnv, thiz); if (!spNativeContext) { throwIllegalStateException(pEnv, "Already disposed"); return 0; } return spNativeContext->pixelWidth; } /** * Gets the height of the image. * * @return the height of the image */ jint GifImage_nativeGetHeight(JNIEnv* pEnv, jobject thiz) { auto spNativeContext = getGifImageNativeContext(pEnv, thiz); if (!spNativeContext) { throwIllegalStateException(pEnv, "Already disposed"); return 0; } return spNativeContext->pixelHeight; } /** * Gets the number of frames in the image. * * @return the number of frames in the image */ jint GifImage_nativeGetFrameCount(JNIEnv* pEnv, jobject thiz) { auto spNativeContext = getGifImageNativeContext(pEnv, thiz); if (!spNativeContext) { throwIllegalStateException(pEnv, "Already disposed"); return 0; } return spNativeContext->numFrames; } /** * Gets the duration of the animated image. * * @return the duration of the animated image in milliseconds */ jint GifImage_nativeGetDuration(JNIEnv* pEnv, jobject thiz) { auto spNativeContext = getGifImageNativeContext(pEnv, thiz); if (!spNativeContext) { throwIllegalStateException(pEnv, "Already disposed"); return 0; } return spNativeContext->durationMs; } /** * Gets the number of loops to run the animation for. * * @return the number of loops, or 0 to indicate infinite */ jint GifImage_nativeGetLoopCount(JNIEnv* pEnv, jobject thiz) { auto spNativeContext = getGifImageNativeContext(pEnv, thiz); if (!spNativeContext) { throwIllegalStateException(pEnv, "Already disposed"); return 0; } return spNativeContext->loopCount; } /** * Gets the duration of each frame of the animated image. * * @return an array that is the size of the number of frames containing the * duration of each frame in milliseconds */ jintArray GifImage_nativeGetFrameDurations(JNIEnv* pEnv, jobject thiz) { auto spNativeContext = getGifImageNativeContext(pEnv, thiz); if (!spNativeContext) { throwIllegalStateException(pEnv, "Already disposed"); return NULL; } jintArray result = pEnv->NewIntArray(spNativeContext->numFrames); if (result == nullptr) { // pEnv->NewIntArray will have already instructed the environment to throw // an exception. return nullptr; } pEnv->SetIntArrayRegion( result, 0, spNativeContext->numFrames, spNativeContext->frameDurationsMs.data()); return result; } /** * Gets the Frame at the specified index. * * @param index the index of the frame (0-based) * @return a newly created GifFrame for the specified frame */ jobject GifImage_nativeGetFrame(JNIEnv* pEnv, jobject thiz, jint index) { auto spNativeContext = getGifImageNativeContext(pEnv, thiz); if (!spNativeContext) { throwIllegalStateException(pEnv, "Already disposed"); return nullptr; } GifFileType* pGifFile = spNativeContext->spGifWrapper->get(); ReaderLock rlock_{spNativeContext->spGifWrapper->getSavedImagesRWLock()}; if (index < 0 || index >= pGifFile->ImageCount) { throwIllegalStateException(pEnv, "Index exceeds GIF file image count"); return nullptr; } SavedImage* pSavedImage = &pGifFile->SavedImages[index]; std::unique_ptr spFrameNativeContext( new GifFrameNativeContext()); if (!spFrameNativeContext) { throwOutOfMemoryError(pEnv, "Unable to allocate GifFrameNativeContext"); return nullptr; } spFrameNativeContext->spGifWrapper = spNativeContext->spGifWrapper; spFrameNativeContext->frameNum = index; spFrameNativeContext->xOffset = pSavedImage->ImageDesc.Left; spFrameNativeContext->yOffset = pSavedImage->ImageDesc.Top; spFrameNativeContext->durationMs = spNativeContext->frameDurationsMs[index]; spFrameNativeContext->width = pSavedImage->ImageDesc.Width; spFrameNativeContext->height = pSavedImage->ImageDesc.Height; GraphicsControlBlock gcp; if (getGraphicsControlBlockForImage(pSavedImage, &gcp)) { spFrameNativeContext->transparentIndex = gcp.TransparentColor; spFrameNativeContext->disposalMode = gcp.DisposalMode; } else { spFrameNativeContext->transparentIndex = NO_TRANSPARENT_COLOR; spFrameNativeContext->disposalMode = DISPOSAL_UNSPECIFIED; } jobject ret = pEnv->NewObject( sClazzGifFrame, sGifFrameConstructor, (jlong)spFrameNativeContext.get()); if (ret != nullptr) { // pEnv->NewObject will have already instructed the environment to throw an // exception. spFrameNativeContext->refCount = 1; spFrameNativeContext.release(); } return ret; } /** * Releases a reference to the WebPFrameNativeContext and deletes it when the * reference count reaches 0 */ void GifFrameNativeContext_releaseRef( JNIEnv* pEnv, jobject thiz, GifFrameNativeContext* p) { pEnv->MonitorEnter(thiz); p->refCount--; if (p->refCount == 0) { delete p; } pEnv->MonitorExit(thiz); } /** * Functor for getGifFrameNativeContext. */ struct GifFrameNativeContextReleaser { JNIEnv* pEnv; jobject gifFrame; GifFrameNativeContextReleaser(JNIEnv* pEnv, jobject gifFrame) : pEnv(pEnv), gifFrame(gifFrame) {} void operator()(GifFrameNativeContext* pNativeContext) { GifFrameNativeContext_releaseRef(pEnv, gifFrame, pNativeContext); } }; /** * Gets the GifFrameNativeContext from the mNativeContext of the GifFrame * object. This returns a reference counted pointer. * * @return the reference counted pointer which will be a nullptr in the case * where the object has already been disposed */ std::unique_ptr getGifFrameNativeContext(JNIEnv* pEnv, jobject thiz) { GifFrameNativeContextReleaser releaser(pEnv, thiz); std::unique_ptr ret( nullptr, releaser); pEnv->MonitorEnter(thiz); GifFrameNativeContext* pNativeContext = (GifFrameNativeContext*)pEnv->GetLongField( thiz, sGifFrameFieldNativeContext); if (pNativeContext != nullptr) { pNativeContext->refCount++; ret.reset(pNativeContext); } pEnv->MonitorExit(thiz); return ret; } /** * Gets the size in bytes used by the {@link GifImage}. The implementation only * takes into account the encoded data buffer as the other data structures are * relatively tiny. * * @return approximate size in bytes used by the {@link GifImage} */ jint GifImage_nativeGetSizeInBytes(JNIEnv* pEnv, jobject thiz) { auto spNativeContext = getGifImageNativeContext(pEnv, thiz); if (!spNativeContext) { throwIllegalStateException(pEnv, "Already disposed"); return 0; } // This is an approximate amount based on the data buffer and the saved images int size = 0; size += spNativeContext->spGifWrapper->getData()->getBufferSize(); size += spNativeContext->spGifWrapper->getRasterBitsCapacity(); return size; } /** * Gets information whether {@link GifImage} is animated (has more than 1 * frame). It will return `true`, even if animated file was opened as static * image. * * @return whether {@link GifImage} is animated image */ jint GifImage_nativeIsAnimated(JNIEnv* pEnv, jobject thiz) { auto spNativeContext = getGifImageNativeContext(pEnv, thiz); if (!spNativeContext) { throwIllegalStateException(pEnv, "Already disposed"); return 0; } return spNativeContext->spGifWrapper->isAnimated(); } /** * Disposes the GifImage, freeing native resources. */ void GifImage_nativeDispose(JNIEnv* pEnv, jobject thiz) { pEnv->MonitorEnter(thiz); GifImageNativeContext* pNativeContext = (GifImageNativeContext*)pEnv->GetLongField( thiz, sGifImageFieldNativeContext); if (pNativeContext != nullptr) { pEnv->SetLongField(thiz, sGifImageFieldNativeContext, 0); GifImageNativeContext_releaseRef(pEnv, thiz, pNativeContext); } pEnv->MonitorExit(thiz); } /** * Finalizer for GifImage that frees native resources. */ void GifImage_nativeFinalize(JNIEnv* pEnv, jobject thiz) { GifImage_nativeDispose(pEnv, thiz); } //////////////////////////////////////////////////////////////// /// Related to GifFrame //////////////////////////////////////////////////////////////// /** * Packs a components of a pixel into a 32-bit PixelType32. */ static PixelType32 packARGB32( GifByteType alpha, GifByteType red, GifByteType green, GifByteType blue) { PixelType32 pixel; pixel.alpha = alpha; pixel.red = red; pixel.green = green; pixel.blue = blue; return pixel; } /** * Gets a color from the color table given an index. * * @param idx the index of the color * @param pColorMap the color map * @return a 32-bit pixel */ static PixelType32 getColorFromTable( unsigned int idx, const ColorMapObject* pColorMap) { if (pColorMap == NULL) { return TRANSPARENT; } int colIdx = (idx >= pColorMap->ColorCount) ? 0 : idx; GifColorType* pColor = &pColorMap->Colors[colIdx]; return packARGB32(0xFF, pColor->Red, pColor->Green, pColor->Blue); } /** * Blits a line of an 8-bit GIF frame to a 32-bit destination performing the * color conversion along the way. * * @param pDest the 32-bit pixel destination to write into * @param pSource the 8-bit frame source where values are indices into the color * table * @param pColorMap the color map * @param transparentIndex index to use for the transparent pixel * @param width number of pixels to copy */ static void blitLine( PixelType32* pDest, const GifByteType* pSource, const ColorMapObject* pColorMap, int transparentIndex, int width) { std::transform(pSource, pSource + width, pDest, [=](uint8_t color) { if (color == transparentIndex) { return TRANSPARENT; } return getColorFromTable(color, pColorMap); }); } /** * Blits an 8-bit GIF frame into a 32-bit destination performing the color * conversion along the way. * * @param pDest the byte buffer to write into * @param destWidth the width of the destination * @param destHeight the height of the destination * @param pFrame the frame to read from * @param pColorMap the color map * @param transparentIndex index to use for the transparent pixel */ static void blitNormal( uint8_t* pDest, int destWidth, int destHeight, int destStride, const SavedImage* pFrame, const GifByteType* pSrcRasterBits, const ColorMapObject* cmap, int transparentIndex) { GifWord copyWidth = pFrame->ImageDesc.Width; if (copyWidth > destWidth) { copyWidth = destWidth; } GifWord copyHeight = pFrame->ImageDesc.Height; if (copyHeight > destHeight) { copyHeight = destHeight; } for (; copyHeight > 0; copyHeight--) { blitLine( (PixelType32*)pDest, pSrcRasterBits, cmap, transparentIndex, copyWidth); pSrcRasterBits += pFrame->ImageDesc.Width; pDest += destStride; } } /** * Renders the frame to the specified pixel array. The array is expected to have * a size that is at least the the width and height of the frame. The frame is * rendered where each pixel is represented as a 32-bit BGRA pixel. The rendered * stride is the same as the frame width. Note, the number of pixels written to * the array may be smaller than the canvas if the frame's width/height is * smaller than the canvas. * * @param jPixels the array to render into */ void GifFrame_nativeRenderFrame( JNIEnv* pEnv, jobject thiz, jint width, jint height, jobject bitmap) { auto spNativeContext = getGifFrameNativeContext(pEnv, thiz); if (!spNativeContext) { throwIllegalStateException(pEnv, "Already disposed"); return; } AndroidBitmapInfo bitmapInfo; if (AndroidBitmap_getInfo(pEnv, bitmap, &bitmapInfo) != ANDROID_BITMAP_RESULT_SUCCESS) { throwIllegalStateException(pEnv, "Bad bitmap"); return; } if (width < 0 || height < 0) { throwIllegalArgumentException(pEnv, "Width or height is negative"); return; } if (bitmapInfo.width < (unsigned)width || bitmapInfo.height < (unsigned)height) { throwIllegalStateException(pEnv, "Width or height is too small"); return; } if (bitmapInfo.format != ANDROID_BITMAP_FORMAT_RGBA_8888) { throwIllegalStateException(pEnv, "Wrong color format"); return; } GifWrapper* pGifWrapper = spNativeContext->spGifWrapper.get(); // Note, there is some major hackery below since giflib is not intended to // incrementally decode arbitrary frames on demand. // We need to lock because the raster data and the data offset are shared // resources and only one thread can use them at a time. std::unique_lock lock(pGifWrapper->getRasterMutex()); // We set the data buffer that giflib will read from to be at the beginning of // where the encoded data for the frame starts. We know this offset because we // stored it when we originally decoded the GIF. int frameNum = spNativeContext->frameNum; int byteOffset = pGifWrapper->getFrameByteOffset(frameNum); if (!pGifWrapper->getData()->setPosition(byteOffset)) { // Unable to position to frame, ignore it return; } // Now we kick off the decoding process. int readRes = readSingleFrame( pGifWrapper, true, // Decode frame pixels false, // Don't add frame to saved images INT_MAX // Don't limit the size, it was checked in modifiedDGifSlurp ); if (readRes != GIF_OK) { // Probably, broken canvas, and we can ignore it return; } // Get the right color table to use. ColorMapObject* pColorMap = spNativeContext->spGifWrapper->get()->SColorMap; ReaderLock rlock_{pGifWrapper->getSavedImagesRWLock()}; SavedImage* pSavedImage = &pGifWrapper->get()->SavedImages[frameNum]; if (pSavedImage->ImageDesc.ColorMap != NULL) { // use local color table pColorMap = pSavedImage->ImageDesc.ColorMap; if (pColorMap->ColorCount != (1 << pColorMap->BitsPerPixel)) { pColorMap = sDefaultColorMap; } } uint8_t* pixels; if (AndroidBitmap_lockPixels(pEnv, bitmap, (void**)&pixels) != ANDROID_BITMAP_RESULT_SUCCESS) { throwIllegalStateException(pEnv, "Bad bitmap"); return; } blitNormal( pixels, width, height, bitmapInfo.stride, pSavedImage, spNativeContext->spGifWrapper->getRasterBits(), pColorMap, spNativeContext->transparentIndex); AndroidBitmap_unlockPixels(pEnv, bitmap); } /** * Gets the duration of the frame. * * @return the duration of the frame in milliseconds */ jint GifFrame_nativeGetDurationMs(JNIEnv* pEnv, jobject thiz) { auto spNativeContext = getGifFrameNativeContext(pEnv, thiz); if (!spNativeContext) { throwIllegalStateException(pEnv, "Already disposed"); return -1; } return spNativeContext->durationMs; } /** * Gets the color (as an int, as in Android) of the transparent pixel of this * frame * * @return the color (as an int, as in Android) of the transparent pixel of this * frame */ jint GifFrame_nativeGetTransparentPixelColor(JNIEnv* pEnv, jobject thiz) { auto spNativeContext = getGifFrameNativeContext(pEnv, thiz); auto pGifWrapper = spNativeContext->spGifWrapper; // // Get the right color table to use, then get index of transparent pixel into // that table // int frameNum = spNativeContext->frameNum; ColorMapObject* pColorMap = pGifWrapper->get()->SColorMap; ReaderLock rlock_{pGifWrapper->getSavedImagesRWLock()}; SavedImage* pSavedImage = &pGifWrapper->get()->SavedImages[frameNum]; if (pSavedImage->ImageDesc.ColorMap != NULL) { // use local color table pColorMap = pSavedImage->ImageDesc.ColorMap; if (pColorMap->ColorCount != (1 << pColorMap->BitsPerPixel)) { pColorMap = sDefaultColorMap; } } int colorIndex = spNativeContext->transparentIndex; if (pColorMap != NULL && colorIndex >= 0) { PixelType32 color = getColorFromTable(colorIndex, pColorMap); // // convert PixelType32 to Android-style int color value. // the c++ compiler will optimize these four lines of bit-shifting -- there // is no need to collapse them into a single confusing expression // int alphaShifted = color.alpha << 24; int redShifted = color.red << 16; int greenShifted = color.green << 8; int blueShifted = color.blue << 0; int iColor = alphaShifted | redShifted | greenShifted | blueShifted; return iColor; } else { return 0; // in android, 0 == Color.TRANSPARENT } } jboolean GifFrame_nativeHasTransparency(JNIEnv* pEnv, jobject thiz) { auto spNativeContext = getGifFrameNativeContext(pEnv, thiz); if (!spNativeContext) { throwIllegalStateException(pEnv, "Already disposed"); return -1; } return spNativeContext->transparentIndex >= 0; } /** * Gets the width of the frame. * * @return the width of the frame */ jint GifFrame_nativeGetWidth(JNIEnv* pEnv, jobject thiz) { auto spNativeContext = getGifFrameNativeContext(pEnv, thiz); if (!spNativeContext) { throwIllegalStateException(pEnv, "Already disposed"); return -1; } return spNativeContext->width; } /** * Gets the height of the frame. * * @return the height of the frame */ jint GifFrame_nativeGetHeight(JNIEnv* pEnv, jobject thiz) { auto spNativeContext = getGifFrameNativeContext(pEnv, thiz); if (!spNativeContext) { throwIllegalStateException(pEnv, "Already disposed"); return -1; } return spNativeContext->height; } /** * Gets the x-offset of the frame relative to the image canvas. * * @return the x-offset of the frame */ jint GifFrame_nativeGetXOffset(JNIEnv* pEnv, jobject thiz) { auto spNativeContext = getGifFrameNativeContext(pEnv, thiz); if (!spNativeContext) { throwIllegalStateException(pEnv, "Already disposed"); return -1; } return spNativeContext->xOffset; } /** * Gets the y-offset of the frame relative to the image canvas. * * @return the y-offset of the frame */ jint GifFrame_nativeGetYOffset(JNIEnv* pEnv, jobject thiz) { auto spNativeContext = getGifFrameNativeContext(pEnv, thiz); if (!spNativeContext) { throwIllegalStateException(pEnv, "Already disposed"); return -1; } return spNativeContext->yOffset; } /** * Gets the constant of the disposal mode for the frame. * * @return one of the GIF disposal mode constants */ jint GifFrame_nativeGetDisposalMode(JNIEnv* pEnv, jobject thiz) { auto spNativeContext = getGifFrameNativeContext(pEnv, thiz); if (!spNativeContext) { throwIllegalStateException(pEnv, "Already disposed"); return -1; } return spNativeContext->disposalMode; } /** * Disposes the GifFrame, freeing native resources. */ void GifFrame_nativeDispose(JNIEnv* pEnv, jobject thiz) { pEnv->MonitorEnter(thiz); GifFrameNativeContext* pNativeContext = (GifFrameNativeContext*)pEnv->GetLongField( thiz, sGifFrameFieldNativeContext); if (pNativeContext) { pEnv->SetLongField(thiz, sGifFrameFieldNativeContext, 0); GifFrameNativeContext_releaseRef(pEnv, thiz, pNativeContext); } pEnv->MonitorExit(thiz); } /** * Finalizer for GifFrame that frees native resources. */ void GifFrame_nativeFinalize(JNIEnv* pEnv, jobject thiz) { GifFrame_nativeDispose(pEnv, thiz); } static JNINativeMethod sGifImageMethods[] = { {"nativeCreateFromDirectByteBuffer", "(Ljava/nio/ByteBuffer;IZ)Lcom/facebook/animated/gif/GifImage;", (void*)GifImage_nativeCreateFromDirectByteBuffer}, {"nativeCreateFromNativeMemory", "(JIIZ)Lcom/facebook/animated/gif/GifImage;", (void*)GifImage_nativeCreateFromNativeMemory}, {"nativeCreateFromFileDescriptor", "(IIZ)Lcom/facebook/animated/gif/GifImage;", (void*)GifImage_nativeCreateFromFileDescriptor}, {"nativeGetWidth", "()I", (void*)GifImage_nativeGetWidth}, {"nativeGetHeight", "()I", (void*)GifImage_nativeGetHeight}, {"nativeGetDuration", "()I", (void*)GifImage_nativeGetDuration}, {"nativeGetFrameCount", "()I", (void*)GifImage_nativeGetFrameCount}, {"nativeGetFrameDurations", "()[I", (void*)GifImage_nativeGetFrameDurations}, {"nativeGetLoopCount", "()I", (void*)GifImage_nativeGetLoopCount}, {"nativeGetFrame", "(I)Lcom/facebook/animated/gif/GifFrame;", (void*)GifImage_nativeGetFrame}, {"nativeGetSizeInBytes", "()I", (void*)GifImage_nativeGetSizeInBytes}, {"nativeIsAnimated", "()Z", (void*)GifImage_nativeIsAnimated}, {"nativeDispose", "()V", (void*)GifImage_nativeDispose}, {"nativeFinalize", "()V", (void*)GifImage_nativeFinalize}}; static JNINativeMethod sGifFrameMethods[] = { {"nativeRenderFrame", "(IILandroid/graphics/Bitmap;)V", (void*)GifFrame_nativeRenderFrame}, {"nativeGetDurationMs", "()I", (void*)GifFrame_nativeGetDurationMs}, {"nativeGetWidth", "()I", (void*)GifFrame_nativeGetWidth}, {"nativeGetHeight", "()I", (void*)GifFrame_nativeGetHeight}, {"nativeGetXOffset", "()I", (void*)GifFrame_nativeGetXOffset}, {"nativeGetYOffset", "()I", (void*)GifFrame_nativeGetYOffset}, {"nativeGetTransparentPixelColor", "()I", (void*)GifFrame_nativeGetTransparentPixelColor}, {"nativeHasTransparency", "()Z", (void*)GifFrame_nativeHasTransparency}, {"nativeGetDisposalMode", "()I", (void*)GifFrame_nativeGetDisposalMode}, {"nativeDispose", "()V", (void*)GifFrame_nativeDispose}, {"nativeFinalize", "()V", (void*)GifFrame_nativeFinalize}, }; /** * Called by JNI_OnLoad to initialize the classes. */ int initGifImage(JNIEnv* pEnv) { // GifImage sClazzGifImage = findClassOrThrow(pEnv, kGifImageClassPathName); if (sClazzGifImage == NULL) { return JNI_ERR; } // GifImage.mNativeContext sGifImageFieldNativeContext = getFieldIdOrThrow(pEnv, sClazzGifImage, "mNativeContext", "J"); if (!sGifImageFieldNativeContext) { return JNI_ERR; } // GifImage. sGifImageConstructor = getMethodIdOrThrow(pEnv, sClazzGifImage, "", "(J)V"); if (!sGifImageConstructor) { return JNI_ERR; } int result = pEnv->RegisterNatives( sClazzGifImage, sGifImageMethods, std::extent::value); if (result != JNI_OK) { return result; } // GifFrame sClazzGifFrame = findClassOrThrow(pEnv, kGifFrameClassPathName); if (sClazzGifFrame == NULL) { return JNI_ERR; } // GifFrame.mNativeContext sGifFrameFieldNativeContext = getFieldIdOrThrow(pEnv, sClazzGifFrame, "mNativeContext", "J"); if (!sGifFrameFieldNativeContext) { return JNI_ERR; } // GifFrame. sGifFrameConstructor = getMethodIdOrThrow(pEnv, sClazzGifFrame, "", "(J)V"); if (!sGifFrameConstructor) { return JNI_ERR; } result = pEnv->RegisterNatives( sClazzGifFrame, sGifFrameMethods, std::extent::value); if (result != JNI_OK) { return result; } sDefaultColorMap = genDefColorMap(); return JNI_OK; } ================================================ FILE: animated-gif/src/main/jni/gifimage/jni_helpers.cpp ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include #include #include #define MSG_SIZE 1024 namespace facebook { /** * Instructs the JNI environment to throw an exception. * * @param pEnv JNI environment * @param szClassName class name to throw * @param szFmt sprintf-style format string * @param ... sprintf-style args * @return 0 on success; a negative value on failure */ jint throwException( JNIEnv* pEnv, const char* szClassName, const char* szFmt, va_list va_args) { char szMsg[MSG_SIZE]; vsnprintf(szMsg, MSG_SIZE, szFmt, va_args); jclass exClass = pEnv->FindClass(szClassName); return pEnv->ThrowNew(exClass, szMsg); } /** * Instructs the JNI environment to throw a NoClassDefFoundError. * * @param pEnv JNI environment * @param szFmt sprintf-style format string * @param ... sprintf-style args * @return 0 on success; a negative value on failure */ jint throwNoClassDefError(JNIEnv* pEnv, const char* szFmt, ...) { va_list va_args; va_start(va_args, szFmt); jint ret = throwException(pEnv, "java/lang/NoClassDefFoundError", szFmt, va_args); va_end(va_args); return ret; } /** * Instructs the JNI environment to throw a RuntimeException. * * @param pEnv JNI environment * @param szFmt sprintf-style format string * @param ... sprintf-style args * @return 0 on success; a negative value on failure */ jint throwRuntimeException(JNIEnv* pEnv, const char* szFmt, ...) { va_list va_args; va_start(va_args, szFmt); jint ret = throwException(pEnv, "java/lang/RuntimeException", szFmt, va_args); va_end(va_args); return ret; } /** * Instructs the JNI environment to throw an IllegalArgumentException. * * @param pEnv JNI environment * @param szFmt sprintf-style format string * @param ... sprintf-style args * @return 0 on success; a negative value on failure */ jint throwIllegalArgumentException(JNIEnv* pEnv, const char* szFmt, ...) { va_list va_args; va_start(va_args, szFmt); jint ret = throwException( pEnv, "java/lang/IllegalArgumentException", szFmt, va_args); va_end(va_args); return ret; } /** * Instructs the JNI environment to throw an IllegalStateException. * * @param pEnv JNI environment * @param szFmt sprintf-style format string * @param ... sprintf-style args * @return 0 on success; a negative value on failure */ jint throwIllegalStateException(JNIEnv* pEnv, const char* szFmt, ...) { va_list va_args; va_start(va_args, szFmt); jint ret = throwException(pEnv, "java/lang/IllegalStateException", szFmt, va_args); va_end(va_args); return ret; } /** * Instructs the JNI environment to throw an OutOfMemoryError. * * @param pEnv JNI environment * @param szFmt sprintf-style format string * @param ... sprintf-style args * @return 0 on success; a negative value on failure */ jint throwOutOfMemoryError(JNIEnv* pEnv, const char* szFmt, ...) { va_list va_args; va_start(va_args, szFmt); jint ret = throwException(pEnv, "java/lang/OutOfMemoryError", szFmt, va_args); va_end(va_args); return ret; } /** * Instructs the JNI environment to throw an AssertionError. * * @param pEnv JNI environment * @param szFmt sprintf-style format string * @param ... sprintf-style args * @return 0 on success; a negative value on failure */ jint throwAssertionError(JNIEnv* pEnv, const char* szFmt, ...) { va_list va_args; va_start(va_args, szFmt); jint ret = throwException(pEnv, "java/lang/AssertionError", szFmt, va_args); va_end(va_args); return ret; } /** * Instructs the JNI environment to throw an IOException. * * @param pEnv JNI environment * @param szFmt sprintf-style format string * @param ... sprintf-style args * @return 0 on success; a negative value on failure */ jint throwIOException(JNIEnv* pEnv, const char* szFmt, ...) { va_list va_args; va_start(va_args, szFmt); jint ret = throwException(pEnv, "java/io/IOException", szFmt, va_args); va_end(va_args); return ret; } /** * Finds the specified class. If it's not found, instructs the JNI environment * to throw an exception. * * @param pEnv JNI environment * @param szClassName the classname to find in JNI format (e.g. * "java/lang/String") * @return the class or NULL if not found (in which case a pending exception * will be queued). This returns a global reference (JNIEnv::NewGlobalRef). */ jclass findClassOrThrow(JNIEnv* pEnv, const char* szClassName) { jclass clazz = pEnv->FindClass(szClassName); if (!clazz) { return NULL; } return (jclass)pEnv->NewGlobalRef(clazz); } /** * Finds the specified field of the specified class. If it's not found, * instructs the JNI environment to throw an exception. * * @param pEnv JNI environment * @param clazz the class to lookup the field in * @param szFieldName the name of the field to find * @param szSig the signature of the field * @return the field or NULL if not found (in which case a pending exception * will be queued) */ jfieldID getFieldIdOrThrow( JNIEnv* pEnv, jclass clazz, const char* szFieldName, const char* szSig) { return pEnv->GetFieldID(clazz, szFieldName, szSig); } /** * Finds the specified method of the specified class. If it's not found, * instructs the JNI environment to throw an exception. * * @param pEnv JNI environment * @param clazz the class to lookup the method in * @param szMethodName the name of the method to find * @param szSig the signature of the method * @return the method or NULL if not found (in which case a pending exception * will be queued) */ jmethodID getMethodIdOrThrow( JNIEnv* pEnv, jclass clazz, const char* szMethodName, const char* szSig) { return pEnv->GetMethodID(clazz, szMethodName, szSig); } } // namespace facebook ================================================ FILE: animated-gif/src/main/jni/gifimage/jni_helpers.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #pragma once #include #include namespace facebook { /** * Instructs the JNI environment to throw an exception. * * @param pEnv JNI environment * @param szClassName class name to throw * @param szFmt sprintf-style format string * @param ... sprintf-style args * @return 0 on success; a negative value on failure */ jint throwException( JNIEnv* pEnv, const char* szClassName, const char* szFmt, va_list va_args); /** * Instructs the JNI environment to throw a NoClassDefFoundError. * * @param pEnv JNI environment * @param szFmt sprintf-style format string * @param ... sprintf-style args * @return 0 on success; a negative value on failure */ jint throwNoClassDefError(JNIEnv* pEnv, const char* szFmt, ...); /** * Instructs the JNI environment to throw a RuntimeException. * * @param pEnv JNI environment * @param szFmt sprintf-style format string * @param ... sprintf-style args * @return 0 on success; a negative value on failure */ jint throwRuntimeException(JNIEnv* pEnv, const char* szFmt, ...); /** * Instructs the JNI environment to throw a IllegalArgumentException. * * @param pEnv JNI environment * @param szFmt sprintf-style format string * @param ... sprintf-style args * @return 0 on success; a negative value on failure */ jint throwIllegalArgumentException(JNIEnv* pEnv, const char* szFmt, ...); /** * Instructs the JNI environment to throw a IllegalStateException. * * @param pEnv JNI environment * @param szFmt sprintf-style format string * @param ... sprintf-style args * @return 0 on success; a negative value on failure */ jint throwIllegalStateException(JNIEnv* pEnv, const char* szFmt, ...); /** * Instructs the JNI environment to throw an IOException. * * @param pEnv JNI environment * @param szFmt sprintf-style format string * @param ... sprintf-style args * @return 0 on success; a negative value on failure */ jint throwIOException(JNIEnv* pEnv, const char* szFmt, ...); /** * Instructs the JNI environment to throw an AssertionError. * * @param pEnv JNI environment * @param szFmt sprintf-style format string * @param ... sprintf-style args * @return 0 on success; a negative value on failure */ jint throwAssertionError(JNIEnv* pEnv, const char* szFmt, ...); /** * Instructs the JNI environment to throw an OutOfMemoryError. * * @param pEnv JNI environment * @param szFmt sprintf-style format string * @param ... sprintf-style args * @return 0 on success; a negative value on failure */ jint throwOutOfMemoryError(JNIEnv* pEnv, const char* szFmt, ...); /** * Finds the specified class. If it's not found, instructs the JNI environment * to throw an exception. * * @param pEnv JNI environment * @param szClassName the classname to find in JNI format (e.g. * "java/lang/String") * @return the class or NULL if not found (in which case a pending exception * will be queued). This returns a global reference (JNIEnv::NewGlobalRef). */ jclass findClassOrThrow(JNIEnv* pEnv, const char* szClassName); /** * Finds the specified field of the specified class. If it's not found, * instructs the JNI environment to throw an exception. * * @param pEnv JNI environment * @param clazz the class to lookup the field in * @param szFieldName the name of the field to find * @param szSig the signature of the field * @return the field or NULL if not found (in which case a pending exception * will be queued) */ jfieldID getFieldIdOrThrow( JNIEnv* pEnv, jclass clazz, const char* szFieldName, const char* szSig); /** * Finds the specified method of the specified class. If it's not found, * instructs the JNI environment to throw an exception. * * @param pEnv JNI environment * @param clazz the class to lookup the method in * @param szMethodName the name of the method to find * @param szSig the signature of the method * @return the method or NULL if not found (in which case a pending exception * will be queued) */ jmethodID getMethodIdOrThrow( JNIEnv* pEnv, jclass clazz, const char* szMethodName, const char* szSig); } // namespace facebook ================================================ FILE: animated-gif/src/main/jni/gifimage/locks.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #pragma once #include #include #include class RWLock { public: RWLock() : mutex_(PTHREAD_RWLOCK_INITIALIZER) {}; // No copying allowed RWLock(const RWLock&) = delete; void operator=(const RWLock&) = delete; ~RWLock() { pthread_rwlock_destroy(&mutex_); }; inline int ReadLock() { return pthread_rwlock_rdlock(&mutex_); }; inline int ReadUnlock() { return pthread_rwlock_unlock(&mutex_); }; inline int WriteLock() { return pthread_rwlock_wrlock(&mutex_); } inline int WriteUnlock() { return pthread_rwlock_unlock(&mutex_); } private: pthread_rwlock_t mutex_; }; class ReaderLock { private: RWLock* rlock_; public: inline explicit ReaderLock(RWLock* rlock) : rlock_(rlock) { int ret = rlock_->ReadLock(); if (ret != 0) { __android_log_print( ANDROID_LOG_ERROR, LOG_TAG, "pthread_rwlock_rdlock returned %s", strerror(ret)); } } inline ~ReaderLock() { int ret = rlock_->ReadUnlock(); if (ret != 0) { __android_log_print( ANDROID_LOG_ERROR, LOG_TAG, "pthread_rwlock_unlock read returned %s", strerror(ret)); } } }; class WriterLock { private: RWLock* wlock_; public: inline explicit WriterLock(RWLock* wlock) : wlock_(wlock) { int ret = wlock_->WriteLock(); if (ret != 0) { __android_log_print( ANDROID_LOG_ERROR, LOG_TAG, "pthread_rwlock_wrlock returned %s", strerror(ret)); } } inline ~WriterLock() { int ret = wlock_->WriteUnlock(); if (ret != 0) { __android_log_print( ANDROID_LOG_ERROR, LOG_TAG, "pthread_rwlock_unlock write returned %s", strerror(ret)); } } }; ================================================ FILE: animated-gif/src/main/jni/gifimage/secure_memcpy.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #define ERR_POTENTIAL_BUFFER_OVERFLOW 34 // matches with ERANGE in errno.h /** * Bounds checking (i.e. destination) wrapper for std::memcpy. This version adds * bounds checking capability and returns an error code if there's any potential * buffer overflow detected. Error handling is mandatory. Note that using this * function without error handling does not guarantee security. * * @param destination * Pointer to the destination where the content is to be copied. * @param destination_size * Max number of bytes to modify in the destination (typically the size of * the destination buffer). * @param source * Pointer to the source of data to be copied. * @param count * Number of bytes to copy. * @return int * Returns zero on success and non-zero value on error. */ __attribute__((warn_unused_result)) inline int try_checked_memcpy( void* destination, size_t destination_size, const void* source, size_t count) { if (destination_size < count) { return ERR_POTENTIAL_BUFFER_OVERFLOW; } memcpy(destination, source, count); return 0; } ================================================ FILE: animated-gif/src/main/jni/third-party/giflib/Android.mk ================================================ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_CFLAGS := -DHAVE_CONFIG_H LOCAL_MODULE := gif LOCAL_SRC_FILES := \ dgif_lib.c \ gifalloc.c \ openbsd-reallocarray.c LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) include $(BUILD_STATIC_LIBRARY) ================================================ FILE: animated-gif/src/test/java/com/facebook/animated/gif/GifImageDecoderTest.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.animated.gif import android.graphics.Bitmap import com.facebook.common.memory.PooledByteBuffer import com.facebook.common.references.CloseableReference import com.facebook.common.references.ResourceReleaser import com.facebook.imageformat.ImageFormat import com.facebook.imagepipeline.animated.base.AnimatedImageResult import com.facebook.imagepipeline.animated.impl.AnimatedImageCompositor import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory import com.facebook.imagepipeline.common.ImageDecodeOptions import com.facebook.imagepipeline.image.CloseableAnimatedImage import com.facebook.imagepipeline.image.CloseableStaticBitmap import com.facebook.imagepipeline.image.EncodedImage import com.facebook.imagepipeline.image.ImmutableQualityInfo import com.facebook.imagepipeline.testing.MockBitmapFactory import com.facebook.imagepipeline.testing.TrivialBufferPooledByteBuffer import com.facebook.imagepipeline.testing.TrivialPooledByteBuffer import java.io.ByteArrayOutputStream import java.io.IOException import java.nio.ByteBuffer import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers import org.mockito.MockedConstruction import org.mockito.MockedStatic import org.mockito.Mockito import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner /** Tests for [GifImageDecoder] */ @RunWith(RobolectricTestRunner::class) class GifImageDecoderTest { private var mockBitmapFactory: PlatformBitmapFactory? = null private var gifImageDecoder: GifImageDecoder? = null private var gifImageStaticMock: MockedStatic? = null @Before fun setup() { gifImageStaticMock = Mockito.mockStatic(GifImage::class.java) mockBitmapFactory = mock() val bitmapFactory = mockBitmapFactory if (bitmapFactory != null) { gifImageDecoder = GifImageDecoder( bitmapFactory, isNewRenderImplementation = false, downscaleFrameToDrawableDimensions = false, treatAnimatedImagesAsStateful = true, ) } } @After fun tearDown() { gifImageStaticMock?.close() } @Test fun testCreateDefaultsUsingPointer() { val mockGifImage: GifImage? = mock() // Expect a call to GifImage.createFromNativeMemory val byteBuffer: TrivialPooledByteBuffer = createByteBuffer() whenever( GifImage.createFromNativeMemory( ArgumentMatchers.eq(byteBuffer.nativePtr), ArgumentMatchers.eq(byteBuffer.size()), ArgumentMatchers.any(ImageDecodeOptions::class.java), ) ) .thenReturn(mockGifImage) testCreateDefaults(mockGifImage, byteBuffer) } @Test fun testCreateDefaultsUsingByteBuffer() { val mockGifImage: GifImage? = mock() // Expect a call to GifImage.createFromByteBuffer val byteBuffer: TrivialBufferPooledByteBuffer = createDirectByteBuffer() whenever( GifImage.createFromByteBuffer( ArgumentMatchers.eq(byteBuffer.byteBuffer), ArgumentMatchers.any(ImageDecodeOptions::class.java), ) ) .thenReturn(mockGifImage) testCreateDefaults(mockGifImage, byteBuffer) } @Test @Throws(Exception::class) fun testCreateWithPreviewBitmapUsingPointer() { val mockGifImage: GifImage = mock() val mockBitmap: Bitmap = MockBitmapFactory.create(50, 50, DEFAULT_BITMAP_CONFIG) // Expect a call to GifImage.createFromNativeMemory val byteBuffer: TrivialPooledByteBuffer = createByteBuffer() whenever( GifImage.createFromNativeMemory( ArgumentMatchers.eq(byteBuffer.nativePtr), ArgumentMatchers.eq(byteBuffer.size()), ArgumentMatchers.any(ImageDecodeOptions::class.java), ) ) .thenReturn(mockGifImage) whenever(mockGifImage.getWidth()).thenReturn(50) whenever(mockGifImage.getHeight()).thenReturn(50) whenever(mockGifImage.getFrameCount()).thenReturn(1) whenever(mockGifImage.getFrameDurations()).thenReturn(intArrayOf(100)) testCreateWithPreviewBitmap(mockGifImage, mockBitmap, byteBuffer) } @Test @Throws(Exception::class) fun testCreateWithPreviewBitmapUsingByteBuffer() { val mockGifImage: GifImage = mock() val mockBitmap: Bitmap = MockBitmapFactory.create(50, 50, DEFAULT_BITMAP_CONFIG) // Expect a call to GifImage.createFromByteBuffer val byteBuffer: TrivialBufferPooledByteBuffer = createDirectByteBuffer() whenever( GifImage.createFromByteBuffer( ArgumentMatchers.eq(byteBuffer.byteBuffer), ArgumentMatchers.any(ImageDecodeOptions::class.java), ) ) .thenReturn(mockGifImage) whenever(mockGifImage.getWidth()).thenReturn(50) whenever(mockGifImage.getHeight()).thenReturn(50) whenever(mockGifImage.getFrameCount()).thenReturn(1) whenever(mockGifImage.getFrameDurations()).thenReturn(intArrayOf(100)) testCreateWithPreviewBitmap(mockGifImage, mockBitmap, byteBuffer) } @Test @Throws(Exception::class) fun testCreateWithDecodeAlFramesUsingPointer() { val mockGifImage: GifImage = mock() val mockBitmap1: Bitmap = MockBitmapFactory.create(50, 50, DEFAULT_BITMAP_CONFIG) val mockBitmap2: Bitmap = MockBitmapFactory.create(50, 50, DEFAULT_BITMAP_CONFIG) // Expect a call to GifImage.createFromNativeMemory val byteBuffer: TrivialPooledByteBuffer = createByteBuffer() whenever( GifImage.createFromNativeMemory( ArgumentMatchers.eq(byteBuffer.nativePtr), ArgumentMatchers.eq(byteBuffer.size()), ArgumentMatchers.any(ImageDecodeOptions::class.java), ) ) .thenReturn(mockGifImage) whenever(mockGifImage.getWidth()).thenReturn(50) whenever(mockGifImage.getHeight()).thenReturn(50) whenever(mockGifImage.getFrameCount()).thenReturn(2) whenever(mockGifImage.getFrameDurations()).thenReturn(intArrayOf(100, 150)) testCreateWithDecodeAllFrames(mockGifImage, mockBitmap1, mockBitmap2, byteBuffer) } @Test @Throws(Exception::class) fun testCreateWithDecodeAlFramesUsingByteBuffer() { val mockGifImage: GifImage = mock() val mockBitmap1: Bitmap = MockBitmapFactory.create(50, 50, DEFAULT_BITMAP_CONFIG) val mockBitmap2: Bitmap = MockBitmapFactory.create(50, 50, DEFAULT_BITMAP_CONFIG) // Expect a call to GifImage.createFromByteBuffer val byteBuffer: TrivialBufferPooledByteBuffer = createDirectByteBuffer() whenever( GifImage.createFromByteBuffer( ArgumentMatchers.eq(byteBuffer.byteBuffer), ArgumentMatchers.any(ImageDecodeOptions::class.java), ) ) .thenReturn(mockGifImage) whenever(mockGifImage.getWidth()).thenReturn(50) whenever(mockGifImage.getHeight()).thenReturn(50) whenever(mockGifImage.getFrameCount()).thenReturn(2) whenever(mockGifImage.getFrameDurations()).thenReturn(intArrayOf(100, 150)) testCreateWithDecodeAllFrames(mockGifImage, mockBitmap1, mockBitmap2, byteBuffer) } private fun testCreateDefaults(mockGifImage: GifImage?, byteBuffer: PooledByteBuffer) { val encodedImage: EncodedImage = EncodedImage(CloseableReference.of(byteBuffer, FAKE_RESOURCE_RELEASER)) encodedImage.setImageFormat(ImageFormat.UNKNOWN) val closeableImage: CloseableAnimatedImage? = gifImageDecoder?.decode( encodedImage, byteBuffer.size(), ImmutableQualityInfo.FULL_QUALITY, ImageDecodeOptions.defaults(), ) as? CloseableAnimatedImage // Verify we got the right result val imageResult: AnimatedImageResult? = closeableImage?.getImageResult() assertThat(imageResult?.getImage()).isSameAs(mockGifImage) assertThat(imageResult?.getPreviewBitmap()).isNull() assertThat(imageResult?.hasDecodedFrame(0) ?: false).isFalse() // Should not have interacted with these. mockBitmapFactory?.let { Mockito.verifyNoInteractions(it) } } @Throws(Exception::class) private fun testCreateWithPreviewBitmap( mockGifImage: GifImage?, mockBitmap: Bitmap, byteBuffer: PooledByteBuffer, ) { whenever(mockBitmapFactory?.createBitmapInternal(50, 50, DEFAULT_BITMAP_CONFIG)) .thenReturn(CloseableReference.of(mockBitmap, FAKE_BITMAP_RESOURCE_RELEASER)) Mockito.mockConstruction(AnimatedImageCompositor::class.java).use { animatedImageCompositorConstruction: MockedConstruction -> val imageDecodeOptions: ImageDecodeOptions = ImageDecodeOptions.newBuilder().setDecodePreviewFrame(true).build() val encodedImage: EncodedImage = EncodedImage(CloseableReference.of(byteBuffer, FAKE_RESOURCE_RELEASER)) encodedImage.setImageFormat(ImageFormat.UNKNOWN) val closeableImage: CloseableAnimatedImage? = gifImageDecoder?.decode( encodedImage, byteBuffer.size(), ImmutableQualityInfo.FULL_QUALITY, imageDecodeOptions, ) as? CloseableAnimatedImage // Verify we got the right result val imageResult: AnimatedImageResult? = closeableImage?.getImageResult() assertThat(imageResult?.getImage()).isSameAs(mockGifImage) assertThat(imageResult?.getPreviewBitmap()).isNotNull() assertThat(imageResult?.hasDecodedFrame(0) ?: false).isFalse() mockBitmapFactory?.let { factory -> verify(factory).createBitmapInternal(50, 50, DEFAULT_BITMAP_CONFIG) verifyNoMoreInteractions(factory) } verify(animatedImageCompositorConstruction.constructed()[0]).renderFrame(0, mockBitmap) } } @Throws(Exception::class) private fun testCreateWithDecodeAllFrames( mockGifImage: GifImage?, mockBitmap1: Bitmap, mockBitmap2: Bitmap, byteBuffer: PooledByteBuffer, ) { whenever(mockBitmapFactory?.createBitmapInternal(50, 50, DEFAULT_BITMAP_CONFIG)) .thenReturn(CloseableReference.of(mockBitmap1, FAKE_BITMAP_RESOURCE_RELEASER)) .thenReturn(CloseableReference.of(mockBitmap2, FAKE_BITMAP_RESOURCE_RELEASER)) Mockito.mockConstruction(AnimatedImageCompositor::class.java).use { animatedImageCompositorConstruction: MockedConstruction -> val imageDecodeOptions: ImageDecodeOptions = ImageDecodeOptions.newBuilder() .setDecodePreviewFrame(true) .setDecodeAllFrames(true) .build() val encodedImage: EncodedImage = EncodedImage(CloseableReference.of(byteBuffer, FAKE_RESOURCE_RELEASER)) encodedImage.setImageFormat(ImageFormat.UNKNOWN) val closeableImage: CloseableAnimatedImage? = gifImageDecoder?.decode( encodedImage, byteBuffer.size(), ImmutableQualityInfo.FULL_QUALITY, imageDecodeOptions, ) as? CloseableAnimatedImage // Verify we got the right result val imageResult: AnimatedImageResult? = closeableImage?.getImageResult() assertThat(imageResult?.getImage()).isSameAs(mockGifImage) assertThat(imageResult?.getDecodedFrame(0)).isNotNull() assertThat(imageResult?.getDecodedFrame(1)).isNotNull() assertThat(imageResult?.getPreviewBitmap()).isNotNull() mockBitmapFactory?.let { factory -> verify(factory, times(2)).createBitmapInternal(50, 50, DEFAULT_BITMAP_CONFIG) verifyNoMoreInteractions(factory) } val mockCompositor: AnimatedImageCompositor? = animatedImageCompositorConstruction.constructed().get(0) mockCompositor?.let { compositor -> verify(compositor).renderFrame(0, mockBitmap1) verify(compositor).renderFrame(1, mockBitmap2) } } } @Test fun testDecodeForceStaticImage() { val mockGifImage: GifImage = mock() val mockBitmap: Bitmap = MockBitmapFactory.create(50, 50, DEFAULT_BITMAP_CONFIG) val byteBuffer: TrivialPooledByteBuffer = createByteBuffer() whenever( GifImage.createFromNativeMemory( ArgumentMatchers.eq(byteBuffer.nativePtr), ArgumentMatchers.eq(byteBuffer.size()), ArgumentMatchers.any(ImageDecodeOptions::class.java), ) ) .thenReturn(mockGifImage) whenever(mockGifImage.getWidth()).thenReturn(50) whenever(mockGifImage.getHeight()).thenReturn(50) whenever(mockGifImage.getFrameCount()).thenReturn(1) whenever(mockGifImage.getFrameDurations()).thenReturn(intArrayOf(100)) whenever(mockBitmapFactory?.createBitmapInternal(50, 50, DEFAULT_BITMAP_CONFIG)) .thenReturn(CloseableReference.of(mockBitmap, FAKE_BITMAP_RESOURCE_RELEASER)) Mockito.mockConstruction(AnimatedImageCompositor::class.java).use { animatedImageCompositorConstruction: MockedConstruction -> val imageDecodeOptions: ImageDecodeOptions = ImageDecodeOptions.newBuilder().setForceStaticImage(true).build() val encodedImage: EncodedImage = EncodedImage(CloseableReference.of(byteBuffer, FAKE_RESOURCE_RELEASER)) encodedImage.setImageFormat(ImageFormat.UNKNOWN) val closeableImage: CloseableStaticBitmap? = gifImageDecoder?.decode( encodedImage, byteBuffer.size(), ImmutableQualityInfo.FULL_QUALITY, imageDecodeOptions, ) as? CloseableStaticBitmap // Verify static bitmap instead of animated image assertThat(closeableImage).isNotNull() assertThat(closeableImage?.underlyingBitmap).isNotNull() mockBitmapFactory?.let { factory -> verify(factory).createBitmapInternal(50, 50, DEFAULT_BITMAP_CONFIG) verifyNoMoreInteractions(factory) } verify(animatedImageCompositorConstruction.constructed()[0]).renderFrame(0, mockBitmap) } } @Test fun testDecodeWithInvalidGif() { // Create an invalid GIF by using an empty byte buffer val invalidGifData = ByteArray(0) // Empty array will cause validation to fail val byteBuffer = TrivialPooledByteBuffer(invalidGifData) val encodedImage: EncodedImage = EncodedImage(CloseableReference.of(byteBuffer, FAKE_RESOURCE_RELEASER)) encodedImage.setImageFormat(ImageFormat.UNKNOWN) assertThatThrownBy { gifImageDecoder?.decode( encodedImage, byteBuffer.size(), ImmutableQualityInfo.FULL_QUALITY, ImageDecodeOptions.defaults(), ) } .isInstanceOf(UnsupportedOperationException::class.java) .hasMessageContaining("Invalid image") } private fun createByteBuffer(): TrivialPooledByteBuffer { val gifData: ByteArray = createValidGif() return TrivialPooledByteBuffer(gifData) } companion object { private val DEFAULT_BITMAP_CONFIG = Bitmap.Config.ARGB_8888 private val FAKE_RESOURCE_RELEASER: ResourceReleaser = object : ResourceReleaser { override fun release(value: PooledByteBuffer) = Unit } private val FAKE_BITMAP_RESOURCE_RELEASER: ResourceReleaser = object : ResourceReleaser { override fun release(value: Bitmap) = Unit } private fun createDirectByteBuffer(): TrivialBufferPooledByteBuffer { val gifData: ByteArray = createValidGif() return TrivialBufferPooledByteBuffer(gifData) } /** * Creates a valid GIF Structure: Header + Logical Screen Descriptor + Image Descriptor + * Image * Data + Trailer */ private fun createValidGif(): ByteArray { val gif = ByteArrayOutputStream() try { // GIF Header (6 bytes) with GIF89a signature gif.write("GIF89a".toByteArray(charset("ASCII"))) // Logical Screen Descriptor (7 bytes) writeShort(gif, 1) // width = 1 writeShort(gif, 1) // height = 1 gif.write(0x00) // Packed field (no global color table) gif.write(0x00) // Background color index gif.write(0x00) // Pixel aspect ratio // Image Descriptor (10 bytes) gif.write(0x2C) // Image separator writeShort(gif, 0) // Left position writeShort(gif, 0) // Top position writeShort(gif, 1) // width = 1 writeShort(gif, 1) // height = 1 gif.write(0x00) // Packed field (no local color table) // Image Data gif.write(0x02) // LZW minimum code size gif.write(0x02) // Sub-block size gif.write(0x4C) // Minimal LZW data gif.write(0x01) // Minimal LZW data gif.write(0x00) // Sub-block terminator // GIF Trailer gif.write(0x3B) return gif.toByteArray() } catch (e: IOException) { throw RuntimeException("Failed to create test GIF data", e) } } /** Helper method to write a 16-bit value in little-endian format */ private fun writeShort(stream: ByteArrayOutputStream, value: Int) { stream.write(value and 0xFF) // Low byte stream.write((value shr 8) and 0xFF) // High byte } } } ================================================ FILE: animated-gif-lite/.gitignore ================================================ ================================================ FILE: animated-gif-lite/build.gradle ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import com.facebook.fresco.buildsrc.Deps import com.facebook.fresco.buildsrc.GradleDeps apply plugin: 'com.android.library' apply plugin: 'kotlin-android' kotlin { jvmToolchain(11) } dependencies { compileOnly Deps.inferAnnotation compileOnly Deps.jsr305 implementation project(':animated-base') implementation project(':fbcore') implementation project(':middleware') } android { ndkVersion GradleDeps.Native.version buildToolsVersion FrescoConfig.buildToolsVersion compileSdkVersion FrescoConfig.compileSdkVersion namespace "com.facebook.animated.giflite" defaultConfig { minSdkVersion FrescoConfig.minSdkVersion targetSdkVersion FrescoConfig.targetSdkVersion } sourceSets { main } } apply plugin: "com.vanniktech.maven.publish" ================================================ FILE: animated-gif-lite/gradle.properties ================================================ POM_NAME=AnimatedGifLite POM_DESCRIPTION=The classes to support animated gif without ndk POM_ARTIFACT_ID=animated-gif-lite POM_PACKAGING=aar ================================================ FILE: animated-gif-lite/src/main/AndroidManifest.xml ================================================ ================================================ FILE: animated-gif-lite/src/main/java/com/facebook/animated/giflite/GifDecoder.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.animated.giflite; import android.graphics.Movie; import com.facebook.animated.giflite.decoder.GifMetadataDecoder; import com.facebook.animated.giflite.draw.MovieAnimatedImage; import com.facebook.animated.giflite.draw.MovieDrawer; import com.facebook.animated.giflite.draw.MovieFrame; import com.facebook.common.internal.Preconditions; import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo; import com.facebook.imagepipeline.animated.base.AnimatedImageResult; import com.facebook.imagepipeline.common.ImageDecodeOptions; import com.facebook.imagepipeline.decoder.ImageDecoder; import com.facebook.imagepipeline.image.CloseableAnimatedImage; import com.facebook.imagepipeline.image.CloseableImage; import com.facebook.imagepipeline.image.EncodedImage; import com.facebook.imagepipeline.image.QualityInfo; import com.facebook.infer.annotation.Nullsafe; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; /** A simple Gif decoder that uses Android's {@link Movie} class to decode Gif images. */ @Nullsafe(Nullsafe.Mode.LOCAL) public class GifDecoder implements ImageDecoder { @Override public CloseableImage decode( final EncodedImage encodedImage, int length, QualityInfo qualityInfo, ImageDecodeOptions options) { InputStream is = encodedImage.getInputStream(); try { ByteArrayOutputStream out = new ByteArrayOutputStream(); // NULLSAFE_FIXME[Parameter Not Nullable] GifMetadataDecoder decoder = GifMetadataDecoder.create(is, out); if (out.size() > 0) { // let's use the fixed gif version if exists Preconditions.checkNotNull(is, "InputStream cannot be null").close(); is = new ByteArrayInputStream(out.toByteArray()); } Preconditions.checkNotNull(is, "InputStream cannot be null").reset(); Movie movie = Movie.decodeStream(is); MovieDrawer drawer = new MovieDrawer(movie); MovieFrame[] frames = new MovieFrame[decoder.getFrameCount()]; int currTime = 0; for (int frameNumber = 0, N = frames.length; frameNumber < N; frameNumber++) { int frameDuration = decoder.getFrameDurationMs(frameNumber); currTime += frameDuration; frames[frameNumber] = new MovieFrame( drawer, currTime, frameDuration, movie.width(), movie.height(), translateFrameDisposal(decoder.getFrameDisposal(frameNumber))); } return new CloseableAnimatedImage( AnimatedImageResult.forAnimatedImage( new MovieAnimatedImage( frames, encodedImage.getSize(), movie.duration(), decoder.getLoopCount(), options.animatedBitmapConfig)), false); } catch (IOException e) { throw new RuntimeException("Error while decoding gif", e); } finally { try { Preconditions.checkNotNull(is, "InputStream cannot be null").close(); } catch (IOException ignored) { } } } private static AnimatedDrawableFrameInfo.DisposalMethod translateFrameDisposal(int raw) { switch (raw) { case 2: // restore to background return AnimatedDrawableFrameInfo.DisposalMethod.DISPOSE_TO_BACKGROUND; case 3: // restore to previous return AnimatedDrawableFrameInfo.DisposalMethod.DISPOSE_TO_PREVIOUS; case 1: // do not dispose // fallthrough default: // unspecified return AnimatedDrawableFrameInfo.DisposalMethod.DISPOSE_DO_NOT; } } } ================================================ FILE: animated-gif-lite/src/main/java/com/facebook/animated/giflite/decoder/GifMetadataDecoder.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.animated.giflite.decoder; import com.facebook.common.internal.Preconditions; import com.facebook.infer.annotation.Nullsafe; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.annotation.Nullable; @Nullsafe(Nullsafe.Mode.LOCAL) public class GifMetadataDecoder { private static final int MAX_BLOCK_SIZE = 256; // blocks sizes are defined by a single byte private static final char[] NETSCAPE = new char[] {'N', 'E', 'T', 'S', 'C', 'A', 'P', 'E', '2', '.', '0'}; private static final int CONTROL_INDEX_DISPOSE = 0; private static final int CONTROL_INDEX_DELAY = 1; private static final int DEFAULT_FRAME_DURATION_MS = 100; private final byte[] block = new byte[MAX_BLOCK_SIZE]; private final InputStream mInputStream; @Nullable private final OutputStream mOutputStream; private boolean shouldFixStream; private int screenWidth; private int screenHeight; private final List mFrameControls = new ArrayList<>(); private int mLoopCount = 1; // default loop count is 1 private boolean mDecoded = false; private int mCurrentOffset = 0; public static GifMetadataDecoder create(InputStream is, @Nullable OutputStream os) throws IOException { GifMetadataDecoder decoder = new GifMetadataDecoder(is, os); decoder.decode(); return decoder; } /** * @param is InputStream to decode * @param os OutputStream to write fixed version of gif, if needed. (optional) */ private GifMetadataDecoder(InputStream is, @Nullable OutputStream os) { mInputStream = is; mOutputStream = os; } public void decode() throws IOException { if (mDecoded) { throw new IllegalStateException("decode called multiple times"); } mDecoded = true; readGifInfo(); } public int getScreenWidth() { if (!mDecoded) { throw new IllegalStateException("getScreenWidth called before decode"); } return screenWidth; } public int getScreenHeight() { if (!mDecoded) { throw new IllegalStateException("getScreenHeight called before decode"); } return screenHeight; } public int getFrameCount() { if (!mDecoded) { throw new IllegalStateException("getFrameCount called before decode"); } return mFrameControls.size(); } public int getLoopCount() { if (!mDecoded) { throw new IllegalStateException("getLoopCount called before decode"); } return mLoopCount; } public int getFrameDisposal(int frameNumber) { if (!mDecoded) { throw new IllegalStateException("getFrameDisposal called before decode"); } return mFrameControls.get(frameNumber)[CONTROL_INDEX_DISPOSE]; } public int getFrameDurationMs(int frameNumber) { if (!mDecoded) { throw new IllegalStateException("getFrameDurationMs called before decode"); } // For frame number higher than frame count, returning 1 ms ensures that animation backend can // fetch loop duration of a frame correctly in case when some frames do not have correct delay. if (frameNumber >= getFrameCount()) { return 1; } return mFrameControls.get(frameNumber)[CONTROL_INDEX_DELAY]; } private void readGifInfo() throws IOException { validateAndIgnoreHeader(); final int[] control = new int[] {0, 0}; boolean done = false; while (!done) { int code = readAndWriteNextByte(); switch (code) { case 0x21: // extension int extCode = readAndWriteNextByte(); switch (extCode) { case 0xff: // application extension readBlock(); if (isNetscape()) { readNetscapeExtension(); } else { skipExtension(); } break; case 0xf9: // graphics control extension readGraphicsControlExtension(control); break; case 0x01: // plain text extension, counts as a frame addFrame(control); skipExtension(); break; default: skipExtension(); } break; case 0x2C: // image addFrame(control); skipImage(); // count as a frame break; case 0x3b: // terminator done = true; break; default: throw new IOException("Unknown block header [" + Integer.toHexString(code) + "]"); } } } private void addFrame(int[] control) { mFrameControls.add(Arrays.copyOf(control, control.length)); } private void validateAndIgnoreHeader() throws IOException { readIntoBlock(0 /* offset */, 6 /* length */); boolean valid = 'G' == (char) block[0] && 'I' == (char) block[1] && 'F' == (char) block[2] && '8' == (char) block[3] && ('7' == (char) block[4] || '9' == (char) block[4]) && 'a' == (char) block[5]; if (!valid) { throw new IOException("Illegal header for gif"); } screenWidth = readAndWriteNextByte() | (readAndWriteNextByte() << 8); screenHeight = readAndWriteNextByte() | (readAndWriteNextByte() << 8); int fields = readAndWriteNextByte(); boolean hasGlobalColorTable = (fields & 0x80) != 0; int globalColorTableSize = 2 << (fields & 7); skipAndWriteBytes(2); // bgc index, aspect ratio if (hasGlobalColorTable) { ignoreColorTable(globalColorTableSize); } } private void ignoreColorTable(int numColors) throws IOException { skipAndWriteBytes(3 * numColors); } private int readBlock() throws IOException { int blockSize = readAndWriteNextByte(); int n = 0; if (blockSize > 0) { while (n < blockSize) { n += readIntoBlock(n, blockSize - n); } } return n; } private void skipExtension() throws IOException { int size; do { size = readBlock(); } while (size > 0); } private void skipImage() throws IOException { skipAndWriteBytes(8); int flags = readAndWriteNextByte(); boolean hasLct = (flags & 0x80) != 0; if (hasLct) { int lctSize = 2 << (flags & 7); ignoreColorTable(lctSize); } skipAndWriteBytes(1); skipExtension(); } private boolean isNetscape() { if (block.length < NETSCAPE.length) { return false; } for (int i = 0, N = NETSCAPE.length; i < N; i++) { if (NETSCAPE[i] != (char) block[i]) { return false; } } return true; } private void readNetscapeExtension() throws IOException { int size; do { size = readBlock(); if (block[0] == 1) { mLoopCount = ((((int) block[2]) & 0xff) << 8) | (((int) block[1]) & 0xff); } } while (size > 0); } private void readGraphicsControlExtension(int[] control) throws IOException { skipAndWriteBytes(1); int flags = readAndWriteNextByte(); control[CONTROL_INDEX_DISPOSE] = (flags & 0x1c) >> 2; // dispose control[CONTROL_INDEX_DELAY] = readTwoByteInt() * 10; // delay if (control[CONTROL_INDEX_DELAY] == 0) { control[CONTROL_INDEX_DELAY] = DEFAULT_FRAME_DURATION_MS; initFixedOutputStream(); } writeTwoByteInt(control[CONTROL_INDEX_DELAY] / 10); skipAndWriteBytes(2); } private int readNextByte() throws IOException { int read = mInputStream.read(); mCurrentOffset++; if (read == -1) { throw new EOFException("Unexpected end of gif file"); } return read; } private int readTwoByteInt() throws IOException { return readNextByte() | (readNextByte() << 8); } private int readIntoBlock(int offset, int length) throws IOException { int count = mInputStream.read(block, offset, length); mCurrentOffset += length; if (shouldFixStream) { Preconditions.checkNotNull(mOutputStream).write(block, offset, length); } if (count == -1) { throw new EOFException("Unexpected end of gif file"); } return count; } private int readAndWriteNextByte() throws IOException { int read = readNextByte(); writeNextByte(read); return read; } private void writeNextByte(int b) throws IOException { if (shouldFixStream) { Preconditions.checkNotNull(mOutputStream, "OutputStream cannot be null when fixing stream") .write(b); } } private void writeTwoByteInt(int content) throws IOException { writeNextByte(content); writeNextByte(content >> 8); } private void skipAndWriteBytes(int length) throws IOException { if (shouldFixStream) { // NULLSAFE_FIXME[Parameter Not Nullable] copyFromIsToOs(mInputStream, mOutputStream, length); } else { mInputStream.skip(length); } mCurrentOffset += length; } private void initFixedOutputStream() throws IOException { if (shouldFixStream || mOutputStream == null) { return; } shouldFixStream = true; mInputStream.reset(); copyFromIsToOs(mInputStream, mOutputStream, mCurrentOffset - 2); mInputStream.skip(2); } private void copyFromIsToOs(InputStream in, OutputStream out, int length) throws IOException { while (length > 0) { int bytesRead = in.read(block, 0, Math.min(MAX_BLOCK_SIZE, length)); length -= MAX_BLOCK_SIZE; out.write(block, 0, bytesRead); } } } ================================================ FILE: animated-gif-lite/src/main/java/com/facebook/animated/giflite/draw/MovieAnimatedImage.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.animated.giflite.draw import android.graphics.Bitmap import android.graphics.Bitmap.Config import android.graphics.Movie import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo.BlendOperation import com.facebook.imagepipeline.animated.base.AnimatedImage import com.facebook.imagepipeline.animated.base.AnimatedImageFrame /** Simple wrapper for an animated image backed by [Movie]. */ class MovieAnimatedImage @JvmOverloads constructor( private val frames: Array, private val _sizeInBytes: Int, private val _duration: Int, private val _loopCount: Int, animatedBitmapConfig: Bitmap.Config? = null, ) : AnimatedImage { private val _frameDurations: IntArray = IntArray(frames.size) private val _animatedBitmapConfig: Bitmap.Config? init { var i = 0 val N = frames.size while (i < N) { _frameDurations[i] = frames[i].durationMs i++ } this._animatedBitmapConfig = animatedBitmapConfig } override fun dispose() = Unit override fun getWidth(): Int = frames[0].width override fun getHeight(): Int = frames[0].height override fun getFrameCount(): Int = frames.size override fun getDuration(): Int = _duration override fun getFrameDurations(): IntArray = _frameDurations override fun getLoopCount(): Int = _loopCount override fun getFrame(frameNumber: Int): AnimatedImageFrame = frames[frameNumber] override fun doesRenderSupportScaling(): Boolean = true override fun getSizeInBytes(): Int = _sizeInBytes override fun getFrameInfo(frameNumber: Int): AnimatedDrawableFrameInfo { val frame = frames[frameNumber] return AnimatedDrawableFrameInfo( frameNumber, frame.xOffset, frame.yOffset, frame.width, frame.height, AnimatedDrawableFrameInfo.BlendOperation.BLEND_WITH_PREVIOUS, frames[frameNumber].disposalMode, ) } override fun getAnimatedBitmapConfig(): Bitmap.Config? = _animatedBitmapConfig } ================================================ FILE: animated-gif-lite/src/main/java/com/facebook/animated/giflite/draw/MovieDrawer.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.animated.giflite.draw import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Movie /** * Pronounced Draw-er Draws frames of a [Movie] to a bitmap. All methods are synchronized, so can be * used in parallel. The underlying [mMovie] is not threadsafe, and should therefore not be accessed * outside of [MovieDrawer]. Attempts to optimize work done by the drawing [Canvas] by detecting if * the underlying [Bitmap] has changed. */ class MovieDrawer(private val movie: Movie) { private val scaleHolder: MovieScaleHolder = MovieScaleHolder(movie.width(), movie.height()) private val canvas: Canvas = Canvas() private var previousBitmap: Bitmap? = null @Synchronized fun drawFrame(movieTime: Int, w: Int, h: Int, bitmap: Bitmap) { movie.setTime(movieTime) if (previousBitmap?.isRecycled == true) { previousBitmap = null } if (previousBitmap != bitmap) { previousBitmap = bitmap canvas.setBitmap(bitmap) } scaleHolder.updateViewPort(w, h) canvas.save() canvas.scale(scaleHolder.scale, scaleHolder.scale) movie.draw(canvas, scaleHolder.left, scaleHolder.top) canvas.restore() } } ================================================ FILE: animated-gif-lite/src/main/java/com/facebook/animated/giflite/draw/MovieFrame.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.animated.giflite.draw import android.graphics.Bitmap import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo.DisposalMethod import com.facebook.imagepipeline.animated.base.AnimatedImageFrame /** * Simple wrapper for an animated image frame back by [MovieDrawer]. All [MovieFrame] for the same * [MovieAnimatedImage] will be backed by the same [MovieDrawer]. */ class MovieFrame( private val movieDrawer: MovieDrawer, private val frameStart: Int, private val frameDuration: Int, private val frameWidth: Int, private val frameHeight: Int, val disposalMode: DisposalMethod, ) : AnimatedImageFrame { override fun dispose() = Unit override fun renderFrame(w: Int, h: Int, bitmap: Bitmap) { movieDrawer.drawFrame(frameStart, w, h, bitmap) } override fun getDurationMs(): Int = frameDuration override fun getWidth(): Int = frameWidth override fun getHeight(): Int = frameHeight override fun getXOffset(): Int = 0 override fun getYOffset(): Int = 0 } ================================================ FILE: animated-gif-lite/src/main/java/com/facebook/animated/giflite/draw/MovieScaleHolder.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.animated.giflite.draw internal class MovieScaleHolder(private val movieWidth: Int, private val movieHeight: Int) { private var viewPortWidth = 0 private var viewPortHeight = 0 @get:Synchronized var scale = 1f private set @get:Synchronized var left = 0f private set @get:Synchronized var top = 0f private set @Synchronized fun updateViewPort(viewPortWidth: Int, viewPortHeight: Int) { if (this.viewPortWidth == viewPortWidth && this.viewPortHeight == viewPortHeight) { return } this.viewPortWidth = viewPortWidth this.viewPortHeight = viewPortHeight determineScaleAndPosition() } @Synchronized private fun determineScaleAndPosition() { val inputRatio = (movieWidth / movieHeight).toFloat() val outputRatio = (viewPortWidth / viewPortHeight).toFloat() var width = viewPortWidth var height = viewPortHeight if (outputRatio > inputRatio) { // Not enough width to fill the output. (Black bars on left and right.) width = (viewPortHeight * inputRatio).toInt() } else if (outputRatio < inputRatio) { // Not enough height to fill the output. (Black bars on top and bottom.) height = (viewPortWidth / inputRatio).toInt() } if (viewPortWidth > movieWidth) { scale = movieWidth / viewPortWidth.toFloat() } else if (movieWidth > viewPortWidth) { scale = viewPortWidth / movieWidth.toFloat() } else { scale = 1f } left = (viewPortWidth - width) / 2f / scale top = (viewPortHeight - height) / 2f / scale } } ================================================ FILE: animated-gif-lite/src/main/java/com/facebook/animated/giflite/drawable/GifAnimationBackend.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.animated.giflite.drawable import android.graphics.Canvas import android.graphics.ColorFilter import android.graphics.Movie import android.graphics.Rect import android.graphics.drawable.Drawable import com.facebook.animated.giflite.decoder.GifMetadataDecoder import com.facebook.fresco.animation.backend.AnimationBackend import java.io.BufferedInputStream import java.io.Closeable import java.io.FileInputStream import java.io.IOException import java.io.InputStream class GifAnimationBackend private constructor(private val gifDecoder: GifMetadataDecoder, private val movie: Movie) : AnimationBackend { private val frameStartTimes = IntArray(gifDecoder.frameCount) private var midX = 0f private var midY = 0f override fun drawFrame(parent: Drawable, canvas: Canvas, frameNumber: Int): Boolean { movie.setTime(getFrameStartTime(frameNumber)) movie.draw(canvas, midX, midY) return true } override fun setAlpha(alpha: Int) { // unimplemented } override fun setColorFilter(colorFilter: ColorFilter?) { // unimplemented } override fun setBounds(bounds: Rect) { scale( bounds.right - bounds.left, /* viewPortWidth */ bounds.bottom - bounds.top, /* viewPortHeight */ movie.width(), /* sourceWidth */ movie.height(), /* sourceHeight */ ) } override fun getIntrinsicWidth(): Int = movie.width() override fun getIntrinsicHeight(): Int = movie.height() override fun getSizeInBytes(): Int = 0 // no cached data override fun clear() { // unimplemented } override fun preloadAnimation() { // unimplemented } override fun setAnimationListener(listener: AnimationBackend.Listener?) { // unimplemented } override fun width(): Int = movie.width() override fun height(): Int = movie.height() override fun getLoopDurationMs(): Int { var total = 0 for (i in 0..= frameStartTimes.size) { return 0 } if (frameStartTimes[frameNumber] != 0) { return frameStartTimes[frameNumber] } for (i in 0.. inputRatio) { // Not enough width to fill the output. (Black bars on left and right.) scaledWidth = (viewPortHeight * inputRatio).toInt() scaledHeight = viewPortHeight } else if (outputRatio < inputRatio) { // Not enough height to fill the output. (Black bars on top and bottom.) scaledHeight = (viewPortWidth / inputRatio).toInt() scaledWidth = viewPortWidth } val scale = scaledWidth / sourceWidth.toFloat() midX = ((viewPortWidth - scaledWidth) / 2f) / scale midY = ((viewPortHeight - scaledHeight) / 2f) / scale } companion object { @JvmStatic @Throws(IOException::class) fun create(filePath: String?): GifAnimationBackend { var `is`: InputStream? = null try { `is` = BufferedInputStream(FileInputStream(filePath)) `is`.mark(Int.MAX_VALUE) val decoder = GifMetadataDecoder.create(`is`, null) `is`.reset() val movie = Movie.decodeStream(`is`) return GifAnimationBackend(decoder, movie) } finally { closeSilently(`is`) } } private fun closeSilently(closeable: Closeable?) { if (closeable == null) { return } try { closeable.close() } catch (ignored: IOException) { // ignore } } } } ================================================ FILE: animated-webp/.gitignore ================================================ nativedeps/ ================================================ FILE: animated-webp/build.gradle ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import com.facebook.fresco.buildsrc.Deps import com.facebook.fresco.buildsrc.GradleDeps import com.facebook.fresco.buildsrc.TestDeps apply plugin: 'com.android.library' apply plugin: 'kotlin-android' kotlin { jvmToolchain(11) } dependencies { compileOnly Deps.inferAnnotation compileOnly Deps.jsr305 compileOnly Deps.javaxAnnotation implementation Deps.Bolts.tasks implementation project(':static-webp') implementation project(':animated-base') implementation project(':middleware') testCompileOnly Deps.inferAnnotation testImplementation project(':imagepipeline-base-test') testImplementation project(':imagepipeline-test') testImplementation project(':middleware') testImplementation TestDeps.junit testImplementation TestDeps.assertjCore testImplementation TestDeps.mockitoCore3 testImplementation TestDeps.mockitoInline3 testImplementation TestDeps.mockitoKotlin3 testImplementation(TestDeps.robolectric) { exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'org.apache.httpcomponents', module: 'httpclient' } } android { ndkVersion GradleDeps.Native.version buildToolsVersion FrescoConfig.buildToolsVersion compileSdkVersion FrescoConfig.compileSdkVersion namespace "com.facebook.animated.webp" defaultConfig { minSdkVersion FrescoConfig.minSdkVersion targetSdkVersion FrescoConfig.targetSdkVersion } sourceSets { main { jni.srcDirs = [] } } lintOptions { abortOnError false } } apply plugin: "com.vanniktech.maven.publish" ================================================ FILE: animated-webp/gradle.properties ================================================ POM_NAME=AnimatedWebp POM_DESCRIPTION=The classes to support animated webp POM_ARTIFACT_ID=animated-webp POM_PACKAGING=aar ================================================ FILE: animated-webp/src/main/AndroidManifest.xml ================================================ ================================================ FILE: animated-webp/src/main/java/com/facebook/animated/webp/WebPFrame.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.animated.webp; import android.graphics.Bitmap; import com.facebook.common.internal.DoNotStrip; import com.facebook.imagepipeline.animated.base.AnimatedImageFrame; import com.facebook.infer.annotation.Nullsafe; import javax.annotation.concurrent.ThreadSafe; /** A single frame of a {@link WebPImage}. */ @Nullsafe(Nullsafe.Mode.LOCAL) @ThreadSafe public class WebPFrame implements AnimatedImageFrame { // Accessed by native methods @SuppressWarnings("unused") @DoNotStrip private long mNativeContext; /** * Constructs the frame with the native pointer. This is called by native code. * * @param nativeContext the native pointer */ @DoNotStrip WebPFrame(long nativeContext) { mNativeContext = nativeContext; } // This is a valid use of finalize. No other mechanism is appropriate. @Override protected void finalize() { nativeFinalize(); } @Override public void dispose() { nativeDispose(); } @Override public void renderFrame(int width, int height, Bitmap bitmap) { nativeRenderFrame(width, height, bitmap); } @Override public int getDurationMs() { return nativeGetDurationMs(); } @Override public int getWidth() { return nativeGetWidth(); } @Override public int getHeight() { return nativeGetHeight(); } @Override public int getXOffset() { return nativeGetXOffset(); } @Override public int getYOffset() { return nativeGetYOffset(); } public boolean shouldDisposeToBackgroundColor() { return nativeShouldDisposeToBackgroundColor(); } public boolean isBlendWithPreviousFrame() { return nativeIsBlendWithPreviousFrame(); } private native void nativeRenderFrame(int width, int height, Bitmap bitmap); private native int nativeGetDurationMs(); private native int nativeGetWidth(); private native int nativeGetHeight(); private native int nativeGetXOffset(); private native int nativeGetYOffset(); private native boolean nativeShouldDisposeToBackgroundColor(); private native boolean nativeIsBlendWithPreviousFrame(); private native void nativeDispose(); private native void nativeFinalize(); } ================================================ FILE: animated-webp/src/main/java/com/facebook/animated/webp/WebPImage.java ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.animated.webp; import static com.facebook.imagepipeline.nativecode.StaticWebpNativeLoader.ensure; import android.graphics.Bitmap; import com.facebook.common.internal.DoNotStrip; import com.facebook.common.internal.Preconditions; import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo; import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo.BlendOperation; import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo.DisposalMethod; import com.facebook.imagepipeline.animated.base.AnimatedImage; import com.facebook.imagepipeline.animated.factory.AnimatedImageDecoder; import com.facebook.imagepipeline.common.ImageDecodeOptions; import com.facebook.infer.annotation.Nullsafe; import java.nio.ByteBuffer; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; /** * A representation of a WebP image. An instance of this class will hold a copy of the encoded data * in memory along with the parsed header data. Frames are decoded on demand via {@link WebPFrame}. */ @Nullsafe(Nullsafe.Mode.LOCAL) @ThreadSafe @DoNotStrip public class WebPImage implements AnimatedImage, AnimatedImageDecoder { // Accessed by native methods @SuppressWarnings("unused") @DoNotStrip private long mNativeContext; @Nullable private Bitmap.Config mDecodeBitmapConfig = null; @DoNotStrip public WebPImage() {} /** * Constructs the image with the native pointer. This is called by native code. * * @param nativeContext the native pointer */ @DoNotStrip WebPImage(long nativeContext) { mNativeContext = nativeContext; } // This is a valid use of finalize. No other mechanism is appropriate. @Override protected void finalize() { nativeFinalize(); } @Override public void dispose() { nativeDispose(); } /** * Creates a {@link WebPImage} from the specified encoded data. This will throw if it fails to * create. This is meant to be called on a worker thread. * * @param source the data to the image (a copy will be made) */ public static WebPImage createFromByteArray(byte[] source, @Nullable ImageDecodeOptions options) { ensure(); Preconditions.checkNotNull(source, "Source byte array cannot be null"); ByteBuffer byteBuffer = ByteBuffer.allocateDirect(source.length); byteBuffer.put(source); byteBuffer.rewind(); WebPImage image = nativeCreateFromDirectByteBuffer(byteBuffer); if (options != null) { image.mDecodeBitmapConfig = options.animatedBitmapConfig; } return image; } /** * Creates a {@link WebPImage} from a ByteBuffer containing the image. This will throw if it fails * to create. * * @param byteBuffer the ByteBuffer containing the image */ public static WebPImage createFromByteBuffer( ByteBuffer byteBuffer, @Nullable ImageDecodeOptions options) { ensure(); byteBuffer.rewind(); WebPImage image = nativeCreateFromDirectByteBuffer(byteBuffer); if (options != null) { image.mDecodeBitmapConfig = options.animatedBitmapConfig; } return image; } public static WebPImage createFromNativeMemory( long nativePtr, int sizeInBytes, @Nullable ImageDecodeOptions options) { ensure(); Preconditions.checkArgument(nativePtr != 0); WebPImage image = nativeCreateFromNativeMemory(nativePtr, sizeInBytes); if (options != null) { image.mDecodeBitmapConfig = options.animatedBitmapConfig; } return image; } @Override public AnimatedImage decodeFromNativeMemory( long nativePtr, int sizeInBytes, ImageDecodeOptions options) { return WebPImage.createFromNativeMemory(nativePtr, sizeInBytes, options); } @Override public AnimatedImage decodeFromByteBuffer(ByteBuffer byteBuffer, ImageDecodeOptions options) { return WebPImage.createFromByteBuffer(byteBuffer, options); } @Override public int getWidth() { return nativeGetWidth(); } @Override public int getHeight() { return nativeGetHeight(); } @Override public int getFrameCount() { return nativeGetFrameCount(); } @Override public int getDuration() { return nativeGetDuration(); } @Override public int[] getFrameDurations() { return nativeGetFrameDurations(); } @Override public int getLoopCount() { return nativeGetLoopCount(); } @Override public WebPFrame getFrame(int frameNumber) { return nativeGetFrame(frameNumber); } @Override public int getSizeInBytes() { return nativeGetSizeInBytes(); } @Override public boolean doesRenderSupportScaling() { return true; } @Override public AnimatedDrawableFrameInfo getFrameInfo(int frameNumber) { WebPFrame frame = getFrame(frameNumber); try { return new AnimatedDrawableFrameInfo( frameNumber, frame.getXOffset(), frame.getYOffset(), frame.getWidth(), frame.getHeight(), frame.isBlendWithPreviousFrame() ? BlendOperation.BLEND_WITH_PREVIOUS : BlendOperation.NO_BLEND, frame.shouldDisposeToBackgroundColor() ? DisposalMethod.DISPOSE_TO_BACKGROUND : DisposalMethod.DISPOSE_DO_NOT); } finally { frame.dispose(); } } @Override @Nullable public Bitmap.Config getAnimatedBitmapConfig() { return mDecodeBitmapConfig; } private static native WebPImage nativeCreateFromDirectByteBuffer(ByteBuffer buffer); private static native WebPImage nativeCreateFromNativeMemory(long nativePtr, int sizeInBytes); private native int nativeGetWidth(); private native int nativeGetHeight(); private native int nativeGetDuration(); private native int nativeGetFrameCount(); private native int[] nativeGetFrameDurations(); private native int nativeGetLoopCount(); private native WebPFrame nativeGetFrame(int frameNumber); private native int nativeGetSizeInBytes(); private native void nativeDispose(); private native void nativeFinalize(); } ================================================ FILE: animated-webp/src/main/java/com/facebook/animated/webp/WebPImageDecoder.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.animated.webp import com.facebook.imagepipeline.animated.factory.AnimatedImageDecoderBase import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory import com.facebook.imagepipeline.common.ImageDecodeOptions import com.facebook.imagepipeline.decoder.ImageDecoder import com.facebook.imagepipeline.image.CloseableImage import com.facebook.imagepipeline.image.EncodedImage import com.facebook.imagepipeline.image.QualityInfo class WebPImageDecoder( platformBitmapFactory: PlatformBitmapFactory, isNewRenderImplementation: Boolean, downscaleFrameToDrawableDimensions: Boolean, treatAnimatedImagesAsStateful: Boolean = true, ) : AnimatedImageDecoderBase( platformBitmapFactory, downscaleFrameToDrawableDimensions, isNewRenderImplementation, treatAnimatedImagesAsStateful, ), ImageDecoder { /** * Decodes an animated WebP image into a CloseableImage. * * @param encodedImage encoded image (native byte array holding the encoded bytes and meta data) * @param length the length of the encoded data * @param qualityInfo quality information about the image * @param options decode options specifying how the image should be decoded * @return a CloseableImage */ override fun decode( encodedImage: EncodedImage, length: Int, qualityInfo: QualityInfo, options: ImageDecodeOptions, ): CloseableImage? { val bytesRef = encodedImage.byteBufferRef checkNotNull(bytesRef) bytesRef.use { val input = bytesRef.get() val image = input.byteBuffer?.let { byteBuffer -> WebPImage.createFromByteBuffer(byteBuffer, options) } ?: WebPImage.createFromNativeMemory(input.nativePtr, input.size(), options) return getCloseableImage( encodedImage.source, options, checkNotNull(image), options.animatedBitmapConfig, ) } } } ================================================ FILE: animated-webp/src/main/java/com/facebook/animated/webpdrawable/WebpAnimationBackend.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.animated.webpdrawable import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.ColorFilter import android.graphics.Rect import android.graphics.drawable.Drawable import com.facebook.animated.webp.WebPImage import com.facebook.fresco.animation.backend.AnimationBackend import java.io.BufferedInputStream import java.io.Closeable import java.io.FileInputStream import java.io.IOException import java.io.InputStream import javax.annotation.concurrent.GuardedBy /** Animation backend that is used to draw webp frames. */ class WebpAnimationBackend private constructor(private val webPImage: WebPImage) : AnimationBackend { private val renderDstRect = Rect() private val renderSrcRect = Rect() private var bounds: Rect? = null @GuardedBy("this") private var tempBitmap: Bitmap? = null override fun drawFrame(parent: Drawable, canvas: Canvas, frameNumber: Int): Boolean { val frame = webPImage.getFrame(frameNumber) val xScale = bounds!!.width().toDouble() / parent.intrinsicWidth.toDouble() val yScale = bounds!!.height().toDouble() / parent.intrinsicHeight.toDouble() val frameWidth = Math.round(frame.width * xScale).toInt() val frameHeight = Math.round(frame.height * yScale).toInt() val xOffset = (frame.xOffset * xScale).toInt() val yOffset = (frame.yOffset * yScale).toInt() synchronized(this) { val renderedWidth = bounds!!.width() val renderedHeight = bounds!!.height() // Update the temp bitmap to be >= rendered dimensions prepareTempBitmapForThisSize(renderedWidth, renderedHeight) if (tempBitmap == null) { return false } frame.renderFrame(frameWidth, frameHeight, tempBitmap!!) // Temporary bitmap can be bigger than frame, so we should draw only rendered area of bitmap renderSrcRect[0, 0, renderedWidth] = renderedHeight renderDstRect[xOffset, yOffset, xOffset + renderedWidth] = yOffset + renderedHeight canvas.drawBitmap(tempBitmap!!, renderSrcRect, renderDstRect, null) } return true } override fun setAlpha(alpha: Int) { // unimplemented } override fun setColorFilter(colorFilter: ColorFilter?) { // unimplemented } @Synchronized override fun setBounds(bounds: Rect) { this.bounds = bounds } override fun getIntrinsicWidth(): Int = webPImage.width override fun getIntrinsicHeight(): Int = webPImage.height override fun getSizeInBytes(): Int = 0 override fun clear() { webPImage.dispose() } override fun getFrameCount(): Int = webPImage.frameCount override fun getFrameDurationMs(frameNumber: Int): Int = webPImage.frameDurations[frameNumber] override fun getLoopDurationMs(): Int = webPImage.duration override fun width(): Int = webPImage.width override fun height(): Int = webPImage.height override fun getLoopCount(): Int = webPImage.loopCount override fun preloadAnimation() { // not needed as bitmaps are extracted on fly } override fun setAnimationListener(listener: AnimationBackend.Listener?) { // unimplementedå } @Synchronized private fun prepareTempBitmapForThisSize(width: Int, height: Int) { // Different webp frames can be different size, // So we need to ensure we can fit next frame to temporary bitmap if (tempBitmap != null && (tempBitmap!!.width < width || tempBitmap!!.height < height)) { clearTempBitmap() } if (tempBitmap == null) { tempBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) } tempBitmap!!.eraseColor(Color.TRANSPARENT) } @Synchronized private fun clearTempBitmap() { if (tempBitmap != null) { tempBitmap!!.recycle() tempBitmap = null } } companion object { @JvmStatic @Throws(IOException::class) fun create(filePath: String?): WebpAnimationBackend { var `is`: InputStream? = null try { `is` = BufferedInputStream(FileInputStream(filePath)) `is`.mark(Int.MAX_VALUE) val targetArray = ByteArray(`is`.available()) `is`.read(targetArray) val webPImage = WebPImage.createFromByteArray(targetArray, null) `is`.reset() return WebpAnimationBackend(webPImage) } finally { closeSilently(`is`) } } private fun closeSilently(closeable: Closeable?) { if (closeable == null) { return } try { closeable.close() } catch (ignored: IOException) { // ignore } } } } ================================================ FILE: animated-webp/src/test/java/com/facebook/animated/webp/WebPImageDecoderTest.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.animated.webp import android.graphics.Bitmap import com.facebook.common.memory.PooledByteBuffer import com.facebook.common.references.CloseableReference import com.facebook.common.references.ResourceReleaser import com.facebook.imageformat.ImageFormat import com.facebook.imagepipeline.animated.base.AnimatedImageResult import com.facebook.imagepipeline.animated.impl.AnimatedImageCompositor import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory import com.facebook.imagepipeline.common.ImageDecodeOptions import com.facebook.imagepipeline.image.CloseableAnimatedImage import com.facebook.imagepipeline.image.CloseableStaticBitmap import com.facebook.imagepipeline.image.EncodedImage import com.facebook.imagepipeline.image.ImmutableQualityInfo import com.facebook.imagepipeline.testing.MockBitmapFactory import com.facebook.imagepipeline.testing.TrivialBufferPooledByteBuffer import com.facebook.imagepipeline.testing.TrivialPooledByteBuffer import java.nio.ByteBuffer import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers import org.mockito.MockedConstruction import org.mockito.MockedStatic import org.mockito.Mockito import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner /** Tests for [WebPImageDecoder] */ @RunWith(RobolectricTestRunner::class) class WebPImageDecoderTest { private var mockBitmapFactory: PlatformBitmapFactory? = null private var webPImageDecoder: WebPImageDecoder? = null private var webPImageMockedStatic: MockedStatic? = null @Before fun setup() { webPImageMockedStatic = Mockito.mockStatic(WebPImage::class.java) mockBitmapFactory = mock() val bitmapFactory = mockBitmapFactory if (bitmapFactory != null) { webPImageDecoder = WebPImageDecoder( bitmapFactory, isNewRenderImplementation = false, downscaleFrameToDrawableDimensions = false, treatAnimatedImagesAsStateful = true, ) } } @After fun tearDown() { webPImageMockedStatic?.close() } @Test fun testCreateDefaultsUsingPointer() { val mockWebPImage: WebPImage = mock() // Expect a call to WebPImage.createFromNativeMemory val byteBuffer: TrivialPooledByteBuffer = createByteBuffer() whenever( WebPImage.createFromNativeMemory( ArgumentMatchers.eq(byteBuffer.nativePtr), ArgumentMatchers.eq(byteBuffer.size()), ArgumentMatchers.any(ImageDecodeOptions::class.java), ) ) .thenReturn(mockWebPImage) testCreateDefaults(mockWebPImage, byteBuffer) } @Test fun testCreateDefaultsUsingByteBuffer() { val mockWebPImage: WebPImage = mock() // Expect a call to WebPImage.createFromByteBuffer val byteBuffer: TrivialBufferPooledByteBuffer = createDirectByteBuffer() whenever( WebPImage.createFromByteBuffer( ArgumentMatchers.any(), ArgumentMatchers.any(ImageDecodeOptions::class.java), ) ) .thenReturn(mockWebPImage) testCreateDefaults(mockWebPImage, byteBuffer) } @Test @Throws(Exception::class) fun testCreateWithPreviewBitmapUsingPointer() { val mockWebPImage: WebPImage = mock() val mockBitmap: Bitmap = MockBitmapFactory.create(50, 50, DEFAULT_BITMAP_CONFIG) // Expect a call to WebPImage.createFromNativeMemory val byteBuffer: TrivialPooledByteBuffer = createByteBuffer() whenever( WebPImage.createFromNativeMemory( ArgumentMatchers.eq(byteBuffer.nativePtr), ArgumentMatchers.eq(byteBuffer.size()), ArgumentMatchers.any(ImageDecodeOptions::class.java), ) ) .thenReturn(mockWebPImage) whenever(mockWebPImage.width).thenReturn(50) whenever(mockWebPImage.height).thenReturn(50) whenever(mockWebPImage.frameCount).thenReturn(1) whenever(mockWebPImage.frameDurations).thenReturn(intArrayOf(100)) testCreateWithPreviewBitmap(mockWebPImage, byteBuffer, mockBitmap) } @Test @Throws(Exception::class) fun testCreateWithPreviewBitmapUsingByteBuffer() { val mockWebPImage: WebPImage = mock() val mockBitmap: Bitmap = MockBitmapFactory.create(50, 50, DEFAULT_BITMAP_CONFIG) // Expect a call to WebPImage.createFromByteBuffer val byteBuffer: TrivialBufferPooledByteBuffer = createDirectByteBuffer() whenever( WebPImage.createFromByteBuffer( ArgumentMatchers.any(), ArgumentMatchers.any(ImageDecodeOptions::class.java), ) ) .thenReturn(mockWebPImage) whenever(mockWebPImage.width).thenReturn(50) whenever(mockWebPImage.height).thenReturn(50) whenever(mockWebPImage.frameCount).thenReturn(1) whenever(mockWebPImage.frameDurations).thenReturn(intArrayOf(100)) testCreateWithPreviewBitmap(mockWebPImage, byteBuffer, mockBitmap) } @Test @Throws(Exception::class) fun testCreateWithDecodeAlFramesUsingPointer() { val mockWebPImage: WebPImage = mock() val mockBitmap1: Bitmap = MockBitmapFactory.create(50, 50, DEFAULT_BITMAP_CONFIG) val mockBitmap2: Bitmap = MockBitmapFactory.create(50, 50, DEFAULT_BITMAP_CONFIG) // Expect a call to WebPImage.createFromNativeMemory val byteBuffer: TrivialPooledByteBuffer = createByteBuffer() whenever( WebPImage.createFromNativeMemory( ArgumentMatchers.eq(byteBuffer.nativePtr), ArgumentMatchers.eq(byteBuffer.size()), ArgumentMatchers.any(ImageDecodeOptions::class.java), ) ) .thenReturn(mockWebPImage) whenever(mockWebPImage.width).thenReturn(50) whenever(mockWebPImage.height).thenReturn(50) whenever(mockWebPImage.frameCount).thenReturn(2) whenever(mockWebPImage.frameDurations).thenReturn(intArrayOf(100, 150)) testCreateWithDecodeAlFrames(mockWebPImage, byteBuffer, mockBitmap1, mockBitmap2) } @Test @Throws(Exception::class) fun testCreateWithDecodeAlFramesUsingByteBuffer() { val mockWebPImage: WebPImage = mock() val mockBitmap1: Bitmap = MockBitmapFactory.create(50, 50, DEFAULT_BITMAP_CONFIG) val mockBitmap2: Bitmap = MockBitmapFactory.create(50, 50, DEFAULT_BITMAP_CONFIG) // Expect a call to WebPImage.createFromByteBuffer val byteBuffer: TrivialBufferPooledByteBuffer = createDirectByteBuffer() whenever( WebPImage.createFromByteBuffer( ArgumentMatchers.any(), ArgumentMatchers.any(ImageDecodeOptions::class.java), ) ) .thenReturn(mockWebPImage) whenever(mockWebPImage.width).thenReturn(50) whenever(mockWebPImage.height).thenReturn(50) whenever(mockWebPImage.frameCount).thenReturn(2) whenever(mockWebPImage.frameDurations).thenReturn(intArrayOf(100, 150)) testCreateWithDecodeAlFrames(mockWebPImage, byteBuffer, mockBitmap1, mockBitmap2) } private fun testCreateDefaults(mockWebPImage: WebPImage, byteBuffer: PooledByteBuffer) { val encodedImage: EncodedImage = EncodedImage(CloseableReference.of(byteBuffer, FAKE_RESOURCE_RELEASER)) encodedImage.imageFormat = ImageFormat.UNKNOWN val closeableImage: CloseableAnimatedImage? = webPImageDecoder?.decode( encodedImage, byteBuffer.size(), ImmutableQualityInfo.FULL_QUALITY, ImageDecodeOptions.defaults(), ) as? CloseableAnimatedImage // Verify we got the right result val imageResult: AnimatedImageResult? = closeableImage?.imageResult assertThat(imageResult?.image).isSameAs(mockWebPImage) assertThat(imageResult?.previewBitmap).isNull() assertThat(imageResult?.hasDecodedFrame(0) == true).isFalse() // Should not have interacted with bitmap factory for basic decoding mockBitmapFactory?.let { Mockito.verifyNoInteractions(it) } } @Throws(Exception::class) private fun testCreateWithPreviewBitmap( mockWebPImage: WebPImage, byteBuffer: PooledByteBuffer, mockBitmap: Bitmap, ) { whenever(mockBitmapFactory?.createBitmapInternal(50, 50, DEFAULT_BITMAP_CONFIG)) .thenReturn(CloseableReference.of(mockBitmap, FAKE_BITMAP_RESOURCE_RELEASER)) Mockito.mockConstruction(AnimatedImageCompositor::class.java).use { animatedImageCompositorConstruction: MockedConstruction -> val imageDecodeOptions: ImageDecodeOptions = ImageDecodeOptions.newBuilder().setDecodePreviewFrame(true).build() val encodedImage: EncodedImage = EncodedImage(CloseableReference.of(byteBuffer, FAKE_RESOURCE_RELEASER)) encodedImage.imageFormat = ImageFormat.UNKNOWN val closeableImage: CloseableAnimatedImage? = webPImageDecoder?.decode( encodedImage, byteBuffer.size(), ImmutableQualityInfo.FULL_QUALITY, imageDecodeOptions, ) as? CloseableAnimatedImage // Verify we got the right result val imageResult: AnimatedImageResult? = closeableImage?.imageResult assertThat(imageResult?.image).isSameAs(mockWebPImage) assertThat(imageResult?.previewBitmap).isNotNull() assertThat(imageResult?.hasDecodedFrame(0) == true).isFalse() mockBitmapFactory?.let { factory -> verify(factory).createBitmapInternal(50, 50, DEFAULT_BITMAP_CONFIG) verifyNoMoreInteractions(factory) } animatedImageCompositorConstruction.constructed().getOrNull(0)?.let { compositor -> verify(compositor).renderFrame(0, mockBitmap) } } } @Throws(Exception::class) private fun testCreateWithDecodeAlFrames( mockWebPImage: WebPImage, byteBuffer: PooledByteBuffer, mockBitmap1: Bitmap, mockBitmap2: Bitmap, ) { whenever(mockBitmapFactory?.createBitmapInternal(50, 50, DEFAULT_BITMAP_CONFIG)) .thenReturn(CloseableReference.of(mockBitmap1, FAKE_BITMAP_RESOURCE_RELEASER)) .thenReturn(CloseableReference.of(mockBitmap2, FAKE_BITMAP_RESOURCE_RELEASER)) Mockito.mockConstruction(AnimatedImageCompositor::class.java).use { animatedImageCompositorConstruction: MockedConstruction -> val imageDecodeOptions: ImageDecodeOptions = ImageDecodeOptions.newBuilder() .setDecodePreviewFrame(true) .setDecodeAllFrames(true) .build() val encodedImage: EncodedImage = EncodedImage(CloseableReference.of(byteBuffer, FAKE_RESOURCE_RELEASER)) encodedImage.imageFormat = ImageFormat.UNKNOWN val closeableImage: CloseableAnimatedImage? = webPImageDecoder?.decode( encodedImage, byteBuffer.size(), ImmutableQualityInfo.FULL_QUALITY, imageDecodeOptions, ) as? CloseableAnimatedImage // Verify we got the right result val imageResult: AnimatedImageResult? = closeableImage?.imageResult assertThat(imageResult?.image).isSameAs(mockWebPImage) assertThat(imageResult?.getDecodedFrame(0)).isNotNull() assertThat(imageResult?.getDecodedFrame(1)).isNotNull() assertThat(imageResult?.previewBitmap).isNotNull() mockBitmapFactory?.let { factory -> verify(factory, times(2)).createBitmapInternal(50, 50, DEFAULT_BITMAP_CONFIG) verifyNoMoreInteractions(factory) } val mockCompositor = animatedImageCompositorConstruction.constructed().getOrNull(0) mockCompositor?.let { compositor -> verify(compositor).renderFrame(0, mockBitmap1) verify(compositor).renderFrame(1, mockBitmap2) } } } @Test fun testDecodeForceStaticImage() { val mockWebPImage: WebPImage = mock() val mockBitmap: Bitmap = MockBitmapFactory.create(50, 50, DEFAULT_BITMAP_CONFIG) val byteBuffer: TrivialPooledByteBuffer = createByteBuffer() whenever( WebPImage.createFromNativeMemory( ArgumentMatchers.eq(byteBuffer.nativePtr), ArgumentMatchers.eq(byteBuffer.size()), ArgumentMatchers.any(ImageDecodeOptions::class.java), ) ) .thenReturn(mockWebPImage) whenever(mockWebPImage.width).thenReturn(50) whenever(mockWebPImage.height).thenReturn(50) whenever(mockWebPImage.frameCount).thenReturn(1) whenever(mockWebPImage.frameDurations).thenReturn(intArrayOf(100)) whenever(mockBitmapFactory?.createBitmapInternal(50, 50, DEFAULT_BITMAP_CONFIG)) .thenReturn(CloseableReference.of(mockBitmap, FAKE_BITMAP_RESOURCE_RELEASER)) Mockito.mockConstruction(AnimatedImageCompositor::class.java).use { animatedImageCompositorConstruction: MockedConstruction -> val imageDecodeOptions: ImageDecodeOptions = ImageDecodeOptions.newBuilder().setForceStaticImage(true).build() val encodedImage: EncodedImage = EncodedImage(CloseableReference.of(byteBuffer, FAKE_RESOURCE_RELEASER)) encodedImage.imageFormat = ImageFormat.UNKNOWN val closeableImage: CloseableStaticBitmap? = webPImageDecoder?.decode( encodedImage, byteBuffer.size(), ImmutableQualityInfo.FULL_QUALITY, imageDecodeOptions, ) as? CloseableStaticBitmap // Verify we got a static bitmap instead of animated image assertThat(closeableImage).isNotNull() assertThat(closeableImage?.underlyingBitmap).isNotNull() mockBitmapFactory?.let { factory -> verify(factory).createBitmapInternal(50, 50, DEFAULT_BITMAP_CONFIG) verifyNoMoreInteractions(factory) } verify(animatedImageCompositorConstruction.constructed()[0]).renderFrame(0, mockBitmap) } } @Test fun testDecodeWithInvalidWebP() { // Create an invalid WebP by using an empty byte buffer val invalidWebPData = ByteArray(0) // Empty array will cause validation to fail val byteBuffer = TrivialPooledByteBuffer(invalidWebPData) val encodedImage: EncodedImage = EncodedImage(CloseableReference.of(byteBuffer, FAKE_RESOURCE_RELEASER)) encodedImage.imageFormat = ImageFormat.UNKNOWN try { webPImageDecoder?.decode( encodedImage, byteBuffer.size(), ImmutableQualityInfo.FULL_QUALITY, ImageDecodeOptions.defaults(), ) assertThat(false).`as`("Expected exception to be thrown").isTrue() } catch (e: Exception) { // Expected - invalid WebP should cause an exception assertThat(e).isNotNull() } } companion object { private val DEFAULT_BITMAP_CONFIG = Bitmap.Config.ARGB_8888 private val FAKE_RESOURCE_RELEASER: ResourceReleaser = object : ResourceReleaser { override fun release(value: PooledByteBuffer) = Unit } private val FAKE_BITMAP_RESOURCE_RELEASER: ResourceReleaser = object : ResourceReleaser { override fun release(value: Bitmap) = Unit } private fun createByteBuffer(): TrivialPooledByteBuffer { val buf = ByteArray(16) return TrivialPooledByteBuffer(buf) } private fun createDirectByteBuffer(): TrivialBufferPooledByteBuffer { val buf = ByteArray(16) return TrivialBufferPooledByteBuffer(buf) } } } ================================================ FILE: bots/IssueCommands.txt ================================================ @facebook-github-bot stack-overflow comment Hey {issue_author} and thanks for posting this! {author} tells me this issue looks like a question that would be best asked on [StackOverflow](http://stackoverflow.com/questions/tagged/fresco). StackOverflow is amazing for Q&A: it has a reputation system, voting, the ability to mark a question as answered. Because of the reputation system it is likely the community will see and answer your question there. This also helps us use the GitHub bug tracker for bugs only. Will close this as this is really a question that should be asked on SO. add-label Stack Overflow close ================================================ FILE: build.gradle ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import de.undercouch.gradle.tasks.download.Download import org.apache.tools.ant.taskdefs.condition.Os import com.facebook.fresco.buildsrc.Deps import com.facebook.fresco.buildsrc.GradleDeps // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() mavenCentral() } dependencies { classpath GradleDeps.Android.gradlePlugin classpath GradleDeps.Kotlin.gradlePlugin // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } plugins { id "de.undercouch.download" version "5.3.1" id "com.vanniktech.maven.publish" version "0.25.3" } // Make sure to add trailing slash to the absolute paths LinkedHashMap commonNdkLocations = [ "macOS-internal":"/opt/android_sdk/ndk/", "linux-ubuntu":"/usr/local/lib/android/sdk/ndk/", ] subprojects { repositories { google() mavenCentral() } // FIXME: This picks up non-Java files by default, failing to parse them. // Need to configure this more carefully. tasks.withType(Javadoc).all { enabled = false } tasks.withType(com.android.build.gradle.tasks.JavaDocGenerationTask).all { enabled = false } if (System.getenv("SANDCASTLE") == "1") { tasks.withType(Test).all { systemProperty("robolectric.dependency.repo.id", "central") systemProperty("robolectric.dependency.repo.url", "https://maven.thefacebook.com/nexus/content/repositories/central/") systemProperty("robolectric.logging", "stdout") systemProperty("robolectric.logging.enabled", "true") systemProperty("java.net.preferIPv6Addresses", "true") systemProperty("java.net.preferIPv4Stack", "false") systemProperty("jdk.attach.allowAttachSelf", "true") jvmArgs '-XX:+StartAttachListener' } } configurations.all { resolutionStrategy.force Deps.jsr305 } task allclean { } // Sets the ndkPath for each module when running on CI if (System.getenv("SANDCASTLE") == "1") { String ndkDir = System.getenv("ANDROID_NDK_HOME") project.plugins.whenPluginAdded { plugin -> if ("com.android.build.gradle.AppPlugin" == plugin.class.name) { project.android.ndkPath = ndkDir } else if ("com.android.build.gradle.LibraryPlugin" == plugin.class.name) { project.android.ndkPath = ndkDir } } } apply plugin: 'de.undercouch.download' ext.makeNdkTasks = { name, deps -> task "ndk_build_${name}"(dependsOn: deps, type: Exec) { inputs.files("src/main/jni/${name}") outputs.dir("$buildDir/${name}") commandLine getNdkBuildFullPath(project), 'NDK_PROJECT_PATH=null', 'NDK_APPLICATION_MK=../Application.mk', 'NDK_OUT=' + temporaryDir, "NDK_LIBS_OUT=$buildDir/${name}", '-C', file("src/main/jni/${name}").absolutePath, '--jobs', Runtime.getRuntime().availableProcessors() } task "ndk_clean_$name"(type: Exec) { ignoreExitValue true commandLine getNdkBuildFullPath(project), 'NDK_PROJECT_PATH=null', 'NDK_APPLICATION_MK=../Application.mk', 'NDK_OUT=' + temporaryDir, "NDK_LIBS_OUT=$buildDir/${name}", '-C', file("src/main/jni/${name}").absolutePath, 'clean' } tasks.withType(JavaCompile) { compileTask -> compileTask.dependsOn "ndk_build_$name" } clean.dependsOn "ndk_clean_$name" } ext.getNdkBuildName = { if (Os.isFamily(Os.FAMILY_WINDOWS)) { return "ndk-build.cmd" } else { return "ndk-build" } } ext.discoverNdkPathFromCommonLocations = { String ndkVersion -> String foundNdkLocation = null for (entry in commonNdkLocations) { String variant = entry.key String location = entry.value String ndkDir = location + ndkVersion File ndkFolder = new File(ndkDir) if (ndkFolder.exists()) { println("NDK Path found using: common $variant location") foundNdkLocation = ndkDir break } } return foundNdkLocation } ext.getNdkBuildFullPath = { Project project -> String path = null // Latest method using the common NDK version used throughout the project and using the standard NDK SxS location on macOS if (path == null) { path = discoverNdkPathFromCommonLocations(GradleDeps.Native.version) } // Fallback method for CI if (path == null) { String ndkDir = System.getenv("ANDROID_NDK_HOME") if (ndkDir != null) { File ndkFolder = new File(ndkDir) if (ndkFolder.exists()) { path = ndkDir println("NDK Path found using: ANDROID_NDK_HOME") } } } // Legacy methods of finding NDK path if (path == null) { File propFile = project.rootProject.file('local.properties') if (!propFile.exists()) { println("NDK Path found using: local.props missing, just the command name") return getNdkBuildName() } Properties properties = new Properties() properties.load(propFile.newDataInputStream()) def ndkCommand = properties.getProperty('ndk.command') if (ndkCommand != null) { println("NDK Path found using: ndk.command") return ndkCommand } def ndkPath = properties.getProperty('ndk.path') if (ndkPath != null) { println("NDK Path found using: ndk.path") path = ndkPath } else { def ndkDir = properties.getProperty('ndk.dir') if (ndkDir != null) { println("NDK Path found using: ndk.dir") path = ndkDir } } } if (path != null) { if (!path.endsWith(File.separator)) { path += File.separator } return path + getNdkBuildName() } else { // if none of above is provided, we assume ndk-build is already in $PATH return getNdkBuildName() } } ext.nativeDepsDir = new File("${projectDir}/nativedeps") ext.downloadsDir = new File("${nativeDepsDir}/downloads") ext.mergeDir = new File("${nativeDepsDir}/merge") task removeNativeDeps(type: Delete) { delete nativeDepsDir } allclean.dependsOn removeNativeDeps task createNativeDepsDirectories { nativeDepsDir.mkdirs() downloadsDir.mkdirs() mergeDir.mkdirs() } ext.createNativeLibrariesTasks = {name, libraryUrl, libraryFileName, libraryDestinationDir, sourceDir, includePaths, destinationDir -> // We create the DownloadTask tasks.create("download${name}", Download) { src libraryUrl onlyIfNewer true overwrite false dest "${downloadsDir}/${libraryFileName}" dependsOn createNativeDepsDirectories } // The unpack task tasks.create("unpack${name}", Copy) { String filePath = "${downloadsDir}/${libraryFileName}" ReadableResource resource if (filePath.endsWith("gz")) { resource = resources.gzip(filePath) } else if (filePath.endsWith("bz2")) { resource = resources.bzip2(filePath) } else { throw new GradleException("Could not unpack library " + filePath) } from tarTree(resource) into "${downloadsDir}/${libraryDestinationDir}" dependsOn "download${name}" } // The copy task Task unpackTask = tasks.getByName("unpack${name}") tasks.create("copy${name}", Copy) { from "${unpackTask.destinationDir}/${sourceDir}" from "src/main/jni/third-party/${destinationDir}" // Allows for overriding when duplicates are encountered. duplicatesStrategy DuplicatesStrategy.INCLUDE include(includePaths) into "${mergeDir}/${destinationDir}" dependsOn "unpack${name}" } } // Libjpeg-turbo createNativeLibrariesTasks( 'Libjpeg', // Name for the tasks "https://github.com/libjpeg-turbo/libjpeg-turbo/archive/${LIBJPEG_TURBO_VERSION}.tar.gz", // The Url for download "${LIBJPEG_TURBO_VERSION}.tar.gz", // The downloaded file 'libjpeg', // The folder where the file is downloaded "libjpeg-turbo-${LIBJPEG_TURBO_VERSION}", // The first dir where we have put our customisation ['**/*.c', '**/*.h','**/*.S', '**/*.asm', '**/*.inc', '*.mk'], // Files to compile "libjpeg-turbo-${LIBJPEG_TURBO_VERSION}" // Final destination dir ) // Libpng createNativeLibrariesTasks( 'Libpng', // Name for the tasks "https://github.com/glennrp/libpng/archive/v${LIBPNG_VERSION}.tar.gz", // The Url for download "v${LIBPNG_VERSION}.tar.gz", // The downloaded file 'libpng', // The folder where the file is downloaded "libpng-${LIBPNG_VERSION}", // The first dir where we have put our customisation ['**/*.c', '**/*.h', '**/*.S', '*.mk'], // Files to compile "libpng-${LIBPNG_VERSION}" // Final destination dir ) // Gif createNativeLibrariesTasks( 'Giflib', // Name for the tasks "https://sourceforge.net/projects/giflib/files/giflib-${GIFLIB_VERSION}.tar.gz/download", // The Url for download "giflib-${GIFLIB_VERSION}.tar.gz", // The downloaded file 'giflib', // The folder where the file is downloaded "giflib-${GIFLIB_VERSION}", // The first dir where we have put our customisation ['*.c', '*.h', '*.mk'], // Files to compile "giflib" // Final destination dir ) // Webp createNativeLibrariesTasks( 'Libwebp', // Name for the tasks "https://github.com/webmproject/libwebp/archive/v${LIBWEBP_VERSION}.tar.gz", // The Url for download "v${LIBWEBP_VERSION}.tar.gz", // The downloaded file 'libwebp', // The folder where the file is downloaded "libwebp-${LIBWEBP_VERSION}", // The first dir where we have put our customisation ['**/*.c', '**/*.h', '*.mk'], // Files to compile "libwebp-${LIBWEBP_VERSION}" // Final destination dir ) } ================================================ FILE: buildSrc/build.gradle.kts ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ repositories { mavenCentral() } plugins { `kotlin-dsl` } ================================================ FILE: buildSrc/src/main/java/com/facebook/fresco/buildsrc/FrescoConfig.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ object FrescoConfig { const val buildToolsVersion = "34.0.0" const val compileSdkVersion = 34 const val minSdkVersion = 21 const val flipperPluginMinSdkVersion = 21 const val vitoLithoMinSdkVersion = 21 const val samplesMinSdkVersion = 21 const val targetSdkVersion = 34 } ================================================ FILE: buildSrc/src/main/java/com/facebook/fresco/buildsrc/GradleDeps.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.buildsrc object GradleDeps { object Android { private const val version = "8.5.2" const val gradlePlugin = "com.android.tools.build:gradle:$version" } object Kotlin { const val version = "2.0.0" const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$version" const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib:$version" } object Native { const val version = "27.1.12297006" } object Publishing { const val gradleMavenPublishPlugin = "com.vanniktech:gradle-maven-publish-plugin:0.25.3" } } ================================================ FILE: buildSrc/src/main/java/com/facebook/fresco/buildsrc/TestDeps.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.buildsrc object TestDeps { const val assertjCore = "org.assertj:assertj-core:2.9.1" const val junit = "junit:junit:4.12" const val mockitoCore = "org.mockito:mockito-core:2.28.2" const val mockitoInline = "org.mockito:mockito-inline:2.28.2" const val mockitoKotlin = "org.mockito.kotlin:mockito-kotlin:2.2.11" const val mockitoCore3 = "org.mockito:mockito-core:3.12.4" const val mockitoInline3 = "org.mockito:mockito-inline:3.12.4" const val mockitoKotlin3 = "org.mockito.kotlin:mockito-kotlin:3.1.0" const val festAssertCore = "org.easytesting:fest-assert-core:2.0M10" const val robolectric = "org.robolectric:robolectric:4.12.2" const val truth = "com.google.truth:truth:1.0.1" object AndroidX { const val espressoCore = "androidx.test.espresso:espresso-core:3.1.1" const val testRules = "androidx.test:rules:1.1.1" const val testRunner = "androidx.test:runner:1.1.1" } } ================================================ FILE: buildSrc/src/main/java/com/facebook/fresco/buildsrc/dependencies-samples.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.buildsrc object SampleDeps { object AndroidX { const val appcompat = "androidx.appcompat:appcompat:1.0.2" const val cardview = "androidx.cardview:cardview:1.0.0" const val multidex = "androidx.multidex:multidex:2.0.1" const val preference = "androidx.preference:preference:1.0.0" const val recyclerview = "androidx.recyclerview:recyclerview:1.0.0" } object Google { const val material = "com.google.android.material:material:1.1.0-alpha03" } object Comparison { object Glide { private const val version = "4.9.0" const val glide = "com.github.bumptech.glide:glide:$version" const val compiler = "com.github.bumptech.glide:compiler:$version" } object Uil { private const val version = "1.9.5" const val uil = "com.nostra13.universalimageloader:universal-image-loader:$version" } object Picasso { private const val version = "2.71828" const val picasso = "com.squareup.picasso:picasso:$version" const val okhttpDownloader = "com.jakewharton.picasso:picasso2-okhttp3-downloader:1.0.2" } object AndroidQuery { private const val version = "0.25.9" const val aquery = "com.googlecode.android-query:android-query:$version" } } object Showcase { const val caverockSvg = "com.caverock:androidsvg-aar:1.4" } object Zoomable { const val legacyAndroidXSupportCoreUi = "androidx.legacy:legacy-support-core-ui:1.0.0" } } ================================================ FILE: buildSrc/src/main/java/com/facebook/fresco/buildsrc/dependencies.kt ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.fresco.buildsrc object Deps { const val javaxAnnotation = "javax.annotation:javax.annotation-api:1.3.2" const val jsr305 = "com.google.code.findbugs:jsr305:3.0.2" const val inferAnnotation = "com.facebook.infer.annotation:infer-annotation:0.18.0" const val okhttp3 = "com.squareup.okhttp3:okhttp:3.14.9" const val volley = "com.android.volley:volley:1.2.1" object AndroidX { const val androidxAnnotation = "androidx.annotation:annotation:1.6.0" const val core = "androidx.core:core:1.13.1" const val exifInterface = "androidx.exifinterface:exifinterface:1.3.7" const val legacySupportCoreUtils = "androidx.legacy:legacy-support-core-utils:1.0.0" } object Bolts { const val tasks = "com.parse.bolts:bolts-tasks:1.4.0" } object Kotlin { const val version = GradleDeps.Kotlin.version const val stdlibJdk = "org.jetbrains.kotlin:kotlin-stdlib:$version" } object Litho { private const val version = "0.50.1" const val core = "com.facebook.litho:litho-core:$version" const val lithoAnnotations = "com.facebook.litho:litho-annotations:$version" const val processor = "com.facebook.litho:litho-processor:$version" const val widget = "com.facebook.litho:litho-widget:$version" object Sections { const val core = "com.facebook.litho:litho-sections-core:$version" const val processor = "com.facebook.litho:litho-sections-processor:$version" const val sectionsAnnotations = "com.facebook.litho:litho-sections-annotations:$version" const val widget = "com.facebook.litho:litho-sections-widget:$version" } } object SoLoader { private const val version = "0.11.0" const val soloaderAnnotation = "com.facebook.soloader:annotation:$version" const val nativeloader = "com.facebook.soloader:nativeloader:$version" const val soloader = "com.facebook.soloader:soloader:$version" } object Tools { object Flipper { private const val version = "0.183.0" const val flipper = "com.facebook.flipper:flipper:$version" } object Stetho { private const val version = "1.6.0" const val stetho = "com.facebook.stetho:stetho:$version" const val okhttp3 = "com.facebook.stetho:stetho-okhttp3:$version" } } } ================================================ FILE: ci/build-and-test.sh ================================================ #!/bin/bash # Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. if [[ $1 == "--local" || $1 == "-l" ]]; then # Local build with less verbose output ./gradlew test assembleDebug assembleDebugAndroidTest else # Standard CI build command with maximum verbose output ./gradlew test assembleDebug assembleDebugAndroidTest --info fi ================================================ FILE: ci/print-debug-info.sh ================================================ #!/bin/bash # Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. echo "" echo "Fresco CI Debug information begin" echo "" echo "env PATH: $PATH" echo "" echo "env ANDROID_NDK_HOME: $ANDROID_NDK_HOME" echo "" echo "Fresco CI Debug information end" ================================================ FILE: docs/.gitignore ================================================ .DS_STORE _site/ *-e Gemfile.lock *.swo *.swp .gradle .DS_Store .idea build/ local.properties obj/ *.iml nativedeps/ .sass-cache/ .jekyll-metadata ================================================ FILE: docs/CNAME ================================================ frescolib.org ================================================ FILE: docs/Gemfile ================================================ source 'https://rubygems.org' gem 'github-pages', '~> 145', group: :jekyll_plugins ================================================ FILE: docs/NOGREP ================================================ ================================================ FILE: docs/README.md ================================================ # User Documentation for frescolib.org This folder contains the user and feature documentation for Fresco. ### Run the Site Locally 1. Make sure you have Ruby and [RubyGems](https://rubygems.org/) installed 2. Make sure you have [Jekyll](https://jekyllrb.com/) installed gem install jekyll 3. Run Jekyll's server jekyll serve 4. The site will be served from http://localhost:4000 ================================================ FILE: docs/_config.yml ================================================ --- permalink: /blog/:categories/:year/:month/:day/:title.html url: "https://frescolib.org" baseurl: "" title: Fresco tagline: An Image Management Library fbappid: "1615782811974223" gacode: "UA-44373548-3" relative_permalinks: false description: > Fresco is a powerful system for displaying images in Android applications. It takes care of image loading and display so you don’t have to. Fresco supports Android 2.3 (Gingerbread) and later. timezone: America/Los_Angeles ghrepo: "facebook/fresco" logo: /static/logo.png current_version: 3.6.0 support_library_version: 24.2.1 markdown: kramdown kramdown: input: GFM syntax_highlighter: rouge syntax_highlighter_opts: css_class: 'rougeHighlight' span: line_numbers: false block: line_numbers: true start_line: 1 sass: style: :compressed collections: docs: output: true permalink: /docs/:name/ support: output: true permalink: /support/ color: primary: "#db6130" secondary: "#f9f9f9" # Use the following to specify whether the previous two colours are 'light' # or 'dark' primary-overlay: "dark" secondary-overlay: "light" # Gems gems: - jekyll-feed - jekyll-seo-tag - jekyll-sitemap - jekyll-redirect-from # Set default open graph image for all pages defaults: - scope: path: "" values: image: /static/og_image.png ================================================ FILE: docs/_data/authors.yml ================================================ balazsbalazs: full_name: Balazs Balazs fbid: 100000563635135 kirwanlyster: full_name: Kirwan Lyster fbid: 688556227 oprisnik: full_name: Alexander Oprisnik fbid: 604555002 tnicholas: full_name: Tyrone Nicholas fbid: 622549419 maxcarli: full_name: Massimo Carli fbid: 1218930908 jiew: full_name: Jie Wang fbid: 100008065290380 ================================================ FILE: docs/_data/nav.yml ================================================ - title: Docs href: /docs/ category: docs - title: Support href: /support.html category: support - title: API href: /javadoc/ category: apidocs - title: GitHub href: https://github.com/facebook/fresco category: github - title: 中文版 href: http://fresco-cn.org/ category: github ================================================ FILE: docs/_data/nav_docs.yml ================================================ - title: Getting Started items: - id: index - id: proguard - title: Basic Features items: - id: using-simpledraweeview - id: rounded-corners-and-circles - id: progress-bars - id: scaletypes - id: placeholder-failure-retry - id: rotation - id: resizing - id: supported-uris - title: Advanced Topics items: - id: caching - id: closeable-references - id: configure-image-pipeline - id: datasources-datasubscribers - id: image-requests - id: images-in-notifications - id: listening-to-events - id: prefetching - id: post-processor - id: requesting-multiple-images - id: shared-transitions - id: using-controllerbuilder - id: using-image-pipeline - id: using-other-network-layers - id: writing-custom-views - title: Image formats items: - id: progressive-jpegs - id: animations - id: webp-support - id: customizing-image-formats - title: Troubleshooting & FAQ items: - id: faq - id: troubleshooting - id: gotchas - title: Contributors Guide items: - id: building-from-source - id: sample-code - id: concepts - id: drawee-branches - id: intro-image-pipeline ================================================ FILE: docs/_data/powered_by.yml ================================================ - title: Apps Using Fresco items: - name: TamTam Messenger url: https://play.google.com/store/apps/details?id=ru.ok.messages - name: Frost Blur url: https://play.google.com/store/apps/details?id=com.rvnd.frostblur - name: Book Share url: https://play.google.com/store/apps/details?id=com.rvnd.bookshare - name: Vimeo url: https://play.google.com/store/apps/details?id=com.vimeo.android.videoapp - name: Best Apps Market url: https://play.google.com/store/apps/details?id=com.bestappsmarket.android.bestapps - name: Redfin url: https://play.google.com/store/apps/details?id=com.redfin.android - name: Memrise url: https://play.google.com/store/apps/details?id=com.memrise.android.memrisecompanion - name: local.ch url: https://play.google.com/store/apps/details?id=ch.local.android - name: Mappy url: https://play.google.com/store/apps/details?id=com.mappy.app - name: YOP url: https://play.google.com/store/apps/details?id=com.yopapp.yop - name: ChatGame url: https://play.google.com/store/apps/details?id=me.chatgame.mobilecg - name: Bobble url: https://play.google.com/store/apps/details?id=com.touchtalent.bobbleapp - name: ShareTheMeal url: https://play.google.com/store/apps/details?id=org.sharethemeal.app - name: NativeScript Apps url: https://www.nativescript.org/ - name: TouTiao url: https://play.google.com/store/apps/details?id=com.ss.android.article.news - name: Playbuzz url: https://play.google.com/store/apps/details?id=com.playbuzz.android.app - name: Camerite url: https://play.google.com/store/apps/details?id=com.camerite - name: nice url: https://play.google.com/store/apps/details?id=com.nice.main - name: JokeEssay url: https://play.google.com/store/apps/details?id=com.ss.android.essay.joke - name: Phonotto url: https://play.google.com/store/apps/details?id=com.duckma.phonotto - name: Bakar url: https://play.google.com/store/apps/details?id=com.bakar - name: LaMaille url: https://play.google.com/store/apps/details?id=net.opalesurfcasting.lamaille - name: Poke url: https://play.google.com/store/apps/details?id=com.netpub.poke - name: SamePinch url: https://play.google.com/store/apps/details?id=co.samepinch.android.app - name: ChacuaTool url: https://play.google.com/store/apps/details?id=com.dak42.chacuatool - name: Facebook for Android url: https://play.google.com/store/apps/details?id=com.facebook.katana - name: Facebook Messenger url: https://play.google.com/store/apps/details?id=com.facebook.orca - name: Facebook Moments url: https://play.google.com/store/apps/details?id=com.facebook.moments - name: Facebook Pages Manager url: https://play.google.com/store/apps/details?id=com.facebook.pages.app - name: Facebook Groups url: https://play.google.com/store/apps/details?id=com.facebook.groups - name: Facebook Ads Manager url: https://play.google.com/store/apps/details?id=com.facebook.adsmanager - name: Snapster url: https://play.google.com/store/apps/details?id=com.vk.snapster - name: Butter Camera (黄油相机) url: https://play.google.com/store/apps/details?id=com.by.butter.camera - name: Myntra url: https://play.google.com/store/apps/details?id=com.myntra.android - name: Photo Safe url: https://play.google.com/store/apps/details?id=com.appmattus.photobackup - name: Tango Messenger url: https://play.google.com/store/apps/details?id=com.sgiggle.production - name: CondomCraze url: https://play.google.com/store/apps/details?id=com.condomcraze.android - name: memeham url: https://play.google.com/store/apps/details?id=com.memeham.beyourself.memeham - name: 9GAG url: https://play.google.com/store/apps/details?id=com.ninegag.android.app - name: Xiami Music url: https://play.google.com/store/apps/details?id=fm.xiami.main - name: Zhihu url: https://play.google.com/store/apps/details?id=com.zhihu.android - name: GigTown url: https://play.google.com/store/apps/details?id=com.gigtown.fender - name: Pics for Reddit url: https://play.google.com/store/apps/details?id=com.sitonmylab.picsforreddit - name: Apartment Buddy url: https://play.google.com/store/apps/details?id=com.apartmentbuddy.android - name: Wikimedia Commons url: https://play.google.com/store/apps/details?id=fr.free.nrw.commons - name: Waldo Photos url: https://play.google.com/store/apps/details?id=com.waldophotos.android - name: Degoo - Cloud Photo Storage url: https://play.google.com/store/apps/details?id=com.degoo.android - name: ROMEO - Gay Chat & Dating url: https://play.google.com/store/apps/details?id=com.planetromeo.android.app ================================================ FILE: docs/_data/powered_by_highlight.yml ================================================ - title: Apps Using Fresco items: - name: Twitter url: https://play.google.com/store/apps/details?id=com.twitter.android img: static/images/twitter.png - name: Wikipedia url: https://play.google.com/store/apps/details?id=org.wikipedia img: static/images/wikipedia.png - name: Facebook url: https://code.facebook.com/posts/366199913563917 img: static/images/facebook.png - name: React Native url: https://facebook.github.io/react-native/ img: static/images/reactnative.png ================================================ FILE: docs/_data/promo.yml ================================================ - type: plugin_row children: - type: button href: docs/index.html text: Get Started - type: github_star ================================================ FILE: docs/_docs/03-customizing-image-formats.md ================================================ --- docid: customizing-image-formats title: Customizing Image Formats layout: docs permalink: /docs/customizing-image-formats.html --- In general, two parts are involved until an image can be displayed on screen: 1. decoding the image 2. rendering the decoded image Fresco allows you to customize both of these parts. For example, it's possible to add a custom image decoder for an existing image format or for a new image format that uses Fresco's built-in rendering architecture to render bitmaps. Or, it's possible to let the built-in decoder handle decoding and then create a custom Drawable used to render the image on screen. And, of course, you can also do both. These customizations can be either registered globally when Fresco is initialized or locally for selected images only. The (much simplified) decoding and rendering process looks like this: 1. The encoded image is downloaded from the network or loaded from the disk cache. 2. The `ImageFormat` of the `EncodedImage` is determined using a class called `ImageFormatChecker`, which has a list of `ImageFormat.FormatChecker` objects, one for each recognized image format. 3. The `EncodedImage` is decoded using a suitable `ImageDecoder` for the given format and returns an object that extends `CloseableImage`, which represents the decoded image. 4. From a list of `DrawableFactory` objects, the first one that is able to handle the `CloseableImage` is used to create a `Drawable`. 5. The drawable is rendered on screen. It is possible to add custom image formats by adding an `ImageFormat.FormatChecker` for step 2. You can supply custom `ImageDecoder`s to add decoding support for new image formats or override built-in decoding. Finally, you can supply a custom `DrawableFactory` to use a custom `Drawable` for rendering the image. All default image formats can be found in `DefaultImageFormats` and `DefaultImageFormatChecker`, the default drawable factory is in `PipelineDraweeController` and several samples for customizing them can be found in the Showcase sample app. ## Custom decoders Let's start with an example. In order to create a custom decoder, simply implement the `ImageDecoder` interface: ```java public class CustomDecoder implements ImageDecoder { @Override public CloseableImage decode( EncodedImage encodedImage, int length, QualityInfo qualityInfo, ImageDecodeOptions options) { // Decode the given encodedImage and return a // corresponding (decoded) CloseableImage. CloseableImage closeableImage = ...; return closeableImage; } } ``` The given encoded image can be used to return a class that extends `CloseableImage`, which represents the decoded image and which will then be automatically cached for you. You can either return one of the existing `CloseableImage` types, like `CloseableStaticBitmap` for bitmaps, or define your own `CloseableImage` class. Custom decoders can be set globally or locally on a per-image basis. For local overrides, you can set the custom decoder as follows: ```java ImageDecoder customDecoder = ...; Uri uri = ...; draweeView.setController( Fresco.newDraweeControllerBuilder() .setImageRequest( ImageRequestBuilder.newBuilderWithSource(uri) .setImageDecodeOptions( ImageDecodeOptions.newBuilder() .setCustomImageDecoder(customDecoder) .build()) .build()) .build()); ``` **NOTE:** If you're supplying a custom decoder, it will be used for all images. The default decoder will be completely bypassed. ## Custom image formats You simply create a new `ImageFormat` object and hold on to it in your code: ```java private static final ImageFormat CUSTOM_FORMAT = new ImageFormat("format name", "format file extension"); ``` All supported default image formats can be found in `DefaultImageFormats`. Then, we need to create a custom `ImageFormat.FormatChecker` that is used to detect your new image format. The format checker has 2 methods, one to determine the number of header bytes required to make the decision (keep this number as small as possible since this operation is performed for all images) and the actual `determineFormat` method, which should return **the same `ImageFormat` instance**, `CUSTOM_FORMAT` in this example - or `null` if the image is of a different format. A simple format checker could look like this: ```java public static class ColorFormatChecker implements ImageFormat.FormatChecker { private static final byte[] HEADER = ImageFormatCheckerUtils.asciiBytes("my_header"); @Override public int getHeaderSize() { return HEADER.length; } @Nullable @Override public ImageFormat determineFormat(byte[] headerBytes, int headerSize) { if (headerSize < getHeaderSize()) { return null; } if (ImageFormatCheckerUtils.startsWithPattern(headerBytes, HEADER)) { return CUSTOM_FORMAT; } return null; } } ``` The third component required for custom image format is a custom decoder as explained above that can create the actual decoded image. You have to register your custom image format with Fresco by supplying a `ImageDecoderConfig` to Fresco when it is initialized. Similarly, you can override the default decoding behavior by using a built-in image format: ```java ImageFormat myFormat = ...; ImageFormat.FormatChecker myFormatChecker = ...; ImageDecoder myDecoder = ...; ImageDecoderConfig imageDecoderConfig = new ImageDecoderConfig.Builder() .addDecodingCapability( myFormat, myFormatChecker, myDecoder) .build(); ImagePipelineConfig config = ImagePipelineConfig.newBuilder() .setImageDecoderConfig(imageDecoderConfig) .build(); Fresco.initialize(context, config); ``` ## Custom drawables If a `DraweeController` is used to load the image (e.g. if you're using a `DraweeView`), a corresponding `DrawableFactory` is used to create a drawable to render the decoded image based on the `CloseableImage`. If you're manually using the image pipeline, you have to handle the `CloseableImage` itself. If you use one of the built-in types, like `CloseableStaticBitmap`, the `PipelineDraweeController` already knows how to handle the format and will create a `BitmapDrawable` for you. If you want to override that behavior or add support for custom `CloseableImage`s, you have to implement a drawable factory: ```java public static class CustomDrawableFactory implements DrawableFactory { @Override public boolean supportsImageType(CloseableImage image) { // You can either override a built-in format, like `CloseableStaticBitmap` // or your own implementations. return image instanceof CustomCloseableImage; } @Nullable @Override public Drawable createDrawable(CloseableImage image) { // Create and return your custom drawable for the given CloseableImage. // It is guaranteed that the `CloseableImage` is an instance of the // declared classes in `supportsImageType` above. CustomCloseableImage myCloseableImage = (CustomCloseableImage) image; Drawable myDrawable = ...; //e.g. new CustomDrawable(myCloseableImage) return myDrawable; } } ``` In order to use your drawable factory, you can either use a global or local override. ### Global custom drawable override You have to register all global drawable factories when Fresco is initialized: ```java DrawableFactory myDrawableFactory = ...; DraweeConfig draweeConfig = DraweeConfig.newBuilder() .addCustomDrawableFactory(myDrawableFactory) .build(); Fresco.initialize(this, imagePipelineConfig, draweeConfig); ``` ### Local custom drawable override For local overrides, the `PipelineDraweeControllerBuilder` offers methods to set custom drawable factories: ```java DrawableFactory myDrawableFactory = ...; Uri uri = ...; simpleDraweeView.setController(Fresco.newDraweeControllerBuilder() .setUri(uri) .setCustomDrawableFactory(factory) .build()); ``` ================================================ FILE: docs/_docs/animations.md ================================================ --- docid: animations title: Animated Images layout: docs permalink: /docs/animations.html --- Fresco supports animated GIF and WebP images. We support WebP animations, even in the extended WebP format, on versions of Android going back to 2.3, even those that don't have built-in native support. For adding this optional modules in your build.gradle please visit [here](index.html): ### Playing animations automatically If you want your animated image to start playing automatically when it comes on-screen, and stop when it goes off, just say so in your [image request](image-requests.html): ```java Uri uri; DraweeController controller = Fresco.newDraweeControllerBuilder() .setUri(uri) .setAutoPlayAnimations(true) . // other setters .build(); mSimpleDraweeView.setController(controller); ``` ### Playing animations manually You may prefer to directly control the animation in your own code. In that case you'll need to listen for when the image has loaded, so it's even possible to do that. ```java ControllerListener controllerListener = new BaseControllerListener() { @Override public void onFinalImageSet( String id, @Nullable ImageInfo imageInfo, @Nullable Animatable anim) { if (anim != null) { // app-specific logic to enable animation starting anim.start(); } } }; Uri uri; DraweeController controller = Fresco.newDraweeControllerBuilder() .setUri(uri) .setControllerListener(controllerListener) // other setters .build(); mSimpleDraweeView.setController(controller); ``` The controller exposes an instance of the [Animatable](http://developer.android.com/reference/android/graphics/drawable/Animatable.html) interface. If non-null, you can drive your animation with it: ```java Animatable animatable = mSimpleDraweeView.getController().getAnimatable(); if (animatable != null) { animatable.start(); // later animatable.stop(); } ``` ### Limitations Animations do not currently support [postprocessors](modifying-image.html). ================================================ FILE: docs/_docs/building-from-source.md ================================================ --- docid: building-from-source title: Building from Source layout: docs permalink: /docs/building-from-source.html --- You should only build from source if you need to modify Fresco code itself. Most applications should simply [include](index.html#_) Fresco in their project. ### Prerequisites The following tools must be installed on your system in order to build Fresco: 1. The Android [SDK](https://developer.android.com/sdk/index.html#Other) 2. From the Android SDK Manager, install/upgrade the latest Support Library **and** Support Repository. Both are found in the Extras section. 2. The Android [NDK](https://developer.android.com/tools/sdk/ndk/index.html). Version 10c or later is required. 3. The [git](http://git-scm.com/) version control system. You don't need to download Gradle itself; the build scripts or Android Studio will do that for you. Fresco does not support source builds with Eclipse, Ant, or Maven. We do not plan to ever add such support. ### Configuring Gradle Both command-line and Android Studio users need to edit the `gradle.properties` file. This is normally located in your home directory, in a subdirectory called `.gradle`. If it is not already there, create it. On Unix-like systems, including Mac OS X, add this line: ```groovy ndk.path=/path/to/android_ndk/r10e ``` On Windows systems, add this line: ```groovy ndk.path=C\:\\path\\to\\android_ndk\\r10e ``` On *both* platforms, add these lines: ```groovy org.gradle.daemon=true org.gradle.parallel=true org.gradle.configureondemand=true ``` Windows' backslashes and colons need to be escaped in order for Gradle to read them correctly. ### Getting the source ```sh git clone https://github.com/facebook/fresco.git ``` This will create a directory `fresco` where the code will live. ### Building from the Command Line On Unix-like systems, `cd` to the directory containing Fresco. Run the following command: ```sh ./gradlew build ``` On Windows, open a Command Prompt, `cd` to the directory containing Fresco, and type in this command: ```bat gradlew.bat build ``` ### Building from Android Studio From Android Studio's Quick Start dialog, click Import Project. Navigate to the directory containing Fresco and click on the `build.gradle` file. Android Studio should build Fresco automatically. ### Offline builds The first time you build Fresco, your computer must be connected to the Internet. Incremental builds can use Gradle's `--offline` option. ### Troubleshooting > Could not find com.android.support:...:x.x.x. Make sure your Support Repository is up to date (see Prerequisites above). ### Windows support We try our best to support building on Windows but we can't commit to it. We do not have a Windows build set up on our CI servers and none of us is using a Windows computer so the builds can break without us noticing it. Please raise github issues if the Windows build is broken or submit a pull request with the fix. We do our best but we'd like the community's help to keep this up to date. ### Contributing code upstream Please see our [CONTRIBUTING](https://github.com/facebook/fresco/blob/main/CONTRIBUTING.md) page. ================================================ FILE: docs/_docs/caching.md ================================================ --- docid: caching title: Caching layout: docs permalink: /docs/caching.html --- Fresco stores images in three different types of caches, organized hierarchically, with the cost of retrieving an image increasing the deeper you go. #### 1. Bitmap cache The bitmap cache stores decoded images as Android `Bitmap` objects. These are ready for display or [postprocessing](modifying-image.html). On Android 4.x and lower, the bitmap cache's data lives in the *ashmem* heap, not in the Java heap. This means that images don't force extra runs of the garbage collector, slowing down your app. Android 5.0 and newer has much improved memory management than earlier versions, so it is safer to leave the bitmap cache on the Java heap. Your app should [clear this cache](#clearing-the-cache) when it is backgrounded. For non-static image formats or custom image formats, the Bitmap cache can hold any decoded image data by extending the `CloseableImage` class. See [customizing image formats](customizing-image-formats.html) for more details. #### 2. Encoded memory cache This cache stores images in their original compressed form. Images retrieved from this cache must be decoded before display. If other transformations, such as [resizing](resizing.html), [rotation](rotation.html) or [transcoding](#webp) were requested, that happens before decode. #### 3. Disk cache (Yes, we know phones don't have disks, but it's too tedious to keep saying *local storage cache*...) Like the encoded memory cache, this cache stores compressed image, which must be decoded and sometimes transformed before display. Unlike the others, this cache is not cleared when your app exits, or even if the device is turned off. When disk cache is about to be to the size limits defined by [DiskCacheConfig](configure-image-pipeline.html#configuring-the-disk-cache) Fresco uses LRU logic of eviction in disk cache (see [DefaultEntryEvictionComparatorSupplier.java](https://github.com/facebook/fresco/blob/main/imagepipeline-base/src/main/java/com/facebook/cache/disk/DefaultEntryEvictionComparatorSupplier.java)). The user can, of course, always clear it from Android's Settings menu. ### Checking to see if an item is in cache You can use the methods in [ImagePipeline](../javadoc/reference/com/facebook/imagepipeline/core/ImagePipeline.html) to see if an item is in cache. The check for the memory cache is synchronous: ```java ImagePipeline imagePipeline = Fresco.getImagePipeline(); Uri uri; boolean inMemoryCache = imagePipeline.isInBitmapMemoryCache(uri); ``` The check for the disk cache is asynchronous, since the disk check must be done on another thread. You can call it like this: ```java DataSource inDiskCacheSource = imagePipeline.isInDiskCache(uri); DataSubscriber subscriber = new BaseDataSubscriber() { @Override protected void onNewResultImpl(DataSource dataSource) { if (!dataSource.isFinished()) { return; } boolean isInCache = dataSource.getResult(); // your code here } }; inDiskCacheSource.subscribe(subscriber, executor); ``` This assumes you are using the default cache key factory. If you have configured a custom one, you may need to use the methods that take an `ImageRequest` argument instead. ### Evicting from cache [ImagePipeline](../javadoc/reference/com/facebook/imagepipeline/core/ImagePipeline.html) also has methods to evict individual entries from cache: ```java ImagePipeline imagePipeline = Fresco.getImagePipeline(); Uri uri; imagePipeline.evictFromMemoryCache(uri); imagePipeline.evictFromDiskCache(uri); // combines above two lines imagePipeline.evictFromCache(uri); ``` As above, `evictFromDiskCache(Uri)` assumes you are using the default cache key factory. Users with a custom factory should use `evictFromDiskCache(ImageRequest)` instead. ### Clearing the cache ```java ImagePipeline imagePipeline = Fresco.getImagePipeline(); imagePipeline.clearMemoryCaches(); imagePipeline.clearDiskCaches(); // combines above two lines imagePipeline.clearCaches(); ``` ### Using one disk cache or two? Most apps need only a single disk cache. But in some circumstances you may want to keep smaller images in a separate cache, to prevent them from getting evicted too soon by larger images. To do this, just call both `setMainDiskCacheConfig` and `setSmallImageDiskCacheConfig` methods when [configuring the image pipeline](configure-image-pipeline.html). What defines *small?* Your app does. When [making an image request](image-requests.html), you set its [CacheChoice](../javadoc/reference/com/facebook/imagepipeline/request/ImageRequest.CacheChoice.html): ```java ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri) .setCacheChoice(ImageRequest.CacheChoice.SMALL) ``` If you need only one cache, you can simply avoid calling `setSmallImageDiskCacheConfig`. The pipeline will default to using the same cache for both and `CacheChoice` will be ignored. ### Trimming the caches When [configuring](configure-image-pipeline.html) the image pipeline, you can set the maximum size of each of the caches. But there are times when you might want to go lower than that. For instance, your application might have caches for other kinds of data that might need more space and crowd out Fresco's. Or you might be checking to see if the device as a whole is running out of storage space. Fresco's caches implement the [DiskTrimmable](../javadoc/reference/com/facebook/common/disk/DiskTrimmable.html) or [MemoryTrimmable](../javadoc/reference/com/facebook/common/memory/MemoryTrimmable.html) interfaces. These are hooks into which your app can tell them to do emergency evictions. Your application can then configure the pipeline with objects implementing the [DiskTrimmableRegistry](../javadoc/reference/com/facebook/common/disk/DiskTrimmableRegistry.html) and [MemoryTrimmableRegistry](../javadoc/reference/com/facebook/common/memory/MemoryTrimmableRegistry.html) interfaces. These objects must keep a list of trimmables. They must use app-specific logic to determine when memory or disk space must be preserved. They then notify the trimmable objects to carry out their trims. ================================================ FILE: docs/_docs/closeable-references.md ================================================ --- docid: closeable-references title: Closeable References layout: docs permalink: /docs/closeable-references.html --- **This page is intended for advanced usage only.** Most apps should use [Drawees](using-simpledraweeview.html) and not worry about closing. The Java language is garbage-collected and most developers are used to creating objects willy-nilly and taking it for granted they will eventually disappear from memory. Until Android 5.0's improvements, this was not at all a good idea for Bitmaps. They take up a large share of the memory of a mobile device. Their existence in memory would make the garbage collector run more frequently, making image-heavy apps slow and janky. Bitmaps were the one thing that makes Java developers miss C++ and its many smart pointer libraries, such as [Boost](http://www.boost.org/doc/libs/1_57_0/libs/smart_ptr/smart_ptr.htm). Fresco's solution is found in the [CloseableReference](../javadoc/reference/com/facebook/common/references/CloseableReference.html) class. In order to use it correctly, you must follow the rules below: #### 1. The caller owns the reference. Here, we create a reference, but since we're passing it to the caller, the caller takes the ownership: ```java CloseableReference foo() { Val val; // We are returning the reference from this method, // so whoever is calling this method is the owner // of the reference and is in charge of closing it. return CloseableReference.of(val); } ``` #### 2. The owner must close the reference before leaving scope. Here we create a reference, but are not passing it to a caller. So we must close it: ```java void gee() { // We are the caller of `foo` and so // we own the returned reference. CloseableReference ref = foo(); try { // `haa` is a callee and not a caller, and so // it is NOT the owner of this reference, and // it must NOT close it. haa(ref); } finally { // We are not returning the reference to the // caller of this method, so we are still the owner, // and must close it before leaving the scope. ref.close(); } } ``` The `finally` block is almost always the best way to do this. #### 3. **Never** close the value. `CloseableReference` wraps a shared resource which gets released when there are no more active references pointing to it. Tracking of active references is done automatically by an internal reference counter. When the reference count drops to 0, `CloseableReference` will release the underlying resource. The very purpose of `CloseableReference` is to manage the underlying resource so that you don't have to. That said, you are responsible for closing the `CloseableReference` if you own it, but **not** the value it points to! If you explicitly close the underlying value, you will erroneously invalidate all the other active references pointing to that same resource. ```java CloseableReference ref = foo(); Val val = ref.get(); // do something with val // ... // Do NOT close the value! //// val.close(); // DO close the reference instead. ref.close(); ``` #### 4. Something other than the owner should *not* close the reference. Here, we are receiving the reference via argument. The caller is still the owner, so we are not supposed to close it. ```java void haa(CloseableReference ref) { // We are callee, and not a caller, and so // we must NOT close the reference. // We are guaranteed that the reference won't // become invalid for the duration of this call. Log.println("Haa: " + ref.get()); } ``` If we called `.close()` here by mistake, then if the caller tried to call `.get()`, an `IllegalStateException` would be thrown. #### 5. Callee should always clone the reference before assigning. If we need to hold onto the reference, we need to clone it. If using it in a class: ```java class MyClass { CloseableReference myValRef; void mmm(CloseableReference ref) { // Some caller called this method. Caller owns the original // reference and if we want to have our own copy, we must clone it. myValRef = ref.clone(); }; // caller can now safely close its copy as we made our own clone. void close() { // We are in charge of closing our copy, of course. CloseableReference.closeSafely(myValRef); } } // Now the caller of MyClass must close it! ``` If using it in an inner class: ```java void haa(CloseableReference ref) { // Here we make our own copy of the original reference, // so that we can guarantee its validity when the executor // executes our runnable in the future. final CloseableReference refClone = ref.clone(); executor.submit(new Runnable() { public void run() { try { Log.println("Haa Async: " + refClone.get()); } finally { // We need to close our copy once we are done with it. refClone.close(); } } }); // caller can now safely close its copy as we made our own clone. } ``` ================================================ FILE: docs/_docs/concepts.md ================================================ --- docid: concepts title: Concepts layout: docs permalink: /docs/concepts.html --- ## Drawees Drawees are spaces in which images are rendered. These are made up of three components, like a Model-View-Controller (MVC) framework. ### DraweeView Descended from the Android [View](http://developer.android.com/reference/android/view/View.html) class. Most apps should use the `SimpleDraweeView` class. Place these in your application using XML or Java code. Set the URI to load with the `setImageURI` method, as explained in the [Getting Started](index.html) page. See [Using SimpleDraweeView](using-simpledraweeview.html). ### DraweeHierarchy This is the hierarchy of Android [Drawable](http://developer.android.com/reference/android/graphics/drawable/Drawable.html) objects that will actually render your content. Think of it as the Model in an MVC. See [Using SimpleDraweeView](using-simpledraweeview.html). ### DraweeController The `DraweeController` is the class responsible for actually dealing with the underlying image loader - whether Fresco's own image pipeline, or another. If you need something more than a single URI to specify the image you want to display, you will need an instance of this class. ### DraweeControllerBuilder `DraweeControllers` are immutable once constructed. They are [built](using-controllerbuilder.html) using the Builder pattern. ### Listeners One use of a builder is to specify a [Listener](listening-to-events.html) to execute code upon the arrival, full or partial, of image data from the server. ## The Image Pipeline Behind the scenes, Fresco's image pipeline deals with the work done in getting an image. It fetches from the network, a local file, a content provider, or a local resource. It keeps a cache of compressed images on local storage, and a second cache of decompressed images in memory. The image pipeline uses a special technique called *pinned purgeables* to keep images off the Java heap. This requires callers to `close` images when they are done with them. `SimpleDraweeView` does this for you automatically, so should be your first choice. Very few apps need to use the image pipeline directly. ================================================ FILE: docs/_docs/configure-image-pipeline.md ================================================ --- docid: configure-image-pipeline title: Configuring the Image Pipeline layout: docs permalink: /docs/configure-image-pipeline.html --- Most apps can initialize Fresco completely by the simple command: ```java Fresco.initialize(context); ``` For those apps that need more advanced customization, we offer it using the [ImagePipelineConfig](../javadoc/reference/com/facebook/imagepipeline/core/ImagePipelineConfig.html) class. Here is a maximal example. Rare is the app that actually needs all of these settings, but here they are for reference. ```java ImagePipelineConfig config = ImagePipelineConfig.newBuilder(context) .setBitmapMemoryCacheParamsSupplier(bitmapCacheParamsSupplier) .setCacheKeyFactory(cacheKeyFactory) .setDownsampleEnabled(true) .setEncodedMemoryCacheParamsSupplier(encodedCacheParamsSupplier) .setExecutorSupplier(executorSupplier) .setImageCacheStatsTracker(imageCacheStatsTracker) .setMainDiskCacheConfig(mainDiskCacheConfig) .setMemoryTrimmableRegistry(memoryTrimmableRegistry) .setNetworkFetchProducer(networkFetchProducer) .setPoolFactory(poolFactory) .setProgressiveJpegConfig(progressiveJpegConfig) .setRequestListeners(requestListeners) .setSmallImageDiskCacheConfig(smallImageDiskCacheConfig) .build(); Fresco.initialize(context, config); ``` Be sure to pass your `ImagePipelineConfig` object to `Fresco.initialize!` Otherwise, Fresco will use a default configuration instead of the one you built. ### Understanding Suppliers Several of the configuration builder's methods take arguments of a [Supplier](../javadoc/reference/com/facebook/common/internal/Supplier.html) of an instance rather than an instance itself. This is a little more complex to create, but allows you to change behaviors while your app is running. Memory caches, for one, check their Supplier every five minutes. If you don't need to dynamically change the params, use a Supplier that returns the same object each time: ```java Supplier xSupplier = new Supplier() { private X mX = new X(xparam1, xparam2...); public X get() { return mX; } ); // when creating image pipeline .setXSupplier(xSupplier); ``` ### Thread pools By default, the image pipeline uses three thread pools: 1. Three threads for network downloads 2. Two threads for all disk operations - local file reads, and the disk cache 3. Two threads for all CPU-bound operations - decodes, transforms, and background operations, such as postprocessing. You can customize networking behavior by [setting your own network layer](using-other-network-layers.html). To change the behavior for all other operations, pass in an instance of [ExecutorSupplier](../javadoc/reference/com/facebook/imagepipeline/core/ExecutorSupplier.html). ### Using a MemoryTrimmableRegistry If your application listens to system memory events, it can pass them over to Fresco to trim memory caches. The easiest way for most apps to listen to events is to override [Activity.onTrimMemory](http://developer.android.com/reference/android/app/Activity.html#onTrimMemory(int)). You can also use any subclass of [ComponentCallbacks2](http://developer.android.com/reference/android/content/ComponentCallbacks2.html). You should have an implementation of [MemoryTrimmableRegistry](http://frescolib.org/javadoc/reference/com/facebook/common/memory/MemoryTrimmableRegistry.html). This object should keep a collection of [MemoryTrimmable](http://frescolib.org/javadoc/reference/com/facebook/common/memory/MemoryTrimmable.html) objects - Fresco's caches will be among them. When getting a system memory event, you call the appropriate `MemoryTrimmable` method on each of the trimmables. ### Configuring the memory caches The bitmap cache and the encoded memory cache are configured by a Supplier of a [MemoryCacheParams](../javadoc/reference/com/facebook/imagepipeline/cache/MemoryCacheParams.html#MemoryCacheParams\(int, int, int, int, int\)) object. ### Configuring the disk cache You use the builder pattern to create a [DiskCacheConfig](../javadoc/reference/com/facebook/cache/disk/DiskCacheConfig.Builder.html) object: ```java DiskCacheConfig diskCacheConfig = DiskCacheConfig.newBuilder() .set.... .set.... .build() // when building ImagePipelineConfig .setMainDiskCacheConfig(diskCacheConfig) ``` ### Keeping cache stats If you want to keep track of metrics like the cache hit rate, you can implement the [ImageCacheStatsTracker](../javadoc/reference/com/facebook/imagepipeline/cache/ImageCacheStatsTracker.html) class. This provides callbacks for every cache event that you can use to keep your own statistics. ================================================ FILE: docs/_docs/datasources-datasubscribers.md ================================================ --- docid: datasources-datasubscribers title: DataSources and DataSubscribers layout: docs permalink: /docs/datasources-datasubscribers.html --- A [DataSource](../javadoc/reference/com/facebook/datasource/DataSource.html) is, like a Java [Future](http://developer.android.com/reference/java/util/concurrent/Future.html), the result of an asynchronous computation. The different is that, unlike a Future, a DataSource can return you a whole series of results from a single command, not just one. After submitting an image request, the image pipeline returns a data source. To get a result out if it, you need to use a [DataSubscriber](../javadoc/reference/com/facebook/datasource/DataSubscriber.html). ### Executors When subscribing to a data source, an executor must be provided. The purpose of executors is to execute runnables (in our case the subscriber callback methods) on a specific thread and with specific policy. Fresco provides several [executors](https://github.com/facebook/fresco/tree/main/fbcore/src/main/java/com/facebook/common/executors) and one should carefully choose which one to be used: * If you need to do any UI stuff from your callback (accessing views, drawables, etc.), you must use `UiThreadImmediateExecutorService.getInstance()`. Android view system is not thread safe and is only to be accessed from the main thread (the UI thread). * If the callback is lightweight, and does not do any UI related stuff, you can simply use `CallerThreadExecutor.getInstance()`. This executor executes runnables on the caller's thread. Depending on what is the calling thread, callback may be executed either on the UI or a background thread. There are no guarantees which thread it is going to be and because of that this executor should be used with great caution. And again, only for lightweight non-UI related stuff. * If you need to do some expensive non-UI related work (database access, disk read/write, or any other slow operation), this should NOT be done either with `CallerThreadExecutor` nor with the `UiThreadExecutorService`, but with one of the background thread executors. See [DefaultExecutorSupplier.forBackgroundTasks](https://github.com/facebook/fresco/blob/main/imagepipeline-base/src/main/java/com/facebook/imagepipeline/core/DefaultExecutorSupplier.java) for an example implementation. ### Getting result from a data source This is a generic example of how to get a result from a data source of `CloseableReference` for arbitrary type `T`. The result is valid only in the scope of the `onNewResultImpl` callback. As soon as the callback gets executed, the result is no longer valid. See the next example if the result needs to be kept around. ```java DataSource> dataSource = ...; DataSubscriber> dataSubscriber = new BaseDataSubscriber>() { @Override protected void onNewResultImpl( DataSource> dataSource) { if (!dataSource.isFinished()) { // if we are not interested in the intermediate images, // we can just return here. return; } CloseableReference ref = dataSource.getResult(); if (ref != null) { try { // do something with the result T result = ref.get(); ... } finally { CloseableReference.closeSafely(ref); } } } @Override protected void onFailureImpl(DataSource> dataSource) { Throwable t = dataSource.getFailureCause(); // handle failure } }; dataSource.subscribe(dataSubscriber, executor); ``` ### Keeping result from a data source The above example closes the reference as soon as the callback gets executed. If the result needs to be kept around, you must keep the corresponding `CloseableReference` for as long as the result is needed. This can be done as follows: ```java DataSource> dataSource = ...; DataSubscriber> dataSubscriber = new BaseDataSubscriber>() { @Override protected void onNewResultImpl( DataSource> dataSource) { if (!dataSource.isFinished()) { // if we are not interested in the intermediate images, // we can just return here. return; } // keep the closeable reference mRef = dataSource.getResult(); // do something with the result T result = mRef.get(); ... } @Override protected void onFailureImpl(DataSource> dataSource) { Throwable t = dataSource.getFailureCause(); // handle failure } }; dataSource.subscribe(dataSubscriber, executor); ``` IMPORTANT: once you don't need the result anymore, you **must close the reference**. Not doing so may cause memory leaks. See [closeable references](closeable-references.html) for more details. ```java CloseableReference.closeSafely(mRef); mRef = null; ``` However, if you are using `BaseDataSubscriber` you do not have to manually close the `dataSource` (closing `mRef` is enough). `BaseDataSubscriber` automatically closes the `dataSource` for you right after `onNewResultImpl` is called. If you are not using `BaseDataSubscriber` (e.g. if you're calling `dataSource.getResult()`), make sure to close the `dataSource` as well. ### To get encoded image... ```java DataSource> dataSource = mImagePipeline.fetchEncodedImage(imageRequest, CALLER_CONTEXT); ``` Image pipeline uses `PooledByteBuffer` for encoded images. This is our `T` in the above examples. Here is an example of creating an `InputStream` out of `PooledByteBuffer` so that we can read the image bytes: ```java InputStream is = new PooledByteBufferInputStream(result); try { // Example: get the image format ImageFormat imageFormat = ImageFormatChecker.getImageFormat(is); // Example: write input stream to a file Files.copy(is, path); } catch (...) { ... } finally { Closeables.closeQuietly(is); } ``` ### To get decoded image... ```java DataSource> dataSource = imagePipeline.fetchDecodedImage(imageRequest, callerContext); ``` Image pipeline uses `CloseableImage` for decoded images. This is our `T` in the above examples. Here is an example of getting a `Bitmap` out of `CloseableImage`: ```java CloseableImage image = ref.get(); if (image instanceof CloseableBitmap) { // do something with the bitmap Bitmap bitmap = (CloseableBitmap image).getUnderlyingBitmap(); ... } ``` ### I just want a bitmap... If your request to the pipeline is for a single [Bitmap](http://developer.android.com/reference/android/graphics/Bitmap.html), you can take advantage of our easier-to-use [BaseBitmapDataSubscriber](../javadoc/reference/com/facebook/imagepipeline/datasource/BaseBitmapDataSubscriber): ```java dataSource.subscribe(new BaseBitmapDataSubscriber() { @Override public void onNewResultImpl(@Nullable Bitmap bitmap) { // You can use the bitmap here, but in limited ways. // No need to do any cleanup. } @Override public void onFailureImpl(DataSource dataSource) { // No cleanup required here. } }, executor); ``` A snap to use, right? There are caveats. This subscriber doesn't work for animated images as those can not be represented as a single bitmap. You can **not** assign the bitmap to any variable not in the scope of the `onNewResultImpl` method. The reason is, as already explained in the above examples that, after the subscriber has finished executing, the image pipeline will recycle the bitmap and free its memory. If you try to draw the bitmap after that, your app will crash with an `IllegalStateException.` You can still safely pass the Bitmap to an Android [notification](https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#setLargeIcon\(android.graphics.Bitmap\)) or [remote view](http://developer.android.com/reference/android/widget/RemoteViews.html#setImageViewBitmap\(int, android.graphics.Bitmap\)). If Android needs your Bitmap in order to pass it to a system process, it makes a copy of the Bitmap data in ashmem - the same heap used by Fresco. So Fresco's automatic cleanup will work without issue. If those requirements prevent you from using `BaseBitmapDataSubscriber`, you can go with a more generic approach as explained above. ================================================ FILE: docs/_docs/drawee-branches.md ================================================ --- docid: drawee-branches title: Drawee Branches layout: docs permalink: /docs/drawee-branches.html --- ### What are Branches? Drawees are made up of different image "branches", one or more of which may be actually displayed at a time. This page outlines the different branches that can be displayed in a Drawee, and how they are set. Except for the actual image, all of them can be set by an XML attribute. The value in XML must be either an Android drawable or color resource. They can also be set by a method in the [GenericDraweeHierarchyBuilder](../javadoc/reference/com/facebook/drawee/generic/GenericDraweeHierarchyBuilder.html) class, if setting programmatically. In code, the value can either be from resources or be a custom subclass of [Drawable](http://developer.android.com/reference/android/graphics/drawable/Drawable.html). Some of the items can even be changed on the fly after the hierarchy has been built. These have a method in the [GenericDraweeHierarchy](../javadoc/reference/com/facebook/drawee/generic/GenericDraweeHierarchy.html) class. Several of the drawables can be [scaled](scaletypes.html). ### Actual The _actual_ image is the target; everything else is either an alternative or a decoration. This is specified using a URI, which can point to an image over the Internet, a local file, a resource, or a content provider. This is a property of the controller, not the hierarchy. It therefore is not set by any of the methods used by the other Drawee branches. Instead, use the `setImageURI` method or [set a controller](using-controllerbuilder.html) programmatically. In addition to the scale type, the hierarchy exposes other methods only for the actual image. These are: * the focus point (used for the [focusCrop](scaletypes.html#FocusCrop) scale type only) * a color filter Default scale type: `centerCrop` ### Placeholder The _placeholder_ is shown in the Drawee when it first appears on screen. After you have called `setController` or `setImageURI` to load an image, the placeholder continues to be shown until the image has loaded. In the case of a progressive JPEG, the placeholder only stays until your image has reached the quality threshold, whether the default, or one set by your app. XML attribute: `placeholderImage` Hierarchy builder method: `setPlaceholderImage` Hierarchy mutation method: `setPlaceholderImage` Default value: None Default scale type: `centerInside` ### Failure The _failure_ image appears if there is an error loading your image. The most common cause of this is an invalid URI, or lack of connection to the network. XML attribute: `failureImage` Hierarchy builder method: `setFailureImage` Hierarchy mutation method: `setFailureImage` Default value: None Default scale type: `centerInside` ### Retry The _retry_ image appears instead of the failure image if you have set your controller to enable the tap-to-retry feature. You must [build your own Controller](using-controllerbuilder.html) to do this. Then add the following line ```java .setTapToRetryEnabled(true) ``` The image pipeline will then attempt to retry an image if the user taps on it. Up to four attempts are allowed before the failure image is shown instead. XML attribute: `retryImage` Hierarchy builder method: `setRetryImage` Hierarchy mutation method: `setRetryImage` Default value: None Default scale type: `centerInside` ### Progress Bar If specified, the _progress bar_ image is shown as an overlay over the Drawee until the final image is set. For more details, see the [progress bar](progress-bars.html) page. XML attribute: `progressBarImage` Hierarchy builder method: `setProgressBarImage` Hierarchy mutation method: `setProgressBarImage` Default value: None Default scale type: `centerInside` ### Backgrounds _Background_ drawables are drawn first, "under" the rest of the hierarchy. Only one can be specified in XML, but in code more than one can be set. In that case, the first one in the list is drawn first, at the bottom. Background images don't support scale-types and are scaled to the Drawee size. XML attribute: `backgroundImage` Hierarchy builder method: `setBackground,` `setBackgrounds` Default value: None Default scale type: N/A ### Overlays _Overlay_ drawables are drawn last, "over" the rest of the hierarchy. Only one can be specified in XML, but in code more than one can be set. In that case, the first one in the list is drawn first, at the bottom. Overlay images don't support scale-types and are scaled to the Drawee size. XML attribute: `overlayImage` Hierarchy builder method: `setOverlay,` `setOverlays` Default value: None Default scale type: N/A ### Pressed State Overlay The _pressed state overlay_ is a special overlay shown only when the user presses the screen area of the Drawee. For example, if the Drawee is showing a button, this overlay could have the button change color when pressed. The pressed state overlay doesn't support scale-types. XML attribute: `pressedStateOverlayImage` Hierarchy builder method: `setPressedStateOverlay` Default value: None Default scale type: N/A ================================================ FILE: docs/_docs/faq.md ================================================ --- docid: faq title: FAQ layout: docs permalink: /docs/faq.html --- These are common questions asked on our GitHub presence. Please create a pull-request if you have a Q&A that others will profit from. ### How do I clear all caches? You can use the following code to delete all cached images (both from storage and memory): ```java // clear both memory and disk caches Fresco.getImagePipeline().clearCaches(); ``` ### How can I create a Drawee that supports zoom gestures? Have a look at the [ZoomableDraweeView](https://github.com/facebook/fresco/tree/main/samples/zoomable) module which is part of our sample code on GitHub. ### How do I create an URI for a local file? Use the `UriUtil` class: ```java final File file = new File("your/file/path/img.jpg"); final URI uri = UriUtil.getUriForFile(file); ``` ### How do I create an URI for a resource? Use the `UriUtil` class: ```java final int resourceId = R.drawable.my_image; final URI uri = UriUtil.getUriForResourceId(resourceId); // alternatively, if it is from another package: final URI uri = UriUtil.getUriForQualifiedResource("com.myapp.plugin", resourceId); ``` ### How do I use Fresco in a RecyclerView? You build your `RecyclerView` just like any other `RecyclerView`. The `DraweeView` is able to attach and detach itself appropriately. When being detached it can free up the memory of the referenced image. When being re-attached, the image is loaded from the BitmapCache if it is still available there. Have a look at [DraweeRecyclerViewFragment.java](https://github.com/facebook/fresco/blob/1472a3e1b1655e9b52c74e0b06d5ba60d15a42f9/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/drawee/DraweeRecyclerViewFragment.java) which is part of our showcase app. ### How do I download a image without decoding? For this, you can use the `imagePipeline#fetchEncodedImage(ImageRequest, ...)` method of the image pipeline. See our section on [Using the Image Pipeline Directly](using-image-pipeline.html) and [DataSources & DataSubscribers](datasources-datasubscribers.html) for detailed samples. ### How do I modify an image before displaying? The best way is to implement a [PostProcessor](post-processor.html). This allows the image pipeline to schedule the modification on the background and allocates the Bitmaps efficiently. ### How large is Fresco? If you are correctly following the steps from [Shipping Your App with Fresco](proguard.html), your release builds should not grow more than 500 KiB when adding Fresco. Adding support for animations (`com.facebook.fresco:animated-gif`, `com.facebook.fresco:animated-webp`) and WebP on old devices (`com.facebook.fresco:webpsupport`) is optional. This modularization allows the base Fresco library to be light-weight. Adding those additional libraries would account for ~100 KiB each. ### Why can’t I use Android’s wrap_content attribute on a DraweeView? The reason is that Drawee always returns -1 for [getIntrinsicHeight](http://developer.android.com/reference/android/graphics/drawable/Drawable.html#getIntrinsicHeight()) and getIntrinsicWidth methods. And the reason for that is that unlike a simple ImageView, Drawee may show more than one thing at the same time. For example, during the fade transition from the placeholder to the actual image, both images are visible. There may even be more than one actual image, one low-resolution, the other high-resolution. If all these images are not of exactly the same size, and they practically never are, then the concept of an "intrinsic" size cannot be well defined. We could have returned the size of the placeholder until the image has finished loading, and then swap to the actual image's size. If we did that, though, the image would not appear correctly - it would be scaled or cropped to the placeholder's size. The only way to prevent that would be to force an Android layout pass when the image loads. Not only will that hurt your app's scroll perf, but it will be jarring for your users, who will suddenly see your app change on screen. Imagine if the user is reading a text article and all of a sudden the text jumps down because the image above it just loaded and caused everything to re-layout. For this reason, you have to use an actual size or `match_parent` to lay out a DraweeView. If your images are coming from a server, it may be possible to ask that server for the image dimensions, before you download it. This should be a faster request. Then use [setLayoutParams](http://developer.android.com/reference/android/view/View.html#setLayoutParams(android.view.ViewGroup.LayoutParams)) to dynamically size your view upfront. If on the other hand your use case is a legitimate exception, you can actually resize Drawee view dynamically by using a controller listener as explained [here](http://stackoverflow.com/a/34075281/3027862). And remember, we intentionally removed this functionality because it is undesireable. [Ugly things should look ugly.](https://youtu.be/qCdpTji8nxo?t=890). ================================================ FILE: docs/_docs/gotchas.md ================================================ --- docid: gotchas title: Gotchas layout: docs permalink: /docs/gotchas.html --- #### Don't use ScrollViews If you want to scroll through a long list of images, you should use a [RecyclerView](http://developer.android.com/reference/android/support/v7/widget/RecyclerView.html), [ListView](https://developer.android.com/reference/android/widget/ListView.html), or [GridView](https://developer.android.com/reference/android/widget/GridView.html). All of these re-use their child views continually as you scroll through them. Fresco descendant views receive the system events that let them manage memory correctly. `ScrollView` does not do this. Thus, Fresco views aren't told when they have gone off-screen, and hold onto their image memory until your Fragment or Activity is stopped. Your app will be at a much greater risk of OOMs. #### Don't downcast It is tempting to downcast objects returned by Fresco classes into actual objects that appear to give you greater control. At best, this will result in fragile code that gets broken in next release; at worst, it will lead to very subtle bugs. #### Don't use getTopLevelDrawable `DraweeHierarchy.getTopLevelDrawable()` should **only** be used by DraweeViews. Client code should almost never interact with it. The sole exception is [custom views](writing-custom-views.html). Even there, the top-level drawable should never be downcast. We may change the actual type of the drawable in future releases. #### Don't re-use DraweeHierarchies Never call ```DraweeView.setHierarchy``` with the same argument on two different views. Hierarchies are made up of Drawables, and Drawables on Android cannot be shared among multiple views. #### Re-use Drawable resource IDs, not Java Drawable objects This is for the same reason as the above. Drawables cannot be shared in multiple views. You can freely use the same `@drawable` resource ID as a placeholder, error, or retry in multiple `SimpleDraweeViews` in XML. If you are using `GenericDraweeHierarchyBuilder`, you must call [Resources.getDrawable](http://developer.android.com/reference/android/content/res/Resources.html#getDrawable(int)) separate for *each* hierarchy. Do not call it just once and pass it to multiple hierarchies! #### Do not control hierarchy directly Do not interact with `SettableDraweeHierarchy` methods (`reset`, `setImage`, ...). Those are to be used by controller only. Do NOT be tempted to use `setControllerOverlay` in order to set an overlay. This method is to be called by controller only, and it refers to a very special controller overaly. If you just need to display an overlay see [Drawee branches] (http://frescolib.org/docs/drawee-branches.html#Overlays). #### Don't set images directly on a DraweeView Currently ```DraweeView``` is a subclass of Android's ImageView. This has various methods to set an image (such as setImageBitmap, setImageDrawable) If you set an image directly, you will completely lose your ```DraweeHierarchy```, and will not get any results from the image pipeline. #### Don't use ImageView attributes or methods with DraweeView Any XML attribute or method of ImageView not found in [View](http://developer.android.com/reference/android/view/View.html) will not work on a DraweeView. Typical cases are `src`, `scaleType`, `adjustViewBounds`, etc. Don't use those. DraweeView has its own counterparts as explained in the other sections of this documentation. Any ImageView attrribute or method will be removed in the upcoming release, so please don't use those. ================================================ FILE: docs/_docs/image-requests.md ================================================ --- docid: image-requests title: Image Requests layout: docs permalink: /docs/image-requests.html --- If you need an `ImageRequest` that consists only of a URI, you can use the helper method `ImageRequest.fromURI`. Loading [multiple-images](requesting-multiple-images.html) is a common case of this. If you need to tell the image pipeline anything more than a simple URI, you need to use `ImageRequestBuilder`: ```java Uri uri; ImageDecodeOptions decodeOptions = ImageDecodeOptions.newBuilder() .setBackgroundColor(Color.GREEN) .build(); ImageRequest request = ImageRequestBuilder .newBuilderWithSource(uri) .setImageDecodeOptions(decodeOptions) .setAutoRotateEnabled(true) .setLocalThumbnailPreviewsEnabled(true) .setLowestPermittedRequestLevel(RequestLevel.FULL_FETCH) .setProgressiveRenderingEnabled(false) .setResizeOptions(new ResizeOptions(width, height)) .build(); ``` #### Fields in ImageRequest - `uri` - the only mandatory field. See [Supported URIs](supported-uris.html) - `autoRotateEnabled` - whether to enable [auto-rotation](rotation.html). - `progressiveEnabled` - whether to enable [progressive loading](progressive-jpegs.html). - `postprocessor` - component to [postprocess](modifying-image.html) the decoded image. - `resizeOptions` - desired width and height. Use with caution. See [Resizing](resizing.html). #### Lowest Permitted Request Level The image pipeline follows a [definite sequence](intro-image-pipeline.html) in where it looks for the image. 1. Check the bitmap cache. This is nearly instant. If found, return. 2. Check the encoded memory cache. If found, decode the image and return. 3. Check the "disk" (local storage) cache. If found, load from disk, decode, and return. 4. Go to the original file on network or local file. Download, resize and/or rotate if requested, decode, and return. For network images in particular, this will be the slowest by a long shot. The `setLowestPermittedRequestLevel` field lets you control how far down this list the pipeline will go. Possible values are: - `BITMAP_MEMORY_CACHE` - `ENCODED_MEMORY_CACHE` - `DISK_CACHE` - `FULL_FETCH` This is useful in situations where you need an instant, or at least relatively fast, image or none at all. ================================================ FILE: docs/_docs/images-in-notifications.md ================================================ --- docid: images-in-notifications title: Images in Notifications layout: docs permalink: /docs/images-in-notifications.html --- If you need to display an image in a notification, you can use the `BaseBitmapDataSubscriber` for requesting a bitmap from the `ImagePipeline`. This is safe to be passed to a notification as the system will parcel it after the `NotificationManager#notify` method. This page explains a full sample on how to do this. ### Step by step First create an `ImageRequest` with the URI: ```java ImageRequest imageRequest = ImageRequest.fromUri("http://example.org/user/42/profile.jpg")); ``` Then create a `DataSource` and request the decoded image from the `ImagePipeline`: ```java ImagePipeline imagePipeline = Fresco.getImagePipeline(); DataSource> dataSource = imagePipeline.fetchDecodedImage(imageRequest, null); ``` As a `DataSource` is similar to a `Future`, we need to add a `DataSubscriber` to handle the result. The `BaseBitmapDataSubscriber` abstracts some of the complexity away when dealing with `Bitmap`: ```java dataSource.subscribe( new BaseBitmapDataSubscriber() { @Override protected void onNewResultImpl(Bitmap bitmap) { displayNotification(bitmap); } @Override protected void onFailureImpl(DataSource> dataSource) { // In general, failing to fetch the image should not keep us from displaying the // notification. We proceed without the bitmap. displayNotification(null); } }, UiThreadImmediateExecutorService.getInstance()); } ``` The `displayNotification(Bitmap)` method then is similar to the 'normal' way to do this on Android: ```java private void displayNotification(@Nullable Bitmap bitmap) { final NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getContext()) .setSmallIcon(R.drawable.ic_done) .setLargeIcon(bitmap) .setContentTitle("Fresco Says Hello") .setContentText("Notification Text ..."); final NotificationManager notificationManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); } ``` ### Full Sample For the full sample see the `ImagePipelineNotificationFragment` in the showcase app: [ImagePipelineNotificationFragment.java](https://github.com/facebook/fresco/blob/main/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/imagepipeline/ImagePipelineNotificationFragment.java) ![Showcase app with a notification](/static/images/docs/02-images-in-notifications-sample.png) ================================================ FILE: docs/_docs/index.md ================================================ --- docid: index title: Getting Started with Fresco layout: docs permalink: /docs/index.html --- This Guide will walk you through the steps needed to start using Fresco in your app, including loading your first image. ### 1. Update Gradle configuration Edit your `build.gradle` file. You must add the following line to the `dependencies` section: ```groovy dependencies { // your app's other dependencies implementation 'com.facebook.fresco:fresco:{{site.current_version}}' } ``` Starting with Fresco version 2.1.0, you can also use a Java-only Fresco version (without native code). You simply exclude artifacts with native code: ```groovy dependencies { // your app's other dependencies implementation('com.facebook.fresco:fresco:{{site.current_version}}') { exclude group: 'com.facebook.soloader', module: 'soloader' exclude group: 'com.facebook.fresco', module: 'soloader' exclude group: 'com.facebook.fresco', module: 'nativeimagefilters' exclude group: 'com.facebook.fresco', module: 'nativeimagetranscoder' exclude group: 'com.facebook.fresco', module: 'memory-type-native' exclude group: 'com.facebook.fresco', module: 'imagepipeline-native' } } ``` ### 2. Optional: Add additional Fresco feature modules The following optional modules may also be added, depending on the needs of your app. ```groovy dependencies { // For animated GIF support implementation 'com.facebook.fresco:animated-gif:{{site.current_version}}' // For WebP support, including animated WebP implementation 'com.facebook.fresco:animated-webp:{{site.current_version}}' implementation 'com.facebook.fresco:webpsupport:{{site.current_version}}' // For WebP support, without animations implementation 'com.facebook.fresco:webpsupport:{{site.current_version}}' // Provide the Android support library (you might already have this or a similar dependency) implementation 'com.android.support:support-core-utils:{{site.support_library_version}}' } ``` ### 3. Initialize Fresco & Declare Permissions Fresco needs to be initialized. You should only do this 1 time, so placing the initialization in your Application is a good idea. An example for this would be: ```java [MyApplication.java] public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); Fresco.initialize(this); } } ``` *NOTE:* Remember to also declare you Application class in the ```AndroidManifest.xml``` as well as add the required permissions. In most cases you will need the INTERNET permission. ```xml ... ... ``` Optional: For Java-only Fresco, you have to disable native code via `ImagePipelineConfig`. ```java Fresco.initialize( applicationContext, ImagePipelineConfig.newBuilder(applicationContext) .setMemoryChunkType(MemoryChunkType.BUFFER_MEMORY) .setImageTranscoderType(ImageTranscoderType.JAVA_TRANSCODER) .experiment().setNativeCodeDisabled(true) .build()) ``` ### 4. Create a Layout In your layout XML, add a custom namespace to the top-level element. This is needed to access the custom `fresco:` attributes which allows you to control how the image is loaded and displayed. ```xml ``` Then add the ```SimpleDraweeView``` to the layout: ```xml ``` To show an image, you need only do this: ```java Uri uri = Uri.parse("https://raw.githubusercontent.com/facebook/fresco/main/docs/static/logo.png"); SimpleDraweeView draweeView = (SimpleDraweeView) findViewById(R.id.my_image_view); draweeView.setImageURI(uri); ``` and Fresco does the rest. The placeholder is shown until the image is ready. The image will be downloaded, cached, displayed, and cleared from memory when your view goes off-screen. ### 5. Optional: Setting a non-default native library loader (Only for Fresco 2.1 and above) The default Fresco artifact employs SoLoader for loading native libraries. However, starting with Fresco version 2.1.0 this can be customized to use any other native code loading mechanism, such as the built-in `System.loadLibrary(...)`. In order to set this up, you first have to exclude the default SoLoader implementation for all dependencies that are including it. To do this, simply edit your 'build.gradle' file in the following way: ```groovy dependencies { // your app's other dependencies implementation 'com.facebook.fresco:fresco:{{site.current_version}}' { exclude group: 'com.facebook.soloader', module: 'soloader' } } ``` Now, `System.loadLibrary(...)` will be used for native code loading. You can also take a look at 'samples/scrollperf/build.gradle' where we set up two build variants, one with SoLoader, one without. You can also employ your own native library loading mechanism by implementing a custom [`NativeLoaderDelegate`](https://github.com/facebook/SoLoader/blob/cdd144ab84d7af8c370a4a0e1e6b7ce5d7e19d5c/java/com/facebook/soloader/nativeloader/NativeLoaderDelegate.java). Then, simply call `NativeLoader.init(yourDelegate)` before Fresco is initialized. ================================================ FILE: docs/_docs/intro-image-pipeline.md ================================================ --- docid: intro-image-pipeline title: Introduction to the Image Pipeline layout: docs permalink: /docs/intro-image-pipeline.html --- The image pipeline does everything necessary to get an image into a form where it can be rendered into an Android device. The pipeline goes through the following steps when given an image to load: 1. Look in the bitmap cache. If found, return it. 2. Hand off to other threads. 3. Check in the encoded memory cache. If found, decode, transform, and return it. Store in the bitmap cache. 3. Check in the disk cache. If found, decode, transform, and return it. Store in the encoded-memory and bitmap caches. 4. Check on the network (or other original source). If found, decode, transform, and return it. Store in all three caches. This being an image library, an image is worth a thousand words: ![Image Pipeline Diagram](../static/imagepipeline.png "Image Pipeline") (The 'disk' cache as pictured includes the encoded memory cache, to keep the logic path clearer.) See [this page](caching.html) for more details on caching. The pipeline can read from [local files](supported-uris.html) as well as network. PNG, GIF, and WebP are supported as well as JPEG. ================================================ FILE: docs/_docs/listening-to-events.md ================================================ --- docid: listening-to-events title: Listening to Events layout: docs permalink: /docs/listening-to-events.html --- ### Motivation The image pipeline and the view controller in Fresco have built-in instrumentation interfaces. One can employ this to track both performance and to react to events. Fresco comes with two main instrumentation interfaces: - The `RequestListener` is globally registered in the `ImagePipelineConfig` and logs all requests that are handled by the producer-consumer chain - The `ControllerListener` is added to an individual `DraweeView` and is convenient for reacting on events such as "this image is fully loaded" ### ControllerListener While the `RequestListener` is a global listener, the `ControllerListener` is local to a certain `DraweeView`. It is a good way to react to changes to the displayed view such as "image failed to load" or "image is fully loaded". Again, it's best to extend `BaseControllerListener` for this. A simple listener might look like the following: ```java public class MyControllerListener extends new BaseControllerListener() { @Override public void onFinalImageSet(String id, ImageInfo imageInfo, Animatable animatable) { Log.i("DraweeUpdate", "Image is fully loaded!"); } @Override public void onIntermediateImageSet(String id, ImageInfo imageInfo, Animatable animatable) { Log.i("DraweeUpdate", "Image is partly loaded! (maybe it's a progressive JPEG?)"); if (imageInfo != null) { int quality = imageInfo.getQualityInfo().getQuality(); Log.i("DraweeUpdate", "Image quality (number scans) is: " + quality); } } @Override public void onFailure(String id, Throwable throwable) { Log.i("DraweeUpdate", "Image failed to load: " + throwable.getMessage()); } } ``` You add it to your `DraweeController` in the following way: ```java DraweeController controller = Fresco.newDraweeControllerBuilder() .setImageRequest(request) .setControllerListener(new MyControllerListener()) .build(); mSimpleDraweeView.setController(controller); ``` ### RequestListener The `RequestListener` comes with a large interface of callback methods. Most importantly, you will notice that they all provide the unique `requestId` which allows to track a request across multiple stages. Due to the large number of callbacks, it is advisable to extend from `BaseRequestListener` instead and only implement the methods you are interested in. You register your listener in the Application class as follows: ```java final Set listeners = new HashSet<>(); listeners.add(new MyRequestLoggingListener()); ImagePipelineConfig imagePipelineConfig = ImagePipelineConfig.newBuilder(this) .setRequestListeners(listeners) .build(); Fresco.initialize(this, imagePipelineConfig); ``` We will walk through the generated logging of one image request from the showcase app and discuss the individual meanings. You can observe these yourself in `adb logcat` when running the showcase app: ```java RequestLoggingListener: time 2095589: onRequestSubmit: {requestId: 5, callerContext: null, isPrefetch: false} ``` `onRequestSubmit(...)` is called when an `ImageRequest` enters the image pipeline. Here you can make use of the caller context object to identify which feature of the app is sending the request. ```java RequestLoggingListener: time 2095590: onProducerStart: {requestId: 5, producer: BitmapMemoryCacheGetProducer} RequestLoggingListener: time 2095591: onProducerFinishWithSuccess: {requestId: 5, producer: BitmapMemoryCacheGetProducer, elapsedTime: 1 ms, extraMap: {cached_value_found=false}} ``` The `onProducerStart(...)` and `onProducerFinishWithSuccess(...)` (or `onProducerFinishWithFailure(...)`) are called for all producers along the pipeline. The one above is a check of the Bitmap cache. ```java RequestLoggingListener: time 2095592: onProducerStart: {requestId: 5, producer: BackgroundThreadHandoffProducer} RequestLoggingListener: time 2095593: onProducerFinishWithSuccess: {requestId: 5, producer: BackgroundThreadHandoffProducer, elapsedTime: 1 ms, extraMap: null} RequestLoggingListener: time 2095594: onProducerStart: {requestId: 5, producer: BitmapMemoryCacheProducer} RequestLoggingListener: time 2095594: onProducerFinishWithSuccess: {requestId: 5, producer: BitmapMemoryCacheProducer, elapsedTime: 0 ms, extraMap: {cached_value_found=false}} RequestLoggingListener: time 2095595: onProducerStart: {requestId: 5, producer: EncodedMemoryCacheProducer} RequestLoggingListener: time 2095596: onProducerFinishWithSuccess: {requestId: 5, producer: EncodedMemoryCacheProducer, elapsedTime: 1 ms, extraMap: {cached_value_found=false}} RequestLoggingListener: time 2095596: onProducerStart: {requestId: 5, producer: DiskCacheProducer} RequestLoggingListener: time 2095598: onProducerFinishWithSuccess: {requestId: 5, producer: DiskCacheProducer, elapsedTime: 2 ms, extraMap: {cached_value_found=false}} RequestLoggingListener: time 2095598: onProducerStart: {requestId: 5, producer: PartialDiskCacheProducer} RequestLoggingListener: time 2095602: onProducerFinishWithSuccess: {requestId: 5, producer: PartialDiskCacheProducer, elapsedTime: 4 ms, extraMap: {cached_value_found=false}} ``` We see more of these when the request is handed over to the background (`BackgroundThreadHandoffProducer`) and performs look-ups in the caches. ```java RequestLoggingListener: time 2095602: onProducerStart: {requestId: 5, producer: NetworkFetchProducer} RequestLoggingListener: time 2095745: onProducerEvent: {requestId: 5, stage: NetworkFetchProducer, eventName: intermediate_result; elapsedTime: 143 ms} RequestLoggingListener: time 2095764: onProducerFinishWithSuccess: {requestId: 5, producer: NetworkFetchProducer, elapsedTime: 162 ms, extraMap: {queue_time=140, total_time=161, image_size=40502, fetch_time=21}} RequestLoggingListener: time 2095764: onUltimateProducerReached: {requestId: 5, producer: NetworkFetchProducer, elapsedTime: -1 ms, success: true} ``` For this particular request, the `NetworkFetchProducer` is the "ultimate producer". This means, it is the one that provides the definite input source for fulfilling the request. If the image is cached, the `DiskCacheProducer` would be the "ultimate" producer. ```java RequestLoggingListener: time 2095766: onProducerStart: {requestId: 5, producer: DecodeProducer} RequestLoggingListener: time 2095786: onProducerFinishWithSuccess: {requestId: 5, producer: DecodeProducer, elapsedTime: 20 ms, extraMap: {imageFormat=JPEG, ,hasGoodQuality=true, bitmapSize=788x525, isFinal=true, requestedImageSize=unknown, encodedImageSize=788x525, sampleSize=1, queueTime=0} RequestLoggingListener: time 2095788: onRequestSuccess: {requestId: 5, elapsedTime: 198 ms} ``` On the way up, the `DecodeProducer` also succeeds and finally the `onRequestSuccess(...)` method is called. You will notice that most of these methods are given optional information as a `Map extraMap`. The string constants to look-up the elements are usually public constants in the corresponding producer classes. ================================================ FILE: docs/_docs/media-variations.md ================================================ --- docid: media-variations title: Media Variations layout: docs permalink: /docs/media-variations.html --- Coming soon! ================================================ FILE: docs/_docs/placeholder-failure-retry.md ================================================ --- docid: placeholder-failure-retry title: Placeholder, failure and retry images layout: docs permalink: /docs/placeholder-failure-retry.html --- When you're loading network images things can go wrong, take a long time, or some images might not even be available at all. We've seen how to display [progress bars](progress-bars.html). On this page, we look at the other things that a `SimpleDraweeView` can display while the actual image is not available (yet, or at all). Note that all of these can have different [scale types](scaletypes.html), which you can customize. ### Placeholder Image The placeholder image is displayed from before you've set a URI or a controller until it has finished loading (successfully or not). #### XML ```xml ``` #### Code ```java mSimpleDraweeView.getHierarchy().setPlaceholderImage(placeholderImage); ``` ### Failure Image The failure image is displayed when a request has completed in error, either network-related (404, timeout) or image data-related (malformed image, unsupported format). #### XML ```xml ``` #### Code ```java mSimpleDraweeView.getHierarchy().setFailureImage(failureImage); ``` ### Retry Image The retry image appears instead of the failure image. When the user taps on it, the request is retried up to four times, before the failure image is displayed. In order for the retry image to work, you need to enable support for it in your controller, which means setting up your image request like so: ```java mSimpleDraweeView.setController( Fresco.newDraweeControllerBuilder() .setTapToRetryEnabled(true) .setUri(uri) .build()); ``` #### XML ```xml ``` #### Code ```java simpleDraweeView.getHierarchy().setRetryImage(retryImage); ``` ### Further Reading Placeholder, failure and retry images are drawee *branches*. There are others than what is presented on this page, though these are the most commonly used ones. To read about all of the branches and how they work, check out [drawee branches](drawee-branches.html). ### Example The Fresco showcase app has a [DraweeHierarchyFragment](https://github.com/facebook/fresco/blob/main/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/drawee/DraweeHierarchyFragment.java) that demonstrates using placeholder, failure and retry images. ![Showcase app with placeholder, failure and retry images](/static/images/docs/01-placeholder-sample.png) ================================================ FILE: docs/_docs/post-processor.md ================================================ --- docid: post-processor title: Modifying the Image (Post-processing) layout: docs redirect_from: /docs/post-processor.html permalink: /docs/modifying-image.html --- ### Motivation Post-processors allow custom modifications of the fetched image. In most cases image processing should already be done by the server before the image is sent down to the client, as the mobile device's resources are usually more limited. However, there are many instances where client side processing is a valid option. For instance, if the images are being served by a third party which you do not control or if the images are local (on the device). ### Background In Fresco's pipeline, post-processors are applied at the very end when the image already has been decoded as a bitmap and the original version is stored in the in-memory Bitmap cache. While the post-processor can directly work on the provided Bitmap, it can also create a new Bitmap with a different dimension. Ideally, the implemented post-processor should provide a cache key for given parameters. By doing this, the newly generated bitmap is also cached in the in-memory Bitmap cache and don't need to be re-created. All post-processors are executed using background executors. However, naive iteration or complex computations can still take a long time and should be avoided. If you aim for computations that are non-linear in the number of pixels, there is a section which contains tips for you how you can use native code to speed your post-processor up. ### Example: Creating a Grey-Scale Filter Let's start with something simple: a post-processor that converts the bitmap into a grey-scale version. For this we need to iterate over the bitmap's pixels and replace their color value. The image is copied before it enters the post-processor. The original image in cache is *not* affected by any changes you make in your post-processor. On Android 4.x and lower, the copy is stored outside the Java heap, just as the original image was. The `BasePostprocessor` expects our sub-class to override one of its `BasePostprocessor#process` method. The simplest one performs in-place modifications of the provided bitmap. Here, the image is copied before it enters the post-processor. Thus, the original of the image in cache is *not* affected by any changes you make in the post-processor. We will later discuss how we can also modify the configuration and size of the outputted bitmap. ```java public class FastGreyScalePostprocessor extends BasePostprocessor { @Override public void process(Bitmap bitmap) { final int w = bitmap.getWidth(); final int h = bitmap.getHeight(); final int[] pixels = new int[w * h]; bitmap.getPixels(pixels, 0, w, 0, 0, w, h); for (int x = 0; x < w; x++) { for (int y = 0; y < h; y++) { final int offset = y * w + x; pixels[offset] = getGreyColor(pixels[offset]); } } // this is much faster then calling #getPixel and #setPixel as it crosses // the JNI barrier only once bitmap.setPixels(pixels, 0, w, 0, 0, w, h); } static int getGreyColor(int color) { final int alpha = color & 0xFF000000; final int r = (color >> 16) & 0xFF; final int g = (color >> 8) & 0xFF; final int b = color & 0xFF; // see: https://en.wikipedia.org/wiki/Relative_luminance final int luminance = (int) (0.2126 * r + 0.7152 * g + 0.0722 * b); return alpha | luminance << 16 | luminance << 8 | luminance; } } ``` ![Showcase app with grey-scale filter](/static/images/docs/02-post-processor-grey.png) ### Caching Post-Processor Results As we've seen that post-processing computations can be rather resource intensive, we want to cache the results. Cached output bitmaps are stored in the same cache as the decoded input bitmaps. In order to use this feature, the post-processor must override the `PostProcessor#getPostProcessorCacheKey` method. It should return a cache key that is dependent on all important input values that effect the performed modifications. For this example we extend an existing `WatermarkPostprocessor` that draws a watermark text multiple times on the image: ```java public class CachedWatermarkPostprocessor extends WatermarkPostprocessor { @Override public CacheKey getPostprocessorCacheKey() { return new SimpleCacheKey(String.format( (Locale) null, "text=%s,count=%d", mWatermarkText, mCount)); } } ``` ### In-place Bitmap transformation As an alternative to post-processors, you can use a `BitmapTransformation` instead. Compared to normal post-processors, a `BitmapTransformation` will be applied to the original Bitmap immediately after it is decoded, which also means that the original image will not be cached and no additional Bitmap has to be allocated. In-place Bitmap transformations are the preferred way for images where you never need the original version. An example would be a `BitmapTransformation` for fully circular profile pictures: ```java public class CircularBitmapTransformation implements BitmapTransformation { @Override public void transform(Bitmap bitmap) { NativeRoundingFilter.toCircle(bitmap); } @Override public boolean modifiesTransparency() { return true; // We have transparent pixels } } ``` ### Advanced: JNI and Blurring One of the most commonly asked for post-processing effects is blurring. Luckily, Fresco ships with a very efficient implementation in native C code accessible through `NativeBlurFilter#iterativeBoxBlur`. When you are considering more advanced post-processing, using native code is a great way to improve performance. If you go down this path, have a look at the implementation in `blur_filter.c` on how to work with bitmaps in native code. Most importantly it explains you how to lock the pixels in memory and other important tricks. ![Showcase app with blur post-processor](/static/images/docs/02-post-processor-blur.png) ### Advanced: Changing the Bitmap's Size Even with an efficient implementation in native code, the post-processor can take a long time. For more efficient blurring, we can down-scale the image, blur the small version and then let the GPU scale it up when displayed. As blurred images do not have hard edges, this optimization usually goes unrecognized. In our new post-processor we override an overloaded variant of the `BasePostProcessor#process()` method. That variant provides a `PlatformBitmapFactory` that we can use to create a custom output bitmap. Note that we must no longer modify the `sourceBitmap`, as it is not a copy that has been created for us. ```java public class ScalingBlurPostprocessor extends FullResolutionBlurPostprocessor { /** * A scale ration of 4 means that we reduce the total number of pixels to process by factor 16. */ private static final int SCALE_RATIO = 4; @Override public CloseableReference process( Bitmap sourceBitmap, PlatformBitmapFactory bitmapFactory) { final CloseableReference bitmapRef = bitmapFactory.createBitmap( sourceBitmap.getWidth() / SCALE_RATIO, sourceBitmap.getHeight() / SCALE_RATIO); try { final Bitmap destBitmap = bitmapRef.get(); final Canvas canvas = new Canvas(destBitmap); canvas.drawBitmap( sourceBitmap, null, new Rect(0, 0, destBitmap.getWidth(), destBitmap.getHeight()), mPaint); NativeBlurFilter.iterativeBoxBlur(destBitmap, BLUR_RADIUS / SCALE_RATIO, BLUR_ITERATIONS); return CloseableReference.cloneOrNull(bitmapRef); } finally { CloseableReference.closeSafely(bitmapRef); } } } ``` ![Showcase app with scaling blur post-processor](/static/images/docs/02-post-processor-scaling-blur.png) ### Limitations Please keep the following rules in mind when creating post-processors * If you show the same image repeatedly, you must specify the post-processor each time it is requested. You are free to use different post-processors on different requests for the same image. * Post-processors are not currently supported for [animated](animations.html) images. * If you use transparency in your post-processor, call `destinationBitmap.setHasAlpha(true);` * Do **not** override more than one of the three `process` methods. Doing so can produce unpredictable results. * Do **not** modify the source Bitmap when using a `process` methods that requires you to create a new destination bitmap. * Do **not** keep a reference to either bitmap. Both have their memory managed by the image pipeline. The destBitmap will end up in your Drawee or DataSource normally. * Do **not** use the Android `Bitmap.createBitmap` method for creating a new Bitmap. This would work against the central Bitmap pool in Fresco. ### Full Sample For the full sample see the `ImagePipelinePostProcessorFragment` in the showcase app: [ImagePipelinePostProcessorFragment.java](https://github.com/facebook/fresco/blob/main/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/imagepipeline/ImagePipelinePostProcessorFragment.java). It includes all post-processors from this page as well as additional ones. ================================================ FILE: docs/_docs/prefetching.md ================================================ --- docid: prefetching title: Prefetching Images layout: docs permalink: /docs/prefetching.html --- Prefetching images in advance of showing them can sometimes lead to shorter wait times for users. Remember, however, that there are trade-offs. Prefetching takes up your users' data, and uses up its share of CPU and memory. As a rule, prefetching is not recommended for most apps. Nonetheless, the image pipeline allows you to prefetch to either disk or bitmap cache. Both will use more data for network URIs, but the disk cache will not do a decode and will therefore use less CPU. __Note:__ Beware that if your network fetcher doesn't support priorities prefetch requests may slow down images which are immediately required on screen. Neither `OkHttpNetworkFetcher` nor `HttpUrlConnectionNetworkFetcher` currently support priorities. Prefetch to disk: ```java imagePipeline.prefetchToDiskCache(imageRequest, callerContext); ``` Prefetch to bitmap cache: ```java imagePipeline.prefetchToBitmapCache(imageRequest, callerContext); ``` Cancelling prefetch: ```java // keep the reference to the returned data source. DataSource prefetchDataSource = imagePipeline.prefetchTo...; // later on, if/when you need to cancel the prefetch: prefetchDataSource.close(); ``` Closing a prefetch data source after the prefetch has already completed is a no-op and completely safe to do. ### Example See our [showcase app](https://github.com/facebook/fresco/blob/main/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/imagepipeline/ImagePipelinePrefetchFragment.java) for a practical example of how to use prefetching. ================================================ FILE: docs/_docs/progress-bars.md ================================================ --- docid: progress-bars title: Progress Bars layout: docs permalink: /docs/progress-bars.html --- The easiest way to set a progress bar in your application is to use the [ProgressBarDrawable](../javadoc/reference/com/facebook/drawee/drawable/ProgressBarDrawable.html) class when building a hierarchy: ```java .setProgressBarImage(new ProgressBarDrawable()) ``` This shows the progress bar as a dark blue rectangle along the bottom of the Drawee. ### Defining your own progress bar If you wish to customize your own progress indicator, be aware that in order for it to accurately reflect progress while loading, it needs to override the [Drawable.onLevelChange](http://developer.android.com/reference/android/graphics/drawable/Drawable.html#onLevelChange\(int\)) method: ```java class CustomProgressBar extends Drawable { @Override protected boolean onLevelChange(int level) { // level is on a scale of 0-10,000 // where 10,000 means fully downloaded // your app's logic to change the drawable's // appearance here based on progress } } ``` ### Example The Fresco showcase app has a [DraweeHierarchyFragment](https://github.com/facebook/fresco/blob/main/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/drawee/DraweeHierarchyFragment.java) that demonstrates using a progress bar drawable. ================================================ FILE: docs/_docs/progressive-jpegs.md ================================================ --- docid: progressive-jpegs title: Progressive JPEGs layout: docs permalink: /docs/progressive-jpegs.html --- Fresco supports the streaming of progressive JPEG images over the network. Scans of the image will be shown in the view as you download them. Users will see the quality of the image start out low and gradually become clearer. This is only supported for network images. Local images are decoded at once, so no need for progressiveness. Also, keep in mind that not all JPEG images are encoded in progressive format, and for those that are not, it is not possible to display them progressively. #### Building the image request Currently, you must explicitly request progressive rendering while building the image request: ```java Uri uri; ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri) .setProgressiveRenderingEnabled(true) .build(); DraweeController controller = Fresco.newDraweeControllerBuilder() .setImageRequest(request) .setOldController(mSimpleDraweeView.getController()) .build(); mSimpleDraweeView.setController(controller); ``` We hope to add support for using progressive images with `setImageURI` in a future release. ### Full Sample For the full sample see the `ImageFormatProgressiveJpegFragment` in the showcase app: [ImageFormatProgressiveJpegFragment.java](https://github.com/facebook/fresco/blob/main/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/imageformat/pjpeg/ImageFormatProgressiveJpegFragment.java) ================================================ FILE: docs/_docs/proguard.md ================================================ --- docid: proguard title: Shipping Your App with Fresco layout: docs redirect_from: /docs/proguard.html permalink: /docs/shipping.html --- Fresco's large size may seem intimidating, but it need not leave you with a large app. We strongly recommend use of the ProGuard tool as well as building split APKs to keep your app small. ### ProGuard Since Fresco 1.9.0 a ProGuard configuration file is included in Fresco itself which is automatically applied if you enable ProGuard for your app. To enable ProGuard, modify your `build.gradle` file to include the lines contained in the `release` section below. ```groovy android { buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt') } } } ``` ### Build Multiple APKs Fresco is written mostly in Java, but there is some C++ as well. C++ code has to be compiled for each of the CPU types (called "ABIs") Android can run on. Currently, Fresco supports five ABIs. 1. `armeabiv-v7a`: Version 7 or higher of the ARM processor. Most Android phones released from 2011-15 are using this. 2. `arm64-v8a`: 64-bit ARM processors. Found on new devices, like the Samsung Galaxy S6. 3. `x86`: Mostly used by tablets, and by emulators. 4. `x86_64`: Used by 64-bit tablets. Fresco's binary download has copies of native `.so` files for all five platforms. You can reduce the size of your app considerably by creating separate APKs for each processor type. If your app does not support Android 2.3 (Gingerbread) you will not need the `armeabi` flavor. To enable multiple APKs, add the `splits` section below to the `android` section of your `build.gradle` file. ```groovy android { // rest of your app's logic splits { abi { enable true reset() include 'x86', 'x86_64', 'arm64-v8a', 'armeabi-v7a' universalApk false } } } ``` See the [Android publishing documentation](https://developer.android.com/google/play/publishing/multiple-apks.html) for more details on how splits work. ================================================ FILE: docs/_docs/requesting-multiple-images.md ================================================ --- docid: requesting-multiple-images title: Requesting Multiple Images (Multi-URI) layout: docs permalink: /docs/requesting-multiple-images.html --- The methods on this page require [setting your own image request](using-controllerbuilder.html). ### Going from low to high resolution Suppose you want to show users a high-resolution, slow-to-download image. Rather than let them stare a placeholder for a while, you might want to quickly download a smaller thumbnail first. You can set two URIs, one for the low-res image, one for the high one: ```java Uri lowResUri, highResUri; DraweeController controller = Fresco.newDraweeControllerBuilder() .setLowResImageRequest(ImageRequest.fromUri(lowResUri)) .setImageRequest(ImageRequest.fromUri(highResUri)) .setOldController(mSimpleDraweeView.getController()) .build(); mSimpleDraweeView.setController(controller); ``` Animated images are not supported for the low-res request. ### Using thumbnail previews *This option is supported only for local URIs, and only for images in the JPEG format.* If your JPEG has a thumbnail stored in its EXIF metadata, the image pipeline can return that as an intermediate result. Your Drawee will first show the thumbnail preview, then the full image when it has finished loading and decoding. ```java Uri uri; ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri) .setLocalThumbnailPreviewsEnabled(true) .build(); DraweeController controller = Fresco.newDraweeControllerBuilder() .setImageRequest(request) .setOldController(mSimpleDraweeView.getController()) .build(); mSimpleDraweeView.setController(controller); ``` ### Loading the first available image Most of the time, an image has no more than one URI. Load it, and you're done. But suppose you have multiple URIs for the same image. For instance, you might have uploaded an image taken from the camera. Original image would be too big to upload, so the image is downscaled first. In such case, it would be beneficial to first try to get the local-downscaled-uri, then if that fails, try to get the local-original-uri, and if even that fails, try to get the network-uploaded-uri. It would be a shame to download the image that we may have already locally. The image pipeline normally searches for images in the memory cache first, then the disk cache, and only then goes out to the network or other source. Rather than doing this one by one for each image, we can have the pipeline check for *all* the images in the memory cache. Only if none were found would disk cache be searched in. Only if none were found there either would an external request be made. Just create an array of image requests, and pass it to the builder. ```java Uri uri1, uri2; ImageRequest request = ImageRequest.fromUri(uri1); ImageRequest request2 = ImageRequest.fromUri(uri2); ImageRequest[] requests = { request1, request2 }; DraweeController controller = Fresco.newDraweeControllerBuilder() .setFirstAvailableImageRequests(requests) .setOldController(mSimpleDraweeView.getController()) .build(); mSimpleDraweeView.setController(controller); ``` Only one of the requests will be displayed. The first one found, whether at memory, disk, or network level, will be the one returned. The pipeline will assume the order of requests in the array is the preference order. ### Specifying a custom DataSource Supplier For even more flexibility, it is possible to specify a custom `DataSource` `Supplier` while building a Drawee controller. You can implement your own supplier or just compose the existing ones in whichever way you like. See `FirstAvailableDataSourceSupplier` and `IncreasingQualityDataSourceSupplier` for an example implementation. See `AbstractDraweeControllerBuilder` for how those suppliers can be composed together. ================================================ FILE: docs/_docs/resizing.md ================================================ --- docid: resizing title: Resizing layout: docs permalink: /docs/resizing.html redirect_from: - /docs/resizing-rotating.html --- We use the following terminology for this section: - **Scaling** is a canvas operation and is usually hardware accelerated. The bitmap itself is always the same size. It just gets drawn downscaled or upscaled. See [ScaleTypes](scaletypes.html). - **Resizing** is a pipeline operation executed in software. This changes the encoded image in memory before it is being decoded. The decoded bitmap will be smaller than the original image. - **Downsampling** is also a pipeline operation implemented in software. Rather than creating a new encoded image, it simply decodes only a subset of the pixels, resulting in a smaller output bitmap. ### Resizing Resizing does not modify the original file, it just resizes an encoded image in memory, prior to being decoded. To resize pass a `ResizeOptions` object when constructing an `ImageRequest`: ```java ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri) .setResizeOptions(new ResizeOptions(50, 50)) .build(); mSimpleDraweeView.setController( Fresco.newDraweeControllerBuilder() .setOldController(mSimpleDraweeView.getController()) .setImageRequest(request) .build()); ``` Resizing has some limitations: - it only supports JPEG files - the actual resize is carried out to the nearest 1/8 of the original size - it cannot make your image bigger, only smaller (not a real limitation though) ### Downsampling Downsampling is an experimental feature added recently to Fresco. To use it, you must explicitly enable it when [configuring the image pipeline](configure-image-pipeline.html): ```java .setDownsampleEnabled(true) ``` If this option is on, the image pipeline will downsample your images instead of resizing them. You must still call `setResizeOptions` for each image request as above. Downsampling is generally faster than resizing, since it is part of the decode step, rather than a separate step of its own. It also supports PNG and WebP (except animated) images as well as JPEG. The trade-off right now is that, on Android 4.4 (KitKat) it uses more memory than resizing, while the decode is taking place. This should only be an issue for apps decoding a large number of images simultaneously. We hope to find a solution for this and make it the default in a future release. ### Which should you use and when? If the image is **not** much bigger than the view, then only scaling should be done. It's faster, easier to code, and results in a higher quality output. Of course, images smaller than the view are subset of those **not** much bigger than the view. Therefore, if you need to upscale the image, this should too be done by scaling, and not by resizing. That way memory won't be wasted on a larger bitmap that does not provide any better quality. However, for images much bigger than the view, such as **local camera images**, resizing in addition to scaling is highly recommended. As for what exactly "much bigger" means, as a rule of thumb if the image is more than 2 times bigger than the view (in total number of pixels, i.e. width*height), you should resize it. This almost always applies for local images taken by camera. For example, a device with the screen size of 1080 x 1920 pixels (roughly 2MP) and a camera of 16MP produces images 8 times bigger than the display. Without any doubt resizing in such cases is always best to be done. For network images, try to download an image as close as possible to the size you will be displaying. By downloading images of inappropriate size you are wasting the user's time and data. If the image is bigger than the view, by not resizing it the memory gets wasted. However, there is also a performance trade-off to be considered. Clearly, resizing imposes additional CPU cost on its own. But, by not resizing images bigger than the view, more bytes need to be transferred to the GPU, and images get evicted from the bitmap cache more often resulting in more decodes. In other words, not resizing when you should also imposes additional CPU cost. Therefore, there is no silver bullet and depending on the device characteristics there is a threshold point after which it becomes more performant to go with resize than without it. ### Example The Fresco showcase app has a [ImagePipelineResizingFragment](https://github.com/facebook/fresco/blob/main/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/imagepipeline/ImagePipelineResizingFragment.java) that demonstrates using placeholder, failure and retry images. ![Showcase app with resized example image](/static/images/docs/01-resizing-sample.png) ================================================ FILE: docs/_docs/rotation.md ================================================ --- docid: rotation title: Rotation layout: docs permalink: /docs/rotation.html --- You can rotate images by specifying a rotation angle in the image request, like so: ```java final ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(uri) .setRotationOptions(RotationOptions.forceRotation(RotationOptions.ROTATE_90)) .build(); mSimpleDraweeView.setController( Fresco.newDraweeControllerBuilder() .setImageRequest(imageRequest) .build()); ``` ### Auto-rotation JPEG files sometimes store orientation information in the image metadata. If you want images to be automatically rotated to match the device's orientation, you can do so in the image request: ```java final ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(uri) .setRotationOptions(RotationOptions.autoRotate()) .build(); mSimpleDraweeView.setController( Fresco.newDraweeControllerBuilder() .setImageRequest(imageRequest) .build()); ``` ### Combining rotations If you're loading a JPEG file that has rotation information in its EXIF data, calling `forceRotation` will **add** to the default rotation of the image. For example, if the EXIF header specifies 90 degrees, and you call `forceRotation(ROTATE_90)`, the raw image will be rotated 180 degrees. ### Examples The Fresco showcase app has a [DraweeRotationFragment](https://github.com/facebook/fresco/blob/main/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/drawee/DraweeRotationFragment.java) that demonstrates the various rotation settings. You can use it for example with the sample images [from here](https://github.com/recurser/exif-orientation-examples). ![Showcase app with a rotation sample](/static/images/docs/01-rotation-sample.png) ================================================ FILE: docs/_docs/rounded-corners-and-circles.md ================================================ --- docid: rounded-corners-and-circles title: Rounded Corners and Circles layout: docs permalink: /docs/rounded-corners-and-circles.html --- Not every image is a rectangle. Apps frequently need images that appear with softer, rounded corners, or as circles. Drawee supports a variety of scenarios, all without the memory overhead of copying bitmaps. ### What Images can be rounded in two shapes: 1. As a circle - set `roundAsCircle` to true. 2. As a rectangle, but with rounded corners. Set `roundedCornerRadius` to some value. Rectangles support having each of the four corners have a different radius, but this must be specified in Java code rather than XML. ### How Images can be rounded with two different methods: 1. `BITMAP_ONLY` - Uses a bitmap shader to draw the bitmap with rounded corners. This is the default rounding method. It doesn't support animations, and it does **not** support any scale types other than `centerCrop` (the default), `focusCrop` and `fit_xy`. If you use this rounding method with other scale types, such as `center`, you won't get an Exception but the image might look wrong (e.g. repeated edges due to how Android shaders work), especially in cases the source image is smaller than the view. See the Caveats section below. 2. `OVERLAY_COLOR` - Draws rounded corners by overlaying a solid color, specified by the caller. The Drawee's background should be static and of the same solid color. Use `roundWithOverlayColor` in XML, or `setOverlayColor` in code to use this rounding method. ### In XML The `SimpleDraweeView` class will forward several attributes over to `RoundingParams`: ```xml ``` ### In code When constructing a hierarchy, you can pass an instance of [RoundingParams](../javadoc/reference/com/facebook/drawee/generic/RoundingParams.html) to your `GenericDraweeHierarchyBuilder:` ```java RoundingParams roundingParams = RoundingParams.fromCornersRadius(7f); mSimpleDraweeView.setHierarchy(new GenericDraweeHierarchyBuilder(getResources()) .setRoundingParams(roundingParams) .build()); ``` You can also change all of the rounding parameters after the hierarchy has been built: ```java int color = getResources().getColor(R.color.red); RoundingParams roundingParams = RoundingParams.fromCornersRadius(5f); roundingParams.setBorder(color, 1.0f); roundingParams.setRoundAsCircle(true); mSimpleDraweeView.getHierarchy().setRoundingParams(roundingParams); ``` ### Caveats There are some limitations when `BITMAP_ONLY` (the default) mode is used: - Only images that resolve to `BitmapDrawable` or `ColorDrawable` can be rounded. Rounding `NinePatchDrawable`, `ShapeDrawable` and other such drawables is not supported (regardless whether they are specified in XML or programmatically). - Animations are not rounded. - Due to a limitation of Android's `BitmapShader`, if the image doesn't fully cover the view, instead of drawing nothing, edges are repeated. One workaround is to use a different scale type (e.g. centerCrop) that ensures that the whole view is covered. Another workaround is to make the image file contain a 1px transparent border so that the transparent pixels get repeated. This is the best solution for PNG resource images. If the limitations of the `BITMAP_ONLY` mode affect your images, see if the `OVERLAY_COLOR` mode works for you. The `OVERLAY_COLOR` mode doesn't have the aforementioned limitations, but since it simulates rounded corners by overlaying a solid color over the image, this only looks good if the background under the view is static and of the same color. Drawee internally has an implementation for `CLIPPING` mode, but this mode has been disabled and not exposed as some `Canvas` implementation do not support path clipping. Furthermore, canvas clipping doesn't support antialiasing which makes the rounded edges very pixelated. Finally, all of those issues could be avoided by using a temporary bitmap, but this imposes a significant memory overhead and has not been supported because of that. As explained above, there is no really good solution for rounding corners on Android and one has to choose between the aforementioned trade-offs. ### Full Sample For a full sample see the `DraweeRoundedCornersFragment` in the showcase app: [DraweeRoundedCornersFragment.java](https://github.com/facebook/fresco/blob/main/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/drawee/DraweeRoundedCornersFragment.java) ![Showcase app with a scale type example](/static/images/docs/01-rounded-corners-and-circles-sample.png) ================================================ FILE: docs/_docs/sample-apps.md ================================================ --- docid: sample-code title: Sample code layout: docs permalink: /docs/sample-code.html --- *Note: the samples are licensed for non-commercial or evaluation purposes only, not the MIT license used for Fresco itself.* Fresco's GitHub repository contains several samples to demonstrate how to use Fresco in your apps. The samples are available in source form only. Follow the [build instructions](building-from-source.html) to set up your dev environment to build and run them. ### The Showcase app The [Showcase App](https://github.com/facebook/fresco/blob/main/samples/showcase) demonstrates various features and allows to customize parameters to show their effect. It includes samples for Drawee and for the image pipeline. Furthermore, it showcases how to use both built-in and custom image formats. ### The zoomable library The [zoomable library](https://github.com/facebook/fresco/blob/main/samples/zoomable) features a `ZoomableDraweeView` class that supports gestures such as pinch-to-zoom and panning of a Drawee image. ### The comparison app The comparison app lets the user do a proper, apples-to-apples comparison of Fresco with [Picasso](http://square.github.io/picasso), [Universal Image Loader](https://github.com/nostra13/Android-Universal-Image-Loader), [Volley](https://developer.android.com/training/volley/index.html)'s image loader, and [Glide](https://github.com/bumptech/glide). Fresco allows you to also compare its performance with OkHttp as its network layer. You can also see the performance of Drawee running over Volley instead of Fresco's image pipeline. The app offers you a choice of images from your local camera or from the Internet. The network images come from [Imgur](http://imgur.com). You can build, install, and run a controlled test of any combination of loaders using the [run_comparison.py](https://github.com/facebook/fresco/blob/main/run_comparison.py) script. The following command will run them all on a connected ARM v7 device: ```./run_comparison.py -c armeabi-v7a``` ### The round app The round app shows the same image scaled in several different ways, with and without a circle applied. ================================================ FILE: docs/_docs/scaletypes.md ================================================ --- docid: scaletypes title: ScaleTypes layout: docs permalink: /docs/scaletypes.html --- You can specify a different scale type for each of the different drawables in your Drawee. ### Available Scale Types | Scale Type | Preserves Aspect Ratio | Always Fills Entire View | Performs Scaling | Explanation | | --------- | :-: | :-: | :-: | ----------- | | center | ✓ | | | Center the image in the view, but perform no scaling. | | centerCrop | ✓ | ✓ | ✓ | Scales the image so that both dimensions will be greater than or equal
to the corresponding dimension of the parent.
One of width or height will fit exactly.
The image is centered within parent's bounds. | | focusCrop | ✓ | ✓ | ✓ | Same as centerCrop, but based around
a caller-specified focus point instead of the center. | centerInside | ✓ | | ✓ | Downscales the image so that it fits entirely inside the parent.
Unlike `fitCenter,` no upscaling will be performed.
Aspect ratio is preserved.
The image is centered within parent's bounds. | | fitCenter | ✓ | | ✓ | Scales the image so that it fits entirely inside the parent.
One of width or height will fit exactly.
Aspect ratio is preserved.
The image is centered within the parent's bounds. | | fitStart | ✓ | | ✓ | Scales the image so that it fits entirely inside the parent.
One of width or height will fit exactly.
Aspect ratio is preserved.
The image is aligned to the top-left corner of the parent. | fitEnd | ✓ | | ✓ | Scales the image so that it fits entirely inside the parent.
One of width or height will fit exactly.
Aspect ratio is preserved.
The image is aligned to the bottom-right corner of the parent. | fitXY | | ✓ | ✓ | Scales width and height independently.
The image will match the parent exactly.
Aspect ratio is not preserved. | none | ✓ | | | Used for Android's tile mode. | These are mostly the same as those supported by the Android [ImageView](http://developer.android.com/reference/android/widget/ImageView.ScaleType.html) class. The one unsupported type is `matrix`. In its place, Fresco offers `focusCrop,` which will usually work better. ### How to Set a Scale Type ScaleTypes of actual, placeholder, retry, and failure images can all be set in XML, using attributes like `fresco:actualImageScaleType`. You can also set them in code using the [GenericDraweeHierarchyBuilder](../javadoc/reference/com/facebook/drawee/generic/GenericDraweeHierarchyBuilder.html) class. Even after your hierarchy is built, the actual image scale type can be modified on the fly using [GenericDraweeHierarchy](../javadoc/reference/com/facebook/drawee/generic/GenericDraweeHierarchy.html). However, do **not** use the `android:scaleType` attribute, nor the `.setScaleType` method. These have no effect on Drawees. ### Scale Type: "focusCrop" Android, and Fresco, offer a `centerCrop` scale type, which will fill the entire viewing area while preserving the aspect ratio of the image, cropping as necessary. This is very useful, but the trouble is the cropping doesn't always happen where you need it. If, for instance, you want to crop to someone's face in the bottom right corner of the image, `centerCrop` will do the wrong thing. By specifying a focus point, you can say which part of the image should be centered in the view. If you specify the focus point to be at the top of the image, such as (0.5f, 0f), we guarantee that, no matter what, this point will be visible and centered in the view as much as possible. Focus points are specified in a relative coordinate system. That is, (0f, 0f) is the top-left corner, and (1f, 1f) is the bottom-right corner. Relative coordinates allow focus points to be scale-invariant, which is highly useful. A focus point of (0.5f, 0.5f) is equivalent to a scale type of `centerCrop.` To use focus points, you must first set the right scale type in your XML: ```xml fresco:actualImageScaleType="focusCrop" ``` In your Java code, you must programmatically set the correct focus point for your image: ```java PointF focusPoint = new PointF(0f, 0.5f); mSimpleDraweeView .getHierarchy() .setActualImageFocusPoint(focusPoint); ``` ### ScaleType: "none" If you are using Drawables that make use of Android's tile mode, you need to use the `none` scale type for this to work correctly. ### Scale Type: A Custom ScaleType Sometimes you need to scale the image in a way that none of the existing scale types does. Drawee allows you to do that easily by implementing your own `ScalingUtils.ScaleType`. There is only one method in that interface, `getTransform`, which is supposed to compute the transformation matrix based on: * parent bounds (rectangle where the image should be placed in the view's coordinate system) * child size (width and height of the actual bitmap) * focus point (relative coordinates in the child's coordinate system) Of course, your class can contain any additional data you might need to compute the transformation. Let's look at an example. Assume the `parentBounds` are `(100, 150, 500, 450)`, and the child dimensions are `(420,210)`. Observe that the parent width is `500 - 100 = 400`, and the height is `450 - 150 = 300`. If we don't do any transformation (i.e. we set the transformation to be the identity matrix), the image will be drawn in `(0, 0, 420, 210)`. But `ScaleTypeDrawable` has to respect the bounds imposed by the parent and will so clip the canvas to `(100, 150, 500, 450)`. That means that only the bottom-right part of the image will actually be visible: `(100, 150, 420, 210)`. We can fix that by doing a translation by `(parentBounds.left, parentBounds.top)`, which is in this case `(100, 150)`. But now the right part of the image got clipped as the image is actually wider than the parent bounds! Image is now placed at `(100, 150, 500, 360)` in the view coordinates, or equivalently `(0, 0, 400, 210)` in the child coordinates. We lost `20` pixels on the right. To avoid image to be clipped we can downscale it. Here we can scale by `400/420` which will make the image be of the size `(400,200)`. The image now fits exactly in the view horizontally, but it is not centered in it vertically. In order to center the image we need to translate it a bit more. We can see that the amount of empty space in the parent bounds is `400 - 400 = 0` horizontally, and `300 - 200 = 100` vertically. If we translate by half of this empty space, we will leave equal amount of empty space on each side, effectively making the image centered in the parent bounds. Congratulations! You just implemented the `FIT_CENTER` scale type: ```java public class AbstractScaleType implements ScaleType { @Override public Matrix getTransform(Matrix outTransform, Rect parentRect, int childWidth, int childHeight, float focusX, float focusY) { // calculate scale; we take the smaller of the horizontal and vertical scale factor so that the image always fits final float scaleX = (float) parentRect.width() / (float) childWidth; final float scaleY = (float) parentRect.height() / (float) childHeight; final float scale = Math.min(scaleX, scaleY); // calculate translation; we offset by parent bounds, and by half of the empty space // note that the child dimensions need to be adjusted by the scale factor final float dx = parentRect.left + (parentRect.width() - childWidth * scale) * 0.5f; final float dy = parentRect.top + (parentRect.height() - childHeight * scale) * 0.5f; // finally, set and return the transform outTransform.setScale(scale, scale); outTransform.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); return outTransform; } } ``` ### Full Sample For a full sample see the `DraweeScaleTypeFragment` in the showcase app: [DraweeScaleTypeFragment.java](https://github.com/facebook/fresco/blob/main/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/drawee/DraweeScaleTypeFragment.java) ![Showcase app with a scale type example](/static/images/docs/01-scaletypes-sample-1.png) ![Showcase app with a scale type example](/static/images/docs/01-scaletypes-sample-2.png) ================================================ FILE: docs/_docs/shared-transitions.md ================================================ --- docid: shared-transitions title: Shared Transitions layout: docs permalink: /docs/shared-transitions.html --- ## Use ChangeBounds, not ChangeImageTransform Android 5.0 (Lollipop) introduced [shared element transitions](http://developer.android.com/training/material/animations.html#Transitions), allowing apps to share a View between multiple Activities and define a transition between them. You can define your transitions in XML. There is a transform called ChangeImageTransform which captures an ImageView's matrix and animates it during the transition. This will not work in Fresco, which has its own set of matrices to scale with. Fortunately there is an easy workaround. Just use the [ChangeBounds](http://developer.android.com/reference/android/transition/ChangeBounds.html) transition instead. This animates the changes in the layout *bounds*. Fresco will automatically adjust the scaling matrix as you update the bounds, so your animation will appear exactly as you want it. ================================================ FILE: docs/_docs/supported-uris.md ================================================ --- docid: supported-uris title: Supported URIs layout: docs permalink: /docs/supported-uris.html --- Fresco supports images in a variety of locations. Fresco does **not** accept relative URIs. All URIs must be absolute and must include the scheme. These are the URI schemes accepted: | Type | Scheme | Fetch method used | ---------------- | ------- | ------------- | | File on network | `http://,` `https://` | `HttpURLConnection` or [network layer](using-other-network-layers.html) | | File on device | `file://` | `FileInputStream` | | Content provider | `content://` | `ContentResolver` | | Asset in app | `asset://` | `AssetManager` | | Resource in app | `res://` as in `res:///12345` | `Resources.openRawResource` | | Data in URI | `data:mime/type;base64,` | Following [data URI spec](http://tools.ietf.org/html/rfc2397) (UTF-8 only) |
Note: Only image resources can be used with the image pipeline (e.g. a PNG image). Other resource types such as Strings or XML Drawables make no sense in the context of the image pipeline and so cannot be supported by definition. One potentially confusing case is drawable declared in XML (e.g. ShapeDrawable). Important thing to note is that this is **not** an image. If you want to display an XML drawable as the main image, then set it as a [placeholder](placeholder-failure-retry.html) and use the `null` uri. ### Sample: Loading an URI For a sample that just loads an URI see the `DraweeSimpleFragment` in the showcase app: [DraweeSimpleFragment.java](https://github.com/facebook/fresco/blob/main/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/drawee/DraweeSimpleFragment.java) ![A simple URI sample](/static/images/docs/01-using-simpledraweeview-sample.png) ### Sample: Loading a Local File For a sample on how to correctly load user-selected files (e.g. using the `content://` URI) see the `DraweeMediaPickerFragment` in the showcase app: [DraweeMediaPickerFragment.java](https://github.com/facebook/fresco/blob/main/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/drawee/DraweeMediaPickerFragment.java) ![A sample with local files](/static/images/docs/01-supported-uris-sample-local-file.png) ### Sample: Loading a Data URI The Fresco showcase app has a [ImageFormatDataUriFragment](https://github.com/facebook/fresco/blob/main/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/imageformat/datauri/ImageFormatDataUriFragment.java) that demonstrates using placeholder, failure and retry images. ![A data URI sample](/static/images/docs/01-supported-uris-sample-data-uri.png) ### More **Tip:** You can override the displayed image URI in many samples in the showcase app by using the *URI Override* option in the global settings: ![The URI override setting](/static/images/docs/01-supported-uris-sample-override.png) ================================================ FILE: docs/_docs/troubleshooting.md ================================================ --- docid: troubleshooting title: Troubleshooting layout: docs permalink: /docs/troubleshooting.html --- ## Troubleshooting ### Image is displayed with repeated edges This is a known limitation when rounding is used. See [Rounding](http://frescolib.org/docs/rounded-corners-and-circles.html#_) for more information and how to workaround. ### Image doesn't load You can get more information from the image pipeline by examining the verbose logcat as explained later in this section. Here are some common reasons why image loads might fail: #### File not available For example, an incorrect path for local files or an unavailable network URI is given. Try opening a network URI in a mobile browser. If it doesn't work, the issue is likely neither in Fresco nor your app. For a local file, try opening a file input stream directly from your app: ``` FileInputStream fis = new FileInputStream(new File(localUri.getPath())); ``` If that throws an exception, the issue is likely not in Fresco, **but** it may be in your app. One possibility is a permission issue, such as trying to access the SD card without requiring the necessary permission in your application manifest. Another possibility is that the pathy is not correct - perhaps you forgot to properly escape it. Finally, the file may simply not exist. #### OOMs and failing to allocate a bitmap The most common reason for this happening is loading too big images. If the image to be loaded is of considerably bigger size than the view hosting it, it should be [resized](resizing.html). #### Bitmap too large to be uploaded to a texture Android cannot display images more than 2048 pixels long in either dimension. This is beyond the capability of the OpenGL rendering system. Fresco will resize your image if it exceeds this limit. ### Investigating issues with logcat There are various issues one might encounter when it comes to image handling. With Fresco, most of them can be diagnosed by simply looking at the `VERBOSE` logcat. This should be your starting point when investigating an issue with Fresco. #### Setting up logcat By default, Fresco does not write out all its logs. You need to [configure the image pipeline](configure-image-pipeline.html#_) to do so. ```java Set requestListeners = new HashSet<>(); requestListeners.add(new RequestLoggingListener()); ImagePipelineConfig config = ImagePipelineConfig.newBuilder(context) // other setters .setRequestListeners(requestListeners) .build(); Fresco.initialize(context, config); FLog.setMinimumLoggingLevel(FLog.VERBOSE); ``` #### Examining logcat All of Fresco's logs can be examined by this command: ``` adb logcat -v threadtime | grep -iE 'LoggingListener|AbstractDraweeController|BufferedDiskCache' ``` The output shows what is happening with the image requests within the image pipeline. It looks something like this: ``` 08-12 09:11:14.791 6690 6690 V unknown:AbstractDraweeController: controller 28ebe0eb 0 -> 1: initialize 08-12 09:11:14.791 6690 6690 V unknown:AbstractDraweeController: controller 28ebe0eb 1: onDetach 08-12 09:11:14.791 6690 6690 V unknown:AbstractDraweeController: controller 28ebe0eb 1: setHierarchy: null 08-12 09:11:14.791 6690 6690 V unknown:AbstractDraweeController: controller 28ebe0eb 1: setHierarchy: com.facebook.drawee.generic.GenericDraweeHierarchy@2bb88e4 08-12 09:11:14.791 6690 6690 V unknown:AbstractDraweeController: controller 28ebe0eb 1: onAttach: request needs submit 08-12 09:11:14.791 6690 6690 V unknown:PipelineDraweeController: controller 28ebe0eb: getDataSource 08-12 09:11:14.791 6690 6690 V unknown:RequestLoggingListener: time 11201791: onRequestSubmit: {requestId: 1, callerContext: null, isPrefetch: false} 08-12 09:11:14.792 6690 6690 V unknown:RequestLoggingListener: time 11201791: onProducerStart: {requestId: 1, producer: BitmapMemoryCacheGetProducer} 08-12 09:11:14.792 6690 6690 V unknown:RequestLoggingListener: time 11201792: onProducerFinishWithSuccess: {requestId: 1, producer: BitmapMemoryCacheGetProducer, elapsedTime: 1 ms, extraMap: {cached_value_found=false}} 08-12 09:11:14.792 6690 6690 V unknown:RequestLoggingListener: time 11201792: onProducerStart: {requestId: 1, producer: BackgroundThreadHandoffProducer} 08-12 09:11:14.792 6690 6690 V unknown:AbstractDraweeController: controller 28ebe0eb 1: submitRequest: dataSource: 36e95857 08-12 09:11:14.792 6690 6734 V unknown:RequestLoggingListener: time 11201792: onProducerFinishWithSuccess: {requestId: 1, producer: BackgroundThreadHandoffProducer, elapsedTime: 0 ms, extraMap: null} 08-12 09:11:14.792 6690 6734 V unknown:RequestLoggingListener: time 11201792: onProducerStart: {requestId: 1, producer: BitmapMemoryCacheProducer} 08-12 09:11:14.792 6690 6734 V unknown:RequestLoggingListener: time 11201792: onProducerFinishWithSuccess: {requestId: 1, producer: BitmapMemoryCacheProducer, elapsedTime: 0 ms, extraMap: {cached_value_found=false}} 08-12 09:11:14.792 6690 6734 V unknown:RequestLoggingListener: time 11201792: onProducerStart: {requestId: 1, producer: EncodedMemoryCacheProducer} 08-12 09:11:14.792 6690 6734 V unknown:RequestLoggingListener: time 11201792: onProducerFinishWithSuccess: {requestId: 1, producer: EncodedMemoryCacheProducer, elapsedTime: 0 ms, extraMap: {cached_value_found=false}} 08-12 09:11:14.792 6690 6734 V unknown:RequestLoggingListener: time 11201792: onProducerStart: {requestId: 1, producer: DiskCacheProducer} 08-12 09:11:14.792 6690 6735 V unknown:BufferedDiskCache: Did not find image for http://www.example.com/image.jpg in staging area 08-12 09:11:14.793 6690 6735 V unknown:BufferedDiskCache: Disk cache read for http://www.example.com/image.jpg 08-12 09:11:14.793 6690 6735 V unknown:BufferedDiskCache: Disk cache miss for http://www.example.com/image.jpg 08-12 09:11:14.793 6690 6735 V unknown:RequestLoggingListener: time 11201793: onProducerFinishWithSuccess: {requestId: 1, producer: DiskCacheProducer, elapsedTime: 1 ms, extraMap: {cached_value_found=false}} 08-12 09:11:14.793 6690 6735 V unknown:RequestLoggingListener: time 11201793: onProducerStart: {requestId: 1, producer: NetworkFetchProducer} 08-12 09:11:15.161 6690 7358 V unknown:RequestLoggingListener: time 11202161: onProducerFinishWithSuccess: {requestId: 1, producer: NetworkFetchProducer, elapsedTime: 368 ms, extraMap: null} 08-12 09:11:15.162 6690 6742 V unknown:BufferedDiskCache: About to write to disk-cache for key http://www.example.com/image.jpg 08-12 09:11:15.162 6690 6734 V unknown:RequestLoggingListener: time 11202162: onProducerStart: {requestId: 1, producer: DecodeProducer} 08-12 09:11:15.163 6690 6742 V unknown:BufferedDiskCache: Successful disk-cache write for key http://www.example.com/image.jpg 08-12 09:11:15.169 6690 6734 V unknown:RequestLoggingListener: time 11202169: onProducerFinishWithSuccess: {requestId: 1, producer: DecodeProducer, elapsedTime: 7 ms, extraMap: {hasGoodQuality=true, queueTime=0, bitmapSize=600x400, isFinal=true}} 08-12 09:11:15.169 6690 6734 V unknown:RequestLoggingListener: time 11202169: onRequestSuccess: {requestId: 1, elapsedTime: 378 ms} 08-12 09:11:15.184 6690 6690 V unknown:AbstractDraweeController: controller 28ebe0eb 1: set_final_result @ onNewResult: image: CloseableReference 2fd41bb0 ``` In this case, we see that the controller `28ebe0eb` associated with a DraweeView started datasource `36e95857` which issued image request `1`. We can now see that the image was not found in the bitmap cache, nor in the encoded memory cache, nor in the disk cache, and so the network fetch had to be performed. The fetch was successful, the image was decoded and the request finished successfully. Finally, the datasource notified the controller which then set the resulting image to the hierarchy (`set_final_result`). ================================================ FILE: docs/_docs/using-controllerbuilder.md ================================================ --- docid: using-controllerbuilder title: Using the ControllerBuilder layout: docs permalink: /docs/using-controllerbuilder.html --- `SimpleDraweeView` has two methods for specifying an image. The easy way is to just call `setImageURI.` If you want more control over how the Drawee displays your image, you can use a [DraweeController](concepts.html). This page explains how to build and use one. ### Building a DraweeController Pass the uri to a [PipelineDraweeControllerBuilder](../javadoc/reference/com/facebook/drawee/backends/pipeline/PipelineDraweeControllerBuilder.html). Then specify additional options for the controller: ```java ControllerListener listener = new BaseControllerListener() {...} DraweeController controller = Fresco.newDraweeControllerBuilder() .setUri(uri) .setTapToRetryEnabled(true) .setOldController(mSimpleDraweeView.getController()) .setControllerListener(listener) .build(); mSimpleDraweeView.setController(controller); ``` You should call `setOldController` when building a new controller. This will allow for the old controller to be reused and a couple of unnecessary memory allocations to be avoided. More details: * [Controller Listeners](listening-to-events.html) ### Customizing the ImageRequest For still more advanced usage, you might need to set an [ImageRequest](../javadoc/reference/com/facebook/imagepipeline/request/ImageRequest.html) to the pipeline, instead of merely a URI. An example of this is using a [postprocessor](modifying-image.html). ```java Uri uri; Postprocessor myPostprocessor = new Postprocessor() { ... } ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri) .setPostprocessor(myPostprocessor) .build(); DraweeController controller = Fresco.newDraweeControllerBuilder() .setImageRequest(request) .setOldController(mSimpleDraweeView.getController()) // other setters as you need .build(); ``` More details: * [Postprocessors](modifying-image.html) * [Requesting Multiple Images](requesting-multiple-images.html) * [Resizing](resizing.html) * [Rotation](rotation.html) ================================================ FILE: docs/_docs/using-image-pipeline.md ================================================ --- docid: using-image-pipeline title: Using the Image Pipeline Directly layout: docs permalink: /docs/using-image-pipeline.html --- This page is intended for advanced usage only. Most apps should be using [Drawees](using-simpledraweeview.html) to interact with Fresco's image pipeline. Using the image pipeline directly is challenging because of the memory usage. Drawees automatically keep track of whether or not your images need to be in memory. They will swap them out and load them back as soon as they need to be displayed. If you are using the image pipeline directly, your app must repeat this logic. The image pipeline returns objects wrapped in a [CloseableReference](closeable-references.html). Drawees keep these references alive for as long as they need their image, and then call the `.close()` method on these references when they are finished with them. If your app is not using Drawees, it **must** do the same. If you do not keep a Java reference to a `CloseableReference` returned by the pipleine, the `CloseableReference` will get garbage collected and the underlying `Bitmap` may get recycled while still being used. If you do not close the `CloseableReference` once you are done with it, you risk memory leaks and OOMs. To be precise, the Java garbage collector will free image memory when Bitmap objects go out of scope, but this may be too late. Garbage collection is expensive, and relying on it for large objects leads to performance issues. This is especially true on Android 4.x and lower, when Android did not maintain a separate memory space for Bitmaps. ### Calling the pipeline You must [build an image request](image-requests.html). Having done that, you can pass it directly to the `ImagePipeline:` ```java ImagePipeline imagePipeline = Fresco.getImagePipeline(); DataSource> dataSource = imagePipeline.fetchDecodedImage(imageRequest, callerContext); ``` See the page on [DataSources](datasources-datasubscribers.html) for information on how to receive data from them. ### Skipping the decode If you don't want to decode the image, but want to get the image bytes in their original compressed format, just use `fetchEncodedImage` instead: ```java DataSource> dataSource = imagePipeline.fetchEncodedImage(imageRequest, callerContext); ``` ### Instant results from the bitmap cache Lookups to the bitmap cache, unlike the others, are done in the UI thread. If a Bitmap is there, you get it instantly. ```java DataSource> dataSource = imagePipeline.fetchImageFromBitmapCache(imageRequest, callerContext); try { CloseableReference imageReference = dataSource.getResult(); if (imageReference != null) { try { // Do something with the image, but do not keep the reference to it! // The image may get recycled as soon as the reference gets closed below. // If you need to keep a reference to the image, read the following sections. } finally { CloseableReference.closeSafely(imageReference); } } else { // cache miss ... } } finally { dataSource.close(); } ``` ### Synchronous image loading In a similar way to how you can immediately retrieve images from the bitmap cache, it is also possible to load an image from the network synchronously using `DataSources.waitForFinalResult()`. ```java DataSource> dataSource = imagePipeline.fetchImageFromBitmapCache(imageRequest, callerContext); try { CloseableReference result = DataSources.waitForFinalResult(dataSource); if (result != null) { // Do something with the image, but do not keep the reference to it! // The image may get recycled as soon as the reference gets closed below. // If you need to keep a reference to the image, read the following sections. } } finally { dataSource.close(); } ``` Do **not** skip these `finally` blocks! ### The caller Context As we can see, most of the `ImagePipeline` fetch methods contains a second parameter named `callerContext` of type `Object`. We can see it as an implementation of the [Context Object Design Pattern](https://www.dre.vanderbilt.edu/~schmidt/PDF/Context-Object-Pattern.pdf). It's basically an object we bind to a specific `ImageRequest` that can be used for different purposes (e.g. Log). The same object can also be accessed by all the `Producer` implementations into the `ImagePipeline`. The caller Context can also be `null`. ================================================ FILE: docs/_docs/using-other-network-layers.md ================================================ --- docid: using-other-network-layers title: Using Other Network Layers layout: docs permalink: /docs/using-other-network-layers.html --- By default, the image pipeline uses the [HttpURLConnection](https://developer.android.com/training/basics/network-ops/connecting.html) which is included in the Android framework. However, if needed by the app a custom network layer can be used. Fresco already contains one alternative network layer that is based on OkHttp. ### Using OkHttp [OkHttp](http://square.github.io/okhttp) is a popular open-source networking library. ### 1. Gradle setup In order to use it, the `dependencies` section of your `build.gradle` file needs to be changed. Along with the Gradle dependencies given on the [Getting started](index.html) page, add **just one** of these: For OkHttp2: ```groovy dependencies { // your project's other dependencies implementation "com.facebook.fresco:imagepipeline-okhttp:{{site.current_version}}" } ``` For OkHttp3: ```groovy dependencies { // your project's other dependencies implementation "com.facebook.fresco:imagepipeline-okhttp3:{{site.current_version}}" } ``` #### 2. Configuring the image pipeline to use OkHttp You must also configure the image pipeline. Instead of using `ImagePipelineConfig.newBuilder`, use `OkHttpImagePipelineConfigFactory`: ```java Context context; OkHttpClient okHttpClient; // build on your own ImagePipelineConfig config = OkHttpImagePipelineConfigFactory .newBuilder(context, okHttpClient) . // other setters . // setNetworkFetcher is already called for you .build(); Fresco.initialize(context, config); ``` For a more detailed example of this, see how this if configured in the [Fresco showcase app](https://github.com/facebook/fresco/blob/main/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/ShowcaseApplication.java). ### Handling sessions and cookies correctly The `OkHttpClient` you pass to Fresco in the above step should be set up with interceptors needed to handle authentications to your servers. See [this bug](https://github.com/facebook/fresco/issues/385) and the solutions outlined there for some problems that have occurred with cookies. ### Using your own network fetcher (optional) For complete control on how the networking layer should behave, you can provide one for your app. You must subclass [NetworkFetcher](../javadoc/reference/com/facebook/imagepipeline/producers/NetworkFetcher.html), which controls communications to the network. You can also optionally subclass [FetchState](../javadoc/reference/com/facebook/imagepipeline/producers/FetchState.html), which is a data structure for request-specific information. Our implementation for `OkHttp 3` can be used as an example. See [its source code](https://github.com/facebook/fresco/blob/main/imagepipeline-backends/imagepipeline-okhttp3/src/main/java/com/facebook/imagepipeline/backends/okhttp3/OkHttpNetworkFetcher.java). You must pass your network producer to the image pipeline when [configuring it](configure-image-pipeline.html): ```java ImagePipelineConfig config = ImagePipelineConfig.newBuilder() .setNetworkFetcher(myNetworkFetcher); . // other setters .build(); Fresco.initialize(context, config); ``` ================================================ FILE: docs/_docs/using-simpledraweeview.md ================================================ --- docid: using-simpledraweeview title: Using SimpleDraweeView layout: docs permalink: /docs/using-simpledraweeview.html redirect_from: - /docs/using-drawees-xml.html - /docs/using-drawees-code.html --- When using Fresco, you will use `SimpleDraweeView` to display images. These can be used in XML layouts. The simplest usage example of `SimpleDraweeView` is: ```xml ``` **NOTE:** `SimpleDraweeView` does not support `wrap_content` for `layout_width` or `layout_height` attributes. More information can be found [here](faq.html). The only exception to this is when you are setting an aspect ratio, like so: ```xml ``` ### Loading an image The easiest way to load an image into a `SimpleDraweeView` is to call `setImageURI`: ```java mSimpleDraweeView.setImageURI(uri); ``` That's it, you are now displaying images with Fresco! ### Advanced XML attributes `SimpleDraweeView`, despite its name, supports a great deal of customization through XML attributes. The example below presents all of them: ```xml ``` ### Customizing from code Although it's generally recommended to set these options in XML, all of the attributes above can also be set from code. In order to do this, you will need to create a `DraweeHierarchy` before setting the image URI: ```java GenericDraweeHierarchy hierarchy = GenericDraweeHierarchyBuilder.newInstance(getResources()) .setActualImageColorFilter(colorFilter) .setActualImageFocusPoint(focusPoint) .setActualImageScaleType(scaleType) .setBackground(background) .setDesiredAspectRatio(desiredAspectRatio) .setFadeDuration(fadeDuration) .setFailureImage(failureImage) .setFailureImageScaleType(scaleType) .setOverlays(overlays) .setPlaceholderImage(placeholderImage) .setPlaceholderImageScaleType(scaleType) .setPressedStateOverlay(overlay) .setProgressBarImage(progressBarImage) .setProgressBarImageScaleType(scaleType) .setRetryImage(retryImage) .setRetryImageScaleType(scaleType) .setRoundingParams(roundingParams) .build(); mSimpleDraweeView.setHierarchy(hierarchy); mSimpleDraweeView.setImageURI(uri); ``` **NOTE:** some of these options can be set on an existing hierarchy without having to build a new one. To do this, simply get the hierarchy from a `SimpleDraweeView` and call any of the setter methods on it, e.g.: ```java mSimpleDraweeView.getHierarchy().setPlaceHolderImage(placeholderImage); ``` ### Full Sample For a full sample see the `DraweeSimpleFragment` in the showcase app: [DraweeSimpleFragment.java](https://github.com/facebook/fresco/blob/main/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/drawee/DraweeSimpleFragment.java) ![Showcase app with a scale type example](/static/images/docs/01-using-simpledraweeview-sample.png) ================================================ FILE: docs/_docs/webp-support.md ================================================ --- docid: webp-support title: WebP Images layout: docs permalink: /docs/webp-support.html --- [WebP](https://en.wikipedia.org/wiki/WebP) is an image format that supports lossy and lossless compressions. Furthermore, it allows for transparency and animations. ### Support on Android Android added WebP support in version 4.0 and improved it in 4.2.1: * 4.0+ (Ice Cream Sandwich) have basic webp support * 4.2.1+ (Jelly Beam MR1) have support for transparency and lossless WebP By adding the Fresco webpsupport module, apps can display all kinds of WebP images on all versions of Android: | Configuration | Basic WebP | Lossless or Transparent WebP | Animated WebP | |--- |:-: |:-: |:-: | | OS < 4.0 | | | | | OS >= 4.0 | ✓ | | | | OS >= 4.2.1 | ✓ | ✓ | | | Any OS + webpsupport | ✓ | ✓ | | | Any OS + animated-webp | ✓ | (✓ if webpsupport or OS >= 4.2.1) | ✓ | ### Adding Support for Static WebP images on Older Versions The only thing you need to do is add the `webpsupport` library to your dependencies. This adds support for all types of non-animated WebP images. E.g. you can use it to display transparent WebP images on Gingerbread. ```groovy dependencies { // ... your app's other dependencies implementation 'com.facebook.fresco:webpsupport:{{site.current_version}}' } ``` ### Animated WebP In order to display animated WebP images, you have to add the following dependencies: ```groovy dependencies { // ... your app's other dependencies implementation 'com.facebook.fresco:animated-webp:{{site.current_version}}' implementation 'com.facebook.fresco:webpsupport:{{site.current_version}}' } ``` You can then load the animated WebP images like any other URI. In order to auto-start the animation, you can set `setAutoPlayAnimations(true)` on the `DraweeController`: ```java DraweeController controller = Fresco.newDraweeControllerBuilder() .setUri("http://example.org/somefolder/animated.webp") .setAutoPlayAnimations(true) .build(); mSimpleDraweeView.setController(controller); ``` ### Full Sample For the full sample see the `ImageFormatWebpFragment` in the showcase app: [ImageFormatWebpFragment.java](https://github.com/facebook/fresco/blob/main/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/imageformat/webp/ImageFormatWebpFragment.java) ![Showcase app with a notification](/static/images/docs/03-webp-support-sample.png) ================================================ FILE: docs/_docs/writing-custom-views.md ================================================ --- docid: writing-custom-views title: Writing Custom Views layout: docs permalink: /docs/writing-custom-views.html --- ### DraweeHolders There will always be times when `DraweeViews` won't fit your needs. You may need to show additional content inside the same view as your image. You might need to show multiple images inside a single view. We provide two alternative classes you can use to host your Drawee: * `DraweeHolder` for a single image * `MultiDraweeHolder` for multiple images `DraweeHolder` is a class that holds one DraweeHierarchy and the associated DraweeController. It allows you to make use of all the functionality Drawee provides in your custom views and other places where you need a drawable instead of a view. To get the drawable, you just do `mDraweeHolder.getTopLevelDrawable()`. Keep in mind that Android drawables require a bit of housekeeping which we covered below. `MultiDraweeHolder` is basically just an array of `DraweeHolder`s with some syntactic sugar added on top of it. ### Responsibilities of custom views Android lays out View objects, and only they get notified of system events. `DraweeViews` handle these events and use them to manage memory effectively. When using the holders, you must implement some of this functionality yourself. #### Handling attach/detach events **Your app may leak memory, or the image may not be displayed at all, if these steps are not followed.** There is no point in images staying in memory when Android is no longer displaying the view - it may have scrolled off-screen, or otherwise not be drawing. Drawees listen for detaches and release memory when they occur. They will automatically restore the image when it comes back on-screen. All this is automatic in a `DraweeView,` but won't happen in a custom view unless you handle four system events. These must be passed to the `DraweeHolder`. Here's how: ```java DraweeHolder mDraweeHolder; @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); mDraweeHolder.onDetach(); } @Override public void onStartTemporaryDetach() { super.onStartTemporaryDetach(); mDraweeHolder.onDetach(); } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); mDraweeHolder.onAttach(); } @Override public void onFinishTemporaryDetach() { super.onFinishTemporaryDetach(); mDraweeHolder.onAttach(); } ``` It is important that `Holder` receives all the attach/detach events that the view itself receives. If the holder misses an attach event the image may not be displayed because Drawee will think that the view is not visible. Likewise, if the holder misses an detach event, the image may still remain in memory because Drawee will think that the view is still visible. Best way to ensure that is to create the holder from your view's constructor. #### Handling touch events If you have enabled [tap to retry](placeholder-failure-retry.html) in your Drawee, it will not work unless you tell it that the user has touched the screen. Like this: ```java @Override public boolean onTouchEvent(MotionEvent event) { return mDraweeHolder.onTouchEvent(event) || super.onTouchEvent(event); } ``` #### Your custom onDraw You must call ```java Drawable drawable = mDraweeHolder.getTopLevelDrawable(); drawable.setBounds(...); ... drawable.draw(canvas); ``` or the Drawee won't appear at all. * Do not downcast this Drawable. The underlying implementation may change without any notice. * Do not translate it. Just set the proper bounds. * If you need to apply some canvas transformations, then make sure that you properly invalidate the area that the drawable occupies in the view. See below on how to do that. #### Other responsibilities * Set [Drawable.Callback](http://developer.android.com/reference/android/graphics/drawable/Drawable.Callback.html) ```java // When a holder is set to the view for the first time, // don't forget to set the callback to its top-level drawable: mDraweeHolder = ... mDraweeHolder.getTopLevelDrawable().setCallback(this); // In case the old holder is no longer needed, // don't forget to clear the callback from its top-level drawable: mDraweeHolder.getTopLevelDrawable().setCallback(null); mDraweeHolder = ... ``` * Override `verifyDrawable:` ```java @Override protected boolean verifyDrawable(Drawable who) { if (who == mDraweeHolder.getTopLevelDrawable()) { return true; } // other logic for other Drawables in your view, if any } ``` * Make sure `invalidateDrawable` invalidates the region occupied by your Drawee. If you apply some canvas transformations on the drawable before it gets drawn, then those transformations needs to be taken into account in invalidation. The simplest thing to do is what Android ImageView does in its [invalidateDrawable](http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4.4_r1/android/widget/ImageView.java#192) method. That is, to just invalidate the whole view when the drawable gets invalidated. ### Constructing the View and DraweeHolder This should be done carefully. See below. #### Arranging your Constructors We recommend the following pattern for constructors: * Override all three of the three View constructors. * Each constructor calls its superclass counterpart and then a private `init` method. * All of your initialization happens in `init.` That is, do not use the `this` to call one constructor from another. This is because Android View already calls one constructor from another, and it does so in an unintuitive way. This approach guarantees that the correct initialization is called no matter what constructor is used. It is in the `init` method that your holder is created. #### Creating the Holder If possible, always create Drawees when your view gets created. Creating a hierarchy is not cheap so it's best to do it only once. More importantly, holder's lifecycle should be bound to the view's lifecycle for the reasons explained in the attach/detach section. Best way to ensure that is to create the holder when the view gets constructed as explained above. ```java class CustomView extends View { DraweeHolder mDraweeHolder; // constructors following above pattern private void init() { GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(getResources()); .set... .set... .build(); mDraweeHolder = DraweeHolder.create(hierarchy, context); } } ``` #### Setting an image Use a [controller builder](using-controllerbuilder.html), but call `setController` on the holder instead of a View: ```java DraweeController controller = Fresco.newDraweeControllerBuilder() .setUri(uri) .setOldController(mDraweeHolder.getController()) .build(); mDraweeHolder.setController(controller); ``` ### MultiDraweeHolder If you are dealing with multiple drawees in your custom view, `MultiDraweeHolder` might come handy. There are `add`, `remove`, and `clear` methods for dealing with DraweeHalders: ```java MultiDraweeHolder mMultiDraweeHolder; private void init() { GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(getResources()); .set... .build(); mMultiDraweeHolder = new MultiDraweeHolder(); mMultiDraweeHolder.add(new DraweeHolder(hierarchy, context)); // repeat for more hierarchies } ``` You must override system events, set bounds, and do all the same responsibilities as for a single `DraweeHolder.` ================================================ FILE: docs/_includes/blog_pagination.html ================================================ {% if paginator.total_pages > 1 %}


{% endif %} ================================================ FILE: docs/_includes/content/gridblocks.html ================================================
{% for item in {{include.data_source}} %} {% include content/items/gridblock.html item=item gridtype=include.grid_type %} {% endfor %}
================================================ FILE: docs/_includes/content/items/gridblock.html ================================================
{% if item.image %} {{ item.title }} {% endif %}

{{ item.title }}

{% if item.text %} {{ item.text | markdownify }} {% endif %}
================================================ FILE: docs/_includes/doc.html ================================================

{% if include.truncate %}{{ page.title }}{% else %}{{ page.title }}{% endif %}

{% if include.truncate %} {% if page.content contains '' %} {{ page.content | split:'' | first }} {% else %} {{ page.content }} {% endif %} {% else %} {{ content }} {% endif %}
{% include doc_paging.html %}
================================================ FILE: docs/_includes/doc_paging.html ================================================ ================================================ FILE: docs/_includes/footer.html ================================================ ================================================ FILE: docs/_includes/head.html ================================================ {% seo %} {% comment %} For our RSS feed.xml https://help.github.com/articles/atom-rss-feeds-for-github-pages/ {% endcomment %} {% feed_meta %} ================================================ FILE: docs/_includes/hero.html ================================================ ================================================ FILE: docs/_includes/home_header.html ================================================

{{ site.tagline }}

{% if page.excerpt %}{{ page.excerpt | strip_html }}{% else %}{{ site.description }}{% endif %}

{% for promo in site.data.promo %}
{% include plugins/{{promo.type}}.html href=promo.href text=promo.text children=promo.children %}
{% endfor %}
================================================ FILE: docs/_includes/nav.html ================================================

{{ site.title }}

{% include react/header_nav.html %}
================================================ FILE: docs/_includes/nav_search.html ================================================ ================================================ FILE: docs/_includes/plugins/all_share.html ================================================
{% include plugins/like_button.html %}{% include plugins/twitter_share.html %}{% include plugins/google_share.html %}
================================================ FILE: docs/_includes/plugins/button.html ================================================ ================================================ FILE: docs/_includes/plugins/fb_pagelike.html ================================================ ================================================ FILE: docs/_includes/plugins/github_star.html ================================================ ================================================ FILE: docs/_includes/plugins/github_watch.html ================================================ ================================================ FILE: docs/_includes/plugins/google_share.html ================================================
================================================ FILE: docs/_includes/plugins/group_join.html ================================================ {{ include.text }} ================================================ FILE: docs/_includes/plugins/like_button.html ================================================
================================================ FILE: docs/_includes/plugins/plugin_row.html ================================================
{% for child in include.children %} {% include plugins/{{child.type}}.html href=child.href text=child.text %} {% endfor %}
================================================ FILE: docs/_includes/plugins/post_social_plugins.html ================================================ ================================================ FILE: docs/_includes/plugins/slideshow.html ================================================
================================================ FILE: docs/_includes/plugins/twitter_share.html ================================================ ================================================ FILE: docs/_includes/post.html ================================================
{% assign author = site.data.authors[page.author] %}
{% if author.fbid %}
{{ author.fullname }}
{% endif %} {% if author.full_name %} {% endif %}

{% if include.truncate %}{{ page.title }}{% else %}{{ page.title }}{% endif %}

{% if include.truncate %} {% if page.content contains '' %} {{ page.content | split:'' | first | markdownify }} {% else %} {{ page.content | markdownify }} {% endif %} {% else %} {{ content }} {% endif %} {% unless include.truncate %} {% include plugins/all_share.html %} {% endunless %}
================================================ FILE: docs/_includes/powered_by.html ================================================ {% if site.data.powered_by.first.items %}

{{ site.data.powered_by.first.title }}

{% if site.data.powered_by_highlight.first.items %} {% for item in site.data.powered_by_highlight.first.items %}
{{ item.name }}
{% endfor %} {% endif %} {% for item in site.data.powered_by.first.items %} {% endfor %}
Does your app use Fresco? Add it to this list with a pull request!
{% endif %} ================================================ FILE: docs/_includes/react/collection_nav.html ================================================
================================================ FILE: docs/_includes/react/header_nav.html ================================================ ================================================ FILE: docs/_includes/react/nav_blog.html ================================================ {% include react/collection_nav.html %} ================================================ FILE: docs/_includes/react/nav_docs.html ================================================ {% include react/collection_nav.html %} ================================================ FILE: docs/_includes/social_plugins.html ================================================
================================================ FILE: docs/_includes/ui/button.html ================================================ {{ include.button_text }} ================================================ FILE: docs/_layouts/basic.html ================================================ --- layout: doc_default ---
{{ content }}
================================================ FILE: docs/_layouts/blog.html ================================================ --- category: blog layout: blog_default ---
{{ content }}
================================================ FILE: docs/_layouts/blog_default.html ================================================ {% include head.html %} {% include nav.html alwayson=true %} ================================================ FILE: docs/_layouts/default.html ================================================ {% include head.html %} {% include nav.html alwayson=true %} ================================================ FILE: docs/_layouts/doc_default.html ================================================ {% include head.html %} {% include nav.html alwayson=true %} ================================================ FILE: docs/_layouts/doc_page.html ================================================ --- layout: doc_default ---
{{ content }}
================================================ FILE: docs/_layouts/docs.html ================================================ --- layout: doc_page --- {% include doc.html %} ================================================ FILE: docs/_layouts/home.html ================================================ {% include head.html %} {% include nav.html alwayson=true %} ================================================ FILE: docs/_layouts/page.html ================================================ --- layout: blog --- ================================================ FILE: docs/_layouts/plain.html ================================================ --- layout: default ---
{{ content }}
================================================ FILE: docs/_layouts/post.html ================================================ --- collection: blog layout: blog ---
{% include post.html %}
================================================ FILE: docs/_layouts/redirect.html ================================================ ================================================ FILE: docs/_sass/_base.scss ================================================ body { background: $secondary-bg; color: $text; font: normal #{$base-font-size}/#{$base-line-height} $base-font-family; height: 100vh; text-align: left; text-rendering: optimizeLegibility; } img { max-width: 100%; } article { p { img { max-width: 100%; display:block; margin-left: auto; margin-right: auto; } } } a { border-bottom: 1px dotted $primary-bg; color: $text; text-decoration: none; -webkit-transition: background 0.3s, color 0.3s; transition: background 0.3s, color 0.3s; } blockquote { padding: 15px 30px 15px 15px; margin: 20px 0 0 10px; background-color: rgba(204, 122, 111, 0.1); border-left: 10px solid rgba(191, 87, 73, 0.2); } #fb_oss a { border: 0; } h1, h2, h3, h4 { font-family: $header-font-family; font-weight: 900; } .navPusher { border-top: $header-height + $header-ptop + $header-pbot solid $primary-bg; height: 100%; left: 0; position: relative; z-index: 99; } .homeContainer { background: $primary-bg; color: $primary-overlay; a { color: $primary-overlay; } .homeSplashFade { color: white; } .homeWrapper { padding: 2em 10px; text-align: left; .wrapper { margin: 0px auto; max-width: $content-width; padding: 0 20px; } .projectLogo { img { height: 100px; margin-bottom: 0px; } } h1#project_title { font-family: $header-font-family; font-size: 300%; letter-spacing: -0.08em; line-height: 1em; margin-bottom: 80px; } h2#project_tagline { font-family: $header-font-family; font-size: 200%; letter-spacing: -0.04em; line-height: 1em; } } } .wrapper { margin: 0px auto; max-width: $content-width; padding: 0 10px; } .projectLogo { display: none; img { height: 100px; margin-bottom: 0px; } } section#intro { margin: 40px 0; } .fbossFontLight { font-family: $base-font-family; font-weight: 300; font-style: normal; } .fb-like { display: block; margin-bottom: 20px; width: 100%; } .center { display: block; text-align: center; } .mainContainer { background: $secondary-bg; overflow: auto; .mainWrapper { padding: 4vh 10px; text-align: left; .allShareBlock { padding: 10px 0; .pluginBlock { margin: 12px 0; padding: 0; } } a { &:hover, &:focus { background: $primary-bg; color: $primary-overlay; } } em, i { font-style: italic; } strong, b { font-weight: bold; } h1 { font-size: 300%; line-height: 1em; padding: 1.4em 0 1em; text-align: left; } h2 { font-size: 250%; line-height: 1em; margin-bottom: 20px; padding: 1.4em 0 20px; text-align: left; & { border-bottom: 1px solid darken($primary-bg, 10%); color: darken($primary-bg, 10%); font-size: 22px; padding: 10px 0; } &.blockHeader { border-bottom: 1px solid white; color: white; font-size: 22px; margin-bottom: 20px; padding: 10px 0; } } h3 { font-size: 150%; line-height: 1.2em; padding: 1em 0 0.8em; } h4 { font-size: 130%; line-height: 1.2em; padding: 1em 0 0.8em; } p { padding: 0.8em 0; } ul { list-style: disc; } ol, ul { padding-left: 24px; li { padding-bottom: 4px; padding-left: 6px; } } strong { font-weight: bold; } .post { position: relative; &.basicPost { margin-top: 30px; } a { color: $primary-bg; &:hover, &:focus { color: #fff; } } h2 { border-bottom: 4px solid $primary-bg; font-size: 130%; } h3 { border-bottom: 1px solid $primary-bg; font-size: 110%; } ol { list-style: decimal outside none; } .post-header { padding: 1em 0; h1 { font-size: 150%; line-height: 1em; padding: 0.4em 0 0; a { border: none; } } .post-meta { color: $primary-bg; font-family: $header-font-family; text-align: center; } } .postSocialPlugins { padding-top: 1em; } .docPagination { background: $primary-bg; bottom: 0px; left: 0px; position: absolute; right: 0px; .pager { display: inline-block; width: 50%; } .pagingNext { float: right; text-align: right; } a { border: none; color: $primary-overlay; display: block; padding: 4px 12px; &:hover { background-color: $secondary-bg; color: $text; } .pagerLabel { display: inline; } .pagerTitle { display: none; } } } } .posts { .post { margin-bottom: 6vh; } } } } #integrations_title { font-size: 250%; margin: 80px 0; } .ytVideo { height: 0; overflow: hidden; padding-bottom: 53.4%; /* 16:9 */ padding-top: 25px; position: relative; } .ytVideo iframe, .ytVideo object, .ytVideo embed { height: 100%; left: 0; position: absolute; top: 0; width: 100%; } @media only screen and (min-width: 480px) { h1#project_title { font-size: 500%; } h2#project_tagline { font-size: 250%; } .projectLogo { img { margin-bottom: 10px; height: 200px; } } .homeContainer .homeWrapper { padding-left: 10px; padding-right: 10px; } .mainContainer { .mainWrapper { .post { h2 { font-size: 180%; } h3 { font-size: 120%; } .docPagination { a { .pagerLabel { display: none; } .pagerTitle { display: inline; } } } } } } } @media only screen and (min-width: 900px) { .homeContainer { .homeWrapper { position: relative; #inner { box-sizing: border-box; max-width: 600px; padding-right: 40px; } .projectLogo { align-items: center; bottom: 0; display: flex; justify-content: flex-end; left: 0; padding: 2em 20px 4em; position: absolute; right: 20px; top: 0; img { height: 100%; max-height: 250px; } } } } } @media only screen and (min-width: 1024px) { .mainContainer { .mainWrapper { .post { box-sizing: border-box; display: block; .post-header { h1 { font-size: 250%; } } } .posts { .post { margin-bottom: 4vh; width: 100%; } } } } } @media only screen and (min-width: 1200px) { .homeContainer { .homeWrapper { #inner { max-width: 750px; } } } .wrapper { max-width: 1100px; } } @media only screen and (min-width: 1500px) { .homeContainer { .homeWrapper { #inner { max-width: 1100px; padding-bottom: 40px; padding-top: 40px; } } } .wrapper { max-width: 1400px; } } ================================================ FILE: docs/_sass/_blog.scss ================================================ .blogContainer { .posts { margin-top: 60px; .post { border: 1px solid $primary-bg; border-radius: 3px; padding: 10px 20px 20px; } } .lonePost { margin-top: 60px; .post { padding: 10px 0px 0px; } } .post-header { h1 { text-align: center; } .post-authorName { color: rgba($text, 0.7); font-size: 14px; font-weight: 900; margin-top: 0; padding: 0; text-align: center; } .authorPhoto { border-radius: 50%; height: 50px; left: 50%; margin-left: -25px; overflow: hidden; position: absolute; top: -25px; width: 50px; } } } ================================================ FILE: docs/_sass/_buttons.scss ================================================ .button { border: 1px solid $primary-bg; border-radius: 3px; color: $primary-bg; display: inline-block; font-size: 14px; font-weight: 900; line-height: 1.2em; padding: 10px; text-transform: uppercase; transition: background 0.3s, color 0.3s; &:hover { background: $primary-bg; color: $primary-overlay; } } .homeContainer { .button { border-color: $primary-overlay; border-width: 1px; color: $primary-overlay; &:hover { background: $primary-overlay; color: $primary-bg; } } } .blockButton { display: block; } .edit-page-link { float: right; font-size: 14px; font-weight: normal; line-height: 20px; opacity: 0.6; transition: opacity 0.5s; } .edit-page-link:hover { opacity: 1; } ================================================ FILE: docs/_sass/_footer.scss ================================================ .footerContainer { background: $secondary-bg; color: $primary-bg; overflow: hidden; padding: 0 10px; text-align: left; .footerWrapper { border-top: 1px solid $primary-bg; padding: 0; .footerBlocks { align-items: center; align-content: center; display: flex; flex-flow: row wrap; margin: 0 -20px; padding: 10px 0; } .footerSection { box-sizing: border-box; flex: 1 1 25%; font-size: 14px; min-width: 275px; padding: 0px 20px; a { border: 0; color: inherit; display: inline-block; line-height: 1.2em; } .footerLink { padding-right: 20px; } } .fbOpenSourceFooter { align-items: center; display: flex; flex-flow: row nowrap; max-width: 25%; .facebookOSSLogoSvg { flex: 0 0 31px; height: 30px; margin-right: 10px; width: 31px; path { fill: $primary-bg; } .middleRing { opacity: 0.7; } .innerRing { opacity: 0.45; } } h2 { display: block; font-weight: 900; line-height: 1em; } } } } @media only screen and (min-width: 900px) { .footerSection { &.rightAlign { margin-left: auto; max-width: 25%; text-align: right; } } } ================================================ FILE: docs/_sass/_gridBlock.scss ================================================ .gridBlock { margin: -5px 0; padding: 0; padding-bottom: 20px; .twoByGridBlock { padding: 5px 0; img { margin-top: 6vh; max-width: 100%; } &.featureBlock h3 { border-bottom: 1px solid rgba($primary-bg, 0.5); color: $primary-bg; font-size: 18px; margin: 0; padding: 10px 0; } } .gridClear { clear: both; } } @media only screen and (min-width: 1024px) { .gridBlock { display: flex; flex-direction: row; flex-wrap: wrap; margin: -10px -10px 10px -10px; .twoByGridBlock { box-sizing: border-box; flex: 1 0 50%; padding: 10px; } } h2 + .gridBlock { padding-top: 20px; } } @media only screen and (min-width: 1400px) { .gridBlock { display: flex; flex-direction: row; flex-wrap: wrap; margin: -10px -20px 10px -20px; .twoByGridBlock { box-sizing: border-box; flex: 1 0 50%; padding: 10px 20px; } } } .videoBlock { text-align: center; } ================================================ FILE: docs/_sass/_header.scss ================================================ .fixedHeaderContainer { background: $primary-bg; color: $primary-overlay; height: $header-height; padding: $header-ptop 0 $header-pbot; position: fixed; width: 100%; z-index: 9999; a { align-items: center; border: 0; color: $primary-overlay; display: flex; flex-flow: row nowrap; height: $header-height; } header { display: flex; flex-flow: row nowrap; position: relative; text-align: left; img { height: 100%; margin-right: 10px; } h2 { display: block; font-family: $header-font-family; font-weight: 900; line-height: 18px; position: relative; } } } .navigationFull { height: 34px; margin-left: auto; nav { position: relative; ul { display: flex; flex-flow: row nowrap; margin: 0 -10px; li { padding: 0 10px; display: block; a { border: 0; color: $primary-overlay-special; font-size: 16px; font-weight: 400; line-height: 1.2em; &:hover { border-bottom: 2px solid $primary-overlay; color: $primary-overlay; } } &.navItemActive { a { color: $primary-overlay; } } } } } } /* 900px .fixedHeaderContainer { .navigationWrapper { nav { padding: 0 1em; position: relative; top: -9px; ul { margin: 0 -0.4em; li { display: inline-block; a { padding: 14px 0.4em; border: 0; color: $primary-overlay-special; display: inline-block; &:hover { color: $primary-overlay; } } &.navItemActive { a { color: $primary-overlay; } } } } } &.navigationFull { display: inline-block; } &.navigationSlider { display: none; } } } 1200px .fixedHeaderContainer { header { max-width: 1100px; } } 1500px .fixedHeaderContainer { header { max-width: 1400px; } } */ ================================================ FILE: docs/_sass/_poweredby.scss ================================================ .poweredByContainer { background: $primary-bg; color: $primary-overlay; margin-bottom: 20px; a { color: $primary-overlay; } .poweredByWrapper { h2 { border-color: $primary-overlay-special; color: $primary-overlay-special; } } .poweredByMessage { color: $primary-overlay-special; font-size: 14px; padding-top: 20px; } } .poweredByItems { display: flex; flex-flow: row wrap; margin: 0 -10px; } .poweredByItem { box-sizing: border-box; flex: 1 0 50%; line-height: 1.1em; padding: 5px 10px; &.itemLarge { flex-basis: 100%; padding: 10px; text-align: center; &:nth-child(4) { padding-bottom: 20px; } img { max-height: 30px; } } } @media only screen and (min-width: 480px) { .itemLarge { flex-basis: 50%; max-width: 50%; } } @media only screen and (min-width: 1024px) { .poweredByItem { flex-basis: 25%; max-width: 25%; &.itemLarge { padding-bottom: 20px; text-align: left; } } } ================================================ FILE: docs/_sass/_promo.scss ================================================ .promoSection { display: flex; flex-flow: column wrap; font-size: 125%; line-height: 1.6em; margin: -10px 0; position: relative; z-index: 99; .promoRow { padding: 10px 0; .pluginWrapper { display: block; &.ghWatchWrapper, &.ghStarWrapper { height: 28px; } } .pluginRowBlock { display: flex; flex-flow: row wrap; margin: 0 -2px; .pluginWrapper { padding: 0 2px; } } } } ================================================ FILE: docs/_sass/_react_docs_nav.scss ================================================ .docsNavContainer { background: $sidenav; height: 35px; left: 0; position: fixed; width: 100%; z-index: 100; } .docMainWrapper { .wrapper { &.mainWrapper { padding-left: 0; padding-right: 0; padding-top: 10px; } } } .docsSliderActive { .docsNavContainer { box-sizing: border-box; height: 100%; overflow-y: auto; -webkit-overflow-scrolling: touch; padding-bottom: 50px; } .mainContainer { display: none; } } .navBreadcrumb { box-sizing: border-box; display: flex; flex-flow: row nowrap; font-size: 12px; height: 35px; overflow: hidden; padding: 5px 10px; a, span { border: 0; color: $sidenav-text; } i { padding: 0 3px; } } nav.toc { position: relative; section { padding: 0px; position: relative; .navGroups { display: none; padding: 40px 10px 10px; } } .toggleNav { background: $sidenav; color: $sidenav-text; position: relative; transition: background-color 0.3s, color 0.3s; .navToggle { cursor: pointer; height: 24px; margin-right: 10px; position: relative; text-align: left; width: 18px; &::before, &::after { content: ""; position: absolute; top: 50%; left: 0; left: 8px; width: 3px; height: 6px; border: 5px solid $sidenav-text; border-width: 5px 0; margin-top: -8px; transform: rotate(45deg); z-index: 1; } &::after { transform: rotate(-45deg); } i { &::before, &::after { content: ""; position: absolute; top: 50%; left: 2px; background: transparent; border-width: 0 5px 5px; border-style: solid; border-color: transparent $sidenav-text; height: 0; margin-top: -7px; opacity: 1; width: 5px; z-index: 10; } &::after { border-width: 5px 5px 0; margin-top: 2px; } } &.navToggleActive { &::before, &::after { border-width: 6px 0; height: 0px; margin-top: -6px; } i { opacity: 0; } } } .navGroup { background: $sidenav-overlay; margin: 1px 0; ul { display: none; } h3 { background: $sidenav-overlay; color: $sidenav-text; cursor: pointer; font-size: 14px; font-weight: 400; line-height: 1.2em; padding: 10px; transition: color 0.2s; i:not(:empty) { width: 16px; height: 16px; display: inline-block; box-sizing: border-box; text-align: center; color: rgba($sidenav-text, 0.5); margin-right: 10px; transition: color 0.2s; } &:hover { color: $primary-bg; i:not(:empty) { color: $primary-bg; } } } &.navGroupActive { background: $sidenav-active; color: $sidenav-text; ul { display: block; padding-bottom: 10px; padding-top: 10px; } h3 { background: $primary-bg; color: $primary-overlay; } } } ul { padding-left: 0; padding-right: 24px; li { list-style-type: none; padding-bottom: 0; padding-left: 0; a { border: none; color: $sidenav-text; display: inline-block; font-size: 14px; line-height: 1.1em; margin: 2px 10px 5px; padding: 5px 0 2px; transition: color 0.3s; &:hover, &:focus { color: $primary-bg; } &.navItemActive { color: $primary-bg; font-weight: 900; } } } } } .toggleNavActive { .navBreadcrumb { background: $sidenav; margin-bottom: 20px; position: fixed; width: 100%; } section { .navGroups { display: block; } } } } .docsNavVisible { .navPusher { .mainContainer { padding-top: 35px; } } } @media only screen and (min-width: 900px) { .navBreadcrumb { padding: 5px 0; } nav.toc { section { .navGroups { padding: 40px 0 0; } } } } @media only screen and (min-width: 1024px) { .navToggle { display: none; } .docsSliderActive { .mainContainer { display: block; } } .docsNavVisible { .navPusher { .mainContainer { padding-top: 0; } } } .docsNavContainer { background: none; box-sizing: border-box; height: auto; margin: 40px 40px 0 0; overflow-y: auto; position: relative; width: 300px; } nav.toc { section { .navGroups { display: block; padding-top: 0px; } } .toggleNavActive { .navBreadcrumb { margin-bottom: 0; position: relative; } } } .docMainWrapper { display: flex; flex-flow: row nowrap; margin-bottom: 40px; .wrapper { padding-left: 0; padding-right: 0; &.mainWrapper { padding-top: 0; } } } .navBreadcrumb { display: none; h2 { padding: 0 10px; } } } ================================================ FILE: docs/_sass/_react_header_nav.scss ================================================ .navigationFull { display: none; } .navigationSlider { position: absolute; right: 0px; .navSlideout { cursor: pointer; padding-top: 4px; position: absolute; right: 10px; top: 0; transition: top 0.3s; z-index: 101; } .slidingNav { background: $secondary-bg; box-sizing: border-box; height: 0px; padding: 0; position: absolute; right: 0px; top: 0; transition: height 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55), width 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); width: 0; &.slidingNavActive { height: auto; padding-top: $header-height + $header-pbot; width: 300px; } ul { flex-flow: column nowrap; list-style: none; padding: 10px; li { margin: 0; padding: 2px 0; a { color: $primary-bg; display: inline; margin: 3px 5px; padding: 2px 0px; transition: background-color 0.3s; &:focus, &:hover { border-bottom: 2px solid $primary-bg; } } } } } } .menuExpand { display: flex; flex-flow: column nowrap; height: 20px; justify-content: space-between; span { background: $primary-overlay; border-radius: 3px; display: block; flex: 0 0 4px; height: 4px; position: relative; top: 0; transition: background-color 0.3s, top 0.3s, opacity 0.3s, transform 0.3s; width: 20px; } } .navSlideout.navSlideoutActive { top: -2px; .menuExpand { span:nth-child(1) { background-color: $text; top: 16px; transform: rotate(45deg); } span:nth-child(2) { opacity: 0; } span:nth-child(3) { background-color: $text; transform: rotate(-45deg); } } } .navPusher { border-top: $header-height + $header-ptop + $header-pbot solid $primary-bg; position: relative; left: 0; z-index: 99; height: 100%; &::after { position: absolute; top: 0; right: 0; width: 0; height: 0; background: rgba(0,0,0,0.4); content: ''; opacity: 0; -webkit-transition: opacity 0.5s, width 0.1s 0.5s, height 0.1s 0.5s; transition: opacity 0.5s, width 0.1s 0.5s, height 0.1s 0.5s; } .sliderActive &::after { width: 100%; height: 100%; opacity: 1; -webkit-transition: opacity 0.5s; transition: opacity 0.5s; z-index: 100; } } @media only screen and (min-width: 1024px) { .navigationFull { display: block; } .navigationSlider { display: none; } } ================================================ FILE: docs/_sass/_reset.scss ================================================ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block; } body { line-height: 1; } ol, ul { list-style: none; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } table { border-collapse: collapse; border-spacing: 0; } ================================================ FILE: docs/_sass/_search.scss ================================================ input[type="search"] { -moz-appearance: none; -webkit-appearance: none; } .navSearchWrapper { align-self: center; position: relative; &::before { border: 3px solid $primary-overlay-special; border-radius: 50%; content: " "; display: block; height: 6px; left: 15px; width: 6px; position: absolute; top: 4px; z-index: 1; } &::after { background: $primary-overlay-special; content: " "; height: 7px; left: 24px; position: absolute; transform: rotate(-45deg); top: 12px; width: 3px; z-index: 1; } .aa-dropdown-menu { background: $secondary-bg; border: 3px solid rgba($text, 0.25); color: $text; font-size: 14px; left: auto !important; line-height: 1.2em; right: 0 !important; .algolia-docsearch-suggestion--category-header { background: $primary-overlay-special; color: $primary-bg; .algolia-docsearch-suggestion--highlight { background-color: $primary-bg; color: $primary-overlay; } } .algolia-docsearch-suggestion--title .algolia-docsearch-suggestion--highlight, .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight { color: $primary-bg; } .algolia-docsearch-suggestion__secondary, .algolia-docsearch-suggestion--subcategory-column { border-color: rgba($text, 0.3); } } } input#search_input { padding-left: 25px; font-size: 14px; line-height: 20px; border-radius: 20px; background-color: rgba($primary-overlay-special, 0.25); border: none; color: rgba($primary-overlay-special, 0); outline: none; position: relative; transition: background-color .2s cubic-bezier(0.68, -0.55, 0.265, 1.55), width .2s cubic-bezier(0.68, -0.55, 0.265, 1.55), color .2s ease; width: 60px; &:focus, &:active { background-color: $secondary-bg; color: $text; width: 240px; } } .reactNavSearchWrapper { &::before { left: 6px; top: 6px; } &::after { left: 15px; top: 14px; } input#search_input_react { box-sizing: border-box; padding-left: 25px; font-size: 14px; line-height: 20px; border-radius: 20px; background-color: rgba($primary-overlay-special, 0.25); border: none; color: $text; outline: none; position: relative; transition: background-color .2s cubic-bezier(0.68, -0.55, 0.265, 1.55), width .2s cubic-bezier(0.68, -0.55, 0.265, 1.55), color .2s ease; width: 100%; &:focus, &:active { background-color: $primary-bg; color: $primary-overlay; } } .algolia-docsearch-suggestion--subcategory-inline { display: none; } & > span { width: 100%; } .aa-dropdown-menu { background: $secondary-bg; border: 0px solid $secondary-bg; color: $text; font-size: 12px; line-height: 2em; max-height: 140px; min-width: auto; overflow-y: scroll; -webkit-overflow-scrolling: touch; padding: 0; border-radius: 0; position: relative !important; width: 100%; } } ================================================ FILE: docs/_sass/_slideshow.scss ================================================ .slideshow { position: relative; .slide { display: none; img { display: block; margin: 0 auto; } &.slideActive { display: block; } a { border: none; display: block; } } .pagination { display: block; margin: -10px; padding: 1em 0; text-align: center; width: 100%; .pager { background: transparent; border: 2px solid rgba(255, 255, 255, 0.5); border-radius: 50%; cursor: pointer; display: inline-block; height: 12px; margin: 10px; transition: background-color 0.3s, border-color 0.3s; width: 12px; &.pagerActive { background: rgba(255, 255, 255, 0.5); border-width: 4px; height: 8px; width: 8px; } } } } ================================================ FILE: docs/_sass/_syntax-highlighting.scss ================================================ .rougeHighlight { background-color: $code-bg; color: #93a1a1 } .rougeHighlight .c { color: #586e75 } /* Comment */ .rougeHighlight .err { color: #93a1a1 } /* Error */ .rougeHighlight .g { color: #93a1a1 } /* Generic */ .rougeHighlight .k { color: #859900 } /* Keyword */ .rougeHighlight .l { color: #93a1a1 } /* Literal */ .rougeHighlight .n { color: #93a1a1 } /* Name */ .rougeHighlight .o { color: #859900 } /* Operator */ .rougeHighlight .x { color: #cb4b16 } /* Other */ .rougeHighlight .p { color: #93a1a1 } /* Punctuation */ .rougeHighlight .cm { color: #586e75 } /* Comment.Multiline */ .rougeHighlight .cp { color: #859900 } /* Comment.Preproc */ .rougeHighlight .c1 { color: #72c02c; } /* Comment.Single */ .rougeHighlight .cs { color: #859900 } /* Comment.Special */ .rougeHighlight .gd { color: #2aa198 } /* Generic.Deleted */ .rougeHighlight .ge { color: #93a1a1; font-style: italic } /* Generic.Emph */ .rougeHighlight .gr { color: #dc322f } /* Generic.Error */ .rougeHighlight .gh { color: #cb4b16 } /* Generic.Heading */ .rougeHighlight .gi { color: #859900 } /* Generic.Inserted */ .rougeHighlight .go { color: #93a1a1 } /* Generic.Output */ .rougeHighlight .gp { color: #93a1a1 } /* Generic.Prompt */ .rougeHighlight .gs { color: #93a1a1; font-weight: bold } /* Generic.Strong */ .rougeHighlight .gu { color: #cb4b16 } /* Generic.Subheading */ .rougeHighlight .gt { color: #93a1a1 } /* Generic.Traceback */ .rougeHighlight .kc { color: #cb4b16 } /* Keyword.Constant */ .rougeHighlight .kd { color: #268bd2 } /* Keyword.Declaration */ .rougeHighlight .kn { color: #859900 } /* Keyword.Namespace */ .rougeHighlight .kp { color: #859900 } /* Keyword.Pseudo */ .rougeHighlight .kr { color: #268bd2 } /* Keyword.Reserved */ .rougeHighlight .kt { color: #dc322f } /* Keyword.Type */ .rougeHighlight .ld { color: #93a1a1 } /* Literal.Date */ .rougeHighlight .m { color: #2aa198 } /* Literal.Number */ .rougeHighlight .s { color: #2aa198 } /* Literal.String */ .rougeHighlight .na { color: #93a1a1 } /* Name.Attribute */ .rougeHighlight .nb { color: #B58900 } /* Name.Builtin */ .rougeHighlight .nc { color: #268bd2 } /* Name.Class */ .rougeHighlight .no { color: #cb4b16 } /* Name.Constant */ .rougeHighlight .nd { color: #268bd2 } /* Name.Decorator */ .rougeHighlight .ni { color: #cb4b16 } /* Name.Entity */ .rougeHighlight .ne { color: #cb4b16 } /* Name.Exception */ .rougeHighlight .nf { color: #268bd2 } /* Name.Function */ .rougeHighlight .nl { color: #93a1a1 } /* Name.Label */ .rougeHighlight .nn { color: #93a1a1 } /* Name.Namespace */ .rougeHighlight .nx { color: #93a1a1 } /* Name.Other */ .rougeHighlight .py { color: #93a1a1 } /* Name.Property */ .rougeHighlight .nt { color: #268bd2 } /* Name.Tag */ .rougeHighlight .nv { color: #268bd2 } /* Name.Variable */ .rougeHighlight .ow { color: #859900 } /* Operator.Word */ .rougeHighlight .w { color: #93a1a1 } /* Text.Whitespace */ .rougeHighlight .mf { color: #2aa198 } /* Literal.Number.Float */ .rougeHighlight .mh { color: #2aa198 } /* Literal.Number.Hex */ .rougeHighlight .mi { color: #2aa198 } /* Literal.Number.Integer */ .rougeHighlight .mo { color: #2aa198 } /* Literal.Number.Oct */ .rougeHighlight .sb { color: #586e75 } /* Literal.String.Backtick */ .rougeHighlight .sc { color: #2aa198 } /* Literal.String.Char */ .rougeHighlight .sd { color: #93a1a1 } /* Literal.String.Doc */ .rougeHighlight .s2 { color: #2aa198 } /* Literal.String.Double */ .rougeHighlight .se { color: #cb4b16 } /* Literal.String.Escape */ .rougeHighlight .sh { color: #93a1a1 } /* Literal.String.Heredoc */ .rougeHighlight .si { color: #2aa198 } /* Literal.String.Interpol */ .rougeHighlight .sx { color: #2aa198 } /* Literal.String.Other */ .rougeHighlight .sr { color: #dc322f } /* Literal.String.Regex */ .rougeHighlight .s1 { color: #2aa198 } /* Literal.String.Single */ .rougeHighlight .ss { color: #2aa198 } /* Literal.String.Symbol */ .rougeHighlight .bp { color: #268bd2 } /* Name.Builtin.Pseudo */ .rougeHighlight .vc { color: #268bd2 } /* Name.Variable.Class */ .rougeHighlight .vg { color: #268bd2 } /* Name.Variable.Global */ .rougeHighlight .vi { color: #268bd2 } /* Name.Variable.Instance */ .rougeHighlight .il { color: #2aa198 } /* Literal.Number.Integer.Long */ .highlighter-rouge { color: darken(#72c02c, 8%); font: 800 12px/1.5em Hack, monospace; max-width: 100%; .rougeHighlight { border-radius: 3px; margin: 20px 0; padding: 0px; overflow-x: scroll; -webkit-overflow-scrolling: touch; table { background: none; border: none; tbody { tr { background: none; display: flex; flex-flow: row nowrap; td { display: block; flex: 1 1; &.gutter { border-right: 1px solid lighten($code-bg, 10%); color: lighten($code-bg, 15%); margin-right: 10px; max-width: 40px; padding-right: 10px; pre { max-width: 20px; } } } } } } } } p > .highlighter-rouge, li > .highlighter-rouge, a > .highlighter-rouge { font-size: 16px; font-weight: 400; line-height: inherit; } a:hover { .highlighter-rouge { color: white; } } ================================================ FILE: docs/_sass/_tables.scss ================================================ table { background: $lightergrey; border: 1px solid $lightgrey; border-collapse: collapse; display:table; margin: 20px 0; thead { border-bottom: 1px solid $lightgrey; display: table-header-group; } tbody { display: table-row-group; } tr { display: table-row; &:nth-of-type(odd) { background: $greyish; } th, td { border-right: 1px dotted $lightgrey; display: table-cell; font-size: 14px; line-height: 1.3em; padding: 10px; text-align: left; &:last-of-type { border-right: 0; } code { color: $green; display: inline-block; font-size: 12px; } } th { color: #000000; font-weight: bold; font-family: $header-font-family; text-transform: uppercase; } } } ================================================ FILE: docs/css/main.scss ================================================ --- # Only the main Sass file needs front matter (the dashes are enough) --- @charset "utf-8"; @font-face { font-family: 'Lato'; src: url('{{ site.baseurl }}/static/fonts/LatoLatin-Italic.woff2') format('woff2'), url('{{ site.baseurl }}/static/fonts/LatoLatin-Italic.woff') format('woff'); font-weight: normal; font-style: italic; } @font-face { font-family: 'Lato'; src: url('{{ site.baseurl }}/static/fonts/LatoLatin-Black.woff2') format('woff2'), url('{{ site.baseurl }}/static/fonts/LatoLatin-Black.woff') format('woff'); font-weight: 900; font-style: normal; } @font-face { font-family: 'Lato'; src: url('{{ site.baseurl }}/static/fonts/LatoLatin-BlackItalic.woff2') format('woff2'), url('{{ site.baseurl }}/static/fonts/LatoLatin-BlackItalic.woff') format('woff'); font-weight: 900; font-style: italic; } @font-face { font-family: 'Lato'; src: url('{{ site.baseurl }}/static/fonts/LatoLatin-Light.woff2') format('woff2'), url('{{ site.baseurl }}/static/fonts/LatoLatin-Light.woff') format('woff'); font-weight: 300; font-style: normal; } @font-face { font-family: 'Lato'; src: url('{{ site.baseurl }}/static/fonts/LatoLatin-Regular.woff2') format('woff2'), url('{{ site.baseurl }}/static/fonts/LatoLatin-Regular.woff') format('woff'); font-weight: normal; font-style: normal; } // Our variables $base-font-family: 'Lato', Calibri, Arial, sans-serif; $header-font-family: 'Lato', 'Helvetica Neue', Arial, sans-serif; $base-font-size: 18px; $small-font-size: $base-font-size * 0.875; $base-line-height: 1.4em; $spacing-unit: 12px; // Two configured colors (see _config.yml) $primary-bg: {{ site.color.primary }}; $secondary-bg: {{ site.color.secondary }}; // $primary-bg overlays {% if site.color.primary-overlay == 'light' %} $primary-overlay: darken($primary-bg, 70%); $primary-overlay-special: darken($primary-bg, 40%); {% else %} $primary-overlay: #fff; $primary-overlay-special: lighten($primary-bg, 30%); {% endif %} // $secondary-bg overlays {% if site.color.secondary-overlay == 'light' %} $text: #393939; $sidenav: darken($secondary-bg, 20%); $sidenav-text: $text; $sidenav-overlay: darken($sidenav, 10%); $sidenav-active: lighten($sidenav, 10%); {% else %} $text: #fff; $sidenav: lighten($secondary-bg, 20%); $sidenav-text: $text; $sidenav-overlay: lighten($sidenav, 10%); $sidenav-active: darken($sidenav, 10%); {% endif %} $code-bg: #002b36; $header-height: 34px; $header-ptop: 10px; $header-pbot: 8px; // Width of the content area $content-width: 900px; // Table setting variables $lightergrey: #F8F8F8; $greyish: #E8E8E8; $lightgrey: #B0B0B0; $green: #2db04b; // Using media queries with like this: // @include media-query($on-palm) { // .wrapper { // padding-right: $spacing-unit / 2; // padding-left: $spacing-unit / 2; // } // } @mixin media-query($device) { @media screen and (max-width: $device) { @content; } } // Import partials from `sass_dir` (defaults to `_sass`) @import "reset", "base", "header", "search", "syntax-highlighting", "promo", "buttons", "gridBlock", "poweredby", "footer", "react_header_nav", "react_docs_nav", "tables", "blog" ; // Anchor links // http://ben.balter.com/2014/03/13/pages-anchor-links/ .header-link { position: absolute; margin-left: 0.2em; opacity: 0; -webkit-transition: opacity 0.2s ease-in-out 0.1s; -moz-transition: opacity 0.2s ease-in-out 0.1s; -ms-transition: opacity 0.2s ease-in-out 0.1s; } h2:hover .header-link, h3:hover .header-link, h4:hover .header-link, h5:hover .header-link, h6:hover .header-link { opacity: 1; } ================================================ FILE: docs/docs/index.html ================================================ --- id: docs title: Docs layout: redirect destination: /docs/getting-started.html --- ================================================ FILE: docs/index.md ================================================ --- layout: home title: Fresco | An image management library. id: home --- ## Watch Introductory Video
### Image Pipeline Fresco's image pipeline will load images from the network, local storage, or local resources. To save data and CPU, it has three levels of cache; two in memory and another in internal storage.
### Drawees Fresco's `Drawee` shows a placeholder for you until the image has loaded and then automatically shows the image when it arrives. When the image goes off-screen, it automatically releases its memory.
## Features
### Memory A decompressed image - an Android `Bitmap` - takes up a lot of memory. This leads to more frequent runs of the Java garbage collector. This slows apps down. The problem is especially bad without the improvements to the garbage collector made in Android 5.0. On Android 4.x and lower, Fresco puts images in a special region of Android memory. It also makes sure that images are automatically released from memory when they're no longer shown on screen. This lets your application run faster - and suffer fewer crashes. Apps using Fresco can run even on low-end devices without having to constantly struggle to keep their image memory footprint under control.
### Loading Fresco's image pipeline lets you customize the load in a variety of ways: * Specify several different uris for an image, and choose the one already in cache for display * Show a low-resolution image first and swap to a higher-res one when it arrives * Send events back into your app when the image arrives * If the image has an EXIF thumbnail, show it first until the full image loads (local images only) * Resize or rotate the image * Modify the downloaded image in-place * Decode WebP images, even on older versions of Android that don't fully support them
### Drawing Fresco uses `Drawees` for display. These offer a number of useful features: * Scale the image to a custom focus point, instead of the center * Show the image with rounded corners, or a circle * Let users tap the placeholder to retry load of the image, if the network load failed * Show custom backgrounds, overlays, or progress bars on the image * Show a custom overlay when the user presses the image
### Streaming Progressive JPEG images have been on the Web for years. These let a low-resolution scan of the image download first, then gradually improve the quality as more of the image downloads. This is a lifesaver for users on slow networks. Android's own imaging libraries don't support streaming. Fresco does. Just specify a URI, and your app will automatically update its display as more data arrives.
### Animations Animated GIFs and WebPs can be challenging for apps. Each frame is a large Bitmap, and each animation is a series of frames. Fresco takes care of loading and disposing of frames and managing their memory.
================================================ FILE: docs/javadoc/assets/customizations.css ================================================ #header { border-bottom: 3px solid #0767a4; } #search_filtered .jd-selected { background-color: #0767a4; } ================================================ FILE: docs/javadoc/assets/customizations.js ================================================ ================================================ FILE: docs/javadoc/assets/doclava-developer-core.css ================================================ /* file: doclava-developer-core.css info: core developer styles */ /* RESET STYLES */ html,body,div,h1,h2,h3,h4,h5,h6,p,img, dl,dt,dd,ol,ul,li,table,caption,tbody, tfoot,thead,tr,th,td,form,fieldset, embed,object,applet { margin: 0; padding: 0; border: 0; } /* BASICS */ html, body { overflow:hidden; /* keeps scrollbar off IE */ background-color:#fff; } body { font-family:arial,sans-serif; color:#000; font-size:13px; color:#333; background-image:url(images/bg_fade.jpg); background-repeat:repeat-x; } a, a code { color:#006699; } a:active, a:active code { color:#f00; } a:visited, a:visited code { color:#006699; } input, select, textarea, option, label { font-family:inherit; font-size:inherit; padding:0; margin:0; vertical-align:middle; } option { padding:0 4px; } p { padding:0; margin:0 0 1em; } code, pre { color:#007000; font-family:monospace; line-height:1em; } var { color:#007000; font-style:italic; } pre { border:1px solid #ccc; background-color:#fafafa; padding:10px; margin:0 0 1em 1em; overflow:auto; line-height:inherit; /* fixes vertical scrolling in webkit */ } h1,h2,h3,h4,h5 { margin:1em 0; padding:0; } p,ul,ol,dl,dd,dt,li { line-height:1.3em; } ul,ol { margin:0 0 .8em; padding:0 0 0 2em; } li { padding:0 0 .5em; } dl { margin:0 0 1em 0; padding:0; } dt { margin:0; padding:0; } dd { margin:0 0 1em; padding:0 0 0 2em; } li p { margin:.5em 0 0; } dd p { margin:1em 0 0; } li pre, li table, li img { margin:.5em 0 0 1em; } dd pre, #jd-content dd table, #jd-content dd img { margin:1em 0 0 1em; } li ul, li ol, dd ul, dd ol { margin:0; padding: 0 0 0 2em; } li li, dd li { margin:0; padding:.5em 0 0; } dl dl, ol dl, ul dl { margin:0 0 1em; padding:0; } table { font-size:1em; margin:0 0 1em; padding:0; border-collapse:collapse; border-width:0; empty-cells:show; } td,th { border:1px solid #ccc; padding:6px 12px; text-align:left; vertical-align:top; background-color:inherit; } th { background-color:#dee8f1; } td > p:last-child { margin:0; } hr.blue { background-color:#DDF0F2; border:none; height:5px; margin:20px 0 10px; } blockquote { margin: 0 0 1em 1em; padding: 0 4em 0 1em; border-left:2px solid #eee; } /* LAYOUT */ #body-content { /* "Preliminary" watermark for draft documentation. background:transparent url(images/preliminary.png) repeat scroll 0 0; */ margin:0; position:relative; width:100%; } #header { height: 44px; position:relative; z-index:100; min-width:675px; /* min width for the tabs, before they wrap */ padding:0 10px; border-bottom:3px solid #94b922; } #headerLeft{ position:absolute; padding: 10px 0 0; left:8px; bottom:3px; } #headerRight { position:absolute; right:0; bottom:3px; padding:0; text-align:right; } #masthead-title { font-size:28px; color: #2f74ae; } /* Tabs in the header */ #header ul { list-style: none; margin: 7px 0 0; padding: 0; height: 29px; } #header li { float: left; margin: 0px 2px 0px 0px; padding:0; } #header li a { text-decoration: none; display: block; background-image: url(images/bg_images_sprite.png); background-position: 0 -58px; background-repeat: no-repeat; color: #666; font-size: 13px; font-weight: bold; width: 94px; height: 29px; text-align: center; margin: 0px; } #header li a:hover { background-image: url(images/bg_images_sprite.png); background-position: 0 -29px; background-repeat: no-repeat; } #header li a span { position:relative; top:7px; } #header li a span+span { display:none; } /* tab highlighting */ .home #home-link a, .guide #guide-link a, .reference #reference-link a, .sdk #sdk-link a, .resources #resources-link a, .videos #videos-link a { background-image: url(images/bg_images_sprite.png); background-position: 0 0; background-repeat: no-repeat; color: #fff; font-weight: bold; cursor:default; } .home #home-link a:hover, .guide #guide-link a:hover, .reference #reference-link a:hover, .sdk #sdk-link a:hover, .resources #resources-link a:hover, .videos #videos-link a:hover { background-image: url(images/bg_images_sprite.png); background-position: 0 0; } #headerLinks { margin:10px 10px 0 0; height:13px; font-size: 11px; vertical-align: top; } #headerLinks a { color: #7FA9B5; } #headerLinks img { vertical-align:middle; } #language { margin:0 10px 0 4px; } #search { margin:8px 10px 0 0; } /* MAIN BODY */ #mainBodyFluid { margin: 20px 10px; color:#333; } #mainBodyFixed { margin: 20px 10px; color: #333; width:930px; position:relative; } #mainBodyFixed h3, #mainBodyFluid h3 { color:#336666; font-size:1.25em; margin: 0em 0em 0em 0em; padding-bottom:.5em; } #mainBodyFixed h2, #mainBodyFluid h2 { color:#336666; font-size:1.25em; margin: 0; padding-bottom:.5em; } #mainBodyFixed h1, #mainBodyFluid h1 { color:#435A6E; font-size:1.7em; margin: 1em 0; } #mainBodyFixed .green, #mainBodyFluid .green, #jd-content .green { color:#7BB026; background-color:none; } #mainBodyLeft { float: left; width: 600px; margin-right: 20px; color: #333; position:relative; } div.indent { margin-left: 40px; margin-right: 70px; } #mainBodyLeft p { color: #333; font-size: 13px; } #mainBodyLeft p.blue { color: #669999; } #mainBodyLeft #communityDiv { float: left; background-image:url(images/bg_community_leftDiv.jpg); background-repeat: no-repeat; width: 581px; height: 347px; padding: 20px 0px 0px 20px; } #mainBodyRight { float: left; width: 300px; color: #333; } #mainBodyRight p { padding-right: 50px; color: #333; } #mainBodyRight table { width: 100%; } #mainBodyRight td { border:0px solid #666; padding:0px 5px; text-align:left; } #mainBodyRight td p { margin:0 0 1em 0; } #mainBodyRight .blueBorderBox { border:5px solid #ddf0f2; padding:18px 18px 18px 18px; text-align:left; } #mainBodyFixed .separator { background-image:url(images/hr_gray_side.jpg); background-repeat:no-repeat; width: 100%; float: left; clear: both; } #mainBodyBottom { float: left; width: 100%; clear:both; color: #333; } #mainBodyBottom .separator { background-image:url(images/hr_gray_main.jpg); background-repeat:no-repeat; width: 100%; float: left; clear: both; } /* FOOTER */ #footer { float: left; width:90%; margin: 20px; color: #aaa; font-size: 11px; } #footer a { color: #aaa; font-size: 11px; } #footer a:hover { text-decoration: underline; color:#aaa; } #footerlinks { margin-top:2px; } #footerlinks a, #footerlinks a:visited { color:#006699; } /* SEARCH FILTER */ #search_autocomplete { color:#aaa; } #search-button { display:inline; } #search_filtered_div { position:absolute; margin-top:-1px; z-index:101; border:1px solid #BCCDF0; background-color:#fff; } #search_filtered { min-width:100%; } #search_filtered td{ background-color:#fff; border-bottom: 1px solid #669999; line-height:1.5em; } #search_filtered .jd-selected { background-color: #94b922; cursor:pointer; } #search_filtered .jd-selected, #search_filtered .jd-selected a { color:#fff; } .no-display { display: none; } .jd-autocomplete { font-family: Arial, sans-serif; padding-left: 6px; padding-right: 6px; padding-top: 1px; padding-bottom: 1px; font-size: 0.81em; border: none; margin: 0; line-height: 1.05em; } .show-row { display: table-row; } .hide-row { display: hidden; } /* SEARCH */ /* restrict global search form width */ #searchForm { width:350px; } #searchTxt { width:200px; } /* disable twiddle and size selectors for left column */ #leftSearchControl div { width: 100%; } #leftSearchControl .gsc-twiddle { background-image : none; } #leftSearchControl td, #searchForm td { border: 0px solid #000; } #leftSearchControl .gsc-resultsHeader .gsc-title { padding-left : 0px; font-weight : bold; font-size : 13px; color:#006699; display : none; } #leftSearchControl .gsc-resultsHeader div.gsc-results-selector { display : none; } #leftSearchControl .gsc-resultsRoot { padding-top : 6px; } #leftSearchControl div.gs-visibleUrl-long { display : block; color:#006699; } .gsc-webResult div.gs-visibleUrl-short, table.gsc-branding, .gsc-clear-button { display : none; } .gsc-cursor-box .gsc-cursor div.gsc-cursor-page, .gsc-cursor-box .gsc-trailing-more-results a.gsc-trailing-more-results, #leftSearchControl a, #leftSearchControl a b { color:#006699; } .gsc-resultsHeader { display: none; } /* Disable built in search forms */ .gsc-control form.gsc-search-box { display : none; } table.gsc-search-box { margin:6px 0 0 0; border-collapse:collapse; } td.gsc-input { padding:0 2px; width:100%; vertical-align:middle; } input.gsc-input { border:1px solid #BCCDF0; width:99%; padding-left:2px; font-size:.95em; } td.gsc-search-button { text-align: right; padding:0; vertical-align:top; } #search-button { margin:0 0 0 2px; font-size:11px; } /* search result tabs */ #doc-content .gsc-control { position:relative; } #doc-content .gsc-tabsArea { position:relative; white-space:nowrap; } #doc-content .gsc-tabHeader { padding: 3px 6px; position:relative; } #doc-content .gsc-tabHeader.gsc-tabhActive { border-top: 2px solid #94B922; } #doc-content h2#searchTitle { padding:0; } #doc-content .gsc-resultsbox-visible { padding:1em 0 0 6px; } /* Pretty printing styles. Used with prettify.js. */ .str { color: #080; } .kwd { color: #008; } .com { color: #800; } .typ { color: #606; } .lit { color: #066; } .pun { color: #660; } .pln { color: #000; } dl.tag-list dt code, .tag { color: #008; } dl.atn-list dt code, .atn { color: #828; } .atv { color: #080; } .dec { color: #606; } @media print { .str { color: #060; } .kwd { color: #006; font-weight: bold; } .com { color: #600; font-style: italic; } .typ { color: #404; font-weight: bold; } .lit { color: #044; } .pun { color: #440; } .pln { color: #000; } .tag { color: #006; font-weight: bold; } .atn { color: #404; } .atv { color: #060; } } ================================================ FILE: docs/javadoc/assets/doclava-developer-docs.css ================================================ /* file: doclava-developer-docs.css info: developer doc styles */ @import url("doclava-developer-core.css"); #title { border-bottom: 4px solid #ccc; display:none; } #title h1 { color:#336666; margin:0; padding: 5px 10px; font-size: 1em; line-height: 15px; } #title h1 .small{ color:#000; margin:0; font-size: 13px; padding:0 0 0 15px; } /* SIDE NAVIGATION */ #side-nav { padding:0 6px 0 0; background-color: #fff; font-size:12px; } #side-nav.not-resizable { background:url('images/sidenav-rule.png') no-repeat 243px 0; } #resize-packages-nav { /* keeps the resize handle below the h-scroll handle */ height:270px; overflow:hidden; max-height:100%; } #packages-nav { height:270px; max-height:inherit; position:relative; overflow:auto; } #classes-nav, #devdoc-nav { overflow:auto; position:relative; } #side-nav ul { list-style: none; margin: 0; padding:5px 0; } #side-nav ul ul { margin: .35em 0 0 0; padding: 0; } #side-nav li { padding:0; line-height:16px; white-space:nowrap; zoom:1; } #side-nav li h2 { font-size:12px; font-weight: bold; margin:.5em 0 0 0; padding: 3px 0 1px 9px; } #side-nav li a { text-decoration:none; padding: 0 0 0 18px; zoom:1; } #side-nav li a span+span { display:none; } #side-nav li a:hover { text-decoration:underline; } #side-nav li a+a { padding: 0; } /*second level (nested) list*/ #side-nav li li li a { padding: 0 0 0 28px; } /*third level (nested) list*/ #side-nav li li li li a { padding: 0 0 0 38px; } #side-nav .selected { background-color: #435a6e; color: #fff; font-weight:bold; } #side-nav .selected a { color: #fff; text-decoration:none; } #side-nav strong { display:block; } #side-nav .toggle-list .toggle-img { margin:0; padding:0; position:absolute; top:0; left:0; height:16px; width:15px; outline-style:none; } /* second-level toggle */ #side-nav .toggle-list .toggle-list .toggle-img { left:10px; } #side-nav .closed .toggle-img, #side-nav .open .closed .toggle-img { background:url('images/triangle-closed-small.png') 7px 4px no-repeat; } #side-nav .open .toggle-img { background:url('images/triangle-opened-small.png') 7px 4px no-repeat; } #side-nav .toggle-list { position:relative; } #side-nav .toggle-list ul { margin:0; display:none; } #side-nav .toggle-list div { display:block; } #index-links .selected { background-color: #fff; color: #000; font-weight:normal; text-decoration:none; } #index-links { padding:7px 0 4px 10px; } /* nav tree */ #nav-tree ul { padding:5px 0 1.5em; } #side-nav #nav-tree ul li a, #side-nav #nav-tree ul li span.no-children { padding: 0 0 0 0; margin: 0; } #nav-tree .plus { margin: 0 3px 0 0; } #nav-tree ul ul { list-style: none; margin: 0; padding: 0 0 0 0; } #nav-tree ul li { margin: 0; padding: 0 0 0 0; white-space: nowrap; } #nav-tree .children_ul { margin:0; } #nav-tree a.nolink { color: black; text-decoration: none; } #nav-tree span.label { width: 100%; } #nav-tree { overflow-x: auto; overflow-y: scroll; } #nav-swap { font-size:10px; line-height:10px; margin-left:1em; text-decoration:none; display:block; position:absolute; bottom:2px; left:0px; } #tree-link { } /* DOCUMENT BODY */ #doc-content { overflow:auto; } #jd-header { background-color: #E2E2E2; padding: 7px 15px; } #jd-header h1 { margin: 0 0 10px; font-size:1.7em; } #jd-header .crumb { font-size:.9em; line-height:1em; color:#777; } #jd-header .crumb a, #jd-header .crumb a:visited { text-decoration:none; color:#777; } #jd-header .crumb a:hover { text-decoration:underline; } #jd-header table { margin:0; padding:0; } #jd-header td { border:none; padding:0; vertical-align:top; } #jd-header.guide-header { background-color:#fff; color:#435a6e; height:50px; } #jd-descr { position:relative; } /* summary tables for reference pages */ .jd-sumtable { margin: .5em 1em 1em 1em; width:95%; /* consistent table widths; within IE's quirks */ font-size:.9em; } .jd-sumtable a { text-decoration:none; } .jd-sumtable a:hover { text-decoration:underline; } /* the link inside a sumtable for "Show All/Hide All" */ .toggle-all { display:block; float:right; font-weight:normal; font-size:0.9em; } /* adjustments for in/direct subclasses tables */ .jd-sumtable-subclasses { margin: 1em 0 0 0; max-width:968px; } /* extra space between end of method name and open-paren */ .sympad { margin-right: 2px; } /* right alignment for the return type in sumtable */ .jd-sumtable .jd-typecol { text-align:right; white-space: nowrap; } /* adjustments for the expando table-in-table */ .jd-sumtable-expando { margin:.5em 0; padding:0; } /* a div that holds a short description */ .jd-descrdiv { padding:3px 1em 0 1em; margin:0; border:0; } /* page-top-right container for reference pages (holds links to summary tables) */ #api-info-block { font-size:.8em; padding:6px 10px; font-weight:normal; float:right; text-align:right; color:#999; max-width:70%; } #api-level-toggle { padding:0 0px; font-size:11px; margin:3px 10px 0 0; } #api-level-toggle label.disabled { color:#999; } div.api-level { font-size:.8em; font-weight:normal; color:#999; float:right; padding:0 7px 0; margin-top:-25px; } #api-info-block div.api-level { font-size:1.3em; font-weight:bold; float:none; color:#444; padding:0; margin:0; } /* Force link colors for IE6 */ div.api-level a { color:#999; } #api-info-block div.api-level a:link { color:#444; } #api-level-toggle a { color:#999; } div#naMessage { display:none; width:555px; height:0; margin:0 auto; } div#naMessage div { width:450px; position:fixed; margin:50px 0; padding:4em 4em 3em; background:#FFF; background:rgba(255,255,255,0.7); border:1px solid #dddd00; } /* IE6 can't position fixed */ * html div#naMessage div { position:absolute; } div#naMessage strong { font-size:1.1em; } .absent, .absent a:link, .absent a:visited, .absent a:hover, .absent * { color:#bbb !important; cursor:default !important; text-decoration:none !important; } #api-level-toggle a, .api-level a { color:inherit; text-decoration:none; } #api-level-toggle a:hover, .api-level a:hover { color:inherit; text-decoration:underline !important; cursor:pointer !important; } #side-nav li.absent.selected, #side-nav li.absent.selected *, #side-nav div.label.absent.selected, #side-nav div.label.absent.selected * { background-color:#eaeaea !important; } /* IE6 quirk (won't chain classes, so just keep background blue) */ * html #side-nav li.selected, * html #side-nav li.selected *, * html #side-nav div.label.selected, * html #side-nav div.label.selected * { background-color: #435a6e !important; } .absent h4.jd-details-title, .absent h4.jd-details-title * { background-color:#f6f6f6 !important; } .absent img { opacity: .3; filter: alpha(opacity=30); -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; } /* applies to a div containing links to summary tables */ .sum-details-links { padding:0; font-weight:normal; } .sum-details-links a { text-decoration:none; } .sum-details-links a:hover { text-decoration:underline; } /* inheritance table */ .jd-inheritance-table { border-spacing:0; margin:0; padding:0; font-size:.9em; } .jd-inheritance-table td { border: none; margin: 0; padding: 0; } .jd-inheritance-table .jd-inheritance-space { font-weight:bold; width:1em; } .jd-inheritance-table .jd-inheritance-interface-cell { padding-left: 17px; } #jd-content { padding: 18px 15px; } hr { background-color:#ccc; border-color:#fff; margin:2em 0 1em; } /* DOC CLASSES */ #jd-content h1 { /*sdk page*/ font-size:1.6em; color:#336666; margin:0 0 .5em; } #jd-content h2 { font-size:1.45em; color:#111; border-top:2px solid #ccc; padding: .5em 0 0; margin: 2em 0 1em 0; } #jd-content h3 { font-size:1.2em; color:#222; padding: .75em 0 .65em 0; margin:0; } #jd-content h4 { font-size:1.1em; margin-bottom:.5em; color:#222; } #jd-content .small-header { font-size:1em; color:#000; font-weight:bold; border:none; padding:0; margin:1em 0 .5em; position:inherit; } #jd-content table { margin: 0 0 1em 1em; } #jd-content img { margin: 0 0 1em 1em; } #jd-content li img, #jd-content dd img { margin:.5em 0 0 1em; } .nolist { list-style:none; padding:0; margin:0 0 1em 1em; } .nolist li { padding:0 0 2px; margin:0; } h4 .normal { font-size:.9em; font-weight:normal; } .caps { font-variant:small-caps; font-size:1.2em; } dl.tag-list dl.atn-list { padding:0 0 0 2em; } .jd-details { /* border:1px solid #669999; padding:4px; */ margin:0 0 1em; } /* API reference: a container for the .tagdata blocks that make up the detailed description */ .jd-details-descr { padding:0; margin:.5em .25em; } /* API reference: a block containing a detailed description, a params table, seealso list, etc */ .jd-tagdata { margin:.5em 1em; } .jd-tagdata p { margin:0 0 1em 1em; } /* API reference: adjustments to the detailed description block */ .jd-tagdescr { margin:.25em 0 .75em 0; line-height:1em; } .jd-tagdescr p { margin:.5em 0; padding:0; } .jd-tagdescr ol, .jd-tagdescr ul { margin:0 2.5em; padding:0; } .jd-tagdescr table, .jd-tagdescr img { margin:.25em 1em; } .jd-tagdescr li { margin:0 0 .25em 0; padding:0; } /* API reference: heading marking the details section for constants, attrs, methods, etc. */ h4.jd-details-title { font-size:1.15em; background-color: #E2E2E2; margin:1.5em 0 .6em; padding:3px 95px 3px 3px; /* room for api-level */ } h4.jd-tagtitle { margin:0; } /* API reference: heading for "Parameters", "See Also", etc., in details sections */ h5.jd-tagtitle { margin:0 0 .25em 0; font-size:1em; } .jd-tagtable { margin:0; } .jd-tagtable td, .jd-tagtable th { border:none; background-color:#fff; vertical-align:top; font-weight:normal; padding:2px 10px; } .jd-tagtable th { font-style:italic; } #jd-content table h2 { background-color: #d6d6d6; font-size: 1.1em; margin:0 0 10px; padding:5px; left:0; width:auto; } div.special { padding: .5em 1em 1em 1em; margin: 0 0 1em; background-color: #DAF3FC; border:1px solid #d3ecf5; border-radius:5px; -moz-border-radius:5px; -webkit-border-radius:5px; } .toggle-content-toggleme { display:none; } .toggle-content-button { font-size:.9em; line-height:.9em; text-decoration:none; position:relative; top:5px; } .toggle-content-button:hover { text-decoration:underline; } div.special p { margin: .5em 0 0 0; } div.special ol { margin: 0; } div.special ol li { margin: 0; padding: 0; } #jd-content div.special h2, #jd-content div.special h3 { color:#669999; font-size:1.2em; border:none; margin:0 0 .5em; padding:0; } p.note, p.caution, p.warning { margin: 1em; padding: 0 0 0 .5em; border-left: 4px solid; } p.special-note { background-color:#EBF3DB; padding:10px 20px; margin:0 0 1em; } p.note { border-color: #99aacc; } p.warning { border-color: #aa0033; } p.caution { border-color: #ffcf00; } p.warning b, p.warning strong { font-weight: bold; } li p.note, li p.warning { margin: .5em 0 0 0; padding: .2em .5em .2em .9em; } dl.xml dt { font-variant:small-caps; font-size:1.2em; } dl.xml dl { padding:0; } dl.xml dl dt { font-variant:normal; font-size:1em; } .listhead li { font-weight: bold; } .listhead li *, /*ie*/.listhead li li { font-weight: normal; } ol.no-style, ul.no-style { list-style:none; padding-left:1em; } .new { font-size: .78em; font-weight: bold; color: #ff3d3d; text-decoration: none; vertical-align:top; line-height:.9em; } pre.classic { background-color:transparent; border:none; padding:0; } p.img-caption { margin: -0.5em 0 1em 1em; /* matches default img left-margin */ } div.figure { float:right; clear:right; margin:1em 0 0 3em; padding:0; background-color:#fff; /* width must be defined w/ an inline style matching the image width */ } #jd-content div.figure img { margin: 0 0 1em; } div.figure p.img-caption { margin: -0.5em 0 1em 0; } p.table-caption { margin: 0 0 0.5em 1em; /* matches default table left-margin */ } /* Begin sidebox sidebar element styles */ .sidebox-wrapper { float:right; clear:right; width:310px; /* +35px padding */ background-color:#fff; margin:0; padding:0 0 20px 35px; } .sidebox { border-left:1px solid #dee8f1; background-color:#ffffee; margin:0; padding:8px 12px; font-size:0.9em; width:285px; /* +24px padding; +1px border */ } .sidebox p { margin-bottom: .25em; } .sidebox ul { padding: 0 0 0 1.5em; } .sidebox li ul { margin-top:0; margin-bottom:.1em; } .sidebox li { padding:0 0 0 0em; } #jd-content .sidebox h2, #jd-content .sidebox h3, #jd-content .sidebox h4, #jd-content .sidebox h5 { border:none; font-size:1em; margin:0; padding:0 0 8px; left:0; z-index:0; } .sidebox hr { background-color:#ccc; border:none; } /* End sidebox sidebar element styles */ /* table of contents */ ol.toc { margin: 0 0 1em 0; padding: 0; list-style: none; font-size:95%; } ol.toc li { font-weight: bold; margin: 0 0 .5em 1em; padding: 0; } ol.toc li p { font-weight: normal; } ol.toc li ol { margin: 0; padding: 0; } ol.toc li li { padding: 0; margin: 0 0 0 1em; font-weight: normal; list-style: none; } table ol.toc { margin-left: 0; } .columns td { padding:0 5px; border:none; } /* link table */ .jd-linktable { margin: 0 0 1em; border-bottom: 1px solid #888; } .jd-linktable th, .jd-linktable td { padding: 3px 5px; vertical-align: top; text-align: left; border:none; } .jd-linktable tr { background-color: #fff; } .jd-linktable td { border-top: 1px solid #888; background-color: inherit; } .jd-linktable td p { padding: 0 0 5px; } .jd-linktable .jd-linkcol { } .jd-linktable .jd-descrcol { } .jd-linktable .jd-typecol { text-align:right; white-space: nowrap; } .jd-linktable .jd-valcol { } .jd-linktable .jd-commentrow { border-top:none; padding-left:25px; } .jd-deprecated-warning { margin-top: 0; margin-bottom: 10px; } tr.alt-color { background-color: #f6f6f6; } /* expando trigger */ #jd-content .jd-expando-trigger-img { margin:0; } /* jd-expando */ .jd-inheritedlinks { padding:0 0 0 13px } /* SDK PAGE */ table.download tr { background-color:#d9d9d9; } table.download tr.alt-color { background-color:#ededed; } table.download td, table.download th { border:2px solid #fff; padding:10px 5px; } table.download th { background-color:#6d8293; color:#fff; } /* INLAY 180 COPY and 240PX EXTENSION */ /* modified to 43px so that all browsers eliminate the package panel h-scroll */ .g-tpl-240 .g-unit, .g-unit .g-tpl-240 .g-unit, .g-unit .g-unit .g-tpl-240 .g-unit { display: block; margin: 0 0 0 243px; width: auto; float: none; } .g-unit .g-unit .g-tpl-240 .g-first, .g-unit .g-tpl-240 .g-first, .g-tpl-240 .g-first { display: block; margin: 0; width: 243px; float: left; } /* 240px alt */ .g-tpl-240-alt .g-unit, .g-unit .g-tpl-240-alt .g-unit, .g-unit .g-unit .g-tpl-240-alt .g-unit { display: block; margin: 0 243px 0 0; width: auto; float: none; } .g-unit .g-unit .g-tpl-240-alt .g-first, .g-unit .g-tpl-240-alt .g-first, .g-tpl-240-alt .g-first { display: block; margin: 0; width: 243px; float: right; } /* 180px */ .g-tpl-180 .g-unit, .g-unit .g-tpl-180 .g-unit, .g-unit .g-unit .g-tpl-180 .g-unit { display: block; margin: 0 0 0 180px; width: auto; float: none; } .g-unit .g-unit .g-tpl-180 .g-first, .g-unit .g-tpl-180 .g-first, .g-tpl-180 .g-first { display: block; margin: 0; width: 180px; float: left; } /* 180px alt */ .g-tpl-180-alt .g-unit, .g-unit .g-tpl-180-alt .g-unit, .g-unit .g-unit .g-tpl-180-alt .g-unit { display: block; margin: 0 180px 0 0; width: auto; float: none; } .g-unit .g-unit .g-tpl-180-alt .g-first, .g-unit .g-tpl-180-alt .g-first, .g-tpl-180-alt .g-first { display: block; margin: 0; width: 180px; float: right; } /* JQUERY RESIZABLE STYLES */ .ui-resizable { position: relative; } .ui-resizable-handle { position: absolute; display: none; font-size: 0.1px; z-index:1; } .ui-resizable .ui-resizable-handle { display: block; } body .ui-resizable-disabled .ui-resizable-handle { display: none; } body .ui-resizable-autohide .ui-resizable-handle { display: none; } .ui-resizable-s { cursor: s-resize; height: 6px; width: 100%; bottom: 0px; left: 0px; background: transparent url("images/resizable-s2.gif") repeat scroll center top; } .ui-resizable-e { cursor: e-resize; width: 6px; right: 0px; top: 0px; height: 100%; background: transparent url("images/resizable-e2.gif") repeat scroll right center; } @media print { body { overflow:visible; } #header { height:50px; } #header-tabs, #headerRight, #side-nav, #api-info-block { display:none; } #body-content { position:inherit; } #doc-content { margin-left:0 !important; height:auto !important; width:auto !important; overflow:inherit; display:inline; } #jd-header { padding:10px 0; } #jd-content { padding:15px 0 0; } #footer { float:none; margin:2em 0 0; } h4.jd-details-title { border-bottom:1px solid #666; } pre { /* these allow lines to break (if there's a white space) */ overflow: visible; text-wrap: unrestricted; white-space: -moz-pre-wrap; /* Moz */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ white-space: pre-wrap; /* CSS3 */ word-wrap: break-word; /* IE 5.5+ */ } h1, h2, h3, h4, h5, h6 { page-break-after: avoid; } table, img { page-break-inside: avoid; } ================================================ FILE: docs/javadoc/assets/doclava-developer-docs.js ================================================ var resizePackagesNav; var classesNav; var devdocNav; var sidenav; var content; var HEADER_HEIGHT = -1; var cookie_namespace = 'doclava_developer'; var NAV_PREF_TREE = "tree"; var NAV_PREF_PANELS = "panels"; var nav_pref; var toRoot; var toAssets; var isMobile = false; // true if mobile, so we can adjust some layout var isIE6 = false; // true if IE6 // TODO: use $(document).ready instead function addLoadEvent(newfun) { var current = window.onload; if (typeof window.onload != 'function') { window.onload = newfun; } else { window.onload = function() { current(); newfun(); } } } var agent = navigator['userAgent'].toLowerCase(); // If a mobile phone, set flag and do mobile setup if ((agent.indexOf("mobile") != -1) || // android, iphone, ipod (agent.indexOf("blackberry") != -1) || (agent.indexOf("webos") != -1) || (agent.indexOf("mini") != -1)) { // opera mini browsers isMobile = true; addLoadEvent(mobileSetup); // If not a mobile browser, set the onresize event for IE6, and others } else if (agent.indexOf("msie 6") != -1) { isIE6 = true; addLoadEvent(function() { window.onresize = resizeAll; }); } else { addLoadEvent(function() { window.onresize = resizeHeight; }); } function mobileSetup() { $("body").css({'overflow':'auto'}); $("html").css({'overflow':'auto'}); $("#body-content").css({'position':'relative', 'top':'0'}); $("#doc-content").css({'overflow':'visible', 'border-left':'3px solid #DDD'}); $("#side-nav").css({'padding':'0'}); $("#nav-tree").css({'overflow-y': 'auto'}); } /* loads the lists.js file to the page. Loading this in the head was slowing page load time */ addLoadEvent( function() { var lists = document.createElement("script"); lists.setAttribute("type","text/javascript"); lists.setAttribute("src", toRoot+"lists.js"); document.getElementsByTagName("head")[0].appendChild(lists); } ); addLoadEvent( function() { $("pre:not(.no-pretty-print)").addClass("prettyprint"); prettyPrint(); } ); function setToRoot(root, assets) { toRoot = root; toAssets = assets; // note: toRoot also used by carousel.js } function restoreWidth(navWidth) { var windowWidth = $(window).width() + "px"; content.css({marginLeft:parseInt(navWidth) + 6 + "px"}); //account for 6px-wide handle-bar if (isIE6) { content.css({width:parseInt(windowWidth) - parseInt(navWidth) - 6 + "px"}); // necessary in order for scrollbars to be visible } sidenav.css({width:navWidth}); resizePackagesNav.css({width:navWidth}); classesNav.css({width:navWidth}); $("#packages-nav").css({width:navWidth}); } function restoreHeight(packageHeight) { var windowHeight = ($(window).height() - HEADER_HEIGHT); var swapperHeight = windowHeight - 13; $("#swapper").css({height:swapperHeight + "px"}); sidenav.css({height:windowHeight + "px"}); content.css({height:windowHeight + "px"}); resizePackagesNav.css({maxHeight:swapperHeight + "px", height:packageHeight}); classesNav.css({height:swapperHeight - parseInt(packageHeight) + "px"}); $("#packages-nav").css({height:parseInt(packageHeight) - 6 + "px"}); //move 6px to give space for the resize handle devdocNav.css({height:sidenav.css("height")}); $("#nav-tree").css({height:swapperHeight + "px"}); } function readCookie(cookie) { var myCookie = cookie_namespace+"_"+cookie+"="; if (document.cookie) { var index = document.cookie.indexOf(myCookie); if (index != -1) { var valStart = index + myCookie.length; var valEnd = document.cookie.indexOf(";", valStart); if (valEnd == -1) { valEnd = document.cookie.length; } var val = document.cookie.substring(valStart, valEnd); return val; } } return 0; } function writeCookie(cookie, val, section, expiration) { if (val==undefined) return; section = section == null ? "_" : "_"+section+"_"; if (expiration == null) { var date = new Date(); date.setTime(date.getTime()+(10*365*24*60*60*1000)); // default expiration is one week expiration = date.toGMTString(); } document.cookie = cookie_namespace + section + cookie + "=" + val + "; expires=" + expiration+"; path=/"; } function getSection() { if (location.href.indexOf("/reference/") != -1) { return "reference"; } else if (location.href.indexOf("/guide/") != -1) { return "guide"; } else if (location.href.indexOf("/resources/") != -1) { return "resources"; } var basePath = getBaseUri(location.pathname); return basePath.substring(1,basePath.indexOf("/",1)); } function init() { HEADER_HEIGHT = $("#header").height()+3; $("#side-nav").css({position:"absolute",left:0}); content = $("#doc-content"); resizePackagesNav = $("#resize-packages-nav"); classesNav = $("#classes-nav"); sidenav = $("#side-nav"); devdocNav = $("#devdoc-nav"); var cookiePath = getSection() + "_"; if (!isMobile) { $("#resize-packages-nav").resizable({handles: "s", resize: function(e, ui) { resizePackagesHeight(); } }); $(".side-nav-resizable").resizable({handles: "e", resize: function(e, ui) { resizeWidth(); } }); var cookieWidth = readCookie(cookiePath+'width'); var cookieHeight = readCookie(cookiePath+'height'); if (cookieWidth) { restoreWidth(cookieWidth); } else if ($(".side-nav-resizable").length) { resizeWidth(); } if (cookieHeight) { restoreHeight(cookieHeight); } else { resizeHeight(); } } if (devdocNav.length) { // only dev guide, resources, and sdk tryPopulateResourcesNav(); highlightNav(location.href); } } function highlightNav(fullPageName) { var lastSlashPos = fullPageName.lastIndexOf("/"); var firstSlashPos; if (fullPageName.indexOf("/guide/") != -1) { firstSlashPos = fullPageName.indexOf("/guide/"); } else if (fullPageName.indexOf("/sdk/") != -1) { firstSlashPos = fullPageName.indexOf("/sdk/"); } else { firstSlashPos = fullPageName.indexOf("/resources/"); } if (lastSlashPos == (fullPageName.length - 1)) { // if the url ends in slash (add 'index.html') fullPageName = fullPageName + "index.html"; } // First check if the exact URL, with query string and all, is in the navigation menu var pathPageName = fullPageName.substr(firstSlashPos); var link = $("#devdoc-nav a[href$='"+ pathPageName+"']"); if (link.length == 0) { var htmlPos = fullPageName.lastIndexOf(".html", fullPageName.length); pathPageName = fullPageName.slice(firstSlashPos, htmlPos + 5); // +5 advances past ".html" link = $("#devdoc-nav a[href$='"+ pathPageName+"']"); if ((link.length == 0) && ((fullPageName.indexOf("/guide/") != -1) || (fullPageName.indexOf("/resources/") != -1))) { // if there's no match, then let's backstep through the directory until we find an index.html page // that matches our ancestor directories (only for dev guide and resources) lastBackstep = pathPageName.lastIndexOf("/"); while (link.length == 0) { backstepDirectory = pathPageName.lastIndexOf("/", lastBackstep); link = $("#devdoc-nav a[href$='"+ pathPageName.slice(0, backstepDirectory + 1)+"index.html']"); lastBackstep = pathPageName.lastIndexOf("/", lastBackstep - 1); if (lastBackstep == 0) break; } } } // add 'selected' to the
  • or
  • ) and the parent